refactor: block storage (#1952)

This commit is contained in:
Mo
2022-11-06 07:51:41 -06:00
committed by GitHub
parent 7d64b1c0ff
commit 40a1a27444
21 changed files with 417 additions and 221 deletions

View File

@@ -2,7 +2,7 @@ import { WebApplication } from '@/Application/Application'
import { SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useRef } from 'react'
import { BlockEditorController } from './BlockEditorController'
import { AddBlockButton } from './AddButton'
import { AddBlockButton } from './BlockMenu/AddButton'
import { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
import { BlockOption } from './BlockMenu/BlockOption'

View File

@@ -1,9 +1,9 @@
import { WebApplication } from '@/Application/Application'
import { classNames } from '@/Utils/ConcatenateClassNames'
import { FunctionComponent, useCallback, useState } from 'react'
import Icon from '../Icon/Icon'
import { BlockMenu } from './BlockMenu/BlockMenu'
import { BlockOption } from './BlockMenu/BlockOption'
import Icon from '../../Icon/Icon'
import { BlockMenu } from './BlockMenu'
import { BlockOption } from './BlockOption'
type AddButtonProps = {
application: WebApplication
@@ -11,12 +11,20 @@ type AddButtonProps = {
}
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
const [showMenu, setShowMenu] = useState(true)
const [showMenu, setShowMenu] = useState(false)
const toggleMenu = useCallback(() => {
setShowMenu((prevValue) => !prevValue)
}, [])
const handleSelection = useCallback(
(option: BlockOption) => {
onSelectOption(option)
setShowMenu(false)
},
[onSelectOption],
)
return (
<div className="mt-2 flex flex-row flex-wrap">
<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" />
</button>
{showMenu && <BlockMenu application={application} onSelectOption={onSelectOption} />}
{showMenu && <BlockMenu application={application} onSelectOption={handleSelection} />}
</div>
)
}

View File

@@ -10,8 +10,8 @@ export type BlockMenuOptionProps = {
export const BlockMenuOption: FunctionComponent<BlockMenuOptionProps> = ({ option, onSelect }) => {
return (
<div
className={'flex w-full flex-row items-center border-[1px] border-b border-border p-4'}
onClick={() => onSelect}
className={'flex w-full cursor-pointer flex-row items-center border-[1px] border-b border-border p-4'}
onClick={() => onSelect(option)}
>
<Icon type={option.icon} size={'large'} />
<div className={'ml-3 text-base'}>{option.label}</div>

View File

@@ -10,5 +10,6 @@ export function componentToBlockOption(component: SNComponent, iconsController:
label: component.name,
icon: iconType,
iconTint: tint,
component: component,
}
}

View File

@@ -94,7 +94,7 @@ export const SingleBlockRenderer: FunctionComponent<SingleBlockRendererProps> =
)}
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>
)}
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />

View File

