feat: add models package

This commit is contained in:
Karol Sójko
2022-07-05 20:47:11 +02:00
parent 60d1554ff7
commit b614c71e79
199 changed files with 8772 additions and 22 deletions

View File

@@ -0,0 +1,53 @@
import { createNoteWithContent } from '../../Utilities/Test/SpecUtils'
import { ItemCollection } from '../Collection/Item/ItemCollection'
import { SNNote } from '../../Syncable/Note/Note'
import { itemsMatchingOptions } from './Search/SearchUtilities'
import { FilterDisplayOptions } from './DisplayOptions'
describe('item display options', () => {
const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) {
const collection = new ItemCollection()
const notes: SNNote[] = []
titles.forEach((title, index) => {
notes.push(
createNoteWithContent({
title: title,
text: bodies[index],
}),
)
})
collection.set(notes)
return collection
}
it('string query title', () => {
const query = 'foo'
const options: FilterDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo'])
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
it('string query text', async function () {
const query = 'foo'
const options: FilterDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
const collection = collectionWithNotes(
[undefined, undefined, undefined, undefined],
['hello', 'fobar', 'foobar', 'foo'],
)
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
it('string query title and text', async function () {
const query = 'foo'
const options: FilterDisplayOptions = {
searchQuery: { query: query, includeProtectedNoteText: true },
}
const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar'])
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
})
})

View File

@@ -0,0 +1,25 @@
import { ContentType } from '@standardnotes/common'
import { SmartView } from '../../Syncable/SmartView'
import { SNTag } from '../../Syncable/Tag'
import { CollectionSortDirection, CollectionSortProperty } from '../Collection/CollectionSort'
import { SearchQuery } from './Search/Types'
import { DisplayControllerCustomFilter } from './Types'
export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions
export interface FilterDisplayOptions {
tags?: SNTag[]
views?: SmartView[]
searchQuery?: SearchQuery
includePinned?: boolean
includeProtected?: boolean
includeTrashed?: boolean
includeArchived?: boolean
}
export interface DisplayControllerOptions {
sortBy: CollectionSortProperty
sortDirection: CollectionSortDirection
hiddenContentTypes?: ContentType[]
customFilter?: DisplayControllerCustomFilter
}

View File

@@ -0,0 +1,78 @@
import { ContentType } from '@standardnotes/common'
import { DecryptedItem } from '../../Abstract/Item'
import { SNTag } from '../../Syncable/Tag'
import { CompoundPredicate } from '../Predicate/CompoundPredicate'
import { ItemWithTags } from './Search/ItemWithTags'
import { itemMatchesQuery, itemPassesFilters } from './Search/SearchUtilities'
import { ItemFilter, ReferenceLookupCollection, SearchableDecryptedItem } from './Search/Types'
import { FilterDisplayOptions } from './DisplayOptions'
export function computeUnifiedFilterForDisplayOptions(
options: FilterDisplayOptions,
collection: ReferenceLookupCollection,
): ItemFilter {
const filters = computeFiltersForDisplayOptions(options, collection)
return (item: SearchableDecryptedItem) => {
return itemPassesFilters(item, filters)
}
}
export function computeFiltersForDisplayOptions(
options: FilterDisplayOptions,
collection: ReferenceLookupCollection,
): ItemFilter[] {
const filters: ItemFilter[] = []
let viewsPredicate: CompoundPredicate<DecryptedItem> | undefined = undefined
if (options.views && options.views.length > 0) {
const compoundPredicate = new CompoundPredicate(
'and',
options.views.map((t) => t.predicate),
)
viewsPredicate = compoundPredicate
filters.push((item) => {
if (compoundPredicate.keypathIncludesString('tags')) {
const noteWithTags = ItemWithTags.Create(
item.payload,
item,
collection.elementsReferencingElement(item, ContentType.Tag) as SNTag[],
)
return compoundPredicate.matchesItem(noteWithTags)
} else {
return compoundPredicate.matchesItem(item)
}
})
}
if (options.tags && options.tags.length > 0) {
for (const tag of options.tags) {
filters.push((item) => tag.isReferencingItem(item))
}
}
if (options.includePinned === false && !viewsPredicate?.keypathIncludesString('pinned')) {
filters.push((item) => !item.pinned)
}
if (options.includeProtected === false && !viewsPredicate?.keypathIncludesString('protected')) {
filters.push((item) => !item.protected)
}
if (options.includeTrashed === false && !viewsPredicate?.keypathIncludesString('trashed')) {
filters.push((item) => !item.trashed)
}
if (options.includeArchived === false && !viewsPredicate?.keypathIncludesString('archived')) {
filters.push((item) => !item.archived)
}
if (options.searchQuery) {
const query = options.searchQuery
filters.push((item) => itemMatchesQuery(item, query, collection))
}
return filters
}

View File

@@ -0,0 +1,256 @@
import { CreateItemDelta } from './../Index/ItemDelta'
import { DeletedPayload } from './../../Abstract/Payload/Implementations/DeletedPayload'
import { createFile, createNote, createTag, mockUuid, pinnedContent } from './../../Utilities/Test/SpecUtils'
import { ContentType } from '@standardnotes/common'
import { DeletedItem, EncryptedItem } from '../../Abstract/Item'
import { EncryptedPayload, PayloadTimestampDefaults } from '../../Abstract/Payload'
import { createNoteWithContent } from '../../Utilities/Test/SpecUtils'
import { ItemCollection } from './../Collection/Item/ItemCollection'
import { ItemDisplayController } from './ItemDisplayController'
import { SNNote } from '../../Syncable/Note'
describe('item display controller', () => {
it('should sort items', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
const noteB = createNoteWithContent({ title: 'b' })
collection.set([noteA, noteB])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()[0]).toEqual(noteA)
expect(controller.items()[1]).toEqual(noteB)
controller.setDisplayOptions({ sortBy: 'title', sortDirection: 'dsc' })
expect(controller.items()[0]).toEqual(noteB)
expect(controller.items()[1]).toEqual(noteA)
})
it('should filter items', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
const noteB = createNoteWithContent({ title: 'b' })
collection.set([noteA, noteB])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
controller.setDisplayOptions({
customFilter: (note) => {
return note.title !== 'a'
},
})
expect(controller.items()).toHaveLength(1)
expect(controller.items()[0].title).toEqual('b')
})
it('should resort items after collection change', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
collection.set([noteA])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()).toHaveLength(1)
const noteB = createNoteWithContent({ title: 'b' })
const delta = CreateItemDelta({ changed: [noteB] })
collection.onChange(delta)
controller.onCollectionChange(delta)
expect(controller.items()).toHaveLength(2)
})
it('should not display encrypted items', () => {
const collection = new ItemCollection()
const noteA = new EncryptedItem(
new EncryptedPayload({
uuid: mockUuid(),
content_type: ContentType.Note,
content: '004:...',
enc_item_key: '004:...',
items_key_id: mockUuid(),
errorDecrypting: true,
waitingForKey: false,
...PayloadTimestampDefaults(),
}),
)
collection.set([noteA])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()).toHaveLength(0)
})
it('pinned items should come first', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
const noteB = createNoteWithContent({ title: 'b' })
collection.set([noteA, noteB])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()[0]).toEqual(noteA)
expect(controller.items()[1]).toEqual(noteB)
expect(collection.all()).toHaveLength(2)
const pinnedNoteB = new SNNote(
noteB.payload.copy({
content: {
...noteB.content,
...pinnedContent(),
},
}),
)
expect(pinnedNoteB.pinned).toBeTruthy()
const delta = CreateItemDelta({ changed: [pinnedNoteB] })
collection.onChange(delta)
controller.onCollectionChange(delta)
expect(controller.items()[0]).toEqual(pinnedNoteB)
expect(controller.items()[1]).toEqual(noteA)
})
it('should not display deleted items', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
collection.set([noteA])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
const deletedItem = new DeletedItem(
new DeletedPayload({
...noteA.payload,
content: undefined,
deleted: true,
}),
)
const delta = CreateItemDelta({ changed: [deletedItem] })
collection.onChange(delta)
controller.onCollectionChange(delta)
expect(controller.items()).toHaveLength(0)
})
it('discarding elements should remove from display', () => {
const collection = new ItemCollection()
const noteA = createNoteWithContent({ title: 'a' })
collection.set([noteA])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
const delta = CreateItemDelta({ discarded: [noteA] as unknown as DeletedItem[] })
collection.onChange(delta)
controller.onCollectionChange(delta)
expect(controller.items()).toHaveLength(0)
})
it('should ignore items not matching content type on construction', () => {
const collection = new ItemCollection()
const note = createNoteWithContent({ title: 'a' })
const tag = createTag()
collection.set([note, tag])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()).toHaveLength(1)
})
it('should ignore items not matching content type on sort change', () => {
const collection = new ItemCollection()
const note = createNoteWithContent({ title: 'a' })
const tag = createTag()
collection.set([note, tag])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
controller.setDisplayOptions({ sortBy: 'created_at', sortDirection: 'asc' })
expect(controller.items()).toHaveLength(1)
})
it('should ignore collection deltas with items not matching content types', () => {
const collection = new ItemCollection()
const note = createNoteWithContent({ title: 'a' })
collection.set([note])
const controller = new ItemDisplayController(collection, [ContentType.Note], {
sortBy: 'title',
sortDirection: 'asc',
})
const tag = createTag()
const delta = CreateItemDelta({ inserted: [tag], changed: [note] })
collection.onChange(delta)
controller.onCollectionChange(delta)
expect(controller.items()).toHaveLength(1)
})
it('should display compound item types', () => {
const collection = new ItemCollection()
const note = createNoteWithContent({ title: 'Z' })
const file = createFile('A')
collection.set([note, file])
const controller = new ItemDisplayController(collection, [ContentType.Note, ContentType.File], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()[0]).toEqual(file)
expect(controller.items()[1]).toEqual(note)
controller.setDisplayOptions({ sortBy: 'title', sortDirection: 'dsc' })
expect(controller.items()[0]).toEqual(note)
expect(controller.items()[1]).toEqual(file)
})
it('should hide hidden types', () => {
const collection = new ItemCollection()
const note = createNote()
const file = createFile()
collection.set([note, file])
const controller = new ItemDisplayController(collection, [ContentType.Note, ContentType.File], {
sortBy: 'title',
sortDirection: 'asc',
})
expect(controller.items()).toHaveLength(2)
controller.setDisplayOptions({ hiddenContentTypes: [ContentType.File] })
expect(controller.items()).toHaveLength(1)
})
})

