refactor: blocks plugins (#1956)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
63
packages/web/src/javascripts/Utils/Items/Icons/Icons.spec.ts
Normal file
63
packages/web/src/javascripts/Utils/Items/Icons/Icons.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { LinkableItem } from './LinkableItem'
|
||||
|
||||
export type ItemLink<ItemType extends LinkableItem = LinkableItem> = {
|
||||
id: string
|
||||
item: ItemType
|
||||
type: 'linked' | 'linked-by'
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { DecryptedItemInterface, ItemContent } from '@standardnotes/snjs'
|
||||
|
||||
export type LinkableItem = DecryptedItemInterface<ItemContent>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user