feat: ability to cancel multiple selection from UI (#1045)
This commit is contained in:
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user