internal: incomplete vault systems behind feature flag (#2340)
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
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'
|
||||
import { notesAndFilesMatchingOptions } from './Search/SearchUtilities'
|
||||
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
|
||||
|
||||
describe('item display options', () => {
|
||||
const collectionWithNotes = function (titles: (string | undefined)[] = [], bodies: string[] = []) {
|
||||
@@ -23,31 +23,31 @@ describe('item display options', () => {
|
||||
it('string query title', () => {
|
||||
const query = 'foo'
|
||||
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(['hello', 'fobar', 'foobar', 'foo'])
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('string query text', async function () {
|
||||
const query = 'foo'
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(
|
||||
[undefined, undefined, undefined, undefined],
|
||||
['hello', 'fobar', 'foobar', 'foo'],
|
||||
)
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('string query title and text', async function () {
|
||||
const query = 'foo'
|
||||
const options: FilterDisplayOptions = {
|
||||
const options: NotesAndFilesDisplayOptions = {
|
||||
searchQuery: { query: query, includeProtectedNoteText: true },
|
||||
}
|
||||
} as jest.Mocked<NotesAndFilesDisplayOptions>
|
||||
const collection = collectionWithNotes(['hello', 'foobar'], ['foo', 'fobar'])
|
||||
expect(itemsMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
expect(notesAndFilesMatchingOptions(options, collection.all() as SNNote[], collection)).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,21 +5,28 @@ import { CollectionSortDirection, CollectionSortProperty } from '../Collection/C
|
||||
import { SearchQuery } from './Search/Types'
|
||||
import { DisplayControllerCustomFilter } from './Types'
|
||||
|
||||
export type DisplayOptions = FilterDisplayOptions & DisplayControllerOptions
|
||||
|
||||
export interface FilterDisplayOptions {
|
||||
tags?: SNTag[]
|
||||
views?: SmartView[]
|
||||
searchQuery?: SearchQuery
|
||||
export interface GenericDisplayOptions {
|
||||
includePinned?: boolean
|
||||
includeProtected?: boolean
|
||||
includeTrashed?: boolean
|
||||
includeArchived?: boolean
|
||||
}
|
||||
|
||||
export interface DisplayControllerOptions {
|
||||
sortBy: CollectionSortProperty
|
||||
sortDirection: CollectionSortDirection
|
||||
export interface NotesAndFilesDisplayOptions extends GenericDisplayOptions {
|
||||
tags?: SNTag[]
|
||||
views?: SmartView[]
|
||||
searchQuery?: SearchQuery
|
||||
hiddenContentTypes?: ContentType[]
|
||||
customFilter?: DisplayControllerCustomFilter
|
||||
}
|
||||
|
||||
export type TagsDisplayOptions = GenericDisplayOptions
|
||||
|
||||
export interface DisplayControllerDisplayOptions extends GenericDisplayOptions {
|
||||
sortBy: CollectionSortProperty
|
||||
sortDirection: CollectionSortDirection
|
||||
}
|
||||
|
||||
export type NotesAndFilesDisplayControllerOptions = NotesAndFilesDisplayOptions & DisplayControllerDisplayOptions
|
||||
export type TagsDisplayControllerOptions = TagsDisplayOptions & DisplayControllerDisplayOptions
|
||||
export type AnyDisplayOptions = NotesAndFilesDisplayOptions | TagsDisplayOptions | GenericDisplayOptions
|
||||
|
||||
@@ -5,11 +5,11 @@ 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'
|
||||
import { NotesAndFilesDisplayOptions } from './DisplayOptions'
|
||||
import { SystemViewId } from '../../Syncable/SmartView'
|
||||
|
||||
export function computeUnifiedFilterForDisplayOptions(
|
||||
options: FilterDisplayOptions,
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
collection: ReferenceLookupCollection,
|
||||
additionalFilters: ItemFilter[] = [],
|
||||
): ItemFilter {
|
||||
@@ -21,7 +21,7 @@ export function computeUnifiedFilterForDisplayOptions(
|
||||
}
|
||||
|
||||
export function computeFiltersForDisplayOptions(
|
||||
options: FilterDisplayOptions,
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
collection: ReferenceLookupCollection,
|
||||
): ItemFilter[] {
|
||||
const filters: ItemFilter[] = []
|
||||
|
||||
@@ -2,11 +2,19 @@ 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 { AnyDisplayOptions, DisplayControllerDisplayOptions, GenericDisplayOptions } from './DisplayOptions'
|
||||
import { sortTwoItems } from './SortTwoItems'
|
||||
import { UuidToSortedPositionMap, DisplayItem, ReadonlyItemCollection } from './Types'
|
||||
import { CriteriaValidatorInterface } from './Validator/CriteriaValidatorInterface'
|
||||
import { CollectionCriteriaValidator } from './Validator/CollectionCriteriaValidator'
|
||||
import { CustomFilterCriteriaValidator } from './Validator/CustomFilterCriteriaValidator'
|
||||
import { ExcludeVaultsCriteriaValidator } from './Validator/ExcludeVaultsCriteriaValidator'
|
||||
import { ExclusiveVaultCriteriaValidator } from './Validator/ExclusiveVaultCriteriaValidator'
|
||||
import { HiddenContentCriteriaValidator } from './Validator/HiddenContentCriteriaValidator'
|
||||
import { VaultDisplayOptions } from './VaultDisplayOptions'
|
||||
import { isExclusioanaryOptionsValue } from './VaultDisplayOptionsTypes'
|
||||
|
||||
export class ItemDisplayController<I extends DisplayItem> {
|
||||
export class ItemDisplayController<I extends DisplayItem, O extends AnyDisplayOptions = GenericDisplayOptions> {
|
||||
private sortMap: UuidToSortedPositionMap = {}
|
||||
private sortedItems: I[] = []
|
||||
private needsSort = true
|
||||
@@ -14,7 +22,8 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
constructor(
|
||||
private readonly collection: ReadonlyItemCollection,
|
||||
public readonly contentTypes: ContentType[],
|
||||
private options: DisplayControllerOptions,
|
||||
private options: DisplayControllerDisplayOptions & O,
|
||||
private vaultOptions?: VaultDisplayOptions,
|
||||
) {
|
||||
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
|
||||
}
|
||||
@@ -23,7 +32,18 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
return this.sortedItems
|
||||
}
|
||||
|
||||
setDisplayOptions(displayOptions: Partial<DisplayControllerOptions>): void {
|
||||
public getDisplayOptions(): DisplayControllerDisplayOptions & O {
|
||||
return this.options
|
||||
}
|
||||
|
||||
setVaultDisplayOptions(vaultOptions?: VaultDisplayOptions): void {
|
||||
this.vaultOptions = vaultOptions
|
||||
this.needsSort = true
|
||||
|
||||
this.filterThenSortElements(this.collection.all(this.contentTypes) as I[])
|
||||
}
|
||||
|
||||
setDisplayOptions(displayOptions: Partial<DisplayControllerDisplayOptions & O>): void {
|
||||
this.options = { ...this.options, ...displayOptions }
|
||||
this.needsSort = true
|
||||
|
||||
@@ -37,6 +57,29 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
this.filterThenSortElements(items as I[])
|
||||
}
|
||||
|
||||
private passesAllFilters(element: I): boolean {
|
||||
const filters: CriteriaValidatorInterface[] = [new CollectionCriteriaValidator(this.collection, element)]
|
||||
|
||||
if (this.vaultOptions) {
|
||||
const options = this.vaultOptions.getOptions()
|
||||
if (isExclusioanaryOptionsValue(options)) {
|
||||
filters.push(new ExcludeVaultsCriteriaValidator([...options.exclude, ...options.locked], element))
|
||||
} else {
|
||||
filters.push(new ExclusiveVaultCriteriaValidator(options.exclusive, element))
|
||||
}
|
||||
}
|
||||
|
||||
if ('hiddenContentTypes' in this.options && this.options.hiddenContentTypes) {
|
||||
filters.push(new HiddenContentCriteriaValidator(this.options.hiddenContentTypes, element))
|
||||
}
|
||||
|
||||
if ('customFilter' in this.options && this.options.customFilter) {
|
||||
filters.push(new CustomFilterCriteriaValidator(this.options.customFilter, element))
|
||||
}
|
||||
|
||||
return filters.every((f) => f.passes())
|
||||
}
|
||||
|
||||
private filterThenSortElements(elements: I[]): void {
|
||||
for (const element of elements) {
|
||||
const previousIndex = this.sortMap[element.uuid]
|
||||
@@ -61,13 +104,7 @@ export class ItemDisplayController<I extends DisplayItem> {
|
||||
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
|
||||
const passes = this.passesAllFilters(element)
|
||||
|
||||
if (passes) {
|
||||
if (previousElement != undefined) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { SNTag } from '../../../Syncable/Tag'
|
||||
import { FilterDisplayOptions } from '../DisplayOptions'
|
||||
import { NotesAndFilesDisplayOptions } from '../DisplayOptions'
|
||||
import { computeFiltersForDisplayOptions } from '../DisplayOptionsToFilters'
|
||||
import { SearchableItem } from './SearchableItem'
|
||||
import { ReferenceLookupCollection, ItemFilter, SearchQuery, SearchableDecryptedItem } from './Types'
|
||||
@@ -13,8 +13,8 @@ enum MatchResult {
|
||||
Uuid = 5,
|
||||
}
|
||||
|
||||
export function itemsMatchingOptions(
|
||||
options: FilterDisplayOptions,
|
||||
export function notesAndFilesMatchingOptions(
|
||||
options: NotesAndFilesDisplayOptions,
|
||||
fromItems: SearchableDecryptedItem[],
|
||||
collection: ReferenceLookupCollection,
|
||||
): SearchableItem[] {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ItemInterface } from '../../../Abstract/Item'
|
||||
import { ReadonlyItemCollection } from '../Types'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class CollectionCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private collection: ReadonlyItemCollection, private element: ItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.collection.has(this.element.uuid)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export interface CriteriaValidatorInterface {
|
||||
passes(): boolean
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { DisplayControllerCustomFilter } from '../Types'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class CustomFilterCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private customFilter: DisplayControllerCustomFilter, private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.customFilter(this.element)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export class ExcludeVaultsCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private excludeVaults: KeySystemIdentifier[], private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
const doesElementBelongToExcludedVault = this.excludeVaults.some(
|
||||
(vault) => this.element.key_system_identifier === vault,
|
||||
)
|
||||
|
||||
return !doesElementBelongToExcludedVault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
import { DecryptedItemInterface } from '../../../Abstract/Item'
|
||||
import { KeySystemIdentifier } from '../../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export class ExclusiveVaultCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private exclusiveVault: KeySystemIdentifier, private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return this.element.key_system_identifier === this.exclusiveVault
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { DecryptedItemInterface } from './../../../Abstract/Item/Interfaces/DecryptedItem'
|
||||
import { ContentType } from '@standardnotes/common'
|
||||
import { CriteriaValidatorInterface } from './CriteriaValidatorInterface'
|
||||
|
||||
export class HiddenContentCriteriaValidator implements CriteriaValidatorInterface {
|
||||
constructor(private hiddenContentTypes: ContentType[], private element: DecryptedItemInterface) {}
|
||||
|
||||
public passes(): boolean {
|
||||
return !this.hiddenContentTypes.includes(this.element.content_type)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { VaultListingInterface } from '../../Syncable/VaultListing/VaultListingInterface'
|
||||
import { uniqueArray } from '@standardnotes/utils'
|
||||
import {
|
||||
ExclusioanaryOptions,
|
||||
ExclusiveOptions,
|
||||
VaultDisplayOptionsPersistable,
|
||||
isExclusioanaryOptionsValue,
|
||||
} from './VaultDisplayOptionsTypes'
|
||||
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
function KeySystemIdentifiers(vaults: VaultListingInterface[]): KeySystemIdentifier[] {
|
||||
return vaults.map((vault) => vault.systemIdentifier)
|
||||
}
|
||||
|
||||
export class VaultDisplayOptions {
|
||||
constructor(private readonly options: ExclusioanaryOptions | ExclusiveOptions) {}
|
||||
|
||||
public getOptions(): ExclusioanaryOptions | ExclusiveOptions {
|
||||
return this.options
|
||||
}
|
||||
|
||||
public getExclusivelyShownVault(): KeySystemIdentifier {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
throw new Error('Not in exclusive display mode')
|
||||
}
|
||||
|
||||
return this.options.exclusive
|
||||
}
|
||||
|
||||
public isInExclusiveDisplayMode(): boolean {
|
||||
return !isExclusioanaryOptionsValue(this.options)
|
||||
}
|
||||
|
||||
public isVaultExplicitelyExcluded(vault: VaultListingInterface): boolean {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return this.options.exclude.some((excludedVault) => excludedVault === vault.systemIdentifier)
|
||||
} else if (this.options.exclusive) {
|
||||
return this.options.exclusive !== vault.systemIdentifier
|
||||
}
|
||||
|
||||
throw new Error('Invalid vault display options')
|
||||
}
|
||||
|
||||
isVaultExclusivelyShown(vault: VaultListingInterface): boolean {
|
||||
return !isExclusioanaryOptionsValue(this.options) && this.options.exclusive === vault.systemIdentifier
|
||||
}
|
||||
|
||||
isVaultDisabledOrLocked(vault: VaultListingInterface): boolean {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
const matchingLocked = this.options.locked.find((lockedVault) => lockedVault === vault.systemIdentifier)
|
||||
if (matchingLocked) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return this.isVaultExplicitelyExcluded(vault)
|
||||
}
|
||||
|
||||
getPersistableValue(): VaultDisplayOptionsPersistable {
|
||||
return this.options
|
||||
}
|
||||
|
||||
newOptionsByIntakingLockedVaults(lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({ exclude: this.options.exclude, locked: KeySystemIdentifiers(lockedVaults) })
|
||||
} else {
|
||||
return new VaultDisplayOptions({ exclusive: this.options.exclusive })
|
||||
}
|
||||
}
|
||||
|
||||
newOptionsByExcludingVault(vault: VaultListingInterface, lockedVaults: VaultListingInterface[]): VaultDisplayOptions {
|
||||
return this.newOptionsByExcludingVaults([vault], lockedVaults)
|
||||
}
|
||||
|
||||
newOptionsByExcludingVaults(
|
||||
vaults: VaultListingInterface[],
|
||||
lockedVaults: VaultListingInterface[],
|
||||
): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: uniqueArray([...this.options.exclude, ...KeySystemIdentifiers(vaults)]),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
} else {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: KeySystemIdentifiers(vaults),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
newOptionsByUnexcludingVault(
|
||||
vault: VaultListingInterface,
|
||||
lockedVaults: VaultListingInterface[],
|
||||
): VaultDisplayOptions {
|
||||
if (isExclusioanaryOptionsValue(this.options)) {
|
||||
return new VaultDisplayOptions({
|
||||
exclude: this.options.exclude.filter((excludedVault) => excludedVault !== vault.systemIdentifier),
|
||||
locked: KeySystemIdentifiers(lockedVaults),
|
||||
})
|
||||
} else {
|
||||
return new VaultDisplayOptions({ exclude: [], locked: KeySystemIdentifiers(lockedVaults) })
|
||||
}
|
||||
}
|
||||
|
||||
static FromPersistableValue(value: VaultDisplayOptionsPersistable): VaultDisplayOptions {
|
||||
return new VaultDisplayOptions(value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { KeySystemIdentifier } from '../../Syncable/KeySystemRootKey/KeySystemIdentifier'
|
||||
|
||||
export type ExclusioanaryOptions = { exclude: KeySystemIdentifier[]; locked: KeySystemIdentifier[] }
|
||||
export type ExclusiveOptions = { exclusive: KeySystemIdentifier }
|
||||
|
||||
export function isExclusioanaryOptionsValue(
|
||||
options: ExclusioanaryOptions | ExclusiveOptions,
|
||||
): options is ExclusioanaryOptions {
|
||||
return 'exclude' in options || 'locked' in options
|
||||
}
|
||||
|
||||
export type VaultDisplayOptionsPersistable = ExclusioanaryOptions | ExclusiveOptions
|
||||
@@ -6,3 +6,5 @@ export * from './Search/SearchableItem'
|
||||
export * from './Search/SearchUtilities'
|
||||
export * from './Search/Types'
|
||||
export * from './Types'
|
||||
export * from './VaultDisplayOptions'
|
||||
export * from './VaultDisplayOptionsTypes'
|
||||
|
||||
Reference in New Issue
Block a user