fix: select first item when switching tags (#2026)
This commit is contained in:
@@ -0,0 +1,106 @@
|
|||||||
|
import { SNTag } from '@standardnotes/snjs'
|
||||||
|
import { ContentType } from '@standardnotes/common'
|
||||||
|
import { InternalEventBus } from '@standardnotes/services'
|
||||||
|
import { WebApplication } from '@/Application/Application'
|
||||||
|
import { LinkingController } from '../LinkingController'
|
||||||
|
import { NavigationController } from '../Navigation/NavigationController'
|
||||||
|
import { NotesController } from '../NotesController'
|
||||||
|
import { SearchOptionsController } from '../SearchOptionsController'
|
||||||
|
import { SelectedItemsController } from '../SelectedItemsController'
|
||||||
|
import { ItemListController } from './ItemListController'
|
||||||
|
import { ItemsReloadSource } from './ItemsReloadSource'
|
||||||
|
|
||||||
|
describe('item list controller', () => {
|
||||||
|
let controller: ItemListController
|
||||||
|
let navigationController: NavigationController
|
||||||
|
let selectionController: SelectedItemsController
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const application = {} as jest.Mocked<WebApplication>
|
||||||
|
application.streamItems = jest.fn()
|
||||||
|
application.addEventObserver = jest.fn()
|
||||||
|
application.addWebEventObserver = jest.fn()
|
||||||
|
|
||||||
|
navigationController = {} as jest.Mocked<NavigationController>
|
||||||
|
selectionController = {} as jest.Mocked<SelectedItemsController>
|
||||||
|
|
||||||
|
const searchOptionsController = {} as jest.Mocked<SearchOptionsController>
|
||||||
|
const notesController = {} as jest.Mocked<NotesController>
|
||||||
|
const linkingController = {} as jest.Mocked<LinkingController>
|
||||||
|
const eventBus = new InternalEventBus()
|
||||||
|
|
||||||
|
controller = new ItemListController(
|
||||||
|
application,
|
||||||
|
navigationController,
|
||||||
|
searchOptionsController,
|
||||||
|
selectionController,
|
||||||
|
notesController,
|
||||||
|
linkingController,
|
||||||
|
eventBus,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('shouldSelectFirstItem', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
controller.getFirstNonProtectedItem = jest.fn()
|
||||||
|
|
||||||
|
Object.defineProperty(selectionController, 'selectedUuids', {
|
||||||
|
get: () => new Set(),
|
||||||
|
configurable: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false first item is file', () => {
|
||||||
|
controller.getFirstNonProtectedItem = jest.fn().mockReturnValue({
|
||||||
|
content_type: ContentType.File,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(controller.shouldSelectFirstItem(ItemsReloadSource.UserTriggeredTagChange)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if selected tag is daily entry', () => {
|
||||||
|
const tag = {
|
||||||
|
isDailyEntry: true,
|
||||||
|
content_type: ContentType.Tag,
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
Object.defineProperty(navigationController, 'selected', {
|
||||||
|
get: () => tag,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(controller.shouldSelectFirstItem(ItemsReloadSource.UserTriggeredTagChange)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true if user triggered tag change', () => {
|
||||||
|
const tag = {
|
||||||
|
content_type: ContentType.Tag,
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
Object.defineProperty(navigationController, 'selected', {
|
||||||
|
get: () => tag,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(controller.shouldSelectFirstItem(ItemsReloadSource.UserTriggeredTagChange)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if not user triggered tag change and there is an existing selected item', () => {
|
||||||
|
const tag = {
|
||||||
|
content_type: ContentType.Tag,
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
Object.defineProperty(selectionController, 'selectedUuids', {
|
||||||
|
get: () => new Set(['123']),
|
||||||
|
})
|
||||||
|
|
||||||
|
Object.defineProperty(navigationController, 'selected', {
|
||||||
|
get: () => tag,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(controller.shouldSelectFirstItem(ItemsReloadSource.ItemStream)).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return true if there are no selected items, even if not user triggered', () => {
|
||||||
|
expect(controller.shouldSelectFirstItem(ItemsReloadSource.ItemStream)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -38,22 +38,13 @@ import { log, LoggingDomain } from '@/Logging'
|
|||||||
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
import { NoteViewController } from '@/Components/NoteView/Controller/NoteViewController'
|
||||||
import { FileViewController } from '@/Components/NoteView/Controller/FileViewController'
|
import { FileViewController } from '@/Components/NoteView/Controller/FileViewController'
|
||||||
import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions'
|
import { TemplateNoteViewAutofocusBehavior } from '@/Components/NoteView/Controller/TemplateNoteViewControllerOptions'
|
||||||
|
import { ItemsReloadSource } from './ItemsReloadSource'
|
||||||
|
|
||||||
const MinNoteCellHeight = 51.0
|
const MinNoteCellHeight = 51.0
|
||||||
const DefaultListNumNotes = 20
|
const DefaultListNumNotes = 20
|
||||||
const ElementIdSearchBar = 'search-bar'
|
const ElementIdSearchBar = 'search-bar'
|
||||||
const ElementIdScrollContainer = 'notes-scrollable'
|
const ElementIdScrollContainer = 'notes-scrollable'
|
||||||
|
|
||||||
enum ItemsReloadSource {
|
|
||||||
ItemStream,
|
|
||||||
SyncEvent,
|
|
||||||
DisplayOptionsChange,
|
|
||||||
Pagination,
|
|
||||||
TagChange,
|
|
||||||
UserTriggeredTagChange,
|
|
||||||
FilterTextChange,
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
|
export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface {
|
||||||
completedFullSync = false
|
completedFullSync = false
|
||||||
noteFilterText = ''
|
noteFilterText = ''
|
||||||
@@ -122,7 +113,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
application.streamItems<SNTag>([ContentType.Tag, ContentType.SmartView], async ({ changed, inserted }) => {
|
application.streamItems<SNTag>([ContentType.Tag, ContentType.SmartView], async ({ changed, inserted }) => {
|
||||||
const tags = [...changed, ...inserted]
|
const tags = [...changed, ...inserted]
|
||||||
|
|
||||||
const { didReloadItems } = await this.reloadDisplayPreferences()
|
const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered: false })
|
||||||
if (!didReloadItems) {
|
if (!didReloadItems) {
|
||||||
/** A tag could have changed its relationships, so we need to reload the filter */
|
/** A tag could have changed its relationships, so we need to reload the filter */
|
||||||
this.reloadNotesDisplayOptions()
|
this.reloadNotesDisplayOptions()
|
||||||
@@ -138,7 +129,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
application.addEventObserver(async () => {
|
application.addEventObserver(async () => {
|
||||||
void this.reloadDisplayPreferences()
|
void this.reloadDisplayPreferences({ userTriggered: false })
|
||||||
}, ApplicationEvent.PreferencesChanged),
|
}, ApplicationEvent.PreferencesChanged),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -427,11 +418,11 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSelectActiveItem = (activeItem: SNNote | FileItem | undefined) => {
|
private shouldSelectActiveItem = (activeItem: SNNote | FileItem) => {
|
||||||
return activeItem && !this.selectionController.isItemSelected(activeItem)
|
return !this.selectionController.isItemSelected(activeItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
private shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
shouldSelectFirstItem = (itemsReloadSource: ItemsReloadSource) => {
|
||||||
const item = this.getFirstNonProtectedItem()
|
const item = this.getFirstNonProtectedItem()
|
||||||
if (item && isFile(item)) {
|
if (item && isFile(item)) {
|
||||||
return false
|
return false
|
||||||
@@ -445,6 +436,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange
|
const userChangedTag = itemsReloadSource === ItemsReloadSource.UserTriggeredTagChange
|
||||||
const hasNoSelectedItem = !this.selectionController.selectedUuids.size
|
const hasNoSelectedItem = !this.selectionController.selectedUuids.size
|
||||||
|
|
||||||
return userChangedTag || hasNoSelectedItem
|
return userChangedTag || hasNoSelectedItem
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +458,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
|
log(LoggingDomain.Selection, 'Selecting next item after closing active one')
|
||||||
this.selectionController.selectNextItem()
|
this.selectionController.selectNextItem()
|
||||||
}
|
}
|
||||||
} else if (this.shouldSelectActiveItem(activeItem) && activeItem) {
|
} else if (activeItem && this.shouldSelectActiveItem(activeItem)) {
|
||||||
log(LoggingDomain.Selection, 'Selecting active item')
|
log(LoggingDomain.Selection, 'Selecting active item')
|
||||||
await this.selectionController.selectItem(activeItem.uuid).catch(console.error)
|
await this.selectionController.selectItem(activeItem.uuid).catch(console.error)
|
||||||
} else if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
} else if (this.shouldSelectFirstItem(itemsReloadSource)) {
|
||||||
@@ -510,7 +502,11 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
this.application.items.setPrimaryItemDisplayOptions(criteria)
|
this.application.items.setPrimaryItemDisplayOptions(criteria)
|
||||||
}
|
}
|
||||||
|
|
||||||
reloadDisplayPreferences = async (): Promise<{ didReloadItems: boolean }> => {
|
reloadDisplayPreferences = async ({
|
||||||
|
userTriggered,
|
||||||
|
}: {
|
||||||
|
userTriggered: boolean
|
||||||
|
}): Promise<{ didReloadItems: boolean }> => {
|
||||||
const newDisplayOptions = {} as DisplayOptions
|
const newDisplayOptions = {} as DisplayOptions
|
||||||
const newWebDisplayOptions = {} as WebDisplayOptions
|
const newWebDisplayOptions = {} as WebDisplayOptions
|
||||||
const selectedTag = this.navigationController.selected
|
const selectedTag = this.navigationController.selected
|
||||||
@@ -601,7 +597,9 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
this.reloadNotesDisplayOptions()
|
this.reloadNotesDisplayOptions()
|
||||||
|
|
||||||
await this.reloadItems(ItemsReloadSource.DisplayOptionsChange)
|
await this.reloadItems(
|
||||||
|
userTriggered ? ItemsReloadSource.UserTriggeredTagChange : ItemsReloadSource.DisplayOptionsChange,
|
||||||
|
)
|
||||||
|
|
||||||
const didSortByChange = currentSortBy !== this.displayOptions.sortBy
|
const didSortByChange = currentSortBy !== this.displayOptions.sortBy
|
||||||
const didSortDirectionChange = currentSortDirection !== this.displayOptions.sortDirection
|
const didSortDirectionChange = currentSortDirection !== this.displayOptions.sortDirection
|
||||||
@@ -813,7 +811,7 @@ export class ItemListController extends AbstractViewController implements Intern
|
|||||||
|
|
||||||
this.resetPagination()
|
this.resetPagination()
|
||||||
|
|
||||||
const { didReloadItems } = await this.reloadDisplayPreferences()
|
const { didReloadItems } = await this.reloadDisplayPreferences({ userTriggered })
|
||||||
|
|
||||||
if (!didReloadItems) {
|
if (!didReloadItems) {
|
||||||
this.reloadNotesDisplayOptions()
|
this.reloadNotesDisplayOptions()
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export enum ItemsReloadSource {
|
||||||
|
ItemStream,
|
||||||
|
SyncEvent,
|
||||||
|
DisplayOptionsChange,
|
||||||
|
Pagination,
|
||||||
|
TagChange,
|
||||||
|
UserTriggeredTagChange,
|
||||||
|
FilterTextChange,
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user