refactor: extract components to plugin repo (#1933)

This commit is contained in:
Mo
2022-11-04 11:04:53 -05:00
committed by GitHub
parent 5bba4820e4
commit 77d5093f14
1927 changed files with 1655 additions and 167892 deletions

View File

@@ -0,0 +1,36 @@
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'
type AddButtonProps = {
application: WebApplication
onSelectOption: (option: BlockOption) => void
}
export const AddBlockButton: FunctionComponent<AddButtonProps> = ({ application, onSelectOption }) => {
const [showMenu, setShowMenu] = useState(true)
const toggleMenu = useCallback(() => {
setShowMenu((prevValue) => !prevValue)
}, [])
return (
<div className="mt-2 flex flex-row flex-wrap">
<button
className={classNames(
'fixed bottom-6 right-6 z-editor-title-bar ml-3 flex h-15 w-15 cursor-pointer items-center',
`justify-center rounded-full border border-solid border-transparent ${'bg-info text-info-contrast'}`,
'hover:brightness-125 md:static md:h-8 md:w-8',
)}
onClick={toggleMenu}
>
<Icon type="add" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
</button>
{showMenu && <BlockMenu application={application} onSelectOption={onSelectOption} />}
</div>
)
}

View File

@@ -0,0 +1,37 @@
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 { MultiBlockRenderer } from './BlockRender/MultiBlockRenderer'
import { BlockOption } from './BlockMenu/BlockOption'
type Props = {
application: WebApplication
note: SNNote
}
export const BlockEditor: FunctionComponent<Props> = ({ note, application }) => {
const controller = useRef(new BlockEditorController(note, application))
const onSelectOption = useCallback(
(option: BlockOption) => {
void controller.current.addNewBlock(option)
},
[controller],
)
return (
<div className="w-full">
<AddBlockButton application={application} onSelectOption={onSelectOption} />
{note.blocksItem && (
<MultiBlockRenderer
controller={controller.current}
key={note.uuid}
blocksItem={note.blocksItem}
note={note}
application={application}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,54 @@
import { WebApplication } from '@/Application/Application'
import { NoteBlock, NoteMutator, SNComponent, SNNote } from '@standardnotes/snjs'
import { BlockOption } from './BlockMenu/BlockOption'
export class BlockEditorController {
constructor(private note: SNNote, private application: WebApplication) {
this.note = note
this.application = application
}
deinit() {
;(this.note as unknown) = undefined
;(this.application as unknown) = undefined
}
createBlockItem(editor: SNComponent): NoteBlock {
const id = this.application.generateUuid()
const block: NoteBlock = {
id: id,
editorIdentifier: editor.identifier,
type: editor.noteType,
content: '',
}
return block
}
async addNewBlock(option: BlockOption): Promise<void> {
if (!option.component) {
throw new Error('Non-component block options are not supported yet')
}
const block = this.createBlockItem(option.component)
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
mutator.addBlock(block)
})
}
async removeBlock(block: NoteBlock): Promise<void> {
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
mutator.removeBlock(block)
})
}
async saveBlockSize(block: NoteBlock, size: { width: number; height: number }): Promise<void> {
if (block.size?.height === size.height) {
return
}
await this.application.mutator.changeAndSaveItem<NoteMutator>(this.note, (mutator) => {
mutator.changeBlockSize(block.id, size)
})
}
}

View File

@@ -0,0 +1,24 @@
import { WebApplication } from '@/Application/Application'
import { ComponentArea } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import { BlockMenuOption } from './BlockMenuOption'
import { BlockOption } from './BlockOption'
import { componentToBlockOption } from './componentToBlockOption'
type BlockMenuProps = {
application: WebApplication
onSelectOption: (row: BlockOption) => void
}
export const BlockMenu: FunctionComponent<BlockMenuProps> = ({ application, onSelectOption }) => {
const components = application.componentManager.componentsForArea(ComponentArea.Editor)
const options = components.map((component) => componentToBlockOption(component, application.iconsController))
return (
<div className="flex flex-row flex-wrap">
{options.map((option) => {
return <BlockMenuOption option={option} key={option.identifier} onSelect={onSelectOption} />
})}
</div>
)
}

View File

@@ -0,0 +1,20 @@
import Icon from '@/Components/Icon/Icon'
import { FunctionComponent } from 'react'
import { BlockOption } from './BlockOption'
export type BlockMenuOptionProps = {
option: BlockOption
onSelect: (option: BlockOption) => void
}
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}
>
<Icon type={option.icon} size={'large'} />
<div className={'ml-3 text-base'}>{option.label}</div>
</div>
)
}

View File

@@ -0,0 +1,10 @@
import { IconType, SNComponent } from '@standardnotes/snjs'
export type BlockOption = {
editorIdentifier: string
label: string
identifier: string
icon: IconType | string
iconTint: number
component?: SNComponent
}

View File