@@ -5,21 +5,11 @@ import { MenuItemType } from '@/Components/Menu/MenuItemType'
import { usePremiumModal } from '@/Hooks/usePremiumModal'
import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
import { WebApplication } from '@/Application/Application'
import {
ComponentArea,
ItemMutator,
NoteMutator,
NoteType,
PrefKey,
SNComponent,
SNNote,
TransactionalMutation,
} from '@standardnotes/snjs'
import { ComponentArea, NoteMutator, PrefKey, SNComponent, SNNote } from '@standardnotes/snjs'
import { Fragment, FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
import { createEditorMenuGroups } from './createEditorMenuGroups'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import { createEditorMenuGroups } from '../../Utils/createEditorMenuGroups'
import { reloadFont } from '../NoteView/FontFunctions'
import { PremiumFeatureIconClass, PremiumFeatureIconName } from '../Icon/PremiumFeatureIcon'
@@ -48,107 +38,96 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
[application.componentManager],
)
const groups = useMemo(() => createEditorMenuGroups(application, editors), [application, editors])
const [currentEditor, setCurrentEditor] = useState<SNComponent>()
const [currentComponent, setCurrentComponent] = useState<SNComponent>()
useEffect(() => {
if (note) {
setCurrentEditor(application.componentManager.editorForNote(note))
setCurrentComponent(application.componentManager.editorForNote(note))
}
}, [application, note])
const premiumModal = usePremiumModal()
const isSelectedEditor = useCallback(
const isSelected = useCallback(
(item: EditorMenuItem) => {
if (currentEditor) {
if (item?.component?.identifier === currentEditor.identifier) {
return true
}
} else if (item.name === PLAIN_EDITOR_NAME) {
return true
if (currentComponent) {
return item.component?.identifier === currentComponent.identifier
}
return false
return item.noteType === note?.noteType
},
[currentEditor],
[currentComponent, note],
)
const selectComponent = useCallback(
async (component: SNComponent | null, note: SNNote) => {
if (component) {
if (component.conflictOf) {
application.mutator
.changeAndSaveItem(component, (mutator) => {
mutator.conflictOf = undefined
})
.catch(console.error)
}
async (component: SNComponent, note: SNNote) => {
if (component.conflictOf) {
void application.mutator.changeAndSaveItem(component, (mutator) => {
mutator.conflictOf = undefined
})
}
const transactions: TransactionalMutation[] = []
await application.getViewControllerManager().itemListController.insertCurrentIfTemplate()
if (note.locked) {
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
return
}
await application.mutator.changeAndSaveItem(note, (mutator) => {
const noteMutator = mutator as NoteMutator
noteMutator.noteType = component.noteType
noteMutator.editorIdentifier = component.identifier
})
if (!component) {
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))
setCurrentComponent(application.componentManager.editorForNote(note))
},
[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) => {
if (!itemToBeSelected.isEntitled) {
premiumModal.activate(itemToBeSelected.name)
return
}
const areBothEditorsPlain = !currentEditor && !itemToBeSelected.component
if (areBothEditorsPlain) {
if (note?.locked) {
application.alertService.alert(STRING_EDIT_LOCKED_ATTEMPT).catch(console.error)
return
}
let shouldSelectEditor = true
let shouldMakeSelection = true
if (itemToBeSelected.component) {
const changeRequiresAlert = application.componentManager.doesEditorChangeRequireAlert(
currentEditor,
currentComponent,
itemToBeSelected.component,
)
if (changeRequiresAlert) {
shouldSelectEditor = await application.componentManager.showEditorChangeAlert()
shouldMakeSelection = await application.componentManager.showEditorChangeAlert()
}
}
if (shouldSelectEditor && note) {
selectComponent(itemToBeSelected.component ?? null, note).catch(console.error)
if (shouldMakeSelection && note) {
if (itemToBeSelected.component) {
selectComponent(itemToBeSelected.component, note).catch(console.error)
} else {
selectNonComponent(itemToBeSelected, note).catch(console.error)
}
}
closeMenu()
@@ -157,7 +136,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
onSelect(itemToBeSelected.component)
}
},
[application.componentManager, closeMenu, currentEditor, note, onSelect, premiumModal, selectComponent],
[application, closeMenu, currentComponent, note, onSelect, premiumModal, selectComponent, selectNonComponent],
)
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' : ''}`}>
{group.items.map((item) => {
const onClickEditorItem = () => {
selectEditor(item).catch(console.error)
selectItem(item).catch(console.error)
}
return (
<MenuItem
@@ -180,7 +159,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
type={MenuItemType.RadioButton}
onClick={onClickEditorItem}
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 items-center">

View File

@@ -8,8 +8,9 @@ import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedIte
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
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 {
ApplicationEvent,
@@ -55,8 +56,6 @@ function sortAlphabetically(array: SNComponent[]): SNComponent[] {
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
}
const IsBlocksEnabled = isDev
type State = {
availableStackComponents: SNComponent[]
editorComponentViewer?: ComponentViewerInterface
@@ -1028,7 +1027,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
const editorMode =
IsBlocksEnabled && this.note.title.toLowerCase().includes('blocks')
featureTrunkEnabled(FeatureTrunkName.Blocks) && this.note.noteType === NoteType.Blocks
? 'blocks'
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
? 'plain'

View File

@@ -1,7 +1,8 @@
import { SNComponent } from '@standardnotes/snjs'
import { NoteType, SNComponent } from '@standardnotes/snjs'
export type EditorMenuItem = {
name: string
component?: SNComponent
isEntitled: boolean
noteType: NoteType
}

View File

@@ -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 PLAIN_EDITOR_NAME = 'Plain Text'
export const BLOCKS_EDITOR_NAME = 'Blocks'
export const SYNC_TIMEOUT_DEBOUNCE = 350
export const SYNC_TIMEOUT_NO_DEBOUNCE = 100

View 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]
}

View File

@@ -21,6 +21,7 @@ const LoggingStatus: Record<LoggingDomain, boolean> = {
[LoggingDomain.BlockEditor]: true,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function log(domain: LoggingDomain, ...args: any[]): void {
if (!isDev || !LoggingStatus[domain]) {
return

View File

@@ -1,39 +1,50 @@
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
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 BlocksType = 'blocks-editor'
export type EditorOption = DropdownItem & {
value: FeatureIdentifier | typeof PlainEditorType
value: FeatureIdentifier | typeof PlainEditorType | typeof BlocksType
}
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 plaintextOption: EditorOption = {
icon: 'plain-text',
iconClassName: 'text-accessory-tint-1',
label: PLAIN_EDITOR_NAME,
value: PlainEditorType,
}
return {
label: editor.displayName,
value: identifier,
...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
}
})
.concat([
{
icon: 'plain-text',
iconClassName: 'text-accessory-tint-1',
label: PLAIN_EDITOR_NAME,
value: PlainEditorType,
},
])
.sort((a, b) => {
return a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1
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)
return {
label: editor.displayName,
value: identifier,
...(iconType ? { icon: iconType } : null),
...(tint ? { iconClassName: `text-accessory-tint-${tint}` } : null),
}
})
options.push(plaintextOption)
if (featureTrunkEnabled(FeatureTrunkName.Blocks)) {
options.push({
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
}

View File

@@ -1,3 +1,4 @@
import { featureTrunkEnabled, FeatureTrunkName } from '@/FeatureTrunk'
import { WebApplication } from '@/Application/Application'
import {
ContentType,
@@ -12,9 +13,9 @@ import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
import { EditorMenuItem } from '@/Components/NotesOptions/EditorMenuItem'
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) {
return featureDescription.note_type
} else if (featureDescription.file_type) {
@@ -23,106 +24,154 @@ const getEditorGroup = (featureDescription: FeatureDescription): EditorGroup =>
return NoteType.RichText
case 'md':
return NoteType.Markdown
default:
return 'others'
}
}
return 'others'
return NoteType.Unknown
}
export const createEditorMenuGroups = (application: WebApplication, editors: SNComponent[]) => {
const editorItems: Record<EditorGroup, EditorMenuItem[]> = {
'plain-text': [
{
name: PLAIN_EDITOR_NAME,
isEntitled: true,
},
],
'rich-text': [],
markdown: [],
task: [],
code: [],
spreadsheet: [],
authentication: [],
others: [],
blocks: [],
}
const insertNonInstalledNativeComponentsInMap = (
map: NoteTypeToEditorRowsMap,
components: SNComponent[],
application: WebApplication,
): void => {
GetFeatures()
.filter((feature) => feature.content_type === ContentType.Component && feature.area === ComponentArea.Editor)
.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 isDeprecated = editorFeature.deprecated
const isShowable = notInstalled && !isExperimental && !isDeprecated
if (isShowable) {
editorItems[getEditorGroup(editorFeature)].push({
const noteType = getNoteTypeForFeatureDescription(editorFeature)
map[noteType].push({
name: editorFeature.name as string,
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 = {
name: editor.displayName,
component: editor,
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',
iconClassName: 'text-accessory-tint-1',
title: 'Plain text',
items: editorItems['plain-text'],
items: map[NoteType.Plain],
},
{
icon: 'rich-text',
iconClassName: 'text-accessory-tint-1',
title: 'Rich text',
items: editorItems['rich-text'],
items: map[NoteType.RichText],
},
{
icon: 'markdown',
iconClassName: 'text-accessory-tint-2',
title: 'Markdown text',
items: editorItems.markdown,
items: map[NoteType.Markdown],
},
{
icon: 'tasks',
iconClassName: 'text-accessory-tint-3',
title: 'Todo',
items: editorItems.task,
items: map[NoteType.Task],
},
{
icon: 'code',
iconClassName: 'text-accessory-tint-4',
title: 'Code',
items: editorItems.code,
items: map[NoteType.Code],
},
{
icon: 'spreadsheets',
iconClassName: 'text-accessory-tint-5',
title: 'Spreadsheet',
items: editorItems.spreadsheet,
items: map[NoteType.Spreadsheet],
},
{
icon: 'authenticator',
iconClassName: 'text-accessory-tint-6',
title: 'Authentication',
items: editorItems.authentication,
items: map[NoteType.Authentication],
},
{
icon: 'editor',
iconClassName: 'text-neutral',
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)
}