internal: incomplete vault systems behind feature flag (#2340)

This commit is contained in:
Mo
2023-06-30 09:01:56 -05:00
committed by GitHub
parent d16e401bb9
commit b032eb9c9b
638 changed files with 20321 additions and 4813 deletions

View File

@@ -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)
})
})

View File

@@ -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

View File

@@ -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[] = []

View File

@@ -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) {

View File

@@ -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[] {

View File

@@ -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)
}
}

View File

@@ -0,0 +1,3 @@
export interface CriteriaValidatorInterface {
passes(): boolean
}

View File

@@ -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)
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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'