refactor: native feature management (#2350)

This commit is contained in:
Mo
2023-07-12 12:56:08 -05:00
committed by GitHub
parent 49f7581cd8
commit 078ef3772c
223 changed files with 3996 additions and 3438 deletions

View File

@@ -58,7 +58,7 @@ describe('note view controller', () => {
identifier: FeatureIdentifier.MarkdownProEditor,
} as SNComponent)
componentManager.componentWithIdentifier = jest.fn().mockReturnValue({
componentManager.componentOrNativeFeatureForIdentifier = jest.fn().mockReturnValue({
identifier: FeatureIdentifier.MarkdownProEditor,
} as SNComponent)

View File

@@ -105,7 +105,7 @@ const NoteConflictResolutionModal = ({
mutator.conflictOf = undefined
})
setIsPerformingAction(false)
void application.getViewControllerManager().selectionController.selectItem(selectedNotes[0].uuid, true)
void application.controllers.selectionController.selectItem(selectedNotes[0].uuid, true)
void application.sync.sync()
close()
}

View File

@@ -1,8 +1,7 @@
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { classNames } from '@standardnotes/utils'
import { ReactNode, useCallback, useState } from 'react'
import { IconType, PrefKey } from '@standardnotes/snjs'
import { IconType, PrefKey, PrefDefaults } from '@standardnotes/snjs'
import Icon from '../Icon/Icon'
import { useApplication } from '../ApplicationProvider'

View File

@@ -41,8 +41,9 @@ describe('NoteView', () => {
notesController: notesController,
} as jest.Mocked<ViewControllerManager>
application = {} as jest.Mocked<WebApplication>
application.getViewControllerManager = jest.fn().mockReturnValue(viewControllerManager)
application = {
controllers: viewControllerManager,
} as jest.Mocked<WebApplication>
application.hasProtectionSources = jest.fn().mockReturnValue(true)
application.authorizeNoteAccess = jest.fn()
application.addWebEventObserver = jest.fn()

View File

@@ -1,11 +1,10 @@
import { AbstractComponent } from '@/Components/Abstract/PureComponent'
import ChangeEditorButton from '@/Components/ChangeEditor/ChangeEditorButton'
import ComponentView from '@/Components/ComponentView/ComponentView'
import IframeFeatureView from '@/Components/ComponentView/IframeFeatureView'
import NotesOptionsPanel from '@/Components/NotesOptions/NotesOptionsPanel'
import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton'
import ProtectedItemOverlay from '@/Components/ProtectedItemOverlay/ProtectedItemOverlay'
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 } from '@/Utils'
@@ -13,16 +12,20 @@ import { classNames, pluralize } from '@standardnotes/utils'
import {
ApplicationEvent,
ComponentArea,
ComponentInterface,
ComponentOrNativeFeature,
ComponentViewerInterface,
ContentType,
EditorLineWidth,
IframeComponentFeatureDescription,
isIframeUIFeature,
isPayloadSourceInternalChange,
isPayloadSourceRetrieved,
NoteType,
PayloadEmitSource,
PrefDefaults,
PrefKey,
ProposedSecondsToDeferUILevelSessionExpirationDuringActiveInteraction,
SNComponent,
SNNote,
} from '@standardnotes/snjs'
import { confirmDialog, DELETE_NOTE_KEYBOARD_COMMAND, KeyboardKey } from '@standardnotes/ui-services'
@@ -53,12 +56,12 @@ import Icon from '../Icon/Icon'
const MinimumStatusDuration = 400
function sortAlphabetically(array: SNComponent[]): SNComponent[] {
function sortAlphabetically(array: ComponentInterface[]): ComponentInterface[] {
return array.sort((a, b) => (a.name.toLowerCase() < b.name.toLowerCase() ? -1 : 1))
}
type State = {
availableStackComponents: SNComponent[]
availableStackComponents: ComponentInterface[]
editorComponentViewer?: ComponentViewerInterface
editorComponentViewerDidAlreadyReload?: boolean
editorStateDidLoad: boolean
@@ -443,14 +446,21 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
)
this.removeNoteStreamObserver = this.application.streamItems<SNNote>(ContentType.TYPES.Note, async () => {
if (!this.note) {
return
}
this.setState({
conflictedNotes: this.application.items.conflictsOf(this.note.uuid) as SNNote[],
})
})
}
private createComponentViewer(component: SNComponent) {
const viewer = this.application.componentManager.createComponentViewer(component, this.note.uuid)
private createComponentViewer(component: ComponentOrNativeFeature<IframeComponentFeatureDescription>) {
if (!component) {
throw Error('Cannot create component viewer for undefined component')
}
const viewer = this.application.componentManager.createComponentViewer(component, { uuid: this.note.uuid })
return viewer
}
@@ -462,7 +472,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
return
}
const component = viewer.component
const component = viewer.getComponentOrFeatureItem()
this.application.componentManager.destroyComponentViewer(viewer)
this.setState(
{
@@ -496,35 +506,36 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
}
}
async reloadEditorComponent() {
async reloadEditorComponent(): Promise<void> {
log(LoggingDomain.NoteView, 'Reload editor component')
if (this.state.showProtectedWarning) {
this.destroyCurrentEditorComponent()
return
}
const newEditor = this.application.componentManager.editorForNote(this.note)
const newUIFeature = this.application.componentManager.editorForNote(this.note)
/** Editors cannot interact with template notes so the note must be inserted */
if (newEditor && this.controller.isTemplateNote) {
/** Component editors cannot interact with template notes so the note must be inserted */
if (isIframeUIFeature(newUIFeature) && this.controller.isTemplateNote) {
await this.controller.insertTemplatedNote()
}
const currentComponentViewer = this.state.editorComponentViewer
if (currentComponentViewer?.componentUuid !== newEditor?.uuid) {
if (currentComponentViewer) {
if (currentComponentViewer) {
const needsDestroy = currentComponentViewer.componentUniqueIdentifier !== newUIFeature.uniqueIdentifier
if (needsDestroy) {
this.destroyCurrentEditorComponent()
}
}
if (newEditor) {
this.setState({
editorComponentViewer: this.createComponentViewer(newEditor),
editorStateDidLoad: true,
})
}
reloadFont(this.state.monospaceFont)
if (isIframeUIFeature(newUIFeature)) {
this.setState({
editorComponentViewer: this.createComponentViewer(newUIFeature),
editorStateDidLoad: true,
})
} else {
reloadFont(this.state.monospaceFont)
this.setState({
editorStateDidLoad: true,
})
@@ -731,8 +742,8 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
log(LoggingDomain.NoteView, 'Reload stack components')
const stackComponents = sortAlphabetically(
this.application.componentManager
.componentsForArea(ComponentArea.EditorStack)
.filter((component) => component.active),
.thirdPartyComponentsForArea(ComponentArea.EditorStack)
.filter((component) => this.application.componentManager.isComponentActive(component)),
)
const enabledComponents = stackComponents.filter((component) => {
return component.isExplicitlyEnabledForItem(this.note.uuid)
@@ -740,21 +751,28 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
const needsNewViewer = enabledComponents.filter((component) => {
const hasExistingViewer = this.state.stackComponentViewers.find(
(viewer) => viewer.componentUuid === component.uuid,
(viewer) => viewer.componentUniqueIdentifier === component.uuid,
)
return !hasExistingViewer
})
const needsDestroyViewer = this.state.stackComponentViewers.filter((viewer) => {
const viewerComponentExistsInEnabledComponents = enabledComponents.find((component) => {
return component.uuid === viewer.componentUuid
return component.uuid === viewer.componentUniqueIdentifier
})
return !viewerComponentExistsInEnabledComponents
})
const newViewers: ComponentViewerInterface[] = []
for (const component of needsNewViewer) {
newViewers.push(this.application.componentManager.createComponentViewer(component, this.note.uuid))
newViewers.push(
this.application.componentManager.createComponentViewer(
new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component),
{
uuid: this.note.uuid,
},
),
)
}
for (const viewer of needsDestroyViewer) {
@@ -766,11 +784,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
})
}
stackComponentExpanded = (component: SNComponent): boolean => {
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUuid === component.uuid)
stackComponentExpanded = (component: ComponentInterface): boolean => {
return !!this.state.stackComponentViewers.find((viewer) => viewer.componentUniqueIdentifier === component.uuid)
}
toggleStackComponent = async (component: SNComponent) => {
toggleStackComponent = async (component: ComponentInterface) => {
if (!component.isExplicitlyEnabledForItem(this.note.uuid)) {
await this.application.mutator.runTransactionalMutation(
transactionForAssociateComponentWithCurrentNote(component, this.note),
@@ -962,12 +980,11 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
{editorMode === 'component' && this.state.editorComponentViewer && (
<div className="component-view relative flex-grow">
{this.state.paneGestureEnabled && <div className="absolute top-0 left-0 h-full w-[20px] md:hidden" />}
<ComponentView
<IframeFeatureView
key={this.state.editorComponentViewer.identifier}
componentViewer={this.state.editorComponentViewer}
onLoad={this.onEditorComponentLoad}
requestReload={this.editorComponentViewerRequestsReload}
application={this.application}
/>
</div>
)}
@@ -1006,6 +1023,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
>
<div className="flex h-full">
{this.state.availableStackComponents.map((component) => {
const active = this.application.componentManager.isComponentActive(component)
return (
<div
key={component.uuid}
@@ -1015,7 +1033,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
className="flex flex-grow cursor-pointer items-center justify-center [&:not(:first-child)]:ml-3"
>
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
{this.stackComponentExpanded(component) && component.active && <IndicatorCircle style="info" />}
{this.stackComponentExpanded(component) && active && <IndicatorCircle style="info" />}
{!this.stackComponentExpanded(component) && <IndicatorCircle style="neutral" />}
</div>
<div className="flex h-full items-center [&:not(:first-child)]:ml-2">
@@ -1032,7 +1050,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
{this.state.stackComponentViewers.map((viewer) => {
return (
<div className="component-view component-stack-item" key={viewer.identifier}>
<ComponentView key={viewer.identifier} componentViewer={viewer} application={this.application} />
<IframeFeatureView key={viewer.identifier} componentViewer={viewer} />
</div>
)
})}

View File

@@ -1,7 +1,6 @@
import { WebApplication } from '@/Application/WebApplication'
import { usePrevious } from '@/Components/ContentListView/Calendar/usePrevious'
import { ElementIds } from '@/Constants/ElementIDs'
import { PrefDefaults } from '@/Constants/PrefDefaults'
import { log, LoggingDomain } from '@/Logging'
import { Disposer } from '@/Types/Disposer'
import { EditorEventSource } from '@/Types/EditorEventSource'
@@ -14,6 +13,7 @@ import {
isPayloadSourceRetrieved,
PrefKey,
WebAppEvent,
PrefDefaults,
} from '@standardnotes/snjs'
import { TAB_COMMAND } from '@standardnotes/ui-services'
import { ChangeEventHandler, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react'

View File

@@ -1,8 +1,8 @@
import { ContentType, NoteContent, NoteType, SNNote, classNames } from '@standardnotes/snjs'
import { ContentType, NoteContent, NoteType, SNNote, classNames, isIframeUIFeature } from '@standardnotes/snjs'
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
import { useApplication } from '../ApplicationProvider'
import ComponentView from '../ComponentView/ComponentView'
import IframeFeatureView from '../ComponentView/IframeFeatureView'
import { ErrorBoundary } from '@/Utils/ErrorBoundary'
import { BlocksEditor } from '../SuperEditor/BlocksEditor'
import { BlocksEditorComposer } from '../SuperEditor/BlocksEditorComposer'
@@ -30,18 +30,17 @@ export const ReadonlyNoteContent = ({
const isMobileScreen = useMediaQuery(MutuallyExclusiveMediaQueryBreakpoints.sm)
const componentViewer = useMemo(() => {
const editorForCurrentNote = note ? application.componentManager.editorForNote(note) : undefined
if (!editorForCurrentNote) {
const editorForCurrentNote = application.componentManager.editorForNote(note)
if (!isIframeUIFeature(editorForCurrentNote)) {
return undefined
}
const templateNoteForRevision = application.items.createTemplateItem(ContentType.TYPES.Note, note.content) as SNNote
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote)
componentViewer.setReadonly(true)
componentViewer.lockReadonly = true
componentViewer.overrideContextItem = templateNoteForRevision
const componentViewer = application.componentManager.createComponentViewer(editorForCurrentNote, {
readonlyItem: templateNoteForRevision,
})
return componentViewer
}, [application.componentManager, application.items, note])
@@ -91,7 +90,7 @@ export const ReadonlyNoteContent = ({
)}
{componentViewer ? (
<div className="component-view">
<ComponentView key={componentViewer.identifier} componentViewer={componentViewer} application={application} />
<IframeFeatureView key={componentViewer.identifier} componentViewer={componentViewer} />
</div>
) : content.noteType === NoteType.Super ? (
<ErrorBoundary>

View File

@@ -1,6 +1,6 @@
import { SNComponent, SNNote, ComponentMutator, TransactionalMutation, ItemMutator } from '@standardnotes/snjs'
import { SNNote, ComponentMutator, TransactionalMutation, ItemMutator, ComponentInterface } from '@standardnotes/snjs'
export const transactionForAssociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
export const transactionForAssociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
const transaction: TransactionalMutation = {
itemUuid: component.uuid,
mutate: (m: ItemMutator) => {
@@ -12,7 +12,7 @@ export const transactionForAssociateComponentWithCurrentNote = (component: SNCom
return transaction
}
export const transactionForDisassociateComponentWithCurrentNote = (component: SNComponent, note: SNNote) => {
export const transactionForDisassociateComponentWithCurrentNote = (component: ComponentInterface, note: SNNote) => {
const transaction: TransactionalMutation = {
itemUuid: component.uuid,
mutate: (m: ItemMutator) => {