feat: item linking (#1779)

This commit is contained in:
Aman Harwara
2022-10-11 23:54:00 +05:30
committed by GitHub
parent d22c164e5d
commit e3f28421ff
68 changed files with 2064 additions and 1277 deletions

View File

@@ -1,8 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReferenceType } from './ContenteReferenceType'
export interface AnonymousReference {
uuid: string
content_type: ContentType
reference_type: ContenteReferenceType
reference_type: ContentReferenceType
}

View File

@@ -1,5 +1,7 @@
export enum ContenteReferenceType {
export enum ContentReferenceType {
TagToParentTag = 'TagToParentTag',
FileToNote = 'FileToNote',
TagToFile = 'TagToFile',
FileToNote = 'FileToNote',
FileToFile = 'FileToFile',
NoteToNote = 'NoteToNote',
}

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContentReferenceType } from './ContenteReferenceType'
export interface FileToFileReference extends AnonymousReference {
content_type: ContentType.File
reference_type: ContentReferenceType.FileToFile
}

View File

@@ -1,8 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReferenceType } from './ContenteReferenceType'
export interface FileToNoteReference extends AnonymousReference {
content_type: ContentType.Note
reference_type: ContenteReferenceType.FileToNote
reference_type: ContentReferenceType.FileToNote
}

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { ContentType } from '@standardnotes/common'
import { ItemInterface } from '../Item/Interfaces/ItemInterface'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReferenceType } from './ContenteReferenceType'
import { ContentReference } from './ContentReference'
import { LegacyAnonymousReference } from './LegacyAnonymousReference'
import { LegacyTagToNoteReference } from './LegacyTagToNoteReference'
@@ -26,5 +26,5 @@ export const isLegacyTagToNoteReference = (
}
export const isTagToParentTagReference = (x: ContentReference): x is TagToParentTagReference => {
return isReference(x) && x.reference_type === ContenteReferenceType.TagToParentTag
return isReference(x) && x.reference_type === ContentReferenceType.TagToParentTag
}

View File

@@ -0,0 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContentReferenceType } from './ContenteReferenceType'
export interface NoteToNoteReference extends AnonymousReference {
content_type: ContentType.Note
reference_type: ContentReferenceType.NoteToNote
}

View File

@@ -1,8 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReferenceType } from './ContenteReferenceType'
export interface TagToFileReference extends AnonymousReference {
content_type: ContentType.File
reference_type: ContenteReferenceType.TagToFile
reference_type: ContentReferenceType.TagToFile
}

View File

@@ -1,8 +1,8 @@
import { ContentType } from '@standardnotes/common'
import { AnonymousReference } from './AnonymousReference'
import { ContenteReferenceType } from './ContenteReferenceType'
import { ContentReferenceType } from './ContenteReferenceType'
export interface TagToParentTagReference extends AnonymousReference {
content_type: ContentType.Tag
reference_type: ContenteReferenceType.TagToParentTag
reference_type: ContentReferenceType.TagToParentTag
}

View File

@@ -1,10 +1,10 @@
import { NoteContent } from './../../../Syncable/Note/NoteContent'
import { NoteContent } from '../../../Syncable/Note/NoteContent'
import { ContentType } from '@standardnotes/common'
import { DecryptedItem, EncryptedItem } from '../../../Abstract/Item'
import { DecryptedPayload, EncryptedPayload, PayloadTimestampDefaults } from '../../../Abstract/Payload'
import { ItemCollection } from './ItemCollection'
import { FillItemContent } from '../../../Abstract/Content/ItemContent'
import { TagNotesIndex } from './TagNotesIndex'
import { TagItemsIndex } from './TagItemsIndex'
import { ItemDelta } from '../../Index/ItemDelta'
import { AnyItemInterface } from '../../../Abstract/Item/Interfaces/UnionTypes'
@@ -24,10 +24,10 @@ describe('tag notes index', () => {
return new EncryptedItem(payload)
}
const createDecryptedItem = (uuid?: string) => {
const createDecryptedItem = (uuid?: string, content_type = ContentType.Note) => {
const payload = new DecryptedPayload({
uuid: uuid || String(Math.random()),
content_type: ContentType.Note,
content_type,
content: FillItemContent<NoteContent>({
title: 'foo',
}),
@@ -46,20 +46,33 @@ describe('tag notes index', () => {
}
}
it('should count both notes and files', () => {
const collection = new ItemCollection()
const index = new TagItemsIndex(collection)
const decryptedNote = createDecryptedItem('note')
const decryptedFile = createDecryptedItem('file')
collection.set([decryptedNote, decryptedFile])
index.onChange(createChangeDelta(decryptedNote))
index.onChange(createChangeDelta(decryptedFile))
expect(index.allCountableItemsCount()).toEqual(2)
})
it('should decrement count after decrypted note becomes errored', () => {
const collection = new ItemCollection()
const index = new TagNotesIndex(collection)
const index = new TagItemsIndex(collection)
const decryptedItem = createDecryptedItem()
collection.set(decryptedItem)
index.onChange(createChangeDelta(decryptedItem))
expect(index.allCountableNotesCount()).toEqual(1)
expect(index.allCountableItemsCount()).toEqual(1)
const encryptedItem = createEncryptedItem(decryptedItem.uuid)
collection.set(encryptedItem)
index.onChange(createChangeDelta(encryptedItem))
expect(index.allCountableNotesCount()).toEqual(0)
expect(index.allCountableItemsCount()).toEqual(0)
})
})

View File

@@ -7,22 +7,22 @@ import { ItemDelta } from '../../Index/ItemDelta'
import { isDecryptedItem, ItemInterface } from '../../../Abstract/Item'
type AllNotesUuidSignifier = undefined
export type TagNoteCountChangeObserver = (tagUuid: Uuid | AllNotesUuidSignifier) => void
export type TagItemCountChangeObserver = (tagUuid: Uuid | AllNotesUuidSignifier) => void
export class TagNotesIndex implements SNIndex {
private tagToNotesMap: Partial<Record<Uuid, Set<Uuid>>> = {}
private allCountableNotes = new Set<Uuid>()
export class TagItemsIndex implements SNIndex {
private tagToItemsMap: Partial<Record<Uuid, Set<Uuid>>> = {}
private allCountableItems = new Set<Uuid>()
constructor(private collection: ItemCollection, public observers: TagNoteCountChangeObserver[] = []) {}
constructor(private collection: ItemCollection, public observers: TagItemCountChangeObserver[] = []) {}
private isNoteCountable = (note: ItemInterface) => {
if (isDecryptedItem(note)) {
return !note.archived && !note.trashed
private isItemCountable = (item: ItemInterface) => {
if (isDecryptedItem(item)) {
return !item.archived && !item.trashed
}
return false
}
public addCountChangeObserver(observer: TagNoteCountChangeObserver): () => void {
public addCountChangeObserver(observer: TagItemCountChangeObserver): () => void {
this.observers.push(observer)
const thislessEventObservers = this.observers
@@ -37,30 +37,32 @@ export class TagNotesIndex implements SNIndex {
}
}
public allCountableNotesCount(): number {
return this.allCountableNotes.size
public allCountableItemsCount(): number {
return this.allCountableItems.size
}
public countableNotesForTag(tag: SNTag): number {
return this.tagToNotesMap[tag.uuid]?.size || 0
public countableItemsForTag(tag: SNTag): number {
return this.tagToItemsMap[tag.uuid]?.size || 0
}
public onChange(delta: ItemDelta): void {
const notes = [...delta.changed, ...delta.inserted, ...delta.discarded].filter(
(i) => i.content_type === ContentType.Note,
const items = [...delta.changed, ...delta.inserted, ...delta.discarded].filter(
(i) => i.content_type === ContentType.Note || i.content_type === ContentType.File,
)
const tags = [...delta.changed, ...delta.inserted].filter(isDecryptedItem).filter(isTag)
this.receiveNoteChanges(notes)
this.receiveItemChanges(items)
this.receiveTagChanges(tags)
}
private receiveTagChanges(tags: SNTag[]): void {
for (const tag of tags) {
const uuids = tag.noteReferences.map((ref) => ref.uuid)
const countableUuids = uuids.filter((uuid) => this.allCountableNotes.has(uuid))
const previousSet = this.tagToNotesMap[tag.uuid]
this.tagToNotesMap[tag.uuid] = new Set(countableUuids)
const uuids = tag.references
.filter((ref) => ref.content_type === ContentType.Note || ref.content_type === ContentType.File)
.map((ref) => ref.uuid)
const countableUuids = uuids.filter((uuid) => this.allCountableItems.has(uuid))
const previousSet = this.tagToItemsMap[tag.uuid]
this.tagToItemsMap[tag.uuid] = new Set(countableUuids)
if (previousSet?.size !== countableUuids.length) {
this.notifyObservers(tag.uuid)
@@ -68,26 +70,26 @@ export class TagNotesIndex implements SNIndex {
}
}
private receiveNoteChanges(notes: ItemInterface[]): void {
const previousAllCount = this.allCountableNotes.size
private receiveItemChanges(items: ItemInterface[]): void {
const previousAllCount = this.allCountableItems.size
for (const note of notes) {
const isCountable = this.isNoteCountable(note)
for (const item of items) {
const isCountable = this.isItemCountable(item)
if (isCountable) {
this.allCountableNotes.add(note.uuid)
this.allCountableItems.add(item.uuid)
} else {
this.allCountableNotes.delete(note.uuid)
this.allCountableItems.delete(item.uuid)
}
const associatedTagUuids = this.collection.uuidsThatReferenceUuid(note.uuid)
const associatedTagUuids = this.collection.uuidsThatReferenceUuid(item.uuid)
for (const tagUuid of associatedTagUuids) {
const set = this.setForTag(tagUuid)
const previousCount = set.size
if (isCountable) {
set.add(note.uuid)
set.add(item.uuid)
} else {
set.delete(note.uuid)
set.delete(item.uuid)
}
if (previousCount !== set.size) {
this.notifyObservers(tagUuid)
@@ -95,16 +97,16 @@ export class TagNotesIndex implements SNIndex {
}
}
if (previousAllCount !== this.allCountableNotes.size) {
if (previousAllCount !== this.allCountableItems.size) {
this.notifyObservers(undefined)
}
}
private setForTag(uuid: Uuid): Set<Uuid> {
let set = this.tagToNotesMap[uuid]
let set = this.tagToItemsMap[uuid]
if (!set) {
set = new Set()
this.tagToNotesMap[uuid] = set
this.tagToItemsMap[uuid] = set
}
return set
}

View File

@@ -1,9 +1,10 @@
import { ContentType } from '@standardnotes/common'
import { SNNote } from '../Note/Note'
import { FileContent } from './File'
import { FileContent, FileItem } from './File'
import { FileToNoteReference } from '../../Abstract/Reference/FileToNoteReference'
import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { ContentReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { FileToFileReference } from '../../Abstract/Reference/FileToFileReference'
export class FileMutator extends DecryptedItemMutator<FileContent> {
set name(newName: string) {
@@ -16,7 +17,7 @@ export class FileMutator extends DecryptedItemMutator<FileContent> {
public addNote(note: SNNote): void {
const reference: FileToNoteReference = {
reference_type: ContenteReferenceType.FileToNote,
reference_type: ContentReferenceType.FileToNote,
content_type: ContentType.Note,
uuid: note.uuid,
}
@@ -30,4 +31,22 @@ export class FileMutator extends DecryptedItemMutator<FileContent> {
const references = this.immutableItem.references.filter((ref) => ref.uuid !== note.uuid)
this.mutableContent.references = references
}
public addFile(file: FileItem): void {
if (this.immutableItem.isReferencingItem(file)) {
return
}
const reference: FileToFileReference = {
uuid: file.uuid,
content_type: ContentType.File,
reference_type: ContentReferenceType.FileToFile,
}
this.mutableContent.references.push(reference)
}
public removeFile(file: FileItem): void {
this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== file.uuid)
}
}

View File

@@ -1,6 +1,10 @@
import { AppDataField } from '../../Abstract/Item/Types/AppDataField'
import { NoteContent } from './NoteContent'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { SNNote } from './Note'
import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReference'
import { ContentType } from '@standardnotes/common'
import { ContentReferenceType } from '../../Abstract/Item'
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
set title(title: string) {
@@ -38,4 +42,22 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
this.mutableContent.spellcheck = !this.mutableContent.spellcheck
}
}
public addNote(note: SNNote): void {
if (this.immutableItem.isReferencingItem(note)) {
return
}
const reference: NoteToNoteReference = {
uuid: note.uuid,
content_type: ContentType.Note,
reference_type: ContentReferenceType.NoteToNote,
}
this.mutableContent.references.push(reference)
}
public removeNote(note: SNNote): void {
this.mutableContent.references = this.mutableContent.references.filter((r) => r.uuid !== note.uuid)
}
}

View File

@@ -1,5 +1,5 @@
import { ContentType } from '@standardnotes/common'
import { ContenteReferenceType, MutationType } from '../../Abstract/Item'
import { ContentReferenceType, MutationType } from '../../Abstract/Item'
import { createFile, createTag } from '../../Utilities/Test/SpecUtils'
import { SNTag } from './Tag'
import { TagMutator } from './TagMutator'
@@ -16,7 +16,7 @@ describe('tag mutator', () => {
expect(result.content.references[0]).toEqual({
uuid: file.uuid,
content_type: ContentType.File,
reference_type: ContenteReferenceType.TagToFile,
reference_type: ContentReferenceType.TagToFile,
})
})

View File

@@ -4,7 +4,7 @@ import { FileItem } from '../File'
import { SNNote } from '../Note'
import { isTagToParentTagReference } from '../../Abstract/Reference/Functions'
import { TagToParentTagReference } from '../../Abstract/Reference/TagToParentTagReference'
import { ContenteReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { ContentReferenceType } from '../../Abstract/Reference/ContenteReferenceType'
import { DecryptedItemMutator } from '../../Abstract/Item/Mutator/DecryptedItemMutator'
import { TagToFileReference } from '../../Abstract/Reference/TagToFileReference'
@@ -21,7 +21,7 @@ export class TagMutator extends DecryptedItemMutator<TagContent> {
const references = this.immutableItem.references.filter((ref) => !isTagToParentTagReference(ref))
const reference: TagToParentTagReference = {
reference_type: ContenteReferenceType.TagToParentTag,
reference_type: ContentReferenceType.TagToParentTag,
content_type: ContentType.Tag,
uuid: tag.uuid,
}
@@ -41,7 +41,7 @@ export class TagMutator extends DecryptedItemMutator<TagContent> {
}
const reference: TagToFileReference = {
reference_type: ContenteReferenceType.TagToFile,
reference_type: ContentReferenceType.TagToFile,
content_type: ContentType.File,
uuid: file.uuid,
}

View File

@@ -38,7 +38,7 @@ export * from './Local/RootKey/RootKeyContent'
export * from './Local/RootKey/RootKeyInterface'
export * from './Runtime/Collection/CollectionSort'
export * from './Runtime/Collection/Item/ItemCollection'
export * from './Runtime/Collection/Item/TagNotesIndex'
export * from './Runtime/Collection/Item/TagItemsIndex'
export * from './Runtime/Collection/Payload/ImmutablePayloadCollection'
export * from './Runtime/Collection/Payload/PayloadCollection'
export * from './Runtime/Deltas'