refactor: blocks plugins (#1956)

This commit is contained in:
Mo
2022-11-08 13:31:48 -06:00
committed by GitHub
parent bfca244061
commit 70a9dbcea6
73 changed files with 1448 additions and 1049 deletions

View File

@@ -3,6 +3,7 @@ import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplication } from '@/Application/Application'
import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
import { getIconAndTintForNoteType } from './Items/Icons/getIconAndTintForNoteType'
export const PlainEditorType = 'plain-editor'
export const BlocksType = 'blocks-editor'
@@ -21,7 +22,7 @@ export function getDropdownItemsForAllEditors(application: WebApplication) {
const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => {
const identifier = editor.package_info.identifier
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
const [iconType, tint] = getIconAndTintForNoteType(editor.package_info.note_type)
return {
label: editor.displayName,

View File

@@ -0,0 +1,23 @@
import { DecryptedItem, SNTag, WebApplicationInterface } from '@standardnotes/snjs'
type ReturnType =
| {
titlePrefix: string | undefined
longTitle: string
}
| undefined
export function getTitleForLinkedTag(item: DecryptedItem, application: WebApplicationInterface): ReturnType {
const isTag = item instanceof SNTag
if (!isTag) {
return
}
const titlePrefix = application.items.getTagPrefixTitle(item)
const longTitle = application.items.getTagLongTitle(item)
return {
titlePrefix,
longTitle,
}
}

View File

@@ -0,0 +1,63 @@
import { getIconForFileType } from "./getIconForFileType"
describe('icons utils', () => {
describe('getIconForFileType', () => {
it('should return correct icon type for supported mimetypes', () => {
const iconTypeForPdf = getIconForFileType('application/pdf')
expect(iconTypeForPdf).toBe('file-pdf')
const iconTypeForDoc = getIconForFileType('application/msword')
const iconTypeForDocx = getIconForFileType(
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
)
expect(iconTypeForDoc).toBe('file-doc')
expect(iconTypeForDocx).toBe('file-doc')
const iconTypeForPpt = getIconForFileType('application/vnd.ms-powerpoint')
const iconTypeForPptx = getIconForFileType(
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
)
expect(iconTypeForPpt).toBe('file-ppt')
expect(iconTypeForPptx).toBe('file-ppt')
const iconTypeForXls = getIconForFileType('application/vnd.ms-excel')
const iconTypeForXlsx = getIconForFileType(
'application/vnd.openxmlformats-officedocument.spreadsheetml.spreadsheet',
)
expect(iconTypeForXls).toBe('file-xls')
expect(iconTypeForXlsx).toBe('file-xls')
const iconTypeForJpg = getIconForFileType('image/jpeg')
const iconTypeForPng = getIconForFileType('image/png')
expect(iconTypeForJpg).toBe('file-image')
expect(iconTypeForPng).toBe('file-image')
const iconTypeForMpeg = getIconForFileType('video/mpeg')
const iconTypeForMp4 = getIconForFileType('video/mp4')
expect(iconTypeForMpeg).toBe('file-mov')
expect(iconTypeForMp4).toBe('file-mov')
const iconTypeForWav = getIconForFileType('audio/wav')
const iconTypeForMp3 = getIconForFileType('audio/mp3')
expect(iconTypeForWav).toBe('file-music')
expect(iconTypeForMp3).toBe('file-music')
const iconTypeForZip = getIconForFileType('application/zip')
const iconTypeForRar = getIconForFileType('application/vnd.rar')
const iconTypeForTar = getIconForFileType('application/x-tar')
const iconTypeFor7z = getIconForFileType('application/x-7z-compressed')
expect(iconTypeForZip).toBe('file-zip')
expect(iconTypeForRar).toBe('file-zip')
expect(iconTypeForTar).toBe('file-zip')
expect(iconTypeFor7z).toBe('file-zip')
})
it('should return fallback icon type for unsupported mimetypes', () => {
const iconForBin = getIconForFileType('application/octet-stream')
expect(iconForBin).toBe('file-other')
const iconForNoType = getIconForFileType('')
expect(iconForNoType).toBe('file-other')
})
})
})

View File

@@ -0,0 +1,21 @@
import { NoteType } from '@standardnotes/features'
import { IconType } from '@standardnotes/models'
export function getIconAndTintForNoteType(noteType?: NoteType): [IconType, number] {
switch (noteType) {
case NoteType.RichText:
return ['rich-text', 1]
case NoteType.Markdown:
return ['markdown', 2]
case NoteType.Authentication:
return ['authenticator', 6]
case NoteType.Spreadsheet:
return ['spreadsheets', 5]
case NoteType.Task:
return ['tasks', 3]
case NoteType.Code:
return ['code', 4]
default:
return ['plain-text', 1]
}
}

View File

@@ -0,0 +1,39 @@
import { IconType } from '@standardnotes/models'
export function getIconForFileType(type: string): IconType {
let iconType: IconType = 'file-other'
if (type === 'application/pdf') {
iconType = 'file-pdf'
}
if (/word/.test(type)) {
iconType = 'file-doc'
}
if (/powerpoint|presentation/.test(type)) {
iconType = 'file-ppt'
}
if (/excel|spreadsheet/.test(type)) {
iconType = 'file-xls'
}
if (/^image\//.test(type)) {
iconType = 'file-image'
}
if (/^video\//.test(type)) {
iconType = 'file-mov'
}
if (/^audio\//.test(type)) {
iconType = 'file-music'
}
if (/(zip)|([tr]ar)|(7z)/.test(type)) {
iconType = 'file-zip'
}
return iconType
}

View File

@@ -0,0 +1,19 @@
import { IconType, FileItem, SNNote, DecryptedItem, SNTag, WebApplicationInterface } from '@standardnotes/snjs'
import { getIconAndTintForNoteType } from './getIconAndTintForNoteType'
import { getIconForFileType } from './getIconForFileType'
export function getIconForItem(item: DecryptedItem, application: WebApplicationInterface): [IconType, string] {
if (item instanceof SNNote) {
const editorForNote = application.componentManager.editorForNote(item)
const [icon, tint] = getIconAndTintForNoteType(editorForNote?.package_info.note_type)
const className = `text-accessory-tint-${tint}`
return [icon, className]
} else if (item instanceof FileItem) {
const icon = getIconForFileType(item.mimeType)
return [icon, 'text-info']
} else if (item instanceof SNTag) {
return [item.iconString as IconType, 'text-info']
}
throw new Error('Unhandled case in getItemIcon')
}

View File

@@ -0,0 +1,7 @@
import { LinkableItem } from './LinkableItem'
export type ItemLink<ItemType extends LinkableItem = LinkableItem> = {
id: string
item: ItemType
type: 'linked' | 'linked-by'
}

View File

@@ -0,0 +1,3 @@
import { DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
export type LinkableItem = DecryptedItemInterface<ItemContent>

View File

@@ -0,0 +1,13 @@
import { ItemLink } from './ItemLink'
import { LinkableItem } from './LinkableItem'
export function createLinkFromItem<I extends LinkableItem = LinkableItem>(
itemA: I,
type: 'linked' | 'linked-by',
): ItemLink<I> {
return {
id: `${itemA.uuid}-${type}`,
item: itemA,
type,
}
}

View File

@@ -0,0 +1,14 @@
import { SNTag, WebApplicationInterface, DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
export function doesItemMatchSearchQuery(
item: DecryptedItemInterface<ItemContent>,
searchQuery: string,
application: WebApplicationInterface,
) {
const title = item instanceof SNTag ? application.items.getTagLongTitle(item) : item.title ?? ''
const matchesQuery = title.toLowerCase().includes(searchQuery.toLowerCase())
const isArchivedOrTrashed = item.archived || item.trashed
const isValidSearchResult = matchesQuery && !isArchivedOrTrashed
return isValidSearchResult
}

View File

@@ -0,0 +1,118 @@
import { WebApplicationInterface, naturalSort, ContentType } from '@standardnotes/snjs'
import { createLinkFromItem } from './createLinkFromItem'
import { doesItemMatchSearchQuery } from './doesItemMatchSearchQuery'
import { isSearchResultAlreadyLinkedToItem } from './isSearchResultAlreadyLinkedToItem'
import { isSearchResultExistingTag } from './isSearchResultExistingTag'
import { ItemLink } from './ItemLink'
import { LinkableItem } from './LinkableItem'
const ResultLimitPerContentType = 5
const MaxLinkedResults = 20
export function getLinkingSearchResults(
searchQuery: string,
application: WebApplicationInterface,
activeItem?: LinkableItem,
options: {
contentType?: ContentType
returnEmptyIfQueryEmpty?: boolean
} = { returnEmptyIfQueryEmpty: true },
): {
linkedResults: ItemLink<LinkableItem>[]
linkedItems: LinkableItem[]
unlinkedItems: LinkableItem[]
shouldShowCreateTag: boolean
} {
let unlinkedItems: LinkableItem[] = []
const linkedItems: LinkableItem[] = []
const linkedResults: ItemLink<LinkableItem>[] = []
let shouldShowCreateTag = false
const defaultReturnValue = {
linkedResults,
unlinkedItems,
linkedItems,
shouldShowCreateTag,
}
if (!activeItem) {
return defaultReturnValue
}
if (!searchQuery.length && options.returnEmptyIfQueryEmpty) {
return defaultReturnValue
}
const searchableItems = naturalSort(
application.items.getItems([ContentType.Note, ContentType.File, ContentType.Tag]),
'title',
)
const unlinkedTags: LinkableItem[] = []
const unlinkedNotes: LinkableItem[] = []
const unlinkedFiles: LinkableItem[] = []
for (let index = 0; index < searchableItems.length; index++) {
const item = searchableItems[index]
if (activeItem.uuid === item.uuid) {
continue
}
if (options.contentType && item.content_type !== options.contentType) {
continue
}
if (searchQuery.length && !doesItemMatchSearchQuery(item, searchQuery, application)) {
continue
}
if (isSearchResultAlreadyLinkedToItem(item, activeItem)) {
if (linkedResults.length < MaxLinkedResults) {
linkedResults.push(createLinkFromItem(item, 'linked'))
}
linkedItems.push(item)
continue
}
const enforceResultLimit = options.contentType == null
if (
item.content_type === ContentType.Tag &&
(!enforceResultLimit ||
(unlinkedTags.length < ResultLimitPerContentType && item.content_type === ContentType.Tag))
) {
unlinkedTags.push(item)
continue
}
if (
item.content_type === ContentType.Note &&
(!enforceResultLimit || unlinkedNotes.length < ResultLimitPerContentType)
) {
unlinkedNotes.push(item)
continue
}
if (
item.content_type === ContentType.File &&
(!enforceResultLimit || unlinkedFiles.length < ResultLimitPerContentType)
) {
unlinkedFiles.push(item)
continue
}
}
unlinkedItems = [...unlinkedTags, ...unlinkedNotes, ...unlinkedFiles]
shouldShowCreateTag =
!linkedResults.find((link) => isSearchResultExistingTag(link.item, searchQuery)) &&
!unlinkedItems.find((item) => isSearchResultExistingTag(item, searchQuery))
return {
linkedResults,
linkedItems,
unlinkedItems,
shouldShowCreateTag,
}
}

View File

@@ -0,0 +1,16 @@
import { LinkableItem } from './LinkableItem'
export function isSearchResultAlreadyLinkedToItem(searchResult: LinkableItem, item: LinkableItem): boolean {
let isAlreadyLinked = false
const isItemReferencedByActiveItem = item.references.some((ref) => ref.uuid === searchResult.uuid)
const isActiveItemReferencedByItem = searchResult.references.some((ref) => ref.uuid === item?.uuid)
if (item.content_type === searchResult.content_type) {
isAlreadyLinked = isItemReferencedByActiveItem
} else {
isAlreadyLinked = isActiveItemReferencedByItem || isItemReferencedByActiveItem
}
return isAlreadyLinked
}

View File

@@ -0,0 +1,5 @@
import { DecryptedItemInterface, ItemContent, ContentType } from '@standardnotes/snjs'
export function isSearchResultExistingTag(result: DecryptedItemInterface<ItemContent>, searchQuery: string) {
return result.content_type === ContentType.Tag && result.title === searchQuery
}