feat: ability to cancel multiple selection from UI (#1045)

This commit is contained in:
Mo
2022-05-23 17:01:44 -05:00
committed by GitHub
parent acdf442e61
commit 8877c42079
9 changed files with 74 additions and 22 deletions

View File

@@ -27,7 +27,7 @@ export const FileContextMenu: FunctionComponent<Props> = observer(({ appState })
const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open)) const [closeOnBlur] = useCloseOnBlur(contextMenuRef, (open: boolean) => setShowFileContextMenu(open))
useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false)) useCloseOnClickOutside(contextMenuRef, () => appState.files.setShowFileContextMenu(false))
const selectedFile = Object.values(selectedFiles)[0] const selectedFile = selectedFiles[0]
if (!showFileContextMenu || !selectedFile) { if (!showFileContextMenu || !selectedFile) {
return null return null
} }

View File

@@ -4,6 +4,8 @@ import { observer } from 'mobx-react-lite'
import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel' import { NotesOptionsPanel } from '@/Components/NotesOptions/NotesOptionsPanel'
import { WebApplication } from '@/UIModels/Application' import { WebApplication } from '@/UIModels/Application'
import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton' import { PinNoteButton } from '@/Components/PinNoteButton/PinNoteButton'
import { Button } from '../Button/Button'
import { useCallback } from 'preact/hooks'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -13,6 +15,10 @@ type Props = {
export const MultipleSelectedNotes = observer(({ application, appState }: Props) => { export const MultipleSelectedNotes = observer(({ application, appState }: Props) => {
const count = appState.notes.selectedNotesCount const count = appState.notes.selectedNotesCount
const cancelMultipleSelection = useCallback(() => {
appState.selectedItems.cancelMultipleSelection()
}, [appState])
return ( return (
<div className="flex flex-col h-full items-center"> <div className="flex flex-col h-full items-center">
<div className="flex items-center justify-between p-4 w-full"> <div className="flex items-center justify-between p-4 w-full">
@@ -28,6 +34,9 @@ export const MultipleSelectedNotes = observer(({ application, appState }: Props)
<IlNotesIcon className="block" /> <IlNotesIcon className="block" />
<h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2> <h2 className="text-lg m-0 text-center mt-4">{count} selected notes</h2>
<p className="text-sm mt-2 text-center max-w-60">Actions will be performed on all selected notes.</p> <p className="text-sm mt-2 text-center max-w-60">Actions will be performed on all selected notes.</p>
<Button className="mt-2.5" onClick={cancelMultipleSelection}>
Cancel multiple selection
</Button>
</div> </div>
</div> </div>
) )

View File

@@ -185,7 +185,7 @@ export const NotesOptions = observer(({ application, appState, closeOnBlur }: No
return notesMatchingAttribute.length > notesNotMatchingAttribute.length return notesMatchingAttribute.length > notesNotMatchingAttribute.length
} }
const notes = Object.values(appState.notes.selectedNotes) const notes = appState.notes.selectedNotes
const hidePreviews = toggleOn((note) => note.hidePreview) const hidePreviews = toggleOn((note) => note.hidePreview)
const locked = toggleOn((note) => note.locked) const locked = toggleOn((note) => note.locked)
const protect = toggleOn((note) => note.protected) const protect = toggleOn((note) => note.protected)

View File

@@ -18,7 +18,7 @@ export const PinNoteButton: FunctionComponent<Props> = observer(
return null return null
} }
const notes = Object.values(appState.notes.selectedNotes) const notes = appState.notes.selectedNotes
const pinned = notes.some((note) => note.pinned) const pinned = notes.some((note) => note.pinned)
const togglePinned = useCallback(async () => { const togglePinned = useCallback(async () => {

View File

@@ -87,9 +87,14 @@ export class IOService {
if (!modifier) { if (!modifier) {
return return
} }
this.activeModifiers.delete(modifier) this.activeModifiers.delete(modifier)
} }
public cancelAllKeyboardModifiers = (): void => {
this.activeModifiers.clear()
}
public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => { public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => {
this.addActiveModifier(modifier) this.addActiveModifier(modifier)
} }

View File

@@ -62,7 +62,7 @@ export class FilesState extends AbstractState {
) )
} }
get selectedFiles() { get selectedFiles(): FileItem[] {
return this.appState.selectedItems.getSelectedItems<FileItem>(ContentType.File) return this.appState.selectedItems.getSelectedItems<FileItem>(ContentType.File)
} }

View File

@@ -60,12 +60,12 @@ export class NotesState extends AbstractState {
application.streamItems<SNNote>(ContentType.Note, ({ changed, inserted, removed }) => { application.streamItems<SNNote>(ContentType.Note, ({ changed, inserted, removed }) => {
runInAction(() => { runInAction(() => {
for (const removedNote of removed) { for (const removedNote of removed) {
delete this.selectedNotes[removedNote.uuid] this.appState.selectedItems.deselectItem(removedNote)
} }
for (const note of [...changed, ...inserted]) { for (const note of [...changed, ...inserted]) {
if (this.selectedNotes[note.uuid]) { if (this.appState.selectedItems.isItemSelected(note)) {
this.selectedNotes[note.uuid] = note this.appState.selectedItems.updateReferenceOfSelectedItem(note)
} }
} }
}) })
@@ -80,14 +80,14 @@ export class NotesState extends AbstractState {
for (const selectedId of selectedUuids) { for (const selectedId of selectedUuids) {
if (!activeNoteUuids.includes(selectedId)) { 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<SNNote>(ContentType.Note) return this.appState.selectedItems.getSelectedItems<SNNote>(ContentType.Note)
} }
@@ -262,7 +262,7 @@ export class NotesState extends AbstractState {
if (permanently) { if (permanently) {
for (const note of this.getSelectedNotesList()) { for (const note of this.getSelectedNotesList()) {
await this.application.mutator.deleteItem(note) await this.application.mutator.deleteItem(note)
delete this.selectedNotes[note.uuid] this.appState.selectedItems.deselectItem(note)
} }
} else { } else {
await this.changeSelectedNotes((mutator) => { await this.changeSelectedNotes((mutator) => {

View File

@@ -51,17 +51,32 @@ export class SelectedItemsState extends AbstractState {
return Object.keys(this.selectedItems).length return Object.keys(this.selectedItems).length
} }
getSelectedItems = <T extends ListableContentItem>(contentType: ContentType) => { getSelectedItems = <T extends ListableContentItem = ListableContentItem>(contentType?: ContentType): T[] => {
const filteredEntries = Object.entries(this.appState.selectedItems.selectedItems).filter( return Object.values(this.selectedItems).filter((item) => {
([_, item]) => item.content_type === contentType, return !contentType ? true : item.content_type === contentType
) as [UuidString, T][] }) as T[]
return Object.fromEntries<T>(filteredEntries)
} }
setSelectedItems = (selectedItems: SelectedItems) => { setSelectedItems = (selectedItems: SelectedItems) => {
this.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<void> => { private selectItemsRange = async (selectedItem: ListableContentItem): Promise<void> => {
const items = this.appState.contentListView.renderedItems 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 ( selectItem = async (
uuid: UuidString, uuid: UuidString,
userTriggered?: boolean, userTriggered?: boolean,
@@ -119,10 +160,7 @@ export class SelectedItemsState extends AbstractState {
} else { } else {
const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid] const shouldSelectNote = hasMoreThanOneSelected || !this.selectedItems[uuid]
if (shouldSelectNote && isAuthorizedForAccess) { if (shouldSelectNote && isAuthorizedForAccess) {
this.setSelectedItems({ this.replaceSelection(item)
[item.uuid]: item,
})
this.lastSelectedItem = item
} }
} }

View File

@@ -15,17 +15,17 @@
--text-selection-background-color: var(--sn-stylekit-info-color); --text-selection-background-color: var(--sn-stylekit-info-color);
--note-preview-progress-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-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-background-color: var(--sn-stylekit-background-color);
--items-column-items-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-left-color: var(--sn-stylekit-border-color);
--items-column-border-right-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); --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); --item-cell-selected-border-left-color: var(--sn-stylekit-info-color);
--navigation-column-background-color: var(--sn-stylekit-secondary-background-color); --navigation-column-background-color: var(--sn-stylekit-secondary-background-color);