View File

@@ -0,0 +1,138 @@
import { ContentType } from '@standardnotes/common'
import { compareValues } from '@standardnotes/utils'
import { isDeletedItem, isEncryptedItem } from '../../Abstract/Item'
import { ItemDelta } from '../Index/ItemDelta'
import { DisplayControllerOptions } from './DisplayOptions'
import { sortTwoItems } from './SortTwoItems'
import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types'
export class ItemDisplayController<I extends DisplayItem> {
private sortMap: UuidToSortedPositionMap = {}
private sortedItems: I[] = []
private needsSort = true
constructor(
private readonly collection: ReadonlyItemCollection,
public readonly contentTypes: ContentType[],
private options: DisplayControllerOptions,
) {
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
}
public items(): I[] {
return this.sortedItems
}
setDisplayOptions(displayOptions: Partial<DisplayControllerOptions>): void {
this.options = { ...this.options, ...displayOptions }
this.needsSort = true
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
}
onCollectionChange(delta: ItemDelta): void {
const items = [...delta.changed, ...delta.inserted, ...delta.discarded].filter((i) =>
this.contentTypes.includes(i.content_type),
)
this.filterThenSortElements(items as I[])
}
private filterThenSortElements(elements: I[]): void {
for (const element of elements) {
const previousIndex = this.sortMap[element.uuid]
const previousElement = previousIndex != undefined ? this.sortedItems[previousIndex] : undefined
const remove = () => {
if (previousIndex != undefined) {
delete this.sortMap[element.uuid]
/** We don't yet remove the element directly from the array, since mutating
* the array inside a loop could render all other upcoming indexes invalid */
;(this.sortedItems[previousIndex] as unknown) = undefined
/** Since an element is being removed from the array, we need to recompute
* the new positions for elements that are staying */
this.needsSort = true
}
}
if (isDeletedItem(element) || isEncryptedItem(element)) {
remove()
continue
}
const passes = !this.collection.has(element.uuid)
? false
: this.options.hiddenContentTypes?.includes(element.content_type)
? false
: this.options.customFilter
? this.options.customFilter(element)
: true
if (passes) {
if (previousElement != undefined) {
/** Check to see if the element has changed its sort value. If so, we need to re-sort. */
const previousValue = previousElement[this.options.sortBy]
const newValue = element[this.options.sortBy]
/** Replace the current element with the new one. */
this.sortedItems[previousIndex] = element
/** If the pinned status of the element has changed, it needs to be resorted */
const pinChanged = previousElement.pinned !== element.pinned
if (!compareValues(previousValue, newValue) || pinChanged) {
/** Needs resort because its re-sort value has changed,
* and thus its position might change */
this.needsSort = true
}
} else {
/** Has not yet been inserted */
this.sortedItems.push(element)
/** Needs re-sort because we're just pushing the element to the end here */
this.needsSort = true
}
} else {
/** Doesn't pass filter, remove from sorted and filtered */
remove()
}
}
if (this.needsSort) {
this.needsSort = false
this.resortItems()
}
}
/** Resort the sortedItems array, and update the saved positions */
private resortItems() {
const resorted = this.sortedItems.sort((a, b) => {
return sortTwoItems(a, b, this.options.sortBy, this.options.sortDirection)
})
/**
* Now that resorted contains the sorted elements (but also can contain undefined element)
* we create another array that filters out any of the undefinedes. We also keep track of the
* current index while we loop and set that in the this.sortMap.
* */
const results = []
let currentIndex = 0
/** @O(n) */
for (const element of resorted) {
if (!element) {
continue
}
results.push(element)
this.sortMap[element.uuid] = currentIndex
currentIndex++
}
this.sortedItems = results
}
}