@@ -0,0 +1,14 @@
import { IconsController, SNComponent } from '@standardnotes/snjs'
import { BlockOption } from './BlockOption'
export function componentToBlockOption(component: SNComponent, iconsController: IconsController): BlockOption {
const [iconType, tint] = iconsController.getIconAndTintForNoteType(component.package_info.note_type)
return {
identifier: component.uuid,
editorIdentifier: component.identifier,
label: component.name,
icon: iconType,
iconTint: tint,
}
}

View File

@@ -0,0 +1,3 @@
export const MaxBlockHeight: Record<string, number> = {
'org.standardnotes.standard-sheets': 300,
}

View File

@@ -0,0 +1,35 @@
import { WebApplication } from '@/Application/Application'
import { NoteBlocks, SNNote } from '@standardnotes/snjs'
import { FunctionComponent } from 'react'
import { BlockEditorController } from '../BlockEditorController'
import { SingleBlockRenderer } from './SingleBlockRenderer'
type MultiBlockRendererProps = {
application: WebApplication
note: SNNote
blocksItem: NoteBlocks
controller: BlockEditorController
}
export const MultiBlockRenderer: FunctionComponent<MultiBlockRendererProps> = ({
blocksItem,
controller,
note,
application,
}) => {
return (
<div className="w-full">
{blocksItem.blocks.map((block) => {
return (
<SingleBlockRenderer
key={block.id}
block={block}
note={note}
application={application}
controller={controller}
/>
)
})}
</div>
)
}

View File

