refactor: block storage (#1952)
This commit is contained in:
@@ -38,7 +38,7 @@ module.exports = {
|
|||||||
'no-constructor-return': 'error',
|
'no-constructor-return': 'error',
|
||||||
'no-duplicate-imports': 'error',
|
'no-duplicate-imports': 'error',
|
||||||
'no-self-compare': 'error',
|
'no-self-compare': 'error',
|
||||||
'no-console': ['error', { allow: ['warn', 'error'] }],
|
'no-console': ['warn', { allow: ['warn', 'error'] }],
|
||||||
'no-unmodified-loop-condition': 'error',
|
'no-unmodified-loop-condition': 'error',
|
||||||
'no-unused-private-class-members': 'error',
|
'no-unused-private-class-members': 'error',
|
||||||
'object-curly-spacing': ['error', 'always'],
|
'object-curly-spacing': ['error', 'always'],
|
||||||
|
|||||||
@@ -7,4 +7,5 @@ export enum NoteType {
|
|||||||
Task = 'task',
|
Task = 'task',
|
||||||
Plain = 'plain-text',
|
Plain = 'plain-text',
|
||||||
Blocks = 'blocks',
|
Blocks = 'blocks',
|
||||||
|
Unknown = 'unknown',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { NoteType } from '@standardnotes/features'
|
||||||
import { createNote } from './../../Utilities/Test/SpecUtils'
|
import { createNote } from './../../Utilities/Test/SpecUtils'
|
||||||
|
|
||||||
describe('SNNote Tests', () => {
|
describe('SNNote Tests', () => {
|
||||||
@@ -34,4 +35,61 @@ describe('SNNote Tests', () => {
|
|||||||
|
|
||||||
expect(note.noteType).toBe(undefined)
|
expect(note.noteType).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should getBlock', () => {
|
||||||
|
const note = createNote({
|
||||||
|
text: 'some text',
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
type: NoteType.Authentication,
|
||||||
|
editorIdentifier: '456',
|
||||||
|
content: 'foo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(note.getBlock('123')).toStrictEqual({
|
||||||
|
id: '123',
|
||||||
|
type: NoteType.Authentication,
|
||||||
|
editorIdentifier: '456',
|
||||||
|
content: 'foo',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should getBlock with no blocks', () => {
|
||||||
|
const note = createNote({
|
||||||
|
text: 'some text',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(note.getBlock('123')).toBe(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should getBlock with no blocksItem', () => {
|
||||||
|
const note = createNote({
|
||||||
|
text: 'some text',
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(note.getBlock('123')).toBe(undefined)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should get indexOfBlock', () => {
|
||||||
|
const note = createNote({
|
||||||
|
text: 'some text',
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
id: '123',
|
||||||
|
type: NoteType.Authentication,
|
||||||
|
editorIdentifier: '456',
|
||||||
|
content: 'foo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(note.indexOfBlock({ id: '123' })).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,4 +48,17 @@ export class SNNote extends DecryptedItem<NoteContent> implements NoteContentSpe
|
|||||||
getBlock(id: string): NoteBlock | undefined {
|
getBlock(id: string): NoteBlock | undefined {
|
||||||
return this.blocksItem?.blocks.find((block) => block.id === id)
|
return this.blocksItem?.blocks.find((block) => block.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
indexOfBlock(block: { id: string }): number | undefined {
|
||||||
|
if (!this.blocksItem) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = this.blocksItem.blocks.findIndex((b) => b.id === block.id)
|
||||||
|
if (index === -1) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,41 +4,10 @@ export type NoteBlock = {
|
|||||||
id: string
|
id: string
|
||||||
type: NoteType
|
type: NoteType
|
||||||
editorIdentifier: string
|
editorIdentifier: string
|
||||||
size?: { width: number; height: number }
|
|
||||||
content: string
|
content: string
|
||||||
|
size?: { width: number; height: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NoteBlocks {
|
export interface NoteBlocks {
|
||||||
blocks: NoteBlock[]
|
blocks: NoteBlock[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function bracketSyntaxForBlock(block: { id: NoteBlock['id'] }): { open: string; close: string } {
|
|
||||||
return {
|
|
||||||
open: `<Block id=${block.id}>`,
|
|
||||||
close: `</Block id=${block.id}>`,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function stringIndexOfBlock(
|
|
||||||
text: string,
|
|
||||||
block: { id: NoteBlock['id'] },
|
|
||||||
): { begin: number; end: number } | undefined {
|
|
||||||
const brackets = bracketSyntaxForBlock(block)
|
|
||||||
|
|
||||||
const startIndex = text.indexOf(brackets.open)
|
|
||||||
if (startIndex === -1) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const endIndex = text.indexOf(brackets.close) + brackets.close.length
|
|
||||||
|
|
||||||
return {
|
|
||||||
begin: startIndex,
|
|
||||||
end: endIndex,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blockContentToNoteTextRendition(block: { id: NoteBlock['id'] }, content: string): string {
|
|
||||||
const brackets = bracketSyntaxForBlock(block)
|
|
||||||
return `${brackets.open}${content}${brackets.close}`
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -21,4 +21,99 @@ describe('note mutator', () => {
|
|||||||
|
|
||||||
expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
expect(result.content.editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should addBlock to new note', () => {
|
||||||
|
const note = createNote({})
|
||||||
|
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||||
|
mutator.addBlock({ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' })
|
||||||
|
const result = mutator.getResult()
|
||||||
|
|
||||||
|
expect(result.content.blocksItem).toEqual({
|
||||||
|
blocks: [{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should addBlock to existing note', () => {
|
||||||
|
const note = createNote({
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' }],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||||
|
mutator.addBlock({ type: NoteType.RichText, id: '456', editorIdentifier: 'richy', content: 'test' })
|
||||||
|
const result = mutator.getResult()
|
||||||
|
|
||||||
|
expect(result.content.blocksItem).toEqual({
|
||||||
|
blocks: [
|
||||||
|
{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
{ type: NoteType.RichText, id: '456', editorIdentifier: 'richy', content: 'test' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should removeBlock', () => {
|
||||||
|
const note = createNote({
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [
|
||||||
|
{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||||
|
mutator.removeBlock({ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' })
|
||||||
|
const result = mutator.getResult()
|
||||||
|
|
||||||
|
expect(result.content.blocksItem).toEqual({
|
||||||
|
blocks: [{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'test' }],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should changeBlockContent', () => {
|
||||||
|
const note = createNote({
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [
|
||||||
|
{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'old content 1' },
|
||||||
|
{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'old content 2' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||||
|
mutator.changeBlockContent('123', 'new content')
|
||||||
|
const result = mutator.getResult()
|
||||||
|
|
||||||
|
expect(result.content.blocksItem).toEqual({
|
||||||
|
blocks: [
|
||||||
|
{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'new content' },
|
||||||
|
{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'old content 2' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should changeBlockSize', () => {
|
||||||
|
const note = createNote({
|
||||||
|
blocksItem: {
|
||||||
|
blocks: [
|
||||||
|
{ type: NoteType.Code, id: '123', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const mutator = new NoteMutator(note, MutationType.NoUpdateUserTimestamps)
|
||||||
|
mutator.changeBlockSize('123', { width: 10, height: 20 })
|
||||||
|
const result = mutator.getResult()
|
||||||
|
|
||||||
|
expect(result.content.blocksItem).toEqual({
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
type: NoteType.Code,
|
||||||
|
id: '123',
|
||||||
|
editorIdentifier: 'markdown',
|
||||||
|
content: 'test',
|
||||||
|
size: { width: 10, height: 20 },
|
||||||
|
},
|
||||||
|
{ type: NoteType.Code, id: '456', editorIdentifier: 'markdown', content: 'test' },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { NoteToNoteReference } from '../../Abstract/Reference/NoteToNoteReferenc
|
|||||||
import { ContentType } from '@standardnotes/common'
|
import { ContentType } from '@standardnotes/common'
|
||||||
import { ContentReferenceType } from '../../Abstract/Item'
|
import { ContentReferenceType } from '../../Abstract/Item'
|
||||||
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||||
import { blockContentToNoteTextRendition, bracketSyntaxForBlock, NoteBlock, stringIndexOfBlock } from './NoteBlocks'
|
import { NoteBlock } from './NoteBlocks'
|
||||||
import { removeFromArray } from '@standardnotes/utils'
|
import { filterFromArray } from '@standardnotes/utils'
|
||||||
|
|
||||||
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
||||||
set title(title: string) {
|
set title(title: string) {
|
||||||
@@ -51,10 +51,6 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.mutableContent.blocksItem.blocks.push(block)
|
this.mutableContent.blocksItem.blocks.push(block)
|
||||||
|
|
||||||
const brackets = bracketSyntaxForBlock(block)
|
|
||||||
|
|
||||||
this.text += `${brackets.open}${block.content}${brackets.close}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeBlock(block: NoteBlock): void {
|
removeBlock(block: NoteBlock): void {
|
||||||
@@ -62,37 +58,24 @@ export class NoteMutator extends DecryptedItemMutator<NoteContent> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromArray(this.mutableContent.blocksItem.blocks, block)
|
filterFromArray(this.mutableContent.blocksItem.blocks, { id: block.id })
|
||||||
|
|
||||||
const location = stringIndexOfBlock(this.mutableContent.text, block)
|
|
||||||
|
|
||||||
if (location) {
|
|
||||||
this.mutableContent.text = this.mutableContent.text.slice(location.begin, location.end)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBlockContent(blockId: string, content: string): void {
|
changeBlockContent(blockId: string, content: string): void {
|
||||||
const block = this.mutableContent.blocksItem?.blocks.find((b) => b.id === blockId)
|
const blockIndex = this.mutableContent.blocksItem?.blocks.findIndex((b) => {
|
||||||
|
return b.id === blockId
|
||||||
|
})
|
||||||
|
|
||||||
|
if (blockIndex == null || blockIndex === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const block = this.mutableContent.blocksItem?.blocks[blockIndex]
|
||||||
if (!block) {
|
if (!block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
block.content = content
|
block.content = content
|
||||||
|
|
||||||
const location = stringIndexOfBlock(this.mutableContent.text, block)
|
|
||||||
|
|
||||||
if (location) {
|
|
||||||
const replaceRange = (s: string, start: number, end: number, substitute: string) => {
|
|
||||||
return s.substring(0, start) + substitute + s.substring(end)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mutableContent.text = replaceRange(
|
|
||||||
this.mutableContent.text,
|
|
||||||
location.begin,
|
|
||||||
location.end,
|
|
||||||
blockContentToNoteTextRendition({ id: blockId }, content),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeBlockSize(blockId: string, size: { width: number; height: number }): void {
|
changeBlockSize(blockId: string, size: { width: number; height: number }): void {
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
public sessionKey?: string
|
public sessionKey?: string
|
||||||
|
|
||||||
private note: SNNote
|
private note: SNNote
|
||||||
private lastBlockSent?: NoteBlock
|
private lastBlockContentSent?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly component: SNComponent,
|
public readonly component: SNComponent,
|
||||||
@@ -261,8 +261,12 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
|
|
||||||
sendNoteToEditor(source?: PayloadEmitSource): void {
|
sendNoteToEditor(source?: PayloadEmitSource): void {
|
||||||
const block = this.note.getBlock(this.blockId)
|
const block = this.note.getBlock(this.blockId)
|
||||||
|
if (!block) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.lastBlockSent && this.lastBlockSent.content === block?.content) {
|
if (this.lastBlockContentSent && this.lastBlockContentSent === block.content) {
|
||||||
|
this.log('Not sending note to editor, content has not changed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -295,7 +299,7 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
: this.preferencesSerivce.getValue(PrefKey.EditorSpellcheck, true)
|
: this.preferencesSerivce.getValue(PrefKey.EditorSpellcheck, true)
|
||||||
|
|
||||||
params.content = {
|
params.content = {
|
||||||
text: block?.content || '',
|
text: block.content,
|
||||||
spellcheck,
|
spellcheck,
|
||||||
} as NoteContent
|
} as NoteContent
|
||||||
|
|
||||||
@@ -307,9 +311,11 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
item: params,
|
item: params,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.replyToMessage(this.streamContextItemOriginalMessage as ComponentMessage, response)
|
const sent = this.replyToMessage(this.streamContextItemOriginalMessage as ComponentMessage, response)
|
||||||
|
|
||||||
this.lastBlockSent = block
|
if (sent) {
|
||||||
|
this.lastBlockContentSent = block.content
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private log(message: string, ...args: unknown[]): void {
|
private log(message: string, ...args: unknown[]): void {
|
||||||
@@ -318,23 +324,24 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private replyToMessage(originalMessage: ComponentMessage, replyData: MessageReplyData): void {
|
private replyToMessage(originalMessage: ComponentMessage, replyData: MessageReplyData): boolean {
|
||||||
const reply: MessageReply = {
|
const reply: MessageReply = {
|
||||||
action: ComponentAction.Reply,
|
action: ComponentAction.Reply,
|
||||||
original: originalMessage,
|
original: originalMessage,
|
||||||
data: replyData,
|
data: replyData,
|
||||||
}
|
}
|
||||||
this.sendMessage(reply)
|
|
||||||
|
return this.sendMessage(reply)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param essential If the message is non-essential, no alert will be shown
|
* @param essential If the message is non-essential, no alert will be shown
|
||||||
* if we can no longer find the window.
|
* if we can no longer find the window.
|
||||||
*/
|
*/
|
||||||
sendMessage(message: ComponentMessage | MessageReply, essential = true): void {
|
sendMessage(message: ComponentMessage | MessageReply, essential = true): boolean {
|
||||||
if (!this.window && message.action === ComponentAction.Reply) {
|
if (!this.window && message.action === ComponentAction.Reply) {
|
||||||
this.log('Component has been deallocated in between message send and reply', this.component, message)
|
this.log('Component has been deallocated in between message send and reply', this.component, message)
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
this.log('Send message to component', this.component, 'message: ', message)
|
this.log('Send message to component', this.component, 'message: ', message)
|
||||||
@@ -347,7 +354,7 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
'but an error is occurring. Please restart this extension and try again.',
|
'but an error is occurring. Please restart this extension and try again.',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!origin.startsWith('http') && !origin.startsWith('file')) {
|
if (!origin.startsWith('http') && !origin.startsWith('file')) {
|
||||||
@@ -357,6 +364,8 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
|
|
||||||
/* Mobile messaging requires json */
|
/* Mobile messaging requires json */
|
||||||
this.window.postMessage(this.isMobile ? JSON.stringify(message) : message, origin)
|
this.window.postMessage(this.isMobile ? JSON.stringify(message) : message, origin)
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWindow(): Window | undefined {
|
public getWindow(): Window | undefined {
|
||||||
@@ -459,6 +468,11 @@ export class BlocksComponentViewer implements ComponentViewerInterface {
|
|||||||
this.note,
|
this.note,
|
||||||
(mutator) => {
|
(mutator) => {
|
||||||
mutator.changeBlockContent(this.blockId, text)
|
mutator.changeBlockContent(this.blockId, text)
|
||||||
|
|
||||||
|
if (this.note.indexOfBlock({ id: this.blockId }) === 0) {
|
||||||
|
mutator.preview_html = content.preview_html
|
||||||
|
mutator.preview_plain = content.preview_plain || ''
|
||||||
|
}
|
||||||
},
|
},
|
||||||
MutationType.UpdateUserTimestamps,
|
MutationType.UpdateUserTimestamps,
|
||||||
PayloadEmitSource.ComponentRetrieved,
|
PayloadEmitSource.ComponentRetrieved,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { WebApplication } from '@/Application/Application'
|
|||||||
import { SNNote } from '@standardnotes/snjs'
|
import { SNNote } from '@standardnotes/snjs'
|
||||||
import { FunctionComponent, useCallback, useRef } from 'react'
|
import { FunctionComponent, useCallback, useRef } from 'react'
|
||||||
import { BlockEditorController } from './BlockEditorController'
|
import { BlockEditorController } from './BlockEditorController'
|
||||||
import { AddBlockButton } from './AddButton'
|
import { AddBlockButton } from './BlockMenu/AddButton'
|
||||||
import { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
|
import { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
|
||||||
import { BlockOption } from './BlockMenu/BlockOption'
|
import { BlockOption } from './BlockMenu/BlockOption'
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
import { FunctionComponent, useCallback, useState } from 'react'
|
import { FunctionComponent, useCallback, useState } from 'react'
|
||||||
import Icon from '../Icon/Icon'
|
import Icon from '../../Icon/Icon'
|
||||||
import { BlockMenu } from './BlockMenu/BlockMenu'
|
import { BlockMenu } from './BlockMenu'
|
||||||
import { BlockOption } from './BlockMenu/BlockOption'
|
import { BlockOption } from './BlockOption'
|
||||||
|
|
||||||
type AddButtonProps = {
|
type AddButtonProps = {
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
@@ -11,12 +11,20 @@ type AddButtonProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
|
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
|
||||||
const [showMenu, setShowMenu] = useState(true)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
|
||||||
const toggleMenu = useCallback(() => {
|
const toggleMenu = useCallback(() => {
|
||||||
setShowMenu((prevValue) => !prevValue)
|
setShowMenu((prevValue) => !prevValue)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleSelection = useCallback(
|
||||||
|
(option: BlockOption) => {
|
||||||
|
onSelectOption(option)
|
||||||
|
setShowMenu(false)
|
||||||
|
},
|
||||||
|
[onSelectOption],
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex flex-row flex-wrap">
|
<div className="mt-2 flex flex-row flex-wrap">
|
||||||
<button
|
<button
|
||||||
@@ -30,7 +38,7 @@ export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application,
|
|||||||
<Icon type="add" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
<Icon type="add" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{showMenu && <BlockMenu application={application} onSelectOption={onSelectOption} />}
|
{showMenu && <BlockMenu application={application} onSelectOption={handleSelection} />}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -10,8 +10,8 @@ export type BlockMenuOptionProps = {
|
|||||||
export const BlockMenuOption: FunctionComponent<BlockMenuOptionProps> = ({ option, onSelect }) => {
|
export const BlockMenuOption: FunctionComponent<BlockMenuOptionProps> = ({ option, onSelect }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={'flex w-full flex-row items-center border-[1px] border-b border-border p-4'}
|
className={'flex w-full cursor-pointer flex-row items-center border-[1px] border-b border-border p-4'}
|
||||||
onClick={() => onSelect}
|
onClick={() => onSelect(option)}
|
||||||
>
|
>
|
||||||
<Icon type={option.icon} size={'large'} />
|
<Icon type={option.icon} size={'large'} />
|
||||||
<div className={'ml-3 text-base'}>{option.label}</div>
|
<div className={'ml-3 text-base'}>{option.label}</div>
|
||||||
|
|||||||
@@ -10,5 +10,6 @@ export function componentToBlockOption(component: SNComponent, iconsController:
|
|||||||
label: component.name,
|
label: component.name,
|
||||||
icon: iconType,
|
icon: iconType,
|
||||||
iconTint: tint,
|
iconTint: tint,
|
||||||
|
component: component,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export const SingleBlockRenderer: FunctionComponent<SingleBlockRendererProps> =
|
|||||||
)}
|
)}
|
||||||
onClick={onRemoveBlock}
|
onClick={onRemoveBlock}
|
||||||
>
|
>
|
||||||
<Icon type="remove" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
<Icon type="close" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />
|
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />
|
||||||
|
|||||||
@@ -5,21 +5,11 @@ import { MenuItemType } from '@/Components/Menu/MenuItemType'
|
|||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import {
|
import { ComponentArea, NoteMutator, PrefKey, SNComponent, SNNote } from '@standardnotes/snjs'
|
||||||
ComponentArea,
|
|
||||||
ItemMutator,
|
|
||||||
NoteMutator,
|
|
||||||
NoteType,
|
|
||||||
PrefKey,
|
|
||||||
SNComponent,
|
|
||||||
SNNote,
|
|
||||||
TransactionalMutation,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||||
import { createEditorMenuGroups } from './createEditorMenuGroups'
|
import { createEditorMenuGroups } from '../../Utils/createEditorMenuGroups'
|
||||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
|
||||||
import { reloadFont } from '../NoteView/FontFunctions'
|
import { reloadFont } from '../NoteView/FontFunctions'
|
||||||
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
|
||||||
|
|
||||||
@@ -48,107 +38,96 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
[application.componentManager],
|
[application.componentManager],
|
||||||
)
|
)
|
||||||
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
|
||||||
const [currentEditor, setCurrentEditor] = useState<SNComponent>()
|
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (note) {
|
if (note) {
|
||||||
setCurrentEditor(application.componentManager.editorForNote(note))
|
setCurrentComponent(application.componentManager.editorForNote(note))
|
||||||
}
|
}
|
||||||
}, [application, note])
|
}, [application, note])
|
||||||
|
|
||||||
const premiumModal = usePremiumModal()
|
const premiumModal = usePremiumModal()
|
||||||
|
|
||||||
const isSelectedEditor = useCallback(
|
const isSelected = useCallback(
|
||||||
(item: EditorMenuItem) => {
|
(item: EditorMenuItem) => {
|
||||||
if (currentEditor) {
|
if (currentComponent) {
|
||||||
if (item?.component?.identifier === currentEditor.identifier) {
|
return item.component?.identifier === currentComponent.identifier
|
||||||
return true
|
|
||||||
}
|
|
||||||
} else if (item.name === PLAIN_EDITOR_NAME) {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
|
return item.noteType === note?.noteType
|
||||||
},
|
},
|
||||||
[currentEditor],
|
[currentComponent, note],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectComponent = useCallback(
|
const selectComponent = useCallback(
|
||||||
async (component: SNComponent | null, note: SNNote) => {
|
async (component: SNComponent, note: SNNote) => {
|
||||||
if (component) {
|
if (component.conflictOf) {
|
||||||
if (component.conflictOf) {
|
void application.mutator.changeAndSaveItem(component, (mutator) => {
|
||||||
application.mutator
|
mutator.conflictOf = undefined
|
||||||
.changeAndSaveItem(component, (mutator) => {
|
})
|
||||||
mutator.conflictOf = undefined
|
|
||||||
})
|
|
||||||
.catch(console.error)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transactions: TransactionalMutation[] = []
|
|
||||||
|
|
||||||
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
|
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
|
||||||
|
|
||||||
if (note.locked) {
|
await application.mutator.changeAndSaveItem(note, (mutator) => {
|
||||||
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
const noteMutator = mutator as NoteMutator
|
||||||
return
|
noteMutator.noteType = component.noteType
|
||||||
}
|
noteMutator.editorIdentifier = component.identifier
|
||||||
|
})
|
||||||
|
|
||||||
if (!component) {
|
setCurrentComponent(application.componentManager.editorForNote(note))
|
||||||
transactions.push({
|
|
||||||
itemUuid: note.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const noteMutator = m as NoteMutator
|
|
||||||
noteMutator.noteType = NoteType.Plain
|
|
||||||
noteMutator.editorIdentifier = undefined
|
|
||||||
},
|
|
||||||
})
|
|
||||||
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
|
||||||
} else {
|
|
||||||
transactions.push({
|
|
||||||
itemUuid: note.uuid,
|
|
||||||
mutate: (m: ItemMutator) => {
|
|
||||||
const noteMutator = m as NoteMutator
|
|
||||||
noteMutator.noteType = component.noteType
|
|
||||||
noteMutator.editorIdentifier = component.identifier
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
await application.mutator.runTransactionalMutations(transactions)
|
|
||||||
application.sync.sync().catch(console.error)
|
|
||||||
setCurrentEditor(application.componentManager.editorForNote(note))
|
|
||||||
},
|
},
|
||||||
[application],
|
[application],
|
||||||
)
|
)
|
||||||
|
|
||||||
const selectEditor = useCallback(
|
const selectNonComponent = useCallback(
|
||||||
|
async (item: EditorMenuItem, note: SNNote) => {
|
||||||
|
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
|
||||||
|
|
||||||
|
reloadFont(application.getPreference(PrefKey.EditorMonospaceEnabled))
|
||||||
|
|
||||||
|
await application.mutator.changeAndSaveItem(note, (mutator) => {
|
||||||
|
const noteMutator = mutator as NoteMutator
|
||||||
|
noteMutator.noteType = item.noteType
|
||||||
|
noteMutator.editorIdentifier = undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
setCurrentComponent(undefined)
|
||||||
|
},
|
||||||
|
[application],
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectItem = useCallback(
|
||||||
async (itemToBeSelected: EditorMenuItem) => {
|
async (itemToBeSelected: EditorMenuItem) => {
|
||||||
if (!itemToBeSelected.isEntitled) {
|
if (!itemToBeSelected.isEntitled) {
|
||||||
premiumModal.activate(itemToBeSelected.name)
|
premiumModal.activate(itemToBeSelected.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
|
if (note?.locked) {
|
||||||
|
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
|
||||||
if (areBothEditorsPlain) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let shouldSelectEditor = true
|
let shouldMakeSelection = true
|
||||||
|
|
||||||
if (itemToBeSelected.component) {
|
if (itemToBeSelected.component) {
|
||||||
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
|
||||||
currentEditor,
|
currentComponent,
|
||||||
itemToBeSelected.component,
|
itemToBeSelected.component,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (changeRequiresAlert) {
|
if (changeRequiresAlert) {
|
||||||
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
|
shouldMakeSelection = await application.componentManager.showEditorChangeAlert()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldSelectEditor && note) {
|
if (shouldMakeSelection && note) {
|
||||||
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
|
if (itemToBeSelected.component) {
|
||||||
|
selectComponent(itemToBeSelected.component, note).catch(console.error)
|
||||||
|
} else {
|
||||||
|
selectNonComponent(itemToBeSelected, note).catch(console.error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closeMenu()
|
closeMenu()
|
||||||
@@ -157,7 +136,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
onSelect(itemToBeSelected.component)
|
onSelect(itemToBeSelected.component)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[application.componentManager, closeMenu, currentEditor, note, onSelect, premiumModal, selectComponent],
|
[application, closeMenu, currentComponent, note, onSelect, premiumModal, selectComponent, selectNonComponent],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -172,7 +151,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
|
<div className={`border-0 border-t border-solid border-border py-1 ${index === 0 ? 'border-t-0' : ''}`}>
|
||||||
{group.items.map((item) => {
|
{group.items.map((item) => {
|
||||||
const onClickEditorItem = () => {
|
const onClickEditorItem = () => {
|
||||||
selectEditor(item).catch(console.error)
|
selectItem(item).catch(console.error)
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
@@ -180,7 +159,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
type={MenuItemType.RadioButton}
|
type={MenuItemType.RadioButton}
|
||||||
onClick={onClickEditorItem}
|
onClick={onClickEditorItem}
|
||||||
className={'flex-row-reverse py-2'}
|
className={'flex-row-reverse py-2'}
|
||||||
checked={item.isEntitled ? isSelectedEditor(item) : undefined}
|
checked={item.isEntitled ? isSelected(item) : undefined}
|
||||||
>
|
>
|
||||||
<div className="flex flex-grow items-center justify-between">
|
<div className="flex flex-grow items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedIte
|
|||||||
import { ElementIds } from '@/Constants/ElementIDs'
|
import { ElementIds } from '@/Constants/ElementIDs'
|
||||||
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
import { PrefDefaults } from '@/Constants/PrefDefaults'
|
||||||
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
|
||||||
|
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
||||||
import { log, LoggingDomain } from '@/Logging'
|
import { log, LoggingDomain } from '@/Logging'
|
||||||
import { debounce, isDesktopApplication, isDev, isMobileScreen, isTabletScreen } from '@/Utils'
|
import { debounce, isDesktopApplication, isMobileScreen, isTabletScreen } from '@/Utils'
|
||||||
import { classNames } from '@/Utils/ConcatenateClassNames'
|
import { classNames } from '@/Utils/ConcatenateClassNames'
|
||||||
import {
|
import {
|
||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
@@ -55,8 +56,6 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
|
|||||||
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
const IsBlocksEnabled = isDev
|
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
availableStackComponents: SNComponent[]
|
availableStackComponents: SNComponent[]
|
||||||
editorComponentViewer?: ComponentViewerInterface
|
editorComponentViewer?: ComponentViewerInterface
|
||||||
@@ -1028,7 +1027,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
|
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
|
||||||
|
|
||||||
const editorMode =
|
const editorMode =
|
||||||
IsBlocksEnabled && this.note.title.toLowerCase().includes('blocks')
|
featureTrunkEnabled(FeatureTrunkName.Blocks) && this.note.noteType === NoteType.Blocks
|
||||||
? 'blocks'
|
? 'blocks'
|
||||||
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
|
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
|
||||||
? 'plain'
|
? 'plain'
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { SNComponent } from '@standardnotes/snjs'
|
import { NoteType, SNComponent } from '@standardnotes/snjs'
|
||||||
|
|
||||||
export type EditorMenuItem = {
|
export type EditorMenuItem = {
|
||||||
name: string
|
name: string
|
||||||
component?: SNComponent
|
component?: SNComponent
|
||||||
isEntitled: boolean
|
isEntitled: boolean
|
||||||
|
noteType: NoteType
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export const TAG_FOLDERS_FEATURE_TOOLTIP = 'A Plus or Pro plan is required to en
|
|||||||
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
export const SMART_TAGS_FEATURE_NAME = 'Smart Tags'
|
||||||
|
|
||||||
export const PLAIN_EDITOR_NAME = 'Plain Text'
|
export const PLAIN_EDITOR_NAME = 'Plain Text'
|
||||||
|
export const BLOCKS_EDITOR_NAME = 'Blocks'
|
||||||
|
|
||||||
export const SYNC_TIMEOUT_DEBOUNCE = 350
|
export const SYNC_TIMEOUT_DEBOUNCE = 350
|
||||||
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
|
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100
|
||||||
|
|||||||
13
packages/web/src/javascripts/FeatureTrunk.ts
Normal file
13
packages/web/src/javascripts/FeatureTrunk.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { isDev } from '@/Utils'
|
||||||
|
|
||||||
|
export enum FeatureTrunkName {
|
||||||
|
Blocks,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FeatureTrunkStatus: Record<FeatureTrunkName, boolean> = {
|
||||||
|
[FeatureTrunkName.Blocks]: isDev && true,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function featureTrunkEnabled(trunk: FeatureTrunkName): boolean {
|
||||||
|
return FeatureTrunkStatus[trunk]
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
|
|||||||
[LoggingDomain.BlockEditor]: true,
|
[LoggingDomain.BlockEditor]: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
export function log(domain: LoggingDomain, ...args: any[]): void {
|
export function log(domain: LoggingDomain, ...args: any[]): void {
|
||||||
if (!isDev || !LoggingStatus[domain]) {
|
if (!isDev || !LoggingStatus[domain]) {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,39 +1,50 @@
|
|||||||
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
||||||
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
import { BLOCKS_EDITOR_NAME, PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||||
|
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
||||||
|
|
||||||
export const PlainEditorType = 'plain-editor'
|
export const PlainEditorType = 'plain-editor'
|
||||||
|
export const BlocksType = 'blocks-editor'
|
||||||
|
|
||||||
export type EditorOption = DropdownItem & {
|
export type EditorOption = DropdownItem & {
|
||||||
value: FeatureIdentifier | typeof PlainEditorType
|
value: FeatureIdentifier | typeof PlainEditorType | typeof BlocksType
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDropdownItemsForAllEditors(application: WebApplication) {
|
export function getDropdownItemsForAllEditors(application: WebApplication) {
|
||||||
const options = application.componentManager
|
const plaintextOption: EditorOption = {
|
||||||
.componentsForArea(ComponentArea.Editor)
|
icon: 'plain-text',
|
||||||
.map((editor): EditorOption => {
|
iconClassName: 'text-accessory-tint-1',
|
||||||
const identifier = editor.package_info.identifier
|
label: PLAIN_EDITOR_NAME,
|
||||||
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
value: PlainEditorType,
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
const options = application.componentManager.componentsForArea(ComponentArea.Editor).map((editor): EditorOption => {
|
||||||
label: editor.displayName,
|
const identifier = editor.package_info.identifier
|
||||||
value: identifier,
|
const [iconType, tint] = application.iconsController.getIconAndTintForNoteType(editor.package_info.note_type)
|
||||||
...(iconType ? { icon: iconType } : null),
|
|
||||||
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
return {
|
||||||
}
|
label: editor.displayName,
|
||||||
})
|
value: identifier,
|
||||||
.concat([
|
...(iconType ? { icon: iconType } : null),
|
||||||
{
|
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
|
||||||
icon: 'plain-text',
|
}
|
||||||
iconClassName: 'text-accessory-tint-1',
|
})
|
||||||
label: PLAIN_EDITOR_NAME,
|
|
||||||
value: PlainEditorType,
|
options.push(plaintextOption)
|
||||||
},
|
|
||||||
])
|
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
||||||
.sort((a, b) => {
|
options.push({
|
||||||
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
icon: 'dashboard',
|
||||||
|
iconClassName: 'text-accessory-tint-1',
|
||||||
|
label: BLOCKS_EDITOR_NAME,
|
||||||
|
value: BlocksType,
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
options.sort((a, b) => {
|
||||||
|
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
|
||||||
|
})
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
|
||||||
import { WebApplication } from '@/Application/Application'
|
import { WebApplication } from '@/Application/Application'
|
||||||
import {
|
import {
|
||||||
ContentType,
|
ContentType,
|
||||||
@@ -12,9 +13,9 @@ import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
|||||||
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
|
||||||
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
|
||||||
|
|
||||||
type EditorGroup = NoteType | 'others'
|
type NoteTypeToEditorRowsMap = Record<NoteType, EditorMenuItem[]>
|
||||||
|
|
||||||
const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup => {
|
const getNoteTypeForFeatureDescription = (featureDescription: FeatureDescription): NoteType => {
|
||||||
if (featureDescription.note_type) {
|
if (featureDescription.note_type) {
|
||||||
return featureDescription.note_type
|
return featureDescription.note_type
|
||||||
} else if (featureDescription.file_type) {
|
} else if (featureDescription.file_type) {
|
||||||
@@ -23,106 +24,154 @@ const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup =>
|
|||||||
return NoteType.RichText
|
return NoteType.RichText
|
||||||
case 'md':
|
case 'md':
|
||||||
return NoteType.Markdown
|
return NoteType.Markdown
|
||||||
default:
|
|
||||||
return 'others'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 'others'
|
return NoteType.Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createEditorMenuGroups = (application: WebApplication, editors: SNComponent[]) => {
|
const insertNonInstalledNativeComponentsInMap = (
|
||||||
const editorItems: Record<EditorGroup, EditorMenuItem[]> = {
|
map: NoteTypeToEditorRowsMap,
|
||||||
'plain-text': [
|
components: SNComponent[],
|
||||||
{
|
application: WebApplication,
|
||||||
name: PLAIN_EDITOR_NAME,
|
): void => {
|
||||||
isEntitled: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
'rich-text': [],
|
|
||||||
markdown: [],
|
|
||||||
task: [],
|
|
||||||
code: [],
|
|
||||||
spreadsheet: [],
|
|
||||||
authentication: [],
|
|
||||||
others: [],
|
|
||||||
blocks: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
GetFeatures()
|
GetFeatures()
|
||||||
.filter((feature) => feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor)
|
.filter((feature) => feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor)
|
||||||
.forEach((editorFeature) => {
|
.forEach((editorFeature) => {
|
||||||
const notInstalled = !editors.find((editor) => editor.identifier === editorFeature.identifier)
|
const notInstalled = !components.find((editor) => editor.identifier === editorFeature.identifier)
|
||||||
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
|
const isExperimental = application.features.isExperimentalFeature(editorFeature.identifier)
|
||||||
const isDeprecated = editorFeature.deprecated
|
const isDeprecated = editorFeature.deprecated
|
||||||
const isShowable = notInstalled && !isExperimental && !isDeprecated
|
const isShowable = notInstalled && !isExperimental && !isDeprecated
|
||||||
|
|
||||||
if (isShowable) {
|
if (isShowable) {
|
||||||
editorItems[getEditorGroup(editorFeature)].push({
|
const noteType = getNoteTypeForFeatureDescription(editorFeature)
|
||||||
|
map[noteType].push({
|
||||||
name: editorFeature.name as string,
|
name: editorFeature.name as string,
|
||||||
isEntitled: false,
|
isEntitled: false,
|
||||||
|
noteType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertInstalledComponentsInMap = (
|
||||||
|
map: NoteTypeToEditorRowsMap,
|
||||||
|
components: SNComponent[],
|
||||||
|
application: WebApplication,
|
||||||
|
) => {
|
||||||
|
components.forEach((editor) => {
|
||||||
|
const noteType = getNoteTypeForFeatureDescription(editor.package_info)
|
||||||
|
|
||||||
editors.forEach((editor) => {
|
|
||||||
const editorItem: EditorMenuItem = {
|
const editorItem: EditorMenuItem = {
|
||||||
name: editor.displayName,
|
name: editor.displayName,
|
||||||
component: editor,
|
component: editor,
|
||||||
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
|
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
|
||||||
|
noteType,
|
||||||
}
|
}
|
||||||
|
|
||||||
editorItems[getEditorGroup(editor.package_info)].push(editorItem)
|
map[noteType].push(editorItem)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const editorMenuGroups: EditorMenuGroup[] = [
|
const createGroupsFromMap = (map: NoteTypeToEditorRowsMap): EditorMenuGroup[] => {
|
||||||
|
const groups: EditorMenuGroup[] = [
|
||||||
{
|
{
|
||||||
icon: 'plain-text',
|
icon: 'plain-text',
|
||||||
iconClassName: 'text-accessory-tint-1',
|
iconClassName: 'text-accessory-tint-1',
|
||||||
title: 'Plain text',
|
title: 'Plain text',
|
||||||
items: editorItems['plain-text'],
|
items: map[NoteType.Plain],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'rich-text',
|
icon: 'rich-text',
|
||||||
iconClassName: 'text-accessory-tint-1',
|
iconClassName: 'text-accessory-tint-1',
|
||||||
title: 'Rich text',
|
title: 'Rich text',
|
||||||
items: editorItems['rich-text'],
|
items: map[NoteType.RichText],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'markdown',
|
icon: 'markdown',
|
||||||
iconClassName: 'text-accessory-tint-2',
|
iconClassName: 'text-accessory-tint-2',
|
||||||
title: 'Markdown text',
|
title: 'Markdown text',
|
||||||
items: editorItems.markdown,
|
items: map[NoteType.Markdown],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'tasks',
|
icon: 'tasks',
|
||||||
iconClassName: 'text-accessory-tint-3',
|
iconClassName: 'text-accessory-tint-3',
|
||||||
title: 'Todo',
|
title: 'Todo',
|
||||||
items: editorItems.task,
|
items: map[NoteType.Task],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'code',
|
icon: 'code',
|
||||||
iconClassName: 'text-accessory-tint-4',
|
iconClassName: 'text-accessory-tint-4',
|
||||||
title: 'Code',
|
title: 'Code',
|
||||||
items: editorItems.code,
|
items: map[NoteType.Code],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'spreadsheets',
|
icon: 'spreadsheets',
|
||||||
iconClassName: 'text-accessory-tint-5',
|
iconClassName: 'text-accessory-tint-5',
|
||||||
title: 'Spreadsheet',
|
title: 'Spreadsheet',
|
||||||
items: editorItems.spreadsheet,
|
items: map[NoteType.Spreadsheet],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'authenticator',
|
icon: 'authenticator',
|
||||||
iconClassName: 'text-accessory-tint-6',
|
iconClassName: 'text-accessory-tint-6',
|
||||||
title: 'Authentication',
|
title: 'Authentication',
|
||||||
items: editorItems.authentication,
|
items: map[NoteType.Authentication],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'editor',
|
icon: 'editor',
|
||||||
iconClassName: 'text-neutral',
|
iconClassName: 'text-neutral',
|
||||||
title: 'Others',
|
title: 'Others',
|
||||||
items: editorItems.others,
|
items: map[NoteType.Unknown],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return editorMenuGroups
|
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
||||||
|
groups.splice(1, 0, {
|
||||||
|
icon: 'dashboard',
|
||||||
|
iconClassName: 'text-accessory-tint-1',
|
||||||
|
title: 'Blocks',
|
||||||
|
items: map[NoteType.Blocks],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return groups
|
||||||
|
}
|
||||||
|
|
||||||
|
const createBaselineMap = (): NoteTypeToEditorRowsMap => {
|
||||||
|
const map: NoteTypeToEditorRowsMap = {
|
||||||
|
[NoteType.Plain]: [
|
||||||
|
{
|
||||||
|
name: PLAIN_EDITOR_NAME,
|
||||||
|
isEntitled: true,
|
||||||
|
noteType: NoteType.Plain,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[NoteType.Blocks]: [],
|
||||||
|
[NoteType.RichText]: [],
|
||||||
|
[NoteType.Markdown]: [],
|
||||||
|
[NoteType.Task]: [],
|
||||||
|
[NoteType.Code]: [],
|
||||||
|
[NoteType.Spreadsheet]: [],
|
||||||
|
[NoteType.Authentication]: [],
|
||||||
|
[NoteType.Unknown]: [],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
|
||||||
|
map[NoteType.Blocks].push({
|
||||||
|
name: 'Blocks',
|
||||||
|
isEntitled: true,
|
||||||
|
noteType: NoteType.Blocks,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEditorMenuGroups = (application: WebApplication, components: SNComponent[]) => {
|
||||||
|
const map = createBaselineMap()
|
||||||
|
|
||||||
|
insertNonInstalledNativeComponentsInMap(map, components, application)
|
||||||
|
|
||||||
|
insertInstalledComponentsInMap(map, components, application)
|
||||||
|
|
||||||
|
return createGroupsFromMap(map)
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user