View File

@@ -0,0 +1,36 @@
import { SearchableDecryptedItem } from './Types'
import { ItemContent } from '../../../Abstract/Content/ItemContent'
import { DecryptedItem } from '../../../Abstract/Item'
import { DecryptedPayloadInterface } from '../../../Abstract/Payload/Interfaces/DecryptedPayload'
import { SNTag } from '../../../Syncable/Tag'
interface ItemWithTagsContent extends ItemContent {
tags: SNTag[]
}
export class ItemWithTags extends DecryptedItem implements SearchableDecryptedItem {
constructor(
payload: DecryptedPayloadInterface<ItemWithTagsContent>,
private item: SearchableDecryptedItem,
public readonly tags?: SNTag[],
) {
super(payload)
this.tags = tags || payload.content.tags
}
static Create(payload: DecryptedPayloadInterface<ItemContent>, item: SearchableDecryptedItem, tags?: SNTag[]) {
return new ItemWithTags(payload as DecryptedPayloadInterface<ItemWithTagsContent>, item, tags)
}
get tagsCount(): number {
return this.tags?.length || 0
}
get title(): string | undefined {
return this.item.title
}
get text(): string | undefined {
return this.item.text
}
}

View File

@@ -0,0 +1,97 @@
import { ContentType } from '@standardnotes/common'
import { SNTag } from '../../../Syncable/Tag'
import { FilterDisplayOptions } from '../DisplayOptions'
import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters'
import { SearchableItem } from './SearchableItem'
import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types'
enum MatchResult {
None = 0,
Title = 1,
Text = 2,
TitleAndText = Title + Text,
Uuid = 5,
}
export function itemsMatchingOptions(
options: FilterDisplayOptions,
fromItems: SearchableDecryptedItem[],
collection: ReferenceLookupCollection,
): SearchableItem[] {
const filters = computeFiltersForDisplayOptions(options, collection)
return fromItems.filter((item) => {
return itemPassesFilters(item, filters)
})
}
export function itemPassesFilters(item: SearchableDecryptedItem, filters: ItemFilter[]) {
for (const filter of filters) {
if (!filter(item)) {
return false
}
}
return true
}
export function itemMatchesQuery(
itemToMatch: SearchableDecryptedItem,
searchQuery: SearchQuery,
collection: ReferenceLookupCollection,
): boolean {
const itemTags = collection.elementsReferencingElement(itemToMatch, ContentType.Tag) as SNTag[]
const someTagsMatches = itemTags.some((tag) => matchResultForStringQuery(tag, searchQuery.query) !== MatchResult.None)
if (itemToMatch.protected && !searchQuery.includeProtectedNoteText) {
const match = matchResultForStringQuery(itemToMatch, searchQuery.query)
return match === MatchResult.Title || match === MatchResult.TitleAndText || someTagsMatches
}
return matchResultForStringQuery(itemToMatch, searchQuery.query) !== MatchResult.None || someTagsMatches
}
function matchResultForStringQuery(item: SearchableItem, searchString: string): MatchResult {
if (searchString.length === 0) {
return MatchResult.TitleAndText
}
const title = item.title?.toLowerCase()
const text = item.text?.toLowerCase()
const lowercaseText = searchString.toLowerCase()
const words = lowercaseText.split(' ')
const quotedText = stringBetweenQuotes(lowercaseText)
if (quotedText) {
return (
(title?.includes(quotedText) ? MatchResult.Title : MatchResult.None) +
(text?.includes(quotedText) ? MatchResult.Text : MatchResult.None)
)
}
if (stringIsUuid(lowercaseText)) {
return item.uuid === lowercaseText ? MatchResult.Uuid : MatchResult.None
}
const matchesTitle =
title &&
words.every((word) => {
return title.indexOf(word) >= 0
})
const matchesBody =
text &&
words.every((word) => {
return text.indexOf(word) >= 0
})
return (matchesTitle ? MatchResult.Title : 0) + (matchesBody ? MatchResult.Text : 0)
}
function stringBetweenQuotes(text: string) {
const matches = text.match(/"(.*?)"/)
return matches ? matches[1] : null
}
function stringIsUuid(text: string) {
const matches = text.match(/\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b/)
return matches ? true : false
}

View File

@@ -0,0 +1,5 @@
export interface SearchableItem {
uuid: string
title?: string
text?: string
}

View File

@@ -0,0 +1,16 @@
import { ItemCollection } from './../../Collection/Item/ItemCollection'
import { DecryptedItemInterface } from '../../../Abstract/Item'
import { SearchableItem } from './SearchableItem'
export type SearchQuery = {
query: string
includeProtectedNoteText: boolean
}
export interface ReferenceLookupCollection {
elementsReferencingElement: ItemCollection['elementsReferencingElement']
}
export type SearchableDecryptedItem = SearchableItem & DecryptedItemInterface
export type ItemFilter = (item: SearchableDecryptedItem) => boolean

View File

@@ -0,0 +1,29 @@
import { SortLeftFirst, SortRightFirst, sortTwoItems } from './SortTwoItems'
import { createNoteWithContent } from '../../Utilities/Test/SpecUtils'
import { SNNote } from '../../Syncable/Note'
describe('sort two items', () => {
it('should sort correctly by dates', () => {
const noteA = createNoteWithContent({}, new Date(0))
const noteB = createNoteWithContent({}, new Date(1))
expect(sortTwoItems(noteA, noteB, 'created_at', 'asc')).toEqual(SortLeftFirst)
expect(sortTwoItems(noteA, noteB, 'created_at', 'dsc')).toEqual(SortRightFirst)
})
it('should sort by title', () => {
const noteA = createNoteWithContent({ title: 'a' })
const noteB = createNoteWithContent({ title: 'b' })
expect(sortTwoItems(noteA, noteB, 'title', 'asc')).toEqual(SortLeftFirst)
expect(sortTwoItems(noteA, noteB, 'title', 'dsc')).toEqual(SortRightFirst)
})
it('should sort correctly by title and pinned', () => {
const noteA = createNoteWithContent({ title: 'a' })
const noteB = { ...createNoteWithContent({ title: 'b' }), pinned: true } as jest.Mocked<SNNote>
expect(sortTwoItems(noteA, noteB, 'title', 'asc')).toEqual(SortRightFirst)
expect(sortTwoItems(noteA, noteB, 'title', 'dsc')).toEqual(SortRightFirst)
})
})

View File

@@ -0,0 +1,83 @@
import { isString } from '@standardnotes/utils'
import { CollectionSort, CollectionSortDirection, CollectionSortProperty } from '../Collection/CollectionSort'
import { DisplayItem } from './Types'
export const SortLeftFirst = -1
export const SortRightFirst = 1
export const KeepSameOrder = 0
/** @O(n * log(n)) */
export function sortTwoItems(
a: DisplayItem | undefined,
b: DisplayItem | undefined,
sortBy: CollectionSortProperty,
sortDirection: CollectionSortDirection,
bypassPinCheck = false,
): number {
/** If the elements are undefined, move to beginning */
if (!a) {
return SortLeftFirst
}
if (!b) {
return SortRightFirst
}
if (!bypassPinCheck) {
if (a.pinned && b.pinned) {
return sortTwoItems(a, b, sortBy, sortDirection, true)
}
if (a.pinned) {
return SortLeftFirst
}
if (b.pinned) {
return SortRightFirst
}
}
const aValue = a[sortBy] || ''
const bValue = b[sortBy] || ''
const smallerNaturallyComesFirst = sortDirection === 'asc'
let compareResult = KeepSameOrder
/**
* Check for string length due to issue on React Native 0.65.1
* where empty strings causes crash:
* https://github.com/facebook/react-native/issues/32174
* */
if (
sortBy === CollectionSort.Title &&
isString(aValue) &&
isString(bValue) &&
aValue.length > 0 &&
bValue.length > 0
) {
compareResult = aValue.localeCompare(bValue, 'en', { numeric: true })
} else if (aValue > bValue) {
compareResult = SortRightFirst
} else if (aValue < bValue) {
compareResult = SortLeftFirst
} else {
compareResult = KeepSameOrder
}
const isLeftSmaller = compareResult === SortLeftFirst
const isLeftBigger = compareResult === SortRightFirst
if (isLeftSmaller) {
if (smallerNaturallyComesFirst) {
return SortLeftFirst
} else {
return SortRightFirst
}
} else if (isLeftBigger) {
if (smallerNaturallyComesFirst) {
return SortRightFirst
} else {
return SortLeftFirst
}
} else {
return KeepSameOrder
}
}

View File

@@ -0,0 +1,13 @@
import { Uuid } from '@standardnotes/common'
import { DecryptedItemInterface } from '../../Abstract/Item'
import { SortableItem } from '../Collection/CollectionSort'
import { ItemCollection } from '../Collection/Item/ItemCollection'
export type DisplayControllerCustomFilter = (element: DisplayItem) => boolean
export type UuidToSortedPositionMap = Record<Uuid, number>
export type DisplayItem = SortableItem & DecryptedItemInterface
export interface ReadonlyItemCollection {
all: ItemCollection['all']
has: ItemCollection['has']
}

View File

@@ -0,0 +1,8 @@
export * from './DisplayOptions'
export * from './DisplayOptionsToFilters'
export * from './ItemDisplayController'
export * from './Search/ItemWithTags'
export * from './Search/SearchableItem'
export * from './Search/SearchUtilities'
export * from './Search/Types'
export * from './Types'