@@ -0,0 +1,103 @@
import { WebApplication } from '@/Application/Application'
import { log, LoggingDomain } from '@/Logging'
import { ComponentAction, NoteBlock, SNNote } from '@standardnotes/snjs'
import { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import ComponentView from '../../ComponentView/ComponentView'
import { MaxBlockHeight } from './MaxBlockHeight'
import { BlockEditorController } from '../BlockEditorController'
import { classNames } from '@/Utils/ConcatenateClassNames'
import Icon from '@/Components/Icon/Icon'
type SingleBlockRendererProps = {
application: WebApplication
block: NoteBlock
note: SNNote
controller: BlockEditorController
}
export const SingleBlockRenderer: FunctionComponent<SingleBlockRendererProps> = ({
block,
application,
note,
controller,
}) => {
const [height, setHeight] = useState<number | undefined>(block.size?.height)
const [showCloseButton, setShowCloseButton] = useState(false)
const component = useMemo(
() => application.componentManager.componentWithIdentifier(block.editorIdentifier),
[block, application],
)
const viewer = useMemo(
() => component && application.componentManager.createBlockComponentViewer(component, note.uuid, block.id),
[application, component, note.uuid, block.id],
)
useEffect(() => {
const disposer = viewer?.addActionObserver((action, data) => {
if (action === ComponentAction.SetSize) {
if (data.height && data.height > 0) {
const height = Math.min(Number(data.height), MaxBlockHeight[block.editorIdentifier] ?? Number(data.height))
log(LoggingDomain.BlockEditor, `Received block height ${height}`)
setHeight(height)
void controller.saveBlockSize(block, { width: 0, height })
}
}
})
return disposer
}, [viewer, block, controller])
useEffect(() => {
return () => {
if (viewer) {
application.componentManager.destroyComponentViewer(viewer)
}
}
}, [application, viewer])
const onHoverEnter = useCallback(() => {
setShowCloseButton(true)
}, [])
const onHoverExit = useCallback(() => {
setShowCloseButton(false)
}, [])
const onRemoveBlock = useCallback(() => {
void controller.removeBlock(block)
}, [block, controller])
if (!component || !viewer) {
return <div>Unable to find component {block.editorIdentifier}</div>
}
const styles: Record<string, unknown> = {}
if (height) {
styles['height'] = height
}
return (
<div
onMouseEnter={onHoverEnter}
onMouseLeave={onHoverExit}
className="w-full border-info hover:border-[1px]"
style={styles}
>
{showCloseButton && (
<button
className={classNames(
'fixed bottom-6 right-6 z-editor-title-bar ml-3 flex h-15 w-15 cursor-pointer items-center',
`justify-center rounded-full border border-solid border-transparent ${'bg-info text-info-contrast'}`,
'hover:brightness-125 md:static md:h-8 md:w-8',
)}
onClick={onRemoveBlock}
>
<Icon type="remove" size="custom" className="h-8 w-8 md:h-5 md:w-5" />
</button>
)}
<ComponentView key={viewer.identifier} componentViewer={viewer} application={application} />
</div>
)
}

View File

@@ -45,6 +45,7 @@ export const createEditorMenuGroups = (application: WebApplication, editors: SNC
spreadsheet: [],
authentication: [],
others: [],
blocks: [],
}
GetFeatures()

View File

@@ -200,6 +200,7 @@ const ComponentView: FunctionComponent<IProps> = ({ application, onLoad, compone
{error === ComponentViewerError.MissingUrl && <UrlMissing componentName={component.displayName} />}
{component.uuid && isComponentValid && (
<iframe
className="h-full w-full grow bg-transparent"
ref={iframeRef}
onLoad={onIframeLoad}
data-component-viewer-id={componentViewer.identifier}

View File

@@ -13,16 +13,10 @@ import { PrefDefaults } from '@/Constants/PrefDefaults'
import Dropdown from '@/Components/Dropdown/Dropdown'
import { DropdownItem } from '@/Components/Dropdown/DropdownItem'
import { WebApplication } from '@/Application/Application'
import { PLAIN_EDITOR_NAME } from '@/Constants/Constants'
import { AnyTag } from '@/Controllers/Navigation/AnyTagType'
import { PreferenceMode } from './PreferenceMode'
import dayjs from 'dayjs'
const PlainEditorType = 'plain-editor'
type EditorOption = DropdownItem & {
value: FeatureIdentifier | typeof PlainEditorType
}
import { getDropdownItemsForAllEditors, PlainEditorType } from '@/Utils/DropdownItemsForEditors'
const PrefChangeDebounceTimeInMs = 25
@@ -139,32 +133,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
}
useEffect(() => {
const editors = 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),
}
})
.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
})
setEditorItems(editors)
setEditorItems(getDropdownItemsForAllEditors(application))
}, [application])
const setDefaultEditor = (value: string) => {

View File

@@ -9,7 +9,7 @@ import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { StringDeleteNote, STRING_DELETE_LOCKED_ATTEMPT, STRING_DELETE_PLACEHOLDER_ATTEMPT } from '@/Constants/Strings'
import { log, LoggingDomain } from '@/Logging'
import { debounce, isDesktopApplication, isMobileScreen, isTabletScreen } from '@/Utils'
import { debounce, isDesktopApplication, isDev, isMobileScreen, isTabletScreen } from '@/Utils'
import { classNames } from '@/Utils/ConcatenateClassNames'
import {
ApplicationEvent,
@@ -32,6 +32,7 @@ import {
import { confirmDialog, KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services'
import { ChangeEventHandler, createRef, KeyboardEventHandler, RefObject } from 'react'
import { EditorEventSource } from '../../Types/EditorEventSource'
import { BlockEditor } from '../BlockEditor/BlockEditor'
import IndicatorCircle from '../IndicatorCircle/IndicatorCircle'
import LinkedItemBubblesContainer from '../LinkedItems/LinkedItemBubblesContainer'
import LinkedItemsButton from '../LinkedItems/LinkedItemsButton'
@@ -54,6 +55,8 @@ 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
@@ -1024,6 +1027,15 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const renderHeaderOptions = isMobileScreen() ? !this.state.plaintextEditorFocused : true
const editorMode =
IsBlocksEnabled && this.note.title.toLowerCase().includes('blocks')
? 'blocks'
: this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading
? 'plain'
: this.state.editorComponentViewer
? 'component'
: 'plain'
return (
<div aria-label="Note" className="section editor sn-component h-full md:max-h-full" ref={this.noteViewElementRef}>
{this.note && (
@@ -1117,7 +1129,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
<div
id={ElementIds.EditorContent}
className={`${ElementIds.EditorContent} z-editor-content`}
className={`${ElementIds.EditorContent} z-editor-content overflow-scroll`}
ref={this.editorContentRef}
>
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
@@ -1134,7 +1146,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
/>
) : null}
{this.state.editorComponentViewer && (
{editorMode === 'component' && this.state.editorComponentViewer && (
<div className="component-view">
<ComponentView
key={this.state.editorComponentViewer.identifier}
@@ -1146,7 +1158,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
</div>
)}
{this.state.editorStateDidLoad && !this.state.editorComponentViewer && !this.state.textareaUnloading && (
{editorMode === 'plain' && (
<textarea
autoComplete="off"
dir="auto"
@@ -1166,6 +1178,12 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
></textarea>
)}
{editorMode === 'blocks' && (
<div className={classNames('blocks-editor w-full flex-grow overflow-hidden overflow-y-scroll')}>
<BlockEditor key={this.note.uuid} application={this.application} note={this.note} />
</div>
)}
{this.state.marginResizersEnabled && this.editorContentRef.current ? (
<PanelResizer
minWidth={300}

View File

@@ -2,9 +2,6 @@ import { WebApplication } from '@/Application/Application'
import { ClientDisplayableError, FeatureDescription } from '@standardnotes/snjs'
import { makeAutoObservable, observable } from 'mobx'
import { AnyPackageType } from '../Types/AnyPackageType'
import { ComponentChecksumsType } from '@standardnotes/components-meta'
import RawComponentChecksumsFile from '@standardnotes/components-meta/dist/zips/checksums.json'
const ComponentChecksums = RawComponentChecksumsFile as ComponentChecksumsType
export class PackageProvider {
static async load(application: WebApplication): Promise<PackageProvider | undefined> {
@@ -38,6 +35,6 @@ function collectFeatures(features: FeatureDescription[] | undefined, versionMap:
}
for (const feature of features) {
versionMap.set(feature.identifier, ComponentChecksums[feature.identifier].version)
versionMap.set(feature.identifier, 'Latest')
}
}