mirror of
https://github.com/standardnotes/web.git
synced 2024-10-27 16:31:40 +03:00
refactor: component manager usecases (#2354)
This commit is contained in:
parent
ecc5b5e503
commit
2c68ea1d76
@ -9,6 +9,7 @@ import { experimentalFeatures } from '../Lists/ExperimentalFeatures'
|
|||||||
import { IframeEditors } from '../Lists/IframeEditors'
|
import { IframeEditors } from '../Lists/IframeEditors'
|
||||||
import { themes } from '../Lists/Themes'
|
import { themes } from '../Lists/Themes'
|
||||||
import { nativeEditors } from '../Lists/NativeEditors'
|
import { nativeEditors } from '../Lists/NativeEditors'
|
||||||
|
import { IframeComponentFeatureDescription } from './IframeComponentFeatureDescription'
|
||||||
|
|
||||||
export function GetFeatures(): AnyFeatureDescription[] {
|
export function GetFeatures(): AnyFeatureDescription[] {
|
||||||
return [
|
return [
|
||||||
@ -30,10 +31,14 @@ export function FindNativeTheme(identifier: FeatureIdentifier): ThemeFeatureDesc
|
|||||||
return themes().find((t) => t.identifier === identifier)
|
return themes().find((t) => t.identifier === identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetIframeAndNativeEditors(): EditorFeatureDescription[] {
|
export function GetIframeAndNativeEditors(): (IframeComponentFeatureDescription | EditorFeatureDescription)[] {
|
||||||
return [...IframeEditors(), ...nativeEditors()]
|
return [...IframeEditors(), ...nativeEditors()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GetIframeEditors(): IframeComponentFeatureDescription[] {
|
||||||
|
return IframeEditors()
|
||||||
|
}
|
||||||
|
|
||||||
export function GetSuperNoteFeature(): EditorFeatureDescription {
|
export function GetSuperNoteFeature(): EditorFeatureDescription {
|
||||||
return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription
|
return FindNativeFeature(FeatureIdentifier.SuperEditor) as EditorFeatureDescription
|
||||||
}
|
}
|
||||||
|
63
packages/features/src/Domain/Feature/TypeGuards.spec.ts
Normal file
63
packages/features/src/Domain/Feature/TypeGuards.spec.ts
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
|
import { AnyFeatureDescription } from './AnyFeatureDescription'
|
||||||
|
import { ComponentArea } from '../Component/ComponentArea'
|
||||||
|
|
||||||
|
import {
|
||||||
|
isThemeFeatureDescription,
|
||||||
|
isIframeComponentFeatureDescription,
|
||||||
|
isEditorFeatureDescription,
|
||||||
|
} from './TypeGuards'
|
||||||
|
import { ThemeFeatureDescription } from './ThemeFeatureDescription'
|
||||||
|
import { IframeComponentFeatureDescription } from './IframeComponentFeatureDescription'
|
||||||
|
|
||||||
|
describe('TypeGuards', () => {
|
||||||
|
describe('isThemeFeatureDescription', () => {
|
||||||
|
it('should return true if feature is ThemeFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
content_type: ContentType.TYPES.Theme,
|
||||||
|
} as jest.Mocked<ThemeFeatureDescription>
|
||||||
|
expect(isThemeFeatureDescription(feature)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if feature is not ThemeFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
content_type: ContentType.TYPES.Component,
|
||||||
|
} as jest.Mocked<ThemeFeatureDescription>
|
||||||
|
expect(isThemeFeatureDescription(feature)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isIframeComponentFeatureDescription', () => {
|
||||||
|
it('should return true if feature is IframeComponentFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
content_type: ContentType.TYPES.Component,
|
||||||
|
area: ComponentArea.Editor,
|
||||||
|
} as jest.Mocked<IframeComponentFeatureDescription>
|
||||||
|
expect(isIframeComponentFeatureDescription(feature)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if feature is not IframeComponentFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
content_type: ContentType.TYPES.Theme,
|
||||||
|
} as jest.Mocked<IframeComponentFeatureDescription>
|
||||||
|
expect(isIframeComponentFeatureDescription(feature)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isEditorFeatureDescription', () => {
|
||||||
|
it('should return true if feature is EditorFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
note_type: 'test',
|
||||||
|
area: ComponentArea.Editor,
|
||||||
|
} as unknown as jest.Mocked<AnyFeatureDescription>
|
||||||
|
expect(isEditorFeatureDescription(feature)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if feature is not EditorFeatureDescription', () => {
|
||||||
|
const feature = {
|
||||||
|
content_type: ContentType.TYPES.Theme,
|
||||||
|
} as jest.Mocked<AnyFeatureDescription>
|
||||||
|
expect(isEditorFeatureDescription(feature)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -512,15 +512,15 @@ export class MobileDevice implements MobileDeviceInterface {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
addComponentUrl(componentUuid: UuidString, componentUrl: string) {
|
registerComponentUrl(componentUuid: UuidString, componentUrl: string) {
|
||||||
this.componentUrls.set(componentUuid, componentUrl)
|
this.componentUrls.set(componentUuid, componentUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeComponentUrl(componentUuid: UuidString) {
|
deregisterComponentUrl(componentUuid: UuidString) {
|
||||||
this.componentUrls.delete(componentUuid)
|
this.componentUrls.delete(componentUuid)
|
||||||
}
|
}
|
||||||
|
|
||||||
isUrlComponentUrl(url: string): boolean {
|
isUrlRegisteredComponentUrl(url: string): boolean {
|
||||||
return Array.from(this.componentUrls.values()).includes(url)
|
return Array.from(this.componentUrls.values()).includes(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { ApplicationEvent, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
import { ApplicationEvent, ReactNativeToWebEvent } from '@standardnotes/snjs'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Button, Keyboard, Platform, Text, View } from 'react-native'
|
import { Button, Keyboard, Platform, Text, View } from 'react-native'
|
||||||
@ -239,6 +241,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
void onFunctionMessage(functionData.functionName, functionData.messageId, functionData.args)
|
void onFunctionMessage(functionData.functionName, functionData.messageId, functionData.args)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (LoggingEnabled) {
|
if (LoggingEnabled) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log('onGeneralMessage', JSON.stringify(message))
|
console.log('onGeneralMessage', JSON.stringify(message))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -247,6 +250,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
const onFunctionMessage = async (functionName: string, messageId: string, args: any) => {
|
const onFunctionMessage = async (functionName: string, messageId: string, args: any) => {
|
||||||
const returnValue = await (device as any)[functionName](...args)
|
const returnValue = await (device as any)[functionName](...args)
|
||||||
if (LoggingEnabled && functionName !== 'consoleLog') {
|
if (LoggingEnabled && functionName !== 'consoleLog') {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.log(`Native device function ${functionName} called`)
|
console.log(`Native device function ${functionName} called`)
|
||||||
}
|
}
|
||||||
webViewRef.current?.postMessage(JSON.stringify({ messageId, returnValue, messageType: 'reply' }))
|
webViewRef.current?.postMessage(JSON.stringify({ messageId, returnValue, messageType: 'reply' }))
|
||||||
@ -270,7 +274,7 @@ const MobileWebAppContents = ({ destroyAndReload }: { destroyAndReload: () => vo
|
|||||||
(Platform.OS === 'ios' && request.navigationType === 'click') ||
|
(Platform.OS === 'ios' && request.navigationType === 'click') ||
|
||||||
(Platform.OS === 'android' && request.url !== sourceUri)
|
(Platform.OS === 'android' && request.url !== sourceUri)
|
||||||
|
|
||||||
const isComponentUrl = device.isUrlComponentUrl(request.url)
|
const isComponentUrl = device.isUrlRegisteredComponentUrl(request.url)
|
||||||
|
|
||||||
if (shouldStopRequest && !isComponentUrl) {
|
if (shouldStopRequest && !isComponentUrl) {
|
||||||
device.openUrl(request.url)
|
device.openUrl(request.url)
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import {
|
||||||
|
AnyFeatureDescription,
|
||||||
|
ComponentArea,
|
||||||
|
EditorFeatureDescription,
|
||||||
|
IframeComponentFeatureDescription,
|
||||||
|
NoteType,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import {
|
||||||
|
isUIFeatureAnIframeFeature,
|
||||||
|
isComponentOrFeatureDescriptionAComponent,
|
||||||
|
isComponentOrFeatureDescriptionAFeatureDescription,
|
||||||
|
} from './TypeGuards'
|
||||||
|
import { UIFeature } from './UIFeature'
|
||||||
|
import { ComponentInterface } from '../../Syncable/Component'
|
||||||
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
|
describe('TypeGuards', () => {
|
||||||
|
describe('isUIFeatureAnIframeFeature', () => {
|
||||||
|
it('should return true if feature is IframeUIFeature', () => {
|
||||||
|
const x: UIFeature<IframeComponentFeatureDescription> = {
|
||||||
|
featureDescription: {
|
||||||
|
content_type: ContentType.TYPES.Component,
|
||||||
|
area: ComponentArea.Editor,
|
||||||
|
},
|
||||||
|
} as jest.Mocked<UIFeature<IframeComponentFeatureDescription>>
|
||||||
|
|
||||||
|
expect(isUIFeatureAnIframeFeature(x)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if feature is not IframeUIFeature', () => {
|
||||||
|
const x: UIFeature<EditorFeatureDescription> = {
|
||||||
|
featureDescription: {
|
||||||
|
note_type: NoteType.Super,
|
||||||
|
},
|
||||||
|
} as jest.Mocked<UIFeature<EditorFeatureDescription>>
|
||||||
|
|
||||||
|
expect(isUIFeatureAnIframeFeature(x)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isFeatureAComponent', () => {
|
||||||
|
it('should return true if feature is a Component', () => {
|
||||||
|
const x: ComponentInterface | UIFeatureDescriptionTypes = {
|
||||||
|
uuid: 'abc-123',
|
||||||
|
} as ComponentInterface
|
||||||
|
|
||||||
|
expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if feature description is not a component', () => {
|
||||||
|
const x: EditorFeatureDescription = {
|
||||||
|
note_type: NoteType.Super,
|
||||||
|
} as jest.Mocked<EditorFeatureDescription>
|
||||||
|
|
||||||
|
expect(isComponentOrFeatureDescriptionAComponent(x)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('isComponentOrFeatureDescriptionAFeatureDescription', () => {
|
||||||
|
it('should return true if x is a feature description', () => {
|
||||||
|
const x: AnyFeatureDescription = {
|
||||||
|
content_type: 'TestContentType',
|
||||||
|
} as AnyFeatureDescription
|
||||||
|
|
||||||
|
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return false if x is a component', () => {
|
||||||
|
const x: ComponentInterface = {
|
||||||
|
uuid: 'abc-123',
|
||||||
|
} as ComponentInterface
|
||||||
|
|
||||||
|
expect(isComponentOrFeatureDescriptionAFeatureDescription(x)).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
27
packages/models/src/Domain/Runtime/Feature/TypeGuards.ts
Normal file
27
packages/models/src/Domain/Runtime/Feature/TypeGuards.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import {
|
||||||
|
AnyFeatureDescription,
|
||||||
|
EditorFeatureDescription,
|
||||||
|
IframeComponentFeatureDescription,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
isIframeComponentFeatureDescription,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { UIFeatureInterface } from './UIFeatureInterface'
|
||||||
|
import { ComponentInterface } from '../../Syncable/Component'
|
||||||
|
|
||||||
|
export function isUIFeatureAnIframeFeature(
|
||||||
|
x: UIFeatureInterface<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
||||||
|
): x is UIFeatureInterface<IframeComponentFeatureDescription> {
|
||||||
|
return isIframeComponentFeatureDescription(x.featureDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isComponentOrFeatureDescriptionAComponent(
|
||||||
|
x: ComponentInterface | UIFeatureDescriptionTypes,
|
||||||
|
): x is ComponentInterface {
|
||||||
|
return 'uuid' in x
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isComponentOrFeatureDescriptionAFeatureDescription(
|
||||||
|
x: ComponentInterface | AnyFeatureDescription,
|
||||||
|
): x is AnyFeatureDescription {
|
||||||
|
return !('uuid' in x)
|
||||||
|
}
|
@ -1,10 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
AnyFeatureDescription,
|
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentPermission,
|
ComponentPermission,
|
||||||
EditorFeatureDescription,
|
EditorFeatureDescription,
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
IframeComponentFeatureDescription,
|
|
||||||
NoteType,
|
NoteType,
|
||||||
ThemeDockIcon,
|
ThemeDockIcon,
|
||||||
UIFeatureDescriptionTypes,
|
UIFeatureDescriptionTypes,
|
||||||
@ -12,40 +10,31 @@ import {
|
|||||||
isIframeComponentFeatureDescription,
|
isIframeComponentFeatureDescription,
|
||||||
isThemeFeatureDescription,
|
isThemeFeatureDescription,
|
||||||
} from '@standardnotes/features'
|
} from '@standardnotes/features'
|
||||||
import { ComponentInterface } from './ComponentInterface'
|
import { ComponentInterface } from '../../Syncable/Component/ComponentInterface'
|
||||||
import { isTheme } from '../Theme'
|
import { isTheme } from '../../Syncable/Theme'
|
||||||
|
import {
|
||||||
|
isComponentOrFeatureDescriptionAComponent,
|
||||||
|
isComponentOrFeatureDescriptionAFeatureDescription,
|
||||||
|
} from './TypeGuards'
|
||||||
|
import { UIFeatureInterface } from './UIFeatureInterface'
|
||||||
|
|
||||||
function isComponent(x: ComponentInterface | UIFeatureDescriptionTypes): x is ComponentInterface {
|
export class UIFeature<F extends UIFeatureDescriptionTypes> implements UIFeatureInterface<F> {
|
||||||
return 'uuid' in x
|
|
||||||
}
|
|
||||||
|
|
||||||
function isFeatureDescription(x: ComponentInterface | AnyFeatureDescription): x is AnyFeatureDescription {
|
|
||||||
return !('uuid' in x)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isIframeUIFeature(
|
|
||||||
x: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
|
||||||
): x is ComponentOrNativeFeature<IframeComponentFeatureDescription> {
|
|
||||||
return isIframeComponentFeatureDescription(x.featureDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|
||||||
constructor(public readonly item: ComponentInterface | F) {}
|
constructor(public readonly item: ComponentInterface | F) {}
|
||||||
|
|
||||||
get isComponent(): boolean {
|
get isComponent(): boolean {
|
||||||
return isComponent(this.item)
|
return isComponentOrFeatureDescriptionAComponent(this.item)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isFeatureDescription(): boolean {
|
get isFeatureDescription(): boolean {
|
||||||
return isFeatureDescription(this.item)
|
return isComponentOrFeatureDescriptionAFeatureDescription(this.item)
|
||||||
}
|
}
|
||||||
|
|
||||||
get isThemeComponent(): boolean {
|
get isThemeComponent(): boolean {
|
||||||
return isComponent(this.item) && isTheme(this.item)
|
return isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)
|
||||||
}
|
}
|
||||||
|
|
||||||
get asComponent(): ComponentInterface {
|
get asComponent(): ComponentInterface {
|
||||||
if (isComponent(this.item)) {
|
if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||||
return this.item
|
return this.item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +42,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get asFeatureDescription(): F {
|
get asFeatureDescription(): F {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item
|
return this.item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +50,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get uniqueIdentifier(): string {
|
get uniqueIdentifier(): string {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item.identifier
|
return this.item.identifier
|
||||||
} else {
|
} else {
|
||||||
return this.item.uuid
|
return this.item.uuid
|
||||||
@ -73,9 +62,9 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get noteType(): NoteType {
|
get noteType(): NoteType {
|
||||||
if (isFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
||||||
return this.item.note_type ?? NoteType.Unknown
|
return this.item.note_type ?? NoteType.Unknown
|
||||||
} else if (isComponent(this.item)) {
|
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||||
return this.item.noteType
|
return this.item.noteType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +72,12 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get fileType(): EditorFeatureDescription['file_type'] {
|
get fileType(): EditorFeatureDescription['file_type'] {
|
||||||
if (isFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item) && isEditorFeatureDescription(this.item)) {
|
||||||
return this.item.file_type
|
return this.item.file_type
|
||||||
} else if (isComponent(this.item) && isEditorFeatureDescription(this.item.package_info)) {
|
} else if (
|
||||||
|
isComponentOrFeatureDescriptionAComponent(this.item) &&
|
||||||
|
isEditorFeatureDescription(this.item.package_info)
|
||||||
|
) {
|
||||||
return this.item.package_info?.file_type ?? 'txt'
|
return this.item.package_info?.file_type ?? 'txt'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +85,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get displayName(): string {
|
get displayName(): string {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item.name ?? ''
|
return this.item.name ?? ''
|
||||||
} else {
|
} else {
|
||||||
return this.item.displayName
|
return this.item.displayName
|
||||||
@ -101,7 +93,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get description(): string {
|
get description(): string {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item.description ?? ''
|
return this.item.description ?? ''
|
||||||
} else {
|
} else {
|
||||||
return this.item.package_info.description ?? ''
|
return this.item.package_info.description ?? ''
|
||||||
@ -109,7 +101,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get deprecationMessage(): string | undefined {
|
get deprecationMessage(): string | undefined {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item.deprecation_message
|
return this.item.deprecation_message
|
||||||
} else {
|
} else {
|
||||||
return this.item.deprecationMessage
|
return this.item.deprecationMessage
|
||||||
@ -117,7 +109,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get expirationDate(): Date | undefined {
|
get expirationDate(): Date | undefined {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item.expires_at ? new Date(this.item.expires_at) : undefined
|
return this.item.expires_at ? new Date(this.item.expires_at) : undefined
|
||||||
} else {
|
} else {
|
||||||
return this.item.valid_until
|
return this.item.valid_until
|
||||||
@ -125,7 +117,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get featureDescription(): F {
|
get featureDescription(): F {
|
||||||
if (isFeatureDescription(this.item)) {
|
if (isComponentOrFeatureDescriptionAFeatureDescription(this.item)) {
|
||||||
return this.item
|
return this.item
|
||||||
} else {
|
} else {
|
||||||
return this.item.package_info as F
|
return this.item.package_info as F
|
||||||
@ -133,9 +125,12 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get acquiredPermissions(): ComponentPermission[] {
|
get acquiredPermissions(): ComponentPermission[] {
|
||||||
if (isFeatureDescription(this.item) && isIframeComponentFeatureDescription(this.item)) {
|
if (
|
||||||
|
isComponentOrFeatureDescriptionAFeatureDescription(this.item) &&
|
||||||
|
isIframeComponentFeatureDescription(this.item)
|
||||||
|
) {
|
||||||
return this.item.component_permissions ?? []
|
return this.item.component_permissions ?? []
|
||||||
} else if (isComponent(this.item)) {
|
} else if (isComponentOrFeatureDescriptionAComponent(this.item)) {
|
||||||
return this.item.permissions
|
return this.item.permissions
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +146,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get layerable(): boolean {
|
get layerable(): boolean {
|
||||||
if (isComponent(this.item) && isTheme(this.item)) {
|
if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) {
|
||||||
return this.item.layerable
|
return this.item.layerable
|
||||||
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
||||||
return this.asFeatureDescription.layerable ?? false
|
return this.asFeatureDescription.layerable ?? false
|
||||||
@ -161,7 +156,7 @@ export class ComponentOrNativeFeature<F extends UIFeatureDescriptionTypes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get dockIcon(): ThemeDockIcon | undefined {
|
get dockIcon(): ThemeDockIcon | undefined {
|
||||||
if (isComponent(this.item) && isTheme(this.item)) {
|
if (isComponentOrFeatureDescriptionAComponent(this.item) && isTheme(this.item)) {
|
||||||
return this.item.package_info.dock_icon
|
return this.item.package_info.dock_icon
|
||||||
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
} else if (isThemeFeatureDescription(this.asFeatureDescription)) {
|
||||||
return this.asFeatureDescription.dock_icon
|
return this.asFeatureDescription.dock_icon
|
@ -0,0 +1,32 @@
|
|||||||
|
import {
|
||||||
|
ComponentArea,
|
||||||
|
ComponentPermission,
|
||||||
|
EditorFeatureDescription,
|
||||||
|
FeatureIdentifier,
|
||||||
|
NoteType,
|
||||||
|
ThemeDockIcon,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { ComponentInterface } from '../../Syncable/Component'
|
||||||
|
|
||||||
|
export interface UIFeatureInterface<F extends UIFeatureDescriptionTypes> {
|
||||||
|
item: ComponentInterface | F
|
||||||
|
get isComponent(): boolean
|
||||||
|
get isFeatureDescription(): boolean
|
||||||
|
get isThemeComponent(): boolean
|
||||||
|
get asComponent(): ComponentInterface
|
||||||
|
get asFeatureDescription(): F
|
||||||
|
get uniqueIdentifier(): string
|
||||||
|
get featureIdentifier(): FeatureIdentifier
|
||||||
|
get noteType(): NoteType
|
||||||
|
get fileType(): EditorFeatureDescription['file_type']
|
||||||
|
get displayName(): string
|
||||||
|
get description(): string
|
||||||
|
get deprecationMessage(): string | undefined
|
||||||
|
get expirationDate(): Date | undefined
|
||||||
|
get featureDescription(): F
|
||||||
|
get acquiredPermissions(): ComponentPermission[]
|
||||||
|
get area(): ComponentArea
|
||||||
|
get layerable(): boolean
|
||||||
|
get dockIcon(): ThemeDockIcon | undefined
|
||||||
|
}
|
@ -2,5 +2,5 @@ export * from './Component'
|
|||||||
export * from './ComponentMutator'
|
export * from './ComponentMutator'
|
||||||
export * from './ComponentContent'
|
export * from './ComponentContent'
|
||||||
export * from './ComponentInterface'
|
export * from './ComponentInterface'
|
||||||
export * from './ComponentOrNativeFeature'
|
export * from '../../Runtime/Feature/UIFeature'
|
||||||
export * from './PackageInfo'
|
export * from './PackageInfo'
|
||||||
|
@ -43,6 +43,10 @@ export * from './Local/RootKey/RootKeyContent'
|
|||||||
export * from './Local/RootKey/RootKeyInterface'
|
export * from './Local/RootKey/RootKeyInterface'
|
||||||
export * from './Local/RootKey/RootKeyWithKeyPairsInterface'
|
export * from './Local/RootKey/RootKeyWithKeyPairsInterface'
|
||||||
|
|
||||||
|
export * from './Runtime/Feature/TypeGuards'
|
||||||
|
export * from './Runtime/Feature/UIFeature'
|
||||||
|
export * from './Runtime/Feature/UIFeatureInterface'
|
||||||
|
|
||||||
export * from './Runtime/Collection/CollectionSort'
|
export * from './Runtime/Collection/CollectionSort'
|
||||||
export * from './Runtime/Collection/Item/ItemCollection'
|
export * from './Runtime/Collection/Item/ItemCollection'
|
||||||
export * from './Runtime/Collection/Item/ItemCounter'
|
export * from './Runtime/Collection/Item/ItemCounter'
|
||||||
|
@ -3,43 +3,39 @@ import {
|
|||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentFeatureDescription,
|
ComponentFeatureDescription,
|
||||||
EditorFeatureDescription,
|
EditorFeatureDescription,
|
||||||
|
EditorIdentifier,
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
ThemeFeatureDescription,
|
ThemeFeatureDescription,
|
||||||
} from '@standardnotes/features'
|
} from '@standardnotes/features'
|
||||||
import {
|
import { ActionObserver, ComponentInterface, UIFeature, PermissionDialog, SNNote, SNTag } from '@standardnotes/models'
|
||||||
ActionObserver,
|
|
||||||
ComponentInterface,
|
|
||||||
ComponentOrNativeFeature,
|
|
||||||
PermissionDialog,
|
|
||||||
SNNote,
|
|
||||||
} from '@standardnotes/models'
|
|
||||||
|
|
||||||
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
import { DesktopManagerInterface } from '../Device/DesktopManagerInterface'
|
||||||
import { ComponentViewerInterface } from './ComponentViewerInterface'
|
import { ComponentViewerInterface } from './ComponentViewerInterface'
|
||||||
|
|
||||||
export interface ComponentManagerInterface {
|
export interface ComponentManagerInterface {
|
||||||
urlForComponent(uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>): string | undefined
|
urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined
|
||||||
setDesktopManager(desktopManager: DesktopManagerInterface): void
|
setDesktopManager(desktopManager: DesktopManagerInterface): void
|
||||||
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[]
|
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[]
|
||||||
editorForNote(note: SNNote): ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
|
||||||
doesEditorChangeRequireAlert(
|
doesEditorChangeRequireAlert(
|
||||||
from: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
from: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
to: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
to: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
): boolean
|
): boolean
|
||||||
showEditorChangeAlert(): Promise<boolean>
|
showEditorChangeAlert(): Promise<boolean>
|
||||||
destroyComponentViewer(viewer: ComponentViewerInterface): void
|
destroyComponentViewer(viewer: ComponentViewerInterface): void
|
||||||
createComponentViewer(
|
createComponentViewer(
|
||||||
uiFeature: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
|
uiFeature: UIFeature<IframeComponentFeatureDescription>,
|
||||||
item: ComponentViewerItem,
|
item: ComponentViewerItem,
|
||||||
actionObserver?: ActionObserver,
|
actionObserver?: ActionObserver,
|
||||||
urlOverride?: string,
|
urlOverride?: string,
|
||||||
): ComponentViewerInterface
|
): ComponentViewerInterface
|
||||||
presentPermissionsDialog(_dialog: PermissionDialog): void
|
|
||||||
legacyGetDefaultEditor(): ComponentInterface | undefined
|
|
||||||
|
|
||||||
isThemeActive(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): boolean
|
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void
|
||||||
toggleTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void>
|
|
||||||
getActiveThemes(): ComponentOrNativeFeature<ThemeFeatureDescription>[]
|
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||||
|
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier
|
||||||
|
|
||||||
|
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean
|
||||||
|
toggleTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void>
|
||||||
|
getActiveThemes(): UIFeature<ThemeFeatureDescription>[]
|
||||||
getActiveThemesIdentifiers(): string[]
|
getActiveThemesIdentifiers(): string[]
|
||||||
|
|
||||||
isComponentActive(component: ComponentInterface): boolean
|
isComponentActive(component: ComponentInterface): boolean
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { ActionObserver, ComponentEventObserver, ComponentMessage, UIFeature } from '@standardnotes/models'
|
||||||
ActionObserver,
|
|
||||||
ComponentEventObserver,
|
|
||||||
ComponentMessage,
|
|
||||||
ComponentOrNativeFeature,
|
|
||||||
} from '@standardnotes/models'
|
|
||||||
import { FeatureStatus } from '../Feature/FeatureStatus'
|
import { FeatureStatus } from '../Feature/FeatureStatus'
|
||||||
import { ComponentViewerError } from './ComponentViewerError'
|
import { ComponentViewerError } from './ComponentViewerError'
|
||||||
import { IframeComponentFeatureDescription } from '@standardnotes/features'
|
import { IframeComponentFeatureDescription } from '@standardnotes/features'
|
||||||
@ -16,7 +11,7 @@ export interface ComponentViewerInterface {
|
|||||||
get url(): string
|
get url(): string
|
||||||
get componentUniqueIdentifier(): string
|
get componentUniqueIdentifier(): string
|
||||||
|
|
||||||
getComponentOrFeatureItem(): ComponentOrNativeFeature<IframeComponentFeatureDescription>
|
getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription>
|
||||||
|
|
||||||
destroy(): void
|
destroy(): void
|
||||||
setReadonly(readonly: boolean): void
|
setReadonly(readonly: boolean): void
|
||||||
|
@ -15,16 +15,20 @@ export interface MobileDeviceInterface extends DeviceInterface {
|
|||||||
authenticateWithBiometrics(): Promise<boolean>
|
authenticateWithBiometrics(): Promise<boolean>
|
||||||
hideMobileInterfaceFromScreenshots(): void
|
hideMobileInterfaceFromScreenshots(): void
|
||||||
stopHidingMobileInterfaceFromScreenshots(): void
|
stopHidingMobileInterfaceFromScreenshots(): void
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
consoleLog(...args: any[]): void
|
consoleLog(...args: any[]): void
|
||||||
|
|
||||||
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
|
handleThemeSchemeChange(isDark: boolean, bgColor: string): void
|
||||||
shareBase64AsFile(base64: string, filename: string): Promise<void>
|
shareBase64AsFile(base64: string, filename: string): Promise<void>
|
||||||
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
|
downloadBase64AsFile(base64: string, filename: string, saveInTempLocation?: boolean): Promise<string | undefined>
|
||||||
previewFile(base64: string, filename: string): Promise<boolean>
|
previewFile(base64: string, filename: string): Promise<boolean>
|
||||||
exitApp(confirm?: boolean): void
|
exitApp(confirm?: boolean): void
|
||||||
addComponentUrl(componentUuid: string, componentUrl: string): void
|
|
||||||
removeComponentUrl(componentUuid: string): void
|
registerComponentUrl(componentUuid: string, componentUrl: string): void
|
||||||
isUrlComponentUrl(url: string): boolean
|
deregisterComponentUrl(componentUuid: string): void
|
||||||
|
isUrlRegisteredComponentUrl(url: string): boolean
|
||||||
|
|
||||||
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
getAppState(): Promise<'active' | 'background' | 'inactive' | 'unknown' | 'extension'>
|
||||||
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
getColorScheme(): Promise<'light' | 'dark' | null | undefined>
|
||||||
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined>
|
purchaseSubscriptionIAP(plan: AppleIAPProductId): Promise<AppleIAPReceipt | undefined>
|
||||||
|
@ -1,32 +1,6 @@
|
|||||||
/**
|
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { SNPreferencesService } from '../Preferences/PreferencesService'
|
import { SNPreferencesService } from '../Preferences/PreferencesService'
|
||||||
import { createNote } from './../../Spec/SpecUtils'
|
import { GenericItem, Environment, Platform } from '@standardnotes/models'
|
||||||
import {
|
import {
|
||||||
ComponentAction,
|
|
||||||
ComponentPermission,
|
|
||||||
FindNativeFeature,
|
|
||||||
FeatureIdentifier,
|
|
||||||
NoteType,
|
|
||||||
UIFeatureDescriptionTypes,
|
|
||||||
IframeComponentFeatureDescription,
|
|
||||||
} from '@standardnotes/features'
|
|
||||||
import { ContentType } from '@standardnotes/domain-core'
|
|
||||||
import {
|
|
||||||
GenericItem,
|
|
||||||
SNComponent,
|
|
||||||
Environment,
|
|
||||||
Platform,
|
|
||||||
ComponentInterface,
|
|
||||||
ComponentOrNativeFeature,
|
|
||||||
ComponentContent,
|
|
||||||
DecryptedPayload,
|
|
||||||
PayloadTimestampDefaults,
|
|
||||||
} from '@standardnotes/models'
|
|
||||||
import {
|
|
||||||
DesktopManagerInterface,
|
|
||||||
InternalEventBusInterface,
|
InternalEventBusInterface,
|
||||||
AlertService,
|
AlertService,
|
||||||
DeviceInterface,
|
DeviceInterface,
|
||||||
@ -39,7 +13,6 @@ import { ItemManager } from '@Lib/Services/Items/ItemManager'
|
|||||||
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
|
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
|
||||||
import { SNComponentManager } from './ComponentManager'
|
import { SNComponentManager } from './ComponentManager'
|
||||||
import { SNSyncService } from '../Sync/SyncService'
|
import { SNSyncService } from '../Sync/SyncService'
|
||||||
import { ComponentPackageInfo } from '@standardnotes/models'
|
|
||||||
|
|
||||||
describe('featuresService', () => {
|
describe('featuresService', () => {
|
||||||
let items: ItemManagerInterface
|
let items: ItemManagerInterface
|
||||||
@ -51,12 +24,6 @@ describe('featuresService', () => {
|
|||||||
let eventBus: InternalEventBusInterface
|
let eventBus: InternalEventBusInterface
|
||||||
let device: DeviceInterface
|
let device: DeviceInterface
|
||||||
|
|
||||||
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
|
||||||
return new ComponentOrNativeFeature(FindNativeFeature<F>(identifier)!)
|
|
||||||
}
|
|
||||||
|
|
||||||
const desktopExtHost = 'http://localhost:123'
|
|
||||||
|
|
||||||
const createManager = (environment: Environment, platform: Platform) => {
|
const createManager = (environment: Environment, platform: Platform) => {
|
||||||
const manager = new SNComponentManager(
|
const manager = new SNComponentManager(
|
||||||
items,
|
items,
|
||||||
@ -71,23 +38,15 @@ describe('featuresService', () => {
|
|||||||
eventBus,
|
eventBus,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (environment === Environment.Desktop) {
|
|
||||||
const desktopManager: DesktopManagerInterface = {
|
|
||||||
syncComponentsInstallation() {},
|
|
||||||
registerUpdateObserver(_callback: (component: ComponentInterface) => void) {
|
|
||||||
return () => {}
|
|
||||||
},
|
|
||||||
getExtServerHost() {
|
|
||||||
return desktopExtHost
|
|
||||||
},
|
|
||||||
}
|
|
||||||
manager.setDesktopManager(desktopManager)
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager
|
return manager
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
global.window = {
|
||||||
|
addEventListener: jest.fn(),
|
||||||
|
attachEvent: jest.fn(),
|
||||||
|
} as unknown as Window & typeof globalThis
|
||||||
|
|
||||||
sync = {} as jest.Mocked<SNSyncService>
|
sync = {} as jest.Mocked<SNSyncService>
|
||||||
sync.sync = jest.fn()
|
sync.sync = jest.fn()
|
||||||
|
|
||||||
@ -117,336 +76,9 @@ describe('featuresService', () => {
|
|||||||
device = {} as jest.Mocked<DeviceInterface>
|
device = {} as jest.Mocked<DeviceInterface>
|
||||||
})
|
})
|
||||||
|
|
||||||
const thirdPartyFeature = () => {
|
it('should create manager', () => {
|
||||||
const component = new SNComponent(
|
const manager = createManager(Environment.Web, Platform.MacWeb)
|
||||||
new DecryptedPayload({
|
|
||||||
uuid: '789',
|
|
||||||
content_type: ContentType.TYPES.Component,
|
|
||||||
...PayloadTimestampDefaults(),
|
|
||||||
content: {
|
|
||||||
local_url: 'sn://Extensions/non-native-identifier/dist/index.html',
|
|
||||||
hosted_url: 'https://example.com/component',
|
|
||||||
package_info: {
|
|
||||||
identifier: 'non-native-identifier' as FeatureIdentifier,
|
|
||||||
expires_at: new Date().getTime(),
|
|
||||||
availableInRoles: [],
|
|
||||||
} as unknown as jest.Mocked<ComponentPackageInfo>,
|
|
||||||
} as unknown as jest.Mocked<ComponentContent>,
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
return new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component)
|
expect(manager).toBeDefined()
|
||||||
}
|
|
||||||
|
|
||||||
describe('permissions', () => {
|
|
||||||
it('editor should be able to to stream single note', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamContextItem,
|
|
||||||
content_types: [ContentType.TYPES.Note],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('no extension should be able to stream multiple notes', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [ContentType.TYPES.Note],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('no extension should be able to stream multiple tags', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [ContentType.TYPES.Tag],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('no extension should be able to stream multiple notes or tags', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [ContentType.TYPES.Tag, ContentType.TYPES.Note],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('some valid and some invalid permissions should still return invalid permissions', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [ContentType.TYPES.Tag, ContentType.TYPES.FilesafeFileMetadata],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('filesafe should be able to stream its files', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [
|
|
||||||
ContentType.TYPES.FilesafeFileMetadata,
|
|
||||||
ContentType.TYPES.FilesafeCredentials,
|
|
||||||
ContentType.TYPES.FilesafeIntegration,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('bold editor should be able to stream filesafe files', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [
|
|
||||||
ContentType.TYPES.FilesafeFileMetadata,
|
|
||||||
ContentType.TYPES.FilesafeCredentials,
|
|
||||||
ContentType.TYPES.FilesafeIntegration,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(
|
|
||||||
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedBoldEditor),
|
|
||||||
permissions,
|
|
||||||
),
|
|
||||||
).toEqual(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('non bold editor should not able to stream filesafe files', () => {
|
|
||||||
const permissions: ComponentPermission[] = [
|
|
||||||
{
|
|
||||||
name: ComponentAction.StreamItems,
|
|
||||||
content_types: [
|
|
||||||
ContentType.TYPES.FilesafeFileMetadata,
|
|
||||||
ContentType.TYPES.FilesafeCredentials,
|
|
||||||
ContentType.TYPES.FilesafeIntegration,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
expect(
|
|
||||||
manager.areRequestedPermissionsValid(nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor), permissions),
|
|
||||||
).toEqual(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('urlForComponent', () => {
|
|
||||||
describe('desktop', () => {
|
|
||||||
it('returns native path for native component', () => {
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.MarkdownProEditor,
|
|
||||||
)!
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual(
|
|
||||||
`${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns native path for deprecated native component', () => {
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.DeprecatedBoldEditor,
|
|
||||||
)!
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual(
|
|
||||||
`${desktopExtHost}/components/${feature?.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns nonnative path for third party component', () => {
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
const feature = thirdPartyFeature()
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual(`${desktopExtHost}/Extensions/${feature.featureIdentifier}/dist/index.html`)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns hosted url for third party component with no local_url', () => {
|
|
||||||
const manager = createManager(Environment.Desktop, Platform.MacDesktop)
|
|
||||||
const component = new SNComponent({
|
|
||||||
uuid: '789',
|
|
||||||
content_type: ContentType.TYPES.Component,
|
|
||||||
content: {
|
|
||||||
hosted_url: 'https://example.com/component',
|
|
||||||
package_info: {
|
|
||||||
identifier: 'non-native-identifier',
|
|
||||||
valid_until: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as never)
|
|
||||||
const feature = new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component)
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual('https://example.com/component')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('web', () => {
|
|
||||||
it('returns native path for native component', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual(
|
|
||||||
`http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('returns hosted path for third party component', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const feature = thirdPartyFeature()
|
|
||||||
const url = manager.urlForComponent(feature)
|
|
||||||
expect(url).toEqual(feature.asComponent.hosted_url)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('editors', () => {
|
|
||||||
it('getEditorForNote should return plain notes is note type is plain', () => {
|
|
||||||
const note = createNote({
|
|
||||||
noteType: NoteType.Plain,
|
|
||||||
})
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
|
|
||||||
expect(manager.editorForNote(note).featureIdentifier).toBe(FeatureIdentifier.PlainEditor)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => {
|
|
||||||
const note = createNote({})
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
manager['legacyGetEditorForNote'] = jest.fn()
|
|
||||||
manager.editorForNote(note)
|
|
||||||
|
|
||||||
expect(manager['legacyGetEditorForNote']).toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('editor change alert', () => {
|
|
||||||
it('should not require alert switching from plain editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.MarkdownProEditor,
|
|
||||||
)!
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(undefined, component)
|
|
||||||
expect(requiresAlert).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not require alert switching to plain editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.MarkdownProEditor,
|
|
||||||
)!
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(component, undefined)
|
|
||||||
expect(requiresAlert).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not require alert switching from a markdown editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
|
||||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.MarkdownProEditor,
|
|
||||||
)
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(markdownEditor, htmlEditor)
|
|
||||||
expect(requiresAlert).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not require alert switching to a markdown editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
|
||||||
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.MarkdownProEditor,
|
|
||||||
)
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(htmlEditor, markdownEditor)
|
|
||||||
expect(requiresAlert).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should not require alert switching from & to a html editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(htmlEditor, htmlEditor)
|
|
||||||
expect(requiresAlert).toBe(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should require alert switching from a html editor to custom editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
|
||||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.TokenVaultEditor,
|
|
||||||
)
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(htmlEditor, customEditor)
|
|
||||||
expect(requiresAlert).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should require alert switching from a custom editor to html editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
|
||||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.TokenVaultEditor,
|
|
||||||
)
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(customEditor, htmlEditor)
|
|
||||||
expect(requiresAlert).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should require alert switching from a custom editor to custom editor', () => {
|
|
||||||
const manager = createManager(Environment.Web, Platform.MacWeb)
|
|
||||||
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
|
||||||
FeatureIdentifier.TokenVaultEditor,
|
|
||||||
)
|
|
||||||
const requiresAlert = manager.doesEditorChangeRequireAlert(customEditor, customEditor)
|
|
||||||
expect(requiresAlert).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,49 +1,37 @@
|
|||||||
import { AllowedBatchStreaming } from './Types'
|
|
||||||
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
|
import { SNFeaturesService } from '@Lib/Services/Features/FeaturesService'
|
||||||
import { ContentType } from '@standardnotes/domain-core'
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
import {
|
import {
|
||||||
ActionObserver,
|
ActionObserver,
|
||||||
SNNote,
|
|
||||||
ComponentMutator,
|
|
||||||
PayloadEmitSource,
|
PayloadEmitSource,
|
||||||
PermissionDialog,
|
PermissionDialog,
|
||||||
Environment,
|
Environment,
|
||||||
Platform,
|
Platform,
|
||||||
ComponentMessage,
|
ComponentMessage,
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
ComponentInterface,
|
ComponentInterface,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
ThemeInterface,
|
ThemeInterface,
|
||||||
ComponentPreferencesEntry,
|
ComponentPreferencesEntry,
|
||||||
AllComponentPreferences,
|
AllComponentPreferences,
|
||||||
|
SNNote,
|
||||||
|
SNTag,
|
||||||
|
DeletedItemInterface,
|
||||||
|
EncryptedItemInterface,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import {
|
import {
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentAction,
|
|
||||||
ComponentPermission,
|
|
||||||
FindNativeFeature,
|
FindNativeFeature,
|
||||||
NoteType,
|
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
EditorFeatureDescription,
|
EditorFeatureDescription,
|
||||||
GetIframeAndNativeEditors,
|
|
||||||
FindNativeTheme,
|
FindNativeTheme,
|
||||||
UIFeatureDescriptionTypes,
|
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
GetPlainNoteFeature,
|
|
||||||
GetSuperNoteFeature,
|
|
||||||
ComponentFeatureDescription,
|
ComponentFeatureDescription,
|
||||||
ThemeFeatureDescription,
|
ThemeFeatureDescription,
|
||||||
|
EditorIdentifier,
|
||||||
|
GetIframeEditors,
|
||||||
|
GetNativeThemes,
|
||||||
} from '@standardnotes/features'
|
} from '@standardnotes/features'
|
||||||
import {
|
import { Copy, removeFromArray, sleep, isNotUndefined } from '@standardnotes/utils'
|
||||||
Copy,
|
|
||||||
filterFromArray,
|
|
||||||
removeFromArray,
|
|
||||||
sleep,
|
|
||||||
assert,
|
|
||||||
uniqueArray,
|
|
||||||
isNotUndefined,
|
|
||||||
} from '@standardnotes/utils'
|
|
||||||
import { AllowedBatchContentTypes } from '@Lib/Services/ComponentManager/Types'
|
|
||||||
import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer'
|
import { ComponentViewer } from '@Lib/Services/ComponentManager/ComponentViewer'
|
||||||
import {
|
import {
|
||||||
AbstractService,
|
AbstractService,
|
||||||
@ -62,12 +50,13 @@ import {
|
|||||||
SyncServiceInterface,
|
SyncServiceInterface,
|
||||||
FeatureStatus,
|
FeatureStatus,
|
||||||
} from '@standardnotes/services'
|
} from '@standardnotes/services'
|
||||||
import { permissionsStringForPermissions } from './permissionsStringForPermissions'
|
import { GetFeatureUrl } from './UseCase/GetFeatureUrl'
|
||||||
|
import { ComponentManagerEventData } from './ComponentManagerEventData'
|
||||||
const DESKTOP_URL_PREFIX = 'sn://'
|
import { ComponentManagerEvent } from './ComponentManagerEvent'
|
||||||
const LOCAL_HOST = 'localhost'
|
import { RunWithPermissionsUseCase } from './UseCase/RunWithPermissionsUseCase'
|
||||||
const CUSTOM_LOCAL_HOST = 'sn.local'
|
import { EditorForNoteUseCase } from './UseCase/EditorForNote'
|
||||||
const ANDROID_LOCAL_HOST = '10.0.2.2'
|
import { GetDefaultEditorIdentifier } from './UseCase/GetDefaultEditorIdentifier'
|
||||||
|
import { DoesEditorChangeRequireAlertUseCase } from './UseCase/DoesEditorChangeRequireAlert'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
@ -77,27 +66,29 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ComponentManagerEvent {
|
|
||||||
ViewerDidFocus = 'ViewerDidFocus',
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventData = {
|
|
||||||
componentViewer?: ComponentViewerInterface
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for orchestrating component functionality, including editors, themes,
|
* Responsible for orchestrating component functionality, including editors, themes,
|
||||||
* and other components. The component manager primarily deals with iframes, and orchestrates
|
* and other components. The component manager primarily deals with iframes, and orchestrates
|
||||||
* sending and receiving messages to and from frames via the postMessage API.
|
* sending and receiving messages to and from frames via the postMessage API.
|
||||||
*/
|
*/
|
||||||
export class SNComponentManager
|
export class SNComponentManager
|
||||||
extends AbstractService<ComponentManagerEvent, EventData>
|
extends AbstractService<ComponentManagerEvent, ComponentManagerEventData>
|
||||||
implements ComponentManagerInterface
|
implements ComponentManagerInterface
|
||||||
{
|
{
|
||||||
private desktopManager?: DesktopManagerInterface
|
private desktopManager?: DesktopManagerInterface
|
||||||
private viewers: ComponentViewerInterface[] = []
|
private viewers: ComponentViewerInterface[] = []
|
||||||
private removeItemObserver!: () => void
|
|
||||||
private permissionDialogs: PermissionDialog[] = []
|
private permissionDialogUIHandler: (dialog: PermissionDialog) => void = () => {
|
||||||
|
throw 'Must call setPermissionDialogUIHandler'
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly runWithPermissionsUseCase = new RunWithPermissionsUseCase(
|
||||||
|
this.permissionDialogUIHandler,
|
||||||
|
this.alerts,
|
||||||
|
this.mutator,
|
||||||
|
this.sync,
|
||||||
|
this.items,
|
||||||
|
)
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private items: ItemManagerInterface,
|
private items: ItemManagerInterface,
|
||||||
@ -114,7 +105,8 @@ export class SNComponentManager
|
|||||||
super(internalEventBus)
|
super(internalEventBus)
|
||||||
this.loggingEnabled = false
|
this.loggingEnabled = false
|
||||||
|
|
||||||
this.addItemObserver()
|
this.addSyncedComponentItemObserver()
|
||||||
|
this.registerMobileNativeComponentUrls()
|
||||||
|
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
preferences.addEventObserver((event) => {
|
preferences.addEventObserver((event) => {
|
||||||
@ -160,7 +152,7 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.viewers.length = 0
|
this.viewers.length = 0
|
||||||
this.permissionDialogs.length = 0
|
this.runWithPermissionsUseCase.deinit()
|
||||||
|
|
||||||
this.desktopManager = undefined
|
this.desktopManager = undefined
|
||||||
;(this.items as unknown) = undefined
|
;(this.items as unknown) = undefined
|
||||||
@ -168,9 +160,7 @@ export class SNComponentManager
|
|||||||
;(this.sync as unknown) = undefined
|
;(this.sync as unknown) = undefined
|
||||||
;(this.alerts as unknown) = undefined
|
;(this.alerts as unknown) = undefined
|
||||||
;(this.preferences as unknown) = undefined
|
;(this.preferences as unknown) = undefined
|
||||||
|
;(this.permissionDialogUIHandler as unknown) = undefined
|
||||||
this.removeItemObserver?.()
|
|
||||||
;(this.removeItemObserver as unknown) = undefined
|
|
||||||
|
|
||||||
if (window) {
|
if (window) {
|
||||||
window.removeEventListener('focus', this.detectFocusChange, true)
|
window.removeEventListener('focus', this.detectFocusChange, true)
|
||||||
@ -182,8 +172,13 @@ export class SNComponentManager
|
|||||||
;(this.onWindowMessage as unknown) = undefined
|
;(this.onWindowMessage as unknown) = undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void {
|
||||||
|
this.permissionDialogUIHandler = handler
|
||||||
|
this.runWithPermissionsUseCase.setPermissionDialogUIHandler(handler)
|
||||||
|
}
|
||||||
|
|
||||||
public createComponentViewer(
|
public createComponentViewer(
|
||||||
component: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
|
component: UIFeature<IframeComponentFeatureDescription>,
|
||||||
item: ComponentViewerItem,
|
item: ComponentViewerItem,
|
||||||
actionObserver?: ActionObserver,
|
actionObserver?: ActionObserver,
|
||||||
): ComponentViewerInterface {
|
): ComponentViewerInterface {
|
||||||
@ -198,7 +193,7 @@ export class SNComponentManager
|
|||||||
features: this.features,
|
features: this.features,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
url: this.urlForComponent(component) ?? '',
|
url: this.urlForFeature(component) ?? '',
|
||||||
item,
|
item,
|
||||||
actionObserver,
|
actionObserver,
|
||||||
},
|
},
|
||||||
@ -206,7 +201,7 @@ export class SNComponentManager
|
|||||||
environment: this.environment,
|
environment: this.environment,
|
||||||
platform: this.platform,
|
platform: this.platform,
|
||||||
componentManagerFunctions: {
|
componentManagerFunctions: {
|
||||||
runWithPermissions: this.runWithPermissions.bind(this),
|
runWithPermissionsUseCase: this.runWithPermissionsUseCase,
|
||||||
urlsForActiveThemes: this.urlsForActiveThemes.bind(this),
|
urlsForActiveThemes: this.urlsForActiveThemes.bind(this),
|
||||||
setComponentPreferences: this.setComponentPreferences.bind(this),
|
setComponentPreferences: this.setComponentPreferences.bind(this),
|
||||||
getComponentPreferences: this.getComponentPreferences.bind(this),
|
getComponentPreferences: this.getComponentPreferences.bind(this),
|
||||||
@ -255,40 +250,68 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private addItemObserver(): void {
|
private addSyncedComponentItemObserver(): void {
|
||||||
this.removeItemObserver = this.items.addObserver<ComponentInterface>(
|
this.eventDisposers.push(
|
||||||
[ContentType.TYPES.Component, ContentType.TYPES.Theme],
|
this.items.addObserver<ComponentInterface>(
|
||||||
({ changed, inserted, removed, source }) => {
|
[ContentType.TYPES.Component, ContentType.TYPES.Theme],
|
||||||
const items = [...changed, ...inserted]
|
({ changed, inserted, removed, source }) => {
|
||||||
this.handleChangedComponents(items, source)
|
const items = [...changed, ...inserted]
|
||||||
|
|
||||||
const device = this.device
|
this.handleChangedComponents(items, source)
|
||||||
if (isMobileDevice(device) && 'addComponentUrl' in device) {
|
|
||||||
inserted.forEach((component) => {
|
|
||||||
const url = this.urlForComponent(new ComponentOrNativeFeature<ComponentFeatureDescription>(component))
|
|
||||||
if (url) {
|
|
||||||
device.addComponentUrl(component.uuid, url)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
removed.forEach((component) => {
|
this.updateMobileRegisteredComponentUrls(inserted, removed)
|
||||||
device.removeComponentUrl(component.uuid)
|
},
|
||||||
})
|
),
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private updateMobileRegisteredComponentUrls(
|
||||||
|
inserted: ComponentInterface[],
|
||||||
|
removed: (EncryptedItemInterface | DeletedItemInterface)[],
|
||||||
|
): void {
|
||||||
|
if (!isMobileDevice(this.device)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const component of inserted) {
|
||||||
|
const feature = new UIFeature<ComponentFeatureDescription>(component)
|
||||||
|
const url = this.urlForFeature(feature)
|
||||||
|
if (url) {
|
||||||
|
this.device.registerComponentUrl(component.uuid, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const component of removed) {
|
||||||
|
this.device.deregisterComponentUrl(component.uuid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerMobileNativeComponentUrls(): void {
|
||||||
|
if (!isMobileDevice(this.device)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nativeComponents = [...GetIframeEditors(), ...GetNativeThemes()]
|
||||||
|
|
||||||
|
for (const component of nativeComponents) {
|
||||||
|
const feature = new UIFeature<ComponentFeatureDescription>(component)
|
||||||
|
const url = this.urlForFeature(feature)
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
this.device.registerComponentUrl(feature.uniqueIdentifier, url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
detectFocusChange = (): void => {
|
detectFocusChange = (): void => {
|
||||||
const activeIframes = this.allComponentIframes()
|
const activeIframes = this.allComponentIframes()
|
||||||
for (const iframe of activeIframes) {
|
for (const iframe of activeIframes) {
|
||||||
if (document.activeElement === iframe) {
|
if (document.activeElement === iframe) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const viewer = this.findComponentViewer(
|
const viewer = this.findComponentViewer(
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
iframe.dataset.componentViewerId as string,
|
||||||
iframe.dataset.componentViewerId!,
|
) as ComponentViewerInterface
|
||||||
)!
|
|
||||||
void this.notifyEvent(ComponentManagerEvent.ViewerDidFocus, {
|
void this.notifyEvent(ComponentManagerEvent.ViewerDidFocus, {
|
||||||
componentViewer: viewer,
|
componentViewer: viewer,
|
||||||
})
|
})
|
||||||
@ -301,6 +324,7 @@ export class SNComponentManager
|
|||||||
onWindowMessage = (event: MessageEvent): void => {
|
onWindowMessage = (event: MessageEvent): void => {
|
||||||
/** Make sure this message is for us */
|
/** Make sure this message is for us */
|
||||||
const data = event.data as ComponentMessage
|
const data = event.data as ComponentMessage
|
||||||
|
|
||||||
if (data.sessionKey) {
|
if (data.sessionKey) {
|
||||||
this.log('Component manager received message', data)
|
this.log('Component manager received message', data)
|
||||||
this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data)
|
this.componentViewerForSessionKey(data.sessionKey)?.handleMessage(data)
|
||||||
@ -324,65 +348,16 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private urlForComponentOnDesktop(
|
urlForFeature(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined {
|
||||||
uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>,
|
const usecase = new GetFeatureUrl(this.desktopManager, this.environment, this.platform)
|
||||||
): string | undefined {
|
return usecase.execute(uiFeature)
|
||||||
assert(this.desktopManager)
|
|
||||||
|
|
||||||
if (uiFeature.isFeatureDescription) {
|
|
||||||
return `${this.desktopManager.getExtServerHost()}/components/${uiFeature.featureIdentifier}/${
|
|
||||||
uiFeature.asFeatureDescription.index_path
|
|
||||||
}`
|
|
||||||
} else {
|
|
||||||
if (uiFeature.asComponent.local_url) {
|
|
||||||
return uiFeature.asComponent.local_url.replace(DESKTOP_URL_PREFIX, this.desktopManager.getExtServerHost() + '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
return uiFeature.asComponent.hosted_url || uiFeature.asComponent.legacy_url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private urlForNativeComponent(feature: ComponentFeatureDescription): string {
|
|
||||||
if (this.isMobile) {
|
|
||||||
const baseUrlRequiredForThemesInsideEditors = window.location.href.split('/index.html')[0]
|
|
||||||
return `${baseUrlRequiredForThemesInsideEditors}/web-src/components/assets/${feature.identifier}/${feature.index_path}`
|
|
||||||
} else {
|
|
||||||
const baseUrlRequiredForThemesInsideEditors = window.location.origin
|
|
||||||
return `${baseUrlRequiredForThemesInsideEditors}/components/assets/${feature.identifier}/${feature.index_path}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urlForComponent(uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>): string | undefined {
|
|
||||||
if (this.desktopManager) {
|
|
||||||
return this.urlForComponentOnDesktop(uiFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiFeature.isFeatureDescription) {
|
|
||||||
return this.urlForNativeComponent(uiFeature.asFeatureDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiFeature.asComponent.offlineOnly) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = uiFeature.asComponent.hosted_url || uiFeature.asComponent.legacy_url
|
|
||||||
if (!url) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isMobile) {
|
|
||||||
const localReplacement = this.platform === Platform.Ios ? LOCAL_HOST : ANDROID_LOCAL_HOST
|
|
||||||
return url.replace(LOCAL_HOST, localReplacement).replace(CUSTOM_LOCAL_HOST, localReplacement)
|
|
||||||
}
|
|
||||||
|
|
||||||
return url
|
|
||||||
}
|
}
|
||||||
|
|
||||||
urlsForActiveThemes(): string[] {
|
urlsForActiveThemes(): string[] {
|
||||||
const themes = this.getActiveThemes()
|
const themes = this.getActiveThemes()
|
||||||
const urls = []
|
const urls = []
|
||||||
for (const theme of themes) {
|
for (const theme of themes) {
|
||||||
const url = this.urlForComponent(theme)
|
const url = this.urlForFeature(theme)
|
||||||
if (url) {
|
if (url) {
|
||||||
urls.push(url)
|
urls.push(url)
|
||||||
}
|
}
|
||||||
@ -390,222 +365,15 @@ export class SNComponentManager
|
|||||||
return urls
|
return urls
|
||||||
}
|
}
|
||||||
|
|
||||||
private findComponent(uuid: string): ComponentInterface | undefined {
|
private findComponentViewer(identifier: string): ComponentViewerInterface | undefined {
|
||||||
return this.items.findItem<ComponentInterface>(uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
private findComponentOrNativeFeature(
|
|
||||||
identifier: string,
|
|
||||||
): ComponentOrNativeFeature<ComponentFeatureDescription> | undefined {
|
|
||||||
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier as FeatureIdentifier)
|
|
||||||
if (nativeFeature) {
|
|
||||||
return new ComponentOrNativeFeature(nativeFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
const componentItem = this.items.findItem<ComponentInterface>(identifier)
|
|
||||||
if (componentItem) {
|
|
||||||
return new ComponentOrNativeFeature<ComponentFeatureDescription>(componentItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
findComponentViewer(identifier: string): ComponentViewerInterface | undefined {
|
|
||||||
return this.viewers.find((viewer) => viewer.identifier === identifier)
|
return this.viewers.find((viewer) => viewer.identifier === identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentViewerForSessionKey(key: string): ComponentViewerInterface | undefined {
|
private componentViewerForSessionKey(key: string): ComponentViewerInterface | undefined {
|
||||||
return this.viewers.find((viewer) => viewer.sessionKey === key)
|
return this.viewers.find((viewer) => viewer.sessionKey === key)
|
||||||
}
|
}
|
||||||
|
|
||||||
areRequestedPermissionsValid(
|
async toggleTheme(uiFeature: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||||
uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>,
|
|
||||||
permissions: ComponentPermission[],
|
|
||||||
): boolean {
|
|
||||||
for (const permission of permissions) {
|
|
||||||
if (permission.name === ComponentAction.StreamItems) {
|
|
||||||
if (!AllowedBatchStreaming.includes(uiFeature.featureIdentifier)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const hasNonAllowedBatchPermission = permission.content_types?.some(
|
|
||||||
(type) => !AllowedBatchContentTypes.includes(type),
|
|
||||||
)
|
|
||||||
if (hasNonAllowedBatchPermission) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
runWithPermissions(
|
|
||||||
componentIdentifier: string,
|
|
||||||
requiredPermissions: ComponentPermission[],
|
|
||||||
runFunction: () => void,
|
|
||||||
): void {
|
|
||||||
const uiFeature = this.findComponentOrNativeFeature(componentIdentifier)
|
|
||||||
|
|
||||||
if (!uiFeature) {
|
|
||||||
void this.alerts.alert(
|
|
||||||
`Unable to find component with ID ${componentIdentifier}. Please restart the app and try again.`,
|
|
||||||
'An unexpected error occurred',
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uiFeature.isFeatureDescription) {
|
|
||||||
runFunction()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.areRequestedPermissionsValid(uiFeature, requiredPermissions)) {
|
|
||||||
console.error('Component is requesting invalid permissions', componentIdentifier, requiredPermissions)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const acquiredPermissions = uiFeature.acquiredPermissions
|
|
||||||
|
|
||||||
/* Make copy as not to mutate input values */
|
|
||||||
requiredPermissions = Copy(requiredPermissions) as ComponentPermission[]
|
|
||||||
for (const required of requiredPermissions.slice()) {
|
|
||||||
/* Remove anything we already have */
|
|
||||||
const respectiveAcquired = acquiredPermissions.find((candidate) => candidate.name === required.name)
|
|
||||||
if (!respectiveAcquired) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
/* We now match on name, lets substract from required.content_types anything we have in acquired. */
|
|
||||||
const requiredContentTypes = required.content_types
|
|
||||||
if (!requiredContentTypes) {
|
|
||||||
/* If this permission does not require any content types (i.e stream-context-item)
|
|
||||||
then we can remove this from required since we match by name (respectiveAcquired.name === required.name) */
|
|
||||||
filterFromArray(requiredPermissions, required)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for (const acquiredContentType of respectiveAcquired.content_types as string[]) {
|
|
||||||
removeFromArray(requiredContentTypes, acquiredContentType)
|
|
||||||
}
|
|
||||||
if (requiredContentTypes.length === 0) {
|
|
||||||
/* We've removed all acquired and end up with zero, means we already have all these permissions */
|
|
||||||
filterFromArray(requiredPermissions, required)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (requiredPermissions.length > 0) {
|
|
||||||
this.promptForPermissionsWithDeferredRendering(
|
|
||||||
uiFeature.asComponent,
|
|
||||||
requiredPermissions,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/require-await
|
|
||||||
async (approved) => {
|
|
||||||
if (approved) {
|
|
||||||
runFunction()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
runFunction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
promptForPermissionsWithDeferredRendering(
|
|
||||||
component: ComponentInterface,
|
|
||||||
permissions: ComponentPermission[],
|
|
||||||
callback: (approved: boolean) => Promise<void>,
|
|
||||||
): void {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.promptForPermissions(component, permissions, callback)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
promptForPermissions(
|
|
||||||
component: ComponentInterface,
|
|
||||||
permissions: ComponentPermission[],
|
|
||||||
callback: (approved: boolean) => Promise<void>,
|
|
||||||
): void {
|
|
||||||
const params: PermissionDialog = {
|
|
||||||
component: component,
|
|
||||||
permissions: permissions,
|
|
||||||
permissionsString: permissionsStringForPermissions(permissions, component),
|
|
||||||
actionBlock: callback,
|
|
||||||
callback: async (approved: boolean) => {
|
|
||||||
const latestComponent = this.findComponent(component.uuid)
|
|
||||||
|
|
||||||
if (!latestComponent) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (approved) {
|
|
||||||
this.log('Changing component to expand permissions', component)
|
|
||||||
const componentPermissions = Copy(latestComponent.permissions) as ComponentPermission[]
|
|
||||||
for (const permission of permissions) {
|
|
||||||
const matchingPermission = componentPermissions.find((candidate) => candidate.name === permission.name)
|
|
||||||
if (!matchingPermission) {
|
|
||||||
componentPermissions.push(permission)
|
|
||||||
} else {
|
|
||||||
/* Permission already exists, but content_types may have been expanded */
|
|
||||||
const contentTypes = matchingPermission.content_types || []
|
|
||||||
matchingPermission.content_types = uniqueArray(contentTypes.concat(permission.content_types as string[]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.mutator.changeItem(component, (m) => {
|
|
||||||
const mutator = m as ComponentMutator
|
|
||||||
mutator.permissions = componentPermissions
|
|
||||||
})
|
|
||||||
|
|
||||||
void this.sync.sync()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
|
||||||
/* Remove self */
|
|
||||||
if (pendingDialog === params) {
|
|
||||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const containsObjectSubset = (source: ComponentPermission[], target: ComponentPermission[]) => {
|
|
||||||
return !target.some((val) => !source.find((candidate) => JSON.stringify(candidate) === JSON.stringify(val)))
|
|
||||||
}
|
|
||||||
if (pendingDialog.component === component) {
|
|
||||||
/* remove pending dialogs that are encapsulated by already approved permissions, and run its function */
|
|
||||||
if (
|
|
||||||
pendingDialog.permissions === permissions ||
|
|
||||||
containsObjectSubset(permissions, pendingDialog.permissions)
|
|
||||||
) {
|
|
||||||
/* If approved, run the action block. Otherwise, if canceled, cancel any
|
|
||||||
pending ones as well, since the user was explicit in their intentions */
|
|
||||||
if (approved) {
|
|
||||||
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
if (this.permissionDialogs.length > 0) {
|
|
||||||
this.presentPermissionsDialog(this.permissionDialogs[0])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Since these calls are asyncronous, multiple dialogs may be requested at the same time.
|
|
||||||
* We only want to present one and trigger all callbacks based on one modal result
|
|
||||||
*/
|
|
||||||
const existingDialog = this.permissionDialogs.find((dialog) => dialog.component === component)
|
|
||||||
this.permissionDialogs.push(params)
|
|
||||||
if (!existingDialog) {
|
|
||||||
this.presentPermissionsDialog(params)
|
|
||||||
} else {
|
|
||||||
this.log('Existing dialog, not presenting.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
presentPermissionsDialog(_dialog: PermissionDialog): void {
|
|
||||||
throw 'Must override SNComponentManager.presentPermissionsDialog'
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleTheme(uiFeature: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void> {
|
|
||||||
this.log('Toggling theme', uiFeature.uniqueIdentifier)
|
this.log('Toggling theme', uiFeature.uniqueIdentifier)
|
||||||
|
|
||||||
if (this.isThemeActive(uiFeature)) {
|
if (this.isThemeActive(uiFeature)) {
|
||||||
@ -638,11 +406,11 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getActiveThemes(): ComponentOrNativeFeature<ThemeFeatureDescription>[] {
|
getActiveThemes(): UIFeature<ThemeFeatureDescription>[] {
|
||||||
const activeThemesIdentifiers = this.getActiveThemesIdentifiers()
|
const activeThemesIdentifiers = this.getActiveThemesIdentifiers()
|
||||||
|
|
||||||
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => {
|
const thirdPartyThemes = this.items.findItems<ThemeInterface>(activeThemesIdentifiers).map((item) => {
|
||||||
return new ComponentOrNativeFeature<ThemeFeatureDescription>(item)
|
return new UIFeature<ThemeFeatureDescription>(item)
|
||||||
})
|
})
|
||||||
|
|
||||||
const nativeThemes = activeThemesIdentifiers
|
const nativeThemes = activeThemesIdentifiers
|
||||||
@ -650,7 +418,7 @@ export class SNComponentManager
|
|||||||
return FindNativeTheme(identifier as FeatureIdentifier)
|
return FindNativeTheme(identifier as FeatureIdentifier)
|
||||||
})
|
})
|
||||||
.filter(isNotUndefined)
|
.filter(isNotUndefined)
|
||||||
.map((theme) => new ComponentOrNativeFeature(theme))
|
.map((theme) => new UIFeature(theme))
|
||||||
|
|
||||||
const entitledThemes = [...thirdPartyThemes, ...nativeThemes].filter((theme) => {
|
const entitledThemes = [...thirdPartyThemes, ...nativeThemes].filter((theme) => {
|
||||||
return this.features.getFeatureStatus(theme.featureIdentifier) === FeatureStatus.Entitled
|
return this.features.getFeatureStatus(theme.featureIdentifier) === FeatureStatus.Entitled
|
||||||
@ -681,104 +449,22 @@ export class SNComponentManager
|
|||||||
return viewer.getIframe()
|
return viewer.getIframe()
|
||||||
}
|
}
|
||||||
|
|
||||||
componentOrNativeFeatureForIdentifier<F extends UIFeatureDescriptionTypes>(
|
editorForNote(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> {
|
||||||
identifier: FeatureIdentifier | string,
|
const usecase = new EditorForNoteUseCase(this.items)
|
||||||
): ComponentOrNativeFeature<F> | undefined {
|
return usecase.execute(note)
|
||||||
const nativeFeature = FindNativeFeature<F>(identifier as FeatureIdentifier)
|
|
||||||
if (nativeFeature) {
|
|
||||||
return new ComponentOrNativeFeature(nativeFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
const component = this.thirdPartyComponents.find((component) => {
|
|
||||||
return component.identifier === identifier
|
|
||||||
})
|
|
||||||
if (component) {
|
|
||||||
return new ComponentOrNativeFeature<F>(component)
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
editorForNote(note: SNNote): ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription> {
|
getDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
||||||
if (note.noteType === NoteType.Plain) {
|
const usecase = new GetDefaultEditorIdentifier(this.preferences, this.items)
|
||||||
return new ComponentOrNativeFeature(GetPlainNoteFeature())
|
return usecase.execute(currentTag).getValue()
|
||||||
}
|
|
||||||
|
|
||||||
if (note.noteType === NoteType.Super) {
|
|
||||||
return new ComponentOrNativeFeature(GetSuperNoteFeature())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.editorIdentifier) {
|
|
||||||
const result = this.componentOrNativeFeatureForIdentifier<
|
|
||||||
EditorFeatureDescription | IframeComponentFeatureDescription
|
|
||||||
>(note.editorIdentifier)
|
|
||||||
if (result) {
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (note.noteType && note.noteType !== NoteType.Unknown) {
|
|
||||||
const result = this.nativeEditorForNoteType(note.noteType)
|
|
||||||
if (result) {
|
|
||||||
return new ComponentOrNativeFeature(result)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const legacyResult = this.legacyGetEditorForNote(note)
|
|
||||||
if (legacyResult) {
|
|
||||||
return new ComponentOrNativeFeature<IframeComponentFeatureDescription>(legacyResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ComponentOrNativeFeature(GetPlainNoteFeature())
|
|
||||||
}
|
|
||||||
|
|
||||||
private nativeEditorForNoteType(noteType: NoteType): EditorFeatureDescription | undefined {
|
|
||||||
const nativeEditors = GetIframeAndNativeEditors()
|
|
||||||
return nativeEditors.find((editor) => editor.note_type === noteType)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uses legacy approach of note/editor association. New method uses note.editorIdentifier and note.noteType directly.
|
|
||||||
*/
|
|
||||||
private legacyGetEditorForNote(note: SNNote): ComponentInterface | undefined {
|
|
||||||
const editors = this.thirdPartyComponentsForArea(ComponentArea.Editor)
|
|
||||||
for (const editor of editors) {
|
|
||||||
if (editor.isExplicitlyEnabledForItem(note.uuid)) {
|
|
||||||
return editor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const defaultEditor = this.legacyGetDefaultEditor()
|
|
||||||
|
|
||||||
if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) {
|
|
||||||
return defaultEditor
|
|
||||||
} else {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
legacyGetDefaultEditor(): ComponentInterface | undefined {
|
|
||||||
const editors = this.thirdPartyComponentsForArea(ComponentArea.Editor)
|
|
||||||
return editors.filter((e) => e.legacyIsDefaultEditor())[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
doesEditorChangeRequireAlert(
|
doesEditorChangeRequireAlert(
|
||||||
from: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
from: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
to: ComponentOrNativeFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
to: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!from || !to) {
|
const usecase = new DoesEditorChangeRequireAlertUseCase()
|
||||||
return false
|
return usecase.execute(from, to)
|
||||||
}
|
|
||||||
|
|
||||||
const fromFileType = from.fileType
|
|
||||||
const toFileType = to.fileType
|
|
||||||
const isEitherMarkdown = fromFileType === 'md' || toFileType === 'md'
|
|
||||||
const areBothHtml = fromFileType === 'html' && toFileType === 'html'
|
|
||||||
|
|
||||||
if (isEitherMarkdown || areBothHtml) {
|
|
||||||
return false
|
|
||||||
} else {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async showEditorChangeAlert(): Promise<boolean> {
|
async showEditorChangeAlert(): Promise<boolean> {
|
||||||
@ -792,7 +478,7 @@ export class SNComponentManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setComponentPreferences(
|
async setComponentPreferences(
|
||||||
uiFeature: ComponentOrNativeFeature<ComponentFeatureDescription>,
|
uiFeature: UIFeature<ComponentFeatureDescription>,
|
||||||
preferences: ComponentPreferencesEntry,
|
preferences: ComponentPreferencesEntry,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const mutablePreferencesValue = Copy<AllComponentPreferences>(
|
const mutablePreferencesValue = Copy<AllComponentPreferences>(
|
||||||
@ -806,9 +492,7 @@ export class SNComponentManager
|
|||||||
await this.preferences.setValue(PrefKey.ComponentPreferences, mutablePreferencesValue)
|
await this.preferences.setValue(PrefKey.ComponentPreferences, mutablePreferencesValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
getComponentPreferences(
|
getComponentPreferences(component: UIFeature<ComponentFeatureDescription>): ComponentPreferencesEntry | undefined {
|
||||||
component: ComponentOrNativeFeature<ComponentFeatureDescription>,
|
|
||||||
): ComponentPreferencesEntry | undefined {
|
|
||||||
const preferences = this.preferences.getValue(PrefKey.ComponentPreferences, undefined)
|
const preferences = this.preferences.getValue(PrefKey.ComponentPreferences, undefined)
|
||||||
|
|
||||||
if (!preferences) {
|
if (!preferences) {
|
||||||
@ -820,7 +504,7 @@ export class SNComponentManager
|
|||||||
return preferences[preferencesLookupKey]
|
return preferences[preferencesLookupKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
async addActiveTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void> {
|
async addActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||||
const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice()
|
const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice()
|
||||||
|
|
||||||
activeThemes.push(theme.uniqueIdentifier)
|
activeThemes.push(theme.uniqueIdentifier)
|
||||||
@ -828,11 +512,11 @@ export class SNComponentManager
|
|||||||
await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes)
|
await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes)
|
||||||
}
|
}
|
||||||
|
|
||||||
async replaceActiveTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void> {
|
async replaceActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||||
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier])
|
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier])
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeActiveTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): Promise<void> {
|
async removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> {
|
||||||
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
|
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []
|
||||||
|
|
||||||
const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier)
|
const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier)
|
||||||
@ -840,7 +524,7 @@ export class SNComponentManager
|
|||||||
await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes)
|
await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes)
|
||||||
}
|
}
|
||||||
|
|
||||||
isThemeActive(theme: ComponentOrNativeFeature<ThemeFeatureDescription>): boolean {
|
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean {
|
||||||
if (this.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled) {
|
if (this.features.getFeatureStatus(theme.featureIdentifier) !== FeatureStatus.Entitled) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
export enum ComponentManagerEvent {
|
||||||
|
ViewerDidFocus = 'ViewerDidFocus',
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
import { ComponentViewerInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
export type ComponentManagerEventData = {
|
||||||
|
componentViewer?: ComponentViewerInterface
|
||||||
|
}
|
@ -40,7 +40,7 @@ import {
|
|||||||
Platform,
|
Platform,
|
||||||
OutgoingItemMessagePayload,
|
OutgoingItemMessagePayload,
|
||||||
ComponentPreferencesEntry,
|
ComponentPreferencesEntry,
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
ComponentInterface,
|
ComponentInterface,
|
||||||
} from '@standardnotes/models'
|
} from '@standardnotes/models'
|
||||||
import { environmentToString, platformToString } from '@Lib/Application/Platforms'
|
import { environmentToString, platformToString } from '@Lib/Application/Platforms'
|
||||||
@ -52,7 +52,7 @@ import {
|
|||||||
MessageReplyData,
|
MessageReplyData,
|
||||||
ReadwriteActions,
|
ReadwriteActions,
|
||||||
} from './Types'
|
} from './Types'
|
||||||
import { ComponentViewerRequiresComponentManagerFunctions } from './ComponentViewerRequiresComponentManagerFunctions'
|
import { ComponentViewerRequiresComponentManagerProperties } from './ComponentViewerRequiresComponentManagerFunctions'
|
||||||
import {
|
import {
|
||||||
ComponentAction,
|
ComponentAction,
|
||||||
ComponentPermission,
|
ComponentPermission,
|
||||||
@ -94,7 +94,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
public sessionKey?: string
|
public sessionKey?: string
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private componentOrFeature: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
|
private componentOrFeature: UIFeature<IframeComponentFeatureDescription>,
|
||||||
private services: {
|
private services: {
|
||||||
items: ItemManagerInterface
|
items: ItemManagerInterface
|
||||||
mutator: MutatorClientInterface
|
mutator: MutatorClientInterface
|
||||||
@ -111,7 +111,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
private config: {
|
private config: {
|
||||||
environment: Environment
|
environment: Environment
|
||||||
platform: Platform
|
platform: Platform
|
||||||
componentManagerFunctions: ComponentViewerRequiresComponentManagerFunctions
|
componentManagerFunctions: ComponentViewerRequiresComponentManagerProperties
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
if (isComponentViewerItemReadonlyItem(options.item)) {
|
if (isComponentViewerItemReadonlyItem(options.item)) {
|
||||||
@ -152,7 +152,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
this.log('Constructor', this)
|
this.log('Constructor', this)
|
||||||
}
|
}
|
||||||
|
|
||||||
public getComponentOrFeatureItem(): ComponentOrNativeFeature<IframeComponentFeatureDescription> {
|
public getComponentOrFeatureItem(): UIFeature<IframeComponentFeatureDescription> {
|
||||||
return this.componentOrFeature
|
return this.componentOrFeature
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +269,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = new ComponentOrNativeFeature<IframeComponentFeatureDescription>(updatedComponent)
|
const item = new UIFeature<IframeComponentFeatureDescription>(updatedComponent)
|
||||||
|
|
||||||
this.componentOrFeature = item
|
this.componentOrFeature = item
|
||||||
}
|
}
|
||||||
@ -320,7 +320,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
() => {
|
() => {
|
||||||
@ -335,7 +335,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
name: ComponentAction.StreamContextItem,
|
name: ComponentAction.StreamContextItem,
|
||||||
},
|
},
|
||||||
] as ComponentPermission[]
|
] as ComponentPermission[]
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredContextPermissions,
|
requiredContextPermissions,
|
||||||
() => {
|
() => {
|
||||||
@ -625,7 +625,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
content_types: types,
|
content_types: types,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
() => {
|
() => {
|
||||||
@ -650,7 +650,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
() => {
|
() => {
|
||||||
@ -707,7 +707,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
} as ComponentPermission)
|
} as ComponentPermission)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
|
|
||||||
@ -830,7 +830,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
async () => {
|
async () => {
|
||||||
@ -897,7 +897,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
requiredPermissions,
|
requiredPermissions,
|
||||||
async () => {
|
async () => {
|
||||||
@ -934,7 +934,7 @@ export class ComponentViewer implements ComponentViewerInterface {
|
|||||||
|
|
||||||
handleSetComponentPreferencesMessage(message: ComponentMessage): void {
|
handleSetComponentPreferencesMessage(message: ComponentMessage): void {
|
||||||
const noPermissionsRequired: ComponentPermission[] = []
|
const noPermissionsRequired: ComponentPermission[] = []
|
||||||
this.config.componentManagerFunctions.runWithPermissions(
|
this.config.componentManagerFunctions.runWithPermissionsUseCase.execute(
|
||||||
this.componentUniqueIdentifier,
|
this.componentUniqueIdentifier,
|
||||||
noPermissionsRequired,
|
noPermissionsRequired,
|
||||||
async () => {
|
async () => {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import { ComponentOrNativeFeature, ComponentPreferencesEntry } from '@standardnotes/models'
|
import { UIFeature, ComponentPreferencesEntry } from '@standardnotes/models'
|
||||||
import { RunWithPermissionsCallback } from './Types'
|
|
||||||
import { IframeComponentFeatureDescription } from '@standardnotes/features'
|
import { IframeComponentFeatureDescription } from '@standardnotes/features'
|
||||||
|
import { RunWithPermissionsUseCase } from './UseCase/RunWithPermissionsUseCase'
|
||||||
|
|
||||||
|
export interface ComponentViewerRequiresComponentManagerProperties {
|
||||||
|
runWithPermissionsUseCase: RunWithPermissionsUseCase
|
||||||
|
|
||||||
export interface ComponentViewerRequiresComponentManagerFunctions {
|
|
||||||
runWithPermissions: RunWithPermissionsCallback
|
|
||||||
urlsForActiveThemes: () => string[]
|
urlsForActiveThemes: () => string[]
|
||||||
|
|
||||||
setComponentPreferences(
|
setComponentPreferences(
|
||||||
component: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
|
component: UIFeature<IframeComponentFeatureDescription>,
|
||||||
preferences: ComponentPreferencesEntry,
|
preferences: ComponentPreferencesEntry,
|
||||||
): Promise<void>
|
): Promise<void>
|
||||||
|
|
||||||
getComponentPreferences(
|
getComponentPreferences(
|
||||||
component: ComponentOrNativeFeature<IframeComponentFeatureDescription>,
|
component: UIFeature<IframeComponentFeatureDescription>,
|
||||||
): ComponentPreferencesEntry | undefined
|
): ComponentPreferencesEntry | undefined
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,8 @@
|
|||||||
import {
|
import { ComponentArea, ComponentAction, FeatureIdentifier, LegacyFileSafeIdentifier } from '@standardnotes/features'
|
||||||
ComponentArea,
|
|
||||||
ComponentAction,
|
|
||||||
FeatureIdentifier,
|
|
||||||
LegacyFileSafeIdentifier,
|
|
||||||
ComponentPermission,
|
|
||||||
} from '@standardnotes/features'
|
|
||||||
import { ComponentMessage, MessageData, OutgoingItemMessagePayload } from '@standardnotes/models'
|
import { ComponentMessage, MessageData, OutgoingItemMessagePayload } from '@standardnotes/models'
|
||||||
import { UuidString } from '@Lib/Types/UuidString'
|
import { UuidString } from '@Lib/Types/UuidString'
|
||||||
import { ContentType } from '@standardnotes/domain-core'
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
|
|
||||||
export type RunWithPermissionsCallback = (
|
|
||||||
componentUuid: UuidString,
|
|
||||||
requiredPermissions: ComponentPermission[],
|
|
||||||
runFunction: () => void,
|
|
||||||
) => void
|
|
||||||
|
|
||||||
export const ReadwriteActions = [
|
export const ReadwriteActions = [
|
||||||
ComponentAction.SaveItems,
|
ComponentAction.SaveItems,
|
||||||
ComponentAction.CreateItem,
|
ComponentAction.CreateItem,
|
||||||
|
@ -0,0 +1,76 @@
|
|||||||
|
import {
|
||||||
|
FeatureIdentifier,
|
||||||
|
FindNativeFeature,
|
||||||
|
IframeComponentFeatureDescription,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { DoesEditorChangeRequireAlertUseCase } from './DoesEditorChangeRequireAlert'
|
||||||
|
import { UIFeature } from '@standardnotes/models'
|
||||||
|
|
||||||
|
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||||
|
return new UIFeature(FindNativeFeature<F>(identifier)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('editor change alert', () => {
|
||||||
|
let usecase: DoesEditorChangeRequireAlertUseCase
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
usecase = new DoesEditorChangeRequireAlertUseCase()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not require alert switching from plain editor', () => {
|
||||||
|
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||||
|
const requiresAlert = usecase.execute(undefined, component)
|
||||||
|
expect(requiresAlert).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not require alert switching to plain editor', () => {
|
||||||
|
const component = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||||
|
const requiresAlert = usecase.execute(component, undefined)
|
||||||
|
expect(requiresAlert).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not require alert switching from a markdown editor', () => {
|
||||||
|
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||||
|
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||||
|
FeatureIdentifier.MarkdownProEditor,
|
||||||
|
)
|
||||||
|
const requiresAlert = usecase.execute(markdownEditor, htmlEditor)
|
||||||
|
expect(requiresAlert).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not require alert switching to a markdown editor', () => {
|
||||||
|
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||||
|
const markdownEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||||
|
FeatureIdentifier.MarkdownProEditor,
|
||||||
|
)
|
||||||
|
const requiresAlert = usecase.execute(htmlEditor, markdownEditor)
|
||||||
|
expect(requiresAlert).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not require alert switching from & to a html editor', () => {
|
||||||
|
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||||
|
const requiresAlert = usecase.execute(htmlEditor, htmlEditor)
|
||||||
|
expect(requiresAlert).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require alert switching from a html editor to custom editor', () => {
|
||||||
|
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||||
|
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||||
|
const requiresAlert = usecase.execute(htmlEditor, customEditor)
|
||||||
|
expect(requiresAlert).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require alert switching from a custom editor to html editor', () => {
|
||||||
|
const htmlEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.PlusEditor)!
|
||||||
|
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||||
|
const requiresAlert = usecase.execute(customEditor, htmlEditor)
|
||||||
|
expect(requiresAlert).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should require alert switching from a custom editor to custom editor', () => {
|
||||||
|
const customEditor = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.TokenVaultEditor)
|
||||||
|
const requiresAlert = usecase.execute(customEditor, customEditor)
|
||||||
|
expect(requiresAlert).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,24 @@
|
|||||||
|
import { EditorFeatureDescription, IframeComponentFeatureDescription } from '@standardnotes/features'
|
||||||
|
import { UIFeature } from '@standardnotes/models'
|
||||||
|
|
||||||
|
export class DoesEditorChangeRequireAlertUseCase {
|
||||||
|
execute(
|
||||||
|
from: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
|
to: UIFeature<IframeComponentFeatureDescription | EditorFeatureDescription> | undefined,
|
||||||
|
): boolean {
|
||||||
|
if (!from || !to) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromFileType = from.fileType
|
||||||
|
const toFileType = to.fileType
|
||||||
|
const isEitherMarkdown = fromFileType === 'md' || toFileType === 'md'
|
||||||
|
const areBothHtml = fromFileType === 'html' && toFileType === 'html'
|
||||||
|
|
||||||
|
if (isEitherMarkdown || areBothHtml) {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
import { createNote } from '@Lib/Spec/SpecUtils'
|
||||||
|
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
|
||||||
|
import { EditorForNoteUseCase } from './EditorForNote'
|
||||||
|
import { ItemManagerInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
describe('EditorForNote', () => {
|
||||||
|
let usecase: EditorForNoteUseCase
|
||||||
|
let items: ItemManagerInterface
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
items = {} as jest.Mocked<ItemManagerInterface>
|
||||||
|
usecase = new EditorForNoteUseCase(items)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getEditorForNote should return plain notes is note type is plain', () => {
|
||||||
|
const note = createNote({
|
||||||
|
noteType: NoteType.Plain,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(usecase.execute(note).featureIdentifier).toBe(FeatureIdentifier.PlainEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getEditorForNote should call legacy function if no note editorIdentifier or noteType', () => {
|
||||||
|
const note = createNote({})
|
||||||
|
|
||||||
|
usecase['legacyGetEditorForNote'] = jest.fn()
|
||||||
|
usecase.execute(note)
|
||||||
|
|
||||||
|
expect(usecase['legacyGetEditorForNote']).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,102 @@
|
|||||||
|
import {
|
||||||
|
ComponentArea,
|
||||||
|
EditorFeatureDescription,
|
||||||
|
FeatureIdentifier,
|
||||||
|
FindNativeFeature,
|
||||||
|
GetIframeAndNativeEditors,
|
||||||
|
GetPlainNoteFeature,
|
||||||
|
GetSuperNoteFeature,
|
||||||
|
IframeComponentFeatureDescription,
|
||||||
|
NoteType,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { ComponentInterface, SNNote, UIFeature } from '@standardnotes/models'
|
||||||
|
import { ItemManagerInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
export class EditorForNoteUseCase {
|
||||||
|
constructor(private items: ItemManagerInterface) {}
|
||||||
|
|
||||||
|
execute(note: SNNote): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> {
|
||||||
|
if (note.noteType === NoteType.Plain) {
|
||||||
|
return new UIFeature(GetPlainNoteFeature())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.noteType === NoteType.Super) {
|
||||||
|
return new UIFeature(GetSuperNoteFeature())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.editorIdentifier) {
|
||||||
|
const result = this.componentOrNativeFeatureForIdentifier(note.editorIdentifier)
|
||||||
|
if (result) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.noteType && note.noteType !== NoteType.Unknown) {
|
||||||
|
const result = this.nativeEditorForNoteType(note.noteType)
|
||||||
|
if (result) {
|
||||||
|
return new UIFeature(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyResult = this.legacyGetEditorForNote(note)
|
||||||
|
if (legacyResult) {
|
||||||
|
return new UIFeature<IframeComponentFeatureDescription>(legacyResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UIFeature(GetPlainNoteFeature())
|
||||||
|
}
|
||||||
|
|
||||||
|
private componentOrNativeFeatureForIdentifier(
|
||||||
|
identifier: FeatureIdentifier | string,
|
||||||
|
): UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription> | undefined {
|
||||||
|
const nativeFeature = FindNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(
|
||||||
|
identifier as FeatureIdentifier,
|
||||||
|
)
|
||||||
|
if (nativeFeature) {
|
||||||
|
return new UIFeature(nativeFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
const component = this.items.getDisplayableComponents().find((component) => {
|
||||||
|
return component.identifier === identifier
|
||||||
|
})
|
||||||
|
if (component) {
|
||||||
|
return new UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
private nativeEditorForNoteType(noteType: NoteType): EditorFeatureDescription | undefined {
|
||||||
|
const nativeEditors = GetIframeAndNativeEditors()
|
||||||
|
return nativeEditors.find((editor) => editor.note_type === noteType)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Uses legacy approach of note/editor association. New method uses note.editorIdentifier and note.noteType directly. */
|
||||||
|
private legacyGetEditorForNote(note: SNNote): ComponentInterface | undefined {
|
||||||
|
const editors = this.thirdPartyComponentsForArea(ComponentArea.Editor)
|
||||||
|
for (const editor of editors) {
|
||||||
|
if (editor.isExplicitlyEnabledForItem(note.uuid)) {
|
||||||
|
return editor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultEditor = this.legacyGetDefaultEditor()
|
||||||
|
|
||||||
|
if (defaultEditor && !defaultEditor.isExplicitlyDisabledForItem(note.uuid)) {
|
||||||
|
return defaultEditor
|
||||||
|
} else {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private legacyGetDefaultEditor(): ComponentInterface | undefined {
|
||||||
|
const editors = this.thirdPartyComponentsForArea(ComponentArea.Editor)
|
||||||
|
return editors.filter((e) => e.legacyIsDefaultEditor())[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
private thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
|
||||||
|
return this.items.getDisplayableComponents().filter((component) => {
|
||||||
|
return component.area === area
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
|
||||||
|
import { GetDefaultEditorIdentifier } from './GetDefaultEditorIdentifier'
|
||||||
|
import { ComponentArea, FeatureIdentifier } from '@standardnotes/features'
|
||||||
|
import { SNComponent, SNTag } from '@standardnotes/models'
|
||||||
|
|
||||||
|
describe('getDefaultEditorIdentifier', () => {
|
||||||
|
let usecase: GetDefaultEditorIdentifier
|
||||||
|
let preferences: PreferenceServiceInterface
|
||||||
|
let items: ItemManagerInterface
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
preferences = {} as jest.Mocked<PreferenceServiceInterface>
|
||||||
|
preferences.getValue = jest.fn()
|
||||||
|
|
||||||
|
items = {} as jest.Mocked<ItemManagerInterface>
|
||||||
|
items.getDisplayableComponents = jest.fn().mockReturnValue([])
|
||||||
|
|
||||||
|
usecase = new GetDefaultEditorIdentifier(preferences, items)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return plain editor if no default tag editor or component editor', () => {
|
||||||
|
const editorIdentifier = usecase.execute().getValue()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return pref key based value if available', () => {
|
||||||
|
preferences.getValue = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor)
|
||||||
|
|
||||||
|
const editorIdentifier = usecase.execute().getValue()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return default tag identifier if tag supplied', () => {
|
||||||
|
const tag = {
|
||||||
|
preferences: {
|
||||||
|
editorIdentifier: FeatureIdentifier.SuperEditor,
|
||||||
|
},
|
||||||
|
} as jest.Mocked<SNTag>
|
||||||
|
|
||||||
|
const editorIdentifier = usecase.execute(tag).getValue()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return legacy editor identifier', () => {
|
||||||
|
const editor = {
|
||||||
|
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
|
||||||
|
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||||
|
area: ComponentArea.Editor,
|
||||||
|
} as unknown as jest.Mocked<SNComponent>
|
||||||
|
|
||||||
|
items.getDisplayableComponents = jest.fn().mockReturnValue([editor])
|
||||||
|
|
||||||
|
const editorIdentifier = usecase.execute().getValue()
|
||||||
|
|
||||||
|
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,36 @@
|
|||||||
|
import { Result, SyncUseCaseInterface } from '@standardnotes/domain-core'
|
||||||
|
import { ComponentArea, EditorIdentifier, FeatureIdentifier } from '@standardnotes/features'
|
||||||
|
import { ComponentInterface, PrefKey, SNTag } from '@standardnotes/models'
|
||||||
|
import { ItemManagerInterface, PreferenceServiceInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
export class GetDefaultEditorIdentifier implements SyncUseCaseInterface<EditorIdentifier> {
|
||||||
|
constructor(private preferences: PreferenceServiceInterface, private items: ItemManagerInterface) {}
|
||||||
|
|
||||||
|
execute(currentTag?: SNTag): Result<EditorIdentifier> {
|
||||||
|
if (currentTag) {
|
||||||
|
const editorIdentifier = currentTag?.preferences?.editorIdentifier
|
||||||
|
if (editorIdentifier) {
|
||||||
|
return Result.ok(editorIdentifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const preferenceValue = this.preferences.getValue(PrefKey.DefaultEditorIdentifier)
|
||||||
|
if (preferenceValue) {
|
||||||
|
return Result.ok(preferenceValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editors = this.thirdPartyComponentsForArea(ComponentArea.Editor)
|
||||||
|
const matchingEditor = editors.filter((e) => e.legacyIsDefaultEditor())[0]
|
||||||
|
if (matchingEditor) {
|
||||||
|
return Result.ok(matchingEditor.identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Result.ok(FeatureIdentifier.PlainEditor)
|
||||||
|
}
|
||||||
|
|
||||||
|
thirdPartyComponentsForArea(area: ComponentArea): ComponentInterface[] {
|
||||||
|
return this.items.getDisplayableComponents().filter((component) => {
|
||||||
|
return component.area === area
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
|
import {
|
||||||
|
FeatureIdentifier,
|
||||||
|
FindNativeFeature,
|
||||||
|
IframeComponentFeatureDescription,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import {
|
||||||
|
ComponentContent,
|
||||||
|
ComponentInterface,
|
||||||
|
ComponentPackageInfo,
|
||||||
|
DecryptedPayload,
|
||||||
|
Environment,
|
||||||
|
PayloadTimestampDefaults,
|
||||||
|
Platform,
|
||||||
|
SNComponent,
|
||||||
|
UIFeature,
|
||||||
|
} from '@standardnotes/models'
|
||||||
|
import { DesktopManagerInterface } from '@standardnotes/services'
|
||||||
|
import { GetFeatureUrl } from './GetFeatureUrl'
|
||||||
|
|
||||||
|
const desktopExtHost = 'http://localhost:123'
|
||||||
|
|
||||||
|
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||||
|
return new UIFeature(FindNativeFeature<F>(identifier)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
const thirdPartyFeature = () => {
|
||||||
|
const component = new SNComponent(
|
||||||
|
new DecryptedPayload({
|
||||||
|
uuid: '789',
|
||||||
|
content_type: ContentType.TYPES.Component,
|
||||||
|
...PayloadTimestampDefaults(),
|
||||||
|
content: {
|
||||||
|
local_url: 'sn://Extensions/non-native-identifier/dist/index.html',
|
||||||
|
hosted_url: 'https://example.com/component',
|
||||||
|
package_info: {
|
||||||
|
identifier: 'non-native-identifier' as FeatureIdentifier,
|
||||||
|
expires_at: new Date().getTime(),
|
||||||
|
availableInRoles: [],
|
||||||
|
} as unknown as jest.Mocked<ComponentPackageInfo>,
|
||||||
|
} as unknown as jest.Mocked<ComponentContent>,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
return new UIFeature<IframeComponentFeatureDescription>(component)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('GetFeatureUrl', () => {
|
||||||
|
let usecase: GetFeatureUrl
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
global.window = {
|
||||||
|
location: {
|
||||||
|
origin: 'http://localhost',
|
||||||
|
},
|
||||||
|
} as Window & typeof globalThis
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('desktop', () => {
|
||||||
|
let desktopManager: DesktopManagerInterface | undefined
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
desktopManager = {
|
||||||
|
syncComponentsInstallation() {},
|
||||||
|
registerUpdateObserver(_callback: (component: ComponentInterface) => void) {
|
||||||
|
return () => {}
|
||||||
|
},
|
||||||
|
getExtServerHost() {
|
||||||
|
return desktopExtHost
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
usecase = new GetFeatureUrl(desktopManager, Environment.Desktop, Platform.MacDesktop)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns native path for native component', () => {
|
||||||
|
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)!
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual(
|
||||||
|
`${desktopExtHost}/components/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns native path for deprecated native component', () => {
|
||||||
|
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(
|
||||||
|
FeatureIdentifier.DeprecatedBoldEditor,
|
||||||
|
)!
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual(
|
||||||
|
`${desktopExtHost}/components/${feature?.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns nonnative path for third party component', () => {
|
||||||
|
const feature = thirdPartyFeature()
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual(`${desktopExtHost}/Extensions/${feature.featureIdentifier}/dist/index.html`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns hosted url for third party component with no local_url', () => {
|
||||||
|
const component = new SNComponent({
|
||||||
|
uuid: '789',
|
||||||
|
content_type: ContentType.TYPES.Component,
|
||||||
|
content: {
|
||||||
|
hosted_url: 'https://example.com/component',
|
||||||
|
package_info: {
|
||||||
|
identifier: 'non-native-identifier',
|
||||||
|
valid_until: new Date(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as never)
|
||||||
|
const feature = new UIFeature<IframeComponentFeatureDescription>(component)
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual('https://example.com/component')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('web', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
usecase = new GetFeatureUrl(undefined, Environment.Web, Platform.MacWeb)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns native path for native feature', () => {
|
||||||
|
const feature = nativeFeatureAsUIFeature<IframeComponentFeatureDescription>(FeatureIdentifier.MarkdownProEditor)
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual(
|
||||||
|
`http://localhost/components/assets/${feature.featureIdentifier}/${feature.asFeatureDescription.index_path}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns hosted path for third party component', () => {
|
||||||
|
const feature = thirdPartyFeature()
|
||||||
|
const url = usecase.execute(feature)
|
||||||
|
expect(url).toEqual(feature.asComponent.hosted_url)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,74 @@
|
|||||||
|
import { ComponentFeatureDescription } from '@standardnotes/features'
|
||||||
|
import { Environment, Platform, UIFeature } from '@standardnotes/models'
|
||||||
|
import { DesktopManagerInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
|
const DESKTOP_URL_PREFIX = 'sn://'
|
||||||
|
const LOCAL_HOST = 'localhost'
|
||||||
|
const CUSTOM_LOCAL_HOST = 'sn.local'
|
||||||
|
const ANDROID_LOCAL_HOST = '10.0.2.2'
|
||||||
|
|
||||||
|
export class GetFeatureUrl {
|
||||||
|
constructor(
|
||||||
|
private desktopManager: DesktopManagerInterface | undefined,
|
||||||
|
private environment: Environment,
|
||||||
|
private platform: Platform,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
execute(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined {
|
||||||
|
if (this.desktopManager) {
|
||||||
|
return this.urlForFeatureOnDesktop(uiFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiFeature.isFeatureDescription) {
|
||||||
|
return this.urlForNativeComponent(uiFeature.asFeatureDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiFeature.asComponent.offlineOnly) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = uiFeature.asComponent.hosted_url || uiFeature.asComponent.legacy_url
|
||||||
|
if (!url) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isMobile) {
|
||||||
|
const localReplacement = this.platform === Platform.Ios ? LOCAL_HOST : ANDROID_LOCAL_HOST
|
||||||
|
return url.replace(LOCAL_HOST, localReplacement).replace(CUSTOM_LOCAL_HOST, localReplacement)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
private urlForFeatureOnDesktop(uiFeature: UIFeature<ComponentFeatureDescription>): string | undefined {
|
||||||
|
if (!this.desktopManager) {
|
||||||
|
throw new Error('Desktop manager is not defined')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiFeature.isFeatureDescription) {
|
||||||
|
return `${this.desktopManager.getExtServerHost()}/components/${uiFeature.featureIdentifier}/${
|
||||||
|
uiFeature.asFeatureDescription.index_path
|
||||||
|
}`
|
||||||
|
} else {
|
||||||
|
if (uiFeature.asComponent.local_url) {
|
||||||
|
return uiFeature.asComponent.local_url.replace(DESKTOP_URL_PREFIX, this.desktopManager.getExtServerHost() + '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
return uiFeature.asComponent.hosted_url || uiFeature.asComponent.legacy_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private urlForNativeComponent(feature: ComponentFeatureDescription): string {
|
||||||
|
if (this.isMobile) {
|
||||||
|
const baseUrlRequiredForThemesInsideEditors = window.location.href.split('/index.html')[0]
|
||||||
|
return `${baseUrlRequiredForThemesInsideEditors}/web-src/components/assets/${feature.identifier}/${feature.index_path}`
|
||||||
|
} else {
|
||||||
|
const baseUrlRequiredForThemesInsideEditors = window.location.origin
|
||||||
|
return `${baseUrlRequiredForThemesInsideEditors}/components/assets/${feature.identifier}/${feature.index_path}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get isMobile(): boolean {
|
||||||
|
return this.environment === Environment.Mobile
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,173 @@
|
|||||||
|
import { ContentType } from '@standardnotes/domain-core'
|
||||||
|
import {
|
||||||
|
ComponentAction,
|
||||||
|
ComponentPermission,
|
||||||
|
FeatureIdentifier,
|
||||||
|
FindNativeFeature,
|
||||||
|
UIFeatureDescriptionTypes,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { UIFeature } from '@standardnotes/models'
|
||||||
|
import { RunWithPermissionsUseCase } from './RunWithPermissionsUseCase'
|
||||||
|
import {
|
||||||
|
AlertService,
|
||||||
|
ItemManagerInterface,
|
||||||
|
MutatorClientInterface,
|
||||||
|
SyncServiceInterface,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
|
||||||
|
const nativeFeatureAsUIFeature = <F extends UIFeatureDescriptionTypes>(identifier: FeatureIdentifier) => {
|
||||||
|
return new UIFeature(FindNativeFeature<F>(identifier)!)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RunWithPermissionsUseCase', () => {
|
||||||
|
let usecase: RunWithPermissionsUseCase
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
usecase = new RunWithPermissionsUseCase(
|
||||||
|
() => {},
|
||||||
|
{} as jest.Mocked<AlertService>,
|
||||||
|
{} as jest.Mocked<MutatorClientInterface>,
|
||||||
|
{} as jest.Mocked<SyncServiceInterface>,
|
||||||
|
{} as jest.Mocked<ItemManagerInterface>,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('permissions', () => {
|
||||||
|
it('editor should be able to to stream single note', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamContextItem,
|
||||||
|
content_types: [ContentType.TYPES.Note],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no extension should be able to stream multiple notes', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [ContentType.TYPES.Note],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no extension should be able to stream multiple tags', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [ContentType.TYPES.Tag],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('no extension should be able to stream multiple notes or tags', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [ContentType.TYPES.Tag, ContentType.TYPES.Note],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.MarkdownProEditor),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('some valid and some invalid permissions should still return invalid permissions', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [ContentType.TYPES.Tag, ContentType.TYPES.FilesafeFileMetadata],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('filesafe should be able to stream its files', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [
|
||||||
|
ContentType.TYPES.FilesafeFileMetadata,
|
||||||
|
ContentType.TYPES.FilesafeCredentials,
|
||||||
|
ContentType.TYPES.FilesafeIntegration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedFileSafe),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('bold editor should be able to stream filesafe files', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [
|
||||||
|
ContentType.TYPES.FilesafeFileMetadata,
|
||||||
|
ContentType.TYPES.FilesafeCredentials,
|
||||||
|
ContentType.TYPES.FilesafeIntegration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(
|
||||||
|
nativeFeatureAsUIFeature(FeatureIdentifier.DeprecatedBoldEditor),
|
||||||
|
permissions,
|
||||||
|
),
|
||||||
|
).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('non bold editor should not able to stream filesafe files', () => {
|
||||||
|
const permissions: ComponentPermission[] = [
|
||||||
|
{
|
||||||
|
name: ComponentAction.StreamItems,
|
||||||
|
content_types: [
|
||||||
|
ContentType.TYPES.FilesafeFileMetadata,
|
||||||
|
ContentType.TYPES.FilesafeCredentials,
|
||||||
|
ContentType.TYPES.FilesafeIntegration,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
expect(
|
||||||
|
usecase.areRequestedPermissionsValid(nativeFeatureAsUIFeature(FeatureIdentifier.PlusEditor), permissions),
|
||||||
|
).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
@ -0,0 +1,243 @@
|
|||||||
|
import {
|
||||||
|
ComponentAction,
|
||||||
|
ComponentFeatureDescription,
|
||||||
|
ComponentPermission,
|
||||||
|
FeatureIdentifier,
|
||||||
|
FindNativeFeature,
|
||||||
|
} from '@standardnotes/features'
|
||||||
|
import { ComponentInterface, ComponentMutator, PermissionDialog, UIFeature } from '@standardnotes/models'
|
||||||
|
import {
|
||||||
|
AlertService,
|
||||||
|
ItemManagerInterface,
|
||||||
|
MutatorClientInterface,
|
||||||
|
SyncServiceInterface,
|
||||||
|
} from '@standardnotes/services'
|
||||||
|
import { AllowedBatchContentTypes, AllowedBatchStreaming } from '../Types'
|
||||||
|
import { Copy, filterFromArray, removeFromArray, uniqueArray } from '@standardnotes/utils'
|
||||||
|
import { permissionsStringForPermissions } from '../permissionsStringForPermissions'
|
||||||
|
|
||||||
|
export class RunWithPermissionsUseCase {
|
||||||
|
private permissionDialogs: PermissionDialog[] = []
|
||||||
|
private pendingErrorAlerts: Set<string> = new Set()
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private permissionDialogUIHandler: (dialog: PermissionDialog) => void,
|
||||||
|
private alerts: AlertService,
|
||||||
|
private mutator: MutatorClientInterface,
|
||||||
|
private sync: SyncServiceInterface,
|
||||||
|
private items: ItemManagerInterface,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
deinit() {
|
||||||
|
this.permissionDialogs = []
|
||||||
|
;(this.permissionDialogUIHandler as unknown) = undefined
|
||||||
|
;(this.alerts as unknown) = undefined
|
||||||
|
;(this.mutator as unknown) = undefined
|
||||||
|
;(this.sync as unknown) = undefined
|
||||||
|
;(this.items as unknown) = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(
|
||||||
|
componentIdentifier: string,
|
||||||
|
requiredPermissions: ComponentPermission[],
|
||||||
|
runFunction: () => void,
|
||||||
|
): void {
|
||||||
|
const uiFeature = this.findUIFeature(componentIdentifier)
|
||||||
|
|
||||||
|
if (!uiFeature) {
|
||||||
|
if (!this.pendingErrorAlerts.has(componentIdentifier)) {
|
||||||
|
this.pendingErrorAlerts.add(componentIdentifier)
|
||||||
|
void this.alerts
|
||||||
|
.alert(
|
||||||
|
`Unable to find component with ID ${componentIdentifier}. Please restart the app and try again.`,
|
||||||
|
'An unexpected error occurred',
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
this.pendingErrorAlerts.delete(componentIdentifier)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiFeature.isFeatureDescription) {
|
||||||
|
runFunction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.areRequestedPermissionsValid(uiFeature, requiredPermissions)) {
|
||||||
|
console.error('Component is requesting invalid permissions', componentIdentifier, requiredPermissions)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const acquiredPermissions = uiFeature.acquiredPermissions
|
||||||
|
|
||||||
|
/* Make copy as not to mutate input values */
|
||||||
|
requiredPermissions = Copy<ComponentPermission[]>(requiredPermissions)
|
||||||
|
for (const required of requiredPermissions.slice()) {
|
||||||
|
/* Remove anything we already have */
|
||||||
|
const respectiveAcquired = acquiredPermissions.find((candidate) => candidate.name === required.name)
|
||||||
|
if (!respectiveAcquired) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
/* We now match on name, lets substract from required.content_types anything we have in acquired. */
|
||||||
|
const requiredContentTypes = required.content_types
|
||||||
|
if (!requiredContentTypes) {
|
||||||
|
/* If this permission does not require any content types (i.e stream-context-item)
|
||||||
|
then we can remove this from required since we match by name (respectiveAcquired.name === required.name) */
|
||||||
|
filterFromArray(requiredPermissions, required)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for (const acquiredContentType of respectiveAcquired.content_types as string[]) {
|
||||||
|
removeFromArray(requiredContentTypes, acquiredContentType)
|
||||||
|
}
|
||||||
|
if (requiredContentTypes.length === 0) {
|
||||||
|
/* We've removed all acquired and end up with zero, means we already have all these permissions */
|
||||||
|
filterFromArray(requiredPermissions, required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (requiredPermissions.length > 0) {
|
||||||
|
this.promptForPermissionsWithDeferredRendering(
|
||||||
|
uiFeature.asComponent,
|
||||||
|
requiredPermissions,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/require-await
|
||||||
|
async (approved) => {
|
||||||
|
if (approved) {
|
||||||
|
runFunction()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
runFunction()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setPermissionDialogUIHandler(handler: (dialog: PermissionDialog) => void): void {
|
||||||
|
this.permissionDialogUIHandler = handler
|
||||||
|
}
|
||||||
|
|
||||||
|
areRequestedPermissionsValid(
|
||||||
|
uiFeature: UIFeature<ComponentFeatureDescription>,
|
||||||
|
permissions: ComponentPermission[],
|
||||||
|
): boolean {
|
||||||
|
for (const permission of permissions) {
|
||||||
|
if (permission.name === ComponentAction.StreamItems) {
|
||||||
|
if (!AllowedBatchStreaming.includes(uiFeature.featureIdentifier)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const hasNonAllowedBatchPermission = permission.content_types?.some(
|
||||||
|
(type) => !AllowedBatchContentTypes.includes(type),
|
||||||
|
)
|
||||||
|
if (hasNonAllowedBatchPermission) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptForPermissionsWithDeferredRendering(
|
||||||
|
component: ComponentInterface,
|
||||||
|
permissions: ComponentPermission[],
|
||||||
|
callback: (approved: boolean) => Promise<void>,
|
||||||
|
): void {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.promptForPermissions(component, permissions, callback)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private promptForPermissions(
|
||||||
|
component: ComponentInterface,
|
||||||
|
permissions: ComponentPermission[],
|
||||||
|
callback: (approved: boolean) => Promise<void>,
|
||||||
|
): void {
|
||||||
|
const params: PermissionDialog = {
|
||||||
|
component: component,
|
||||||
|
permissions: permissions,
|
||||||
|
permissionsString: permissionsStringForPermissions(permissions, component),
|
||||||
|
actionBlock: callback,
|
||||||
|
callback: async (approved: boolean) => {
|
||||||
|
const latestComponent = this.items.findItem<ComponentInterface>(component.uuid)
|
||||||
|
|
||||||
|
if (!latestComponent) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (approved) {
|
||||||
|
const componentPermissions = Copy(latestComponent.permissions) as ComponentPermission[]
|
||||||
|
for (const permission of permissions) {
|
||||||
|
const matchingPermission = componentPermissions.find((candidate) => candidate.name === permission.name)
|
||||||
|
if (!matchingPermission) {
|
||||||
|
componentPermissions.push(permission)
|
||||||
|
} else {
|
||||||
|
/* Permission already exists, but content_types may have been expanded */
|
||||||
|
const contentTypes = matchingPermission.content_types || []
|
||||||
|
matchingPermission.content_types = uniqueArray(contentTypes.concat(permission.content_types as string[]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.mutator.changeItem(component, (m) => {
|
||||||
|
const mutator = m as ComponentMutator
|
||||||
|
mutator.permissions = componentPermissions
|
||||||
|
})
|
||||||
|
|
||||||
|
void this.sync.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.permissionDialogs = this.permissionDialogs.filter((pendingDialog) => {
|
||||||
|
/* Remove self */
|
||||||
|
if (pendingDialog === params) {
|
||||||
|
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const containsObjectSubset = (source: ComponentPermission[], target: ComponentPermission[]) => {
|
||||||
|
return !target.some((val) => !source.find((candidate) => JSON.stringify(candidate) === JSON.stringify(val)))
|
||||||
|
}
|
||||||
|
if (pendingDialog.component === component) {
|
||||||
|
/* remove pending dialogs that are encapsulated by already approved permissions, and run its function */
|
||||||
|
if (
|
||||||
|
pendingDialog.permissions === permissions ||
|
||||||
|
containsObjectSubset(permissions, pendingDialog.permissions)
|
||||||
|
) {
|
||||||
|
/* If approved, run the action block. Otherwise, if canceled, cancel any
|
||||||
|
pending ones as well, since the user was explicit in their intentions */
|
||||||
|
if (approved) {
|
||||||
|
pendingDialog.actionBlock && pendingDialog.actionBlock(approved)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (this.permissionDialogs.length > 0) {
|
||||||
|
this.permissionDialogUIHandler(this.permissionDialogs[0])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Since these calls are asyncronous, multiple dialogs may be requested at the same time.
|
||||||
|
* We only want to present one and trigger all callbacks based on one modal result
|
||||||
|
*/
|
||||||
|
const existingDialog = this.permissionDialogs.find((dialog) => dialog.component === component)
|
||||||
|
this.permissionDialogs.push(params)
|
||||||
|
if (!existingDialog) {
|
||||||
|
this.permissionDialogUIHandler(params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findUIFeature(identifier: string): UIFeature<ComponentFeatureDescription> | undefined {
|
||||||
|
const nativeFeature = FindNativeFeature<ComponentFeatureDescription>(identifier as FeatureIdentifier)
|
||||||
|
if (nativeFeature) {
|
||||||
|
return new UIFeature(nativeFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentItem = this.items.findItem<ComponentInterface>(identifier)
|
||||||
|
if (componentItem) {
|
||||||
|
return new UIFeature<ComponentFeatureDescription>(componentItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,13 @@
|
|||||||
import { FindNativeTheme, GetNativeThemes, ThemeFeatureDescription } from '@standardnotes/features'
|
import { FindNativeTheme, GetNativeThemes, ThemeFeatureDescription } from '@standardnotes/features'
|
||||||
import { ComponentOrNativeFeature, ThemeInterface } from '@standardnotes/models'
|
import { UIFeature, ThemeInterface } from '@standardnotes/models'
|
||||||
import { ItemManagerInterface } from '@standardnotes/services'
|
import { ItemManagerInterface } from '@standardnotes/services'
|
||||||
|
|
||||||
export class GetAllThemesUseCase {
|
export class GetAllThemesUseCase {
|
||||||
constructor(private readonly items: ItemManagerInterface) {}
|
constructor(private readonly items: ItemManagerInterface) {}
|
||||||
|
|
||||||
execute(options: { excludeLayerable: boolean }): {
|
execute(options: { excludeLayerable: boolean }): {
|
||||||
thirdParty: ComponentOrNativeFeature<ThemeFeatureDescription>[]
|
thirdParty: UIFeature<ThemeFeatureDescription>[]
|
||||||
native: ComponentOrNativeFeature<ThemeFeatureDescription>[]
|
native: UIFeature<ThemeFeatureDescription>[]
|
||||||
} {
|
} {
|
||||||
const nativeThemes = GetNativeThemes().filter((feature) => (options.excludeLayerable ? !feature.layerable : true))
|
const nativeThemes = GetNativeThemes().filter((feature) => (options.excludeLayerable ? !feature.layerable : true))
|
||||||
|
|
||||||
@ -22,8 +22,8 @@ export class GetAllThemesUseCase {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
thirdParty: filteredThirdPartyThemes.map((theme) => new ComponentOrNativeFeature<ThemeFeatureDescription>(theme)),
|
thirdParty: filteredThirdPartyThemes.map((theme) => new UIFeature<ThemeFeatureDescription>(theme)),
|
||||||
native: nativeThemes.map((theme) => new ComponentOrNativeFeature(theme)),
|
native: nativeThemes.map((theme) => new UIFeature(theme)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { dismissToast, ToastType, addTimedToast } from '@standardnotes/toast'
|
import { dismissToast, ToastType, addTimedToast } from '@standardnotes/toast'
|
||||||
import {
|
import {
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
CreateDecryptedLocalStorageContextPayload,
|
CreateDecryptedLocalStorageContextPayload,
|
||||||
LocalStorageDecryptedContextualPayload,
|
LocalStorageDecryptedContextualPayload,
|
||||||
PrefKey,
|
PrefKey,
|
||||||
@ -44,7 +44,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
if (desktopService) {
|
if (desktopService) {
|
||||||
this.eventDisposers.push(
|
this.eventDisposers.push(
|
||||||
desktopService.registerUpdateObserver((component) => {
|
desktopService.registerUpdateObserver((component) => {
|
||||||
const uiFeature = new ComponentOrNativeFeature<ThemeFeatureDescription>(component)
|
const uiFeature = new UIFeature<ThemeFeatureDescription>(component)
|
||||||
if (uiFeature.isThemeComponent) {
|
if (uiFeature.isThemeComponent) {
|
||||||
if (this.components.isThemeActive(uiFeature)) {
|
if (this.components.isThemeActive(uiFeature)) {
|
||||||
this.deactivateThemeInTheUI(uiFeature.uniqueIdentifier)
|
this.deactivateThemeInTheUI(uiFeature.uniqueIdentifier)
|
||||||
@ -81,7 +81,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
this.application.items.findItem<ThemeInterface>(activeTheme)
|
this.application.items.findItem<ThemeInterface>(activeTheme)
|
||||||
|
|
||||||
if (theme) {
|
if (theme) {
|
||||||
const uiFeature = new ComponentOrNativeFeature<ThemeFeatureDescription>(theme)
|
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
|
||||||
this.activateTheme(uiFeature)
|
this.activateTheme(uiFeature)
|
||||||
hasChange = true
|
hasChange = true
|
||||||
}
|
}
|
||||||
@ -296,7 +296,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private activateTheme(theme: ComponentOrNativeFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) {
|
private activateTheme(theme: UIFeature<ThemeFeatureDescription>, skipEntitlementCheck = false) {
|
||||||
if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) {
|
if (this.themesActiveInTheUI.find((uuid) => uuid === theme.uniqueIdentifier)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -308,7 +308,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = this.application.componentManager.urlForComponent(theme)
|
const url = this.application.componentManager.urlForFeature(theme)
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -383,7 +383,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped)
|
return this.application.setValue(CachedThemesKey, mapped, StorageValueModes.Nonwrapped)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCachedThemes(): ComponentOrNativeFeature<ThemeFeatureDescription>[] {
|
private getCachedThemes(): UIFeature<ThemeFeatureDescription>[] {
|
||||||
const cachedThemes = this.application.getValue<LocalStorageDecryptedContextualPayload[]>(
|
const cachedThemes = this.application.getValue<LocalStorageDecryptedContextualPayload[]>(
|
||||||
CachedThemesKey,
|
CachedThemesKey,
|
||||||
StorageValueModes.Nonwrapped,
|
StorageValueModes.Nonwrapped,
|
||||||
@ -396,7 +396,7 @@ export class ThemeManager extends AbstractUIServicee {
|
|||||||
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
|
const theme = this.application.items.createItemFromPayload<ThemeInterface>(payload)
|
||||||
themes.push(theme)
|
themes.push(theme)
|
||||||
}
|
}
|
||||||
return themes.map((theme) => new ComponentOrNativeFeature<ThemeFeatureDescription>(theme))
|
return themes.map((theme) => new UIFeature<ThemeFeatureDescription>(theme))
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
@ -1,24 +1,9 @@
|
|||||||
/**
|
import { Environment, namespacedKey, Platform, RawStorageKey, SNLog } from '@standardnotes/snjs'
|
||||||
* @jest-environment jsdom
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
Environment,
|
|
||||||
FeatureIdentifier,
|
|
||||||
namespacedKey,
|
|
||||||
Platform,
|
|
||||||
RawStorageKey,
|
|
||||||
SNComponent,
|
|
||||||
SNComponentManager,
|
|
||||||
SNLog,
|
|
||||||
SNTag,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
import { WebOrDesktopDevice } from './Device/WebOrDesktopDevice'
|
||||||
|
|
||||||
describe('web application', () => {
|
describe('web application', () => {
|
||||||
let application: WebApplication
|
let application: WebApplication
|
||||||
let componentManager: SNComponentManager
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
SNLog.onLog = console.log
|
SNLog.onLog = console.log
|
||||||
@ -45,51 +30,10 @@ describe('web application', () => {
|
|||||||
|
|
||||||
application = new WebApplication(device, Platform.MacWeb, identifier, 'https://sync', 'https://socket')
|
application = new WebApplication(device, Platform.MacWeb, identifier, 'https://sync', 'https://socket')
|
||||||
|
|
||||||
componentManager = {} as jest.Mocked<SNComponentManager>
|
|
||||||
componentManager.legacyGetDefaultEditor = jest.fn()
|
|
||||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
|
||||||
|
|
||||||
await application.prepareForLaunch({ receiveChallenge: jest.fn() })
|
await application.prepareForLaunch({ receiveChallenge: jest.fn() })
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('geDefaultEditorIdentifier', () => {
|
it('should create application', () => {
|
||||||
it('should return plain editor if no default tag editor or component editor', () => {
|
expect(application).toBeTruthy()
|
||||||
const editorIdentifier = application.geDefaultEditorIdentifier()
|
|
||||||
|
|
||||||
expect(editorIdentifier).toEqual(FeatureIdentifier.PlainEditor)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return pref key based value if available', () => {
|
|
||||||
application.getPreference = jest.fn().mockReturnValue(FeatureIdentifier.SuperEditor)
|
|
||||||
|
|
||||||
const editorIdentifier = application.geDefaultEditorIdentifier()
|
|
||||||
|
|
||||||
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return default tag identifier if tag supplied', () => {
|
|
||||||
const tag = {
|
|
||||||
preferences: {
|
|
||||||
editorIdentifier: FeatureIdentifier.SuperEditor,
|
|
||||||
},
|
|
||||||
} as jest.Mocked<SNTag>
|
|
||||||
|
|
||||||
const editorIdentifier = application.geDefaultEditorIdentifier(tag)
|
|
||||||
|
|
||||||
expect(editorIdentifier).toEqual(FeatureIdentifier.SuperEditor)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should return legacy editor identifier', () => {
|
|
||||||
const editor = {
|
|
||||||
legacyIsDefaultEditor: jest.fn().mockReturnValue(true),
|
|
||||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
|
||||||
} as unknown as jest.Mocked<SNComponent>
|
|
||||||
|
|
||||||
componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue(editor)
|
|
||||||
|
|
||||||
const editorIdentifier = application.geDefaultEditorIdentifier()
|
|
||||||
|
|
||||||
expect(editorIdentifier).toEqual(FeatureIdentifier.MarkdownProEditor)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -17,8 +17,6 @@ import {
|
|||||||
MobileDeviceInterface,
|
MobileDeviceInterface,
|
||||||
MobileUnlockTiming,
|
MobileUnlockTiming,
|
||||||
DecryptedItem,
|
DecryptedItem,
|
||||||
EditorIdentifier,
|
|
||||||
FeatureIdentifier,
|
|
||||||
Environment,
|
Environment,
|
||||||
ApplicationOptionsDefaults,
|
ApplicationOptionsDefaults,
|
||||||
BackupServiceInterface,
|
BackupServiceInterface,
|
||||||
@ -511,15 +509,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter
|
|||||||
return this.environment === Environment.Web
|
return this.environment === Environment.Web
|
||||||
}
|
}
|
||||||
|
|
||||||
geDefaultEditorIdentifier(currentTag?: SNTag): EditorIdentifier {
|
|
||||||
return (
|
|
||||||
currentTag?.preferences?.editorIdentifier ||
|
|
||||||
this.getPreference(PrefKey.DefaultEditorIdentifier) ||
|
|
||||||
this.componentManager.legacyGetDefaultEditor()?.identifier ||
|
|
||||||
FeatureIdentifier.PlainEditor
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
openPreferences(pane?: PreferenceId): void {
|
openPreferences(pane?: PreferenceId): void {
|
||||||
this.controllers.preferencesController.openPreferences()
|
this.controllers.preferencesController.openPreferences()
|
||||||
if (pane) {
|
if (pane) {
|
||||||
|
@ -33,7 +33,7 @@ const ChangeEditorButton: FunctionComponent<Props> = ({
|
|||||||
|
|
||||||
const noteType = noteViewController?.isTemplateNote
|
const noteType = noteViewController?.isTemplateNote
|
||||||
? noteTypeForEditorIdentifier(
|
? noteTypeForEditorIdentifier(
|
||||||
application.geDefaultEditorIdentifier(
|
application.componentManager.getDefaultEditorIdentifier(
|
||||||
noteViewController.templateNoteOptions?.tag
|
noteViewController.templateNoteOptions?.tag
|
||||||
? application.items.findItem(noteViewController.templateNoteOptions.tag)
|
? application.items.findItem(noteViewController.templateNoteOptions.tag)
|
||||||
: undefined,
|
: undefined,
|
||||||
|
@ -4,7 +4,7 @@ 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/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import {
|
import {
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
EditorFeatureDescription,
|
EditorFeatureDescription,
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
@ -30,7 +30,7 @@ type ChangeEditorMenuProps = {
|
|||||||
closeMenu: () => void
|
closeMenu: () => void
|
||||||
isVisible: boolean
|
isVisible: boolean
|
||||||
note: SNNote | undefined
|
note: SNNote | undefined
|
||||||
onSelect?: (component: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>) => void
|
onSelect?: (component: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>) => void
|
||||||
setDisableClickOutside?: (value: boolean) => void
|
setDisableClickOutside?: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
||||||
const [currentFeature, setCurrentFeature] =
|
const [currentFeature, setCurrentFeature] =
|
||||||
useState<ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>>()
|
useState<UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>>()
|
||||||
const [pendingConversionItem, setPendingConversionItem] = useState<EditorMenuItem | null>(null)
|
const [pendingConversionItem, setPendingConversionItem] = useState<EditorMenuItem | null>(null)
|
||||||
|
|
||||||
const showSuperNoteImporter =
|
const showSuperNoteImporter =
|
||||||
@ -83,10 +83,7 @@ const ChangeEditorMenu: FunctionComponent<ChangeEditorMenuProps> = ({
|
|||||||
)
|
)
|
||||||
|
|
||||||
const selectComponent = useCallback(
|
const selectComponent = useCallback(
|
||||||
async (
|
async (uiFeature: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>, note: SNNote) => {
|
||||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
|
||||||
note: SNNote,
|
|
||||||
) => {
|
|
||||||
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
||||||
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
||||||
mutator.conflictOf = undefined
|
mutator.conflictOf = undefined
|
||||||
|
@ -3,7 +3,7 @@ import { STRING_EDIT_LOCKED_ATTEMPT } from '@/Constants/Strings'
|
|||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups'
|
import { createEditorMenuGroups } from '@/Utils/createEditorMenuGroups'
|
||||||
import {
|
import {
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
EditorFeatureDescription,
|
EditorFeatureDescription,
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
NoteMutator,
|
NoteMutator,
|
||||||
@ -40,10 +40,7 @@ const ChangeEditorMultipleMenu = ({ application, notes, setDisableClickOutside }
|
|||||||
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
const groups = useMemo(() => createEditorMenuGroups(application), [application])
|
||||||
|
|
||||||
const selectComponent = useCallback(
|
const selectComponent = useCallback(
|
||||||
async (
|
async (uiFeature: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>, note: SNNote) => {
|
||||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>,
|
|
||||||
note: SNNote,
|
|
||||||
) => {
|
|
||||||
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
if (uiFeature.isComponent && uiFeature.asComponent.conflictOf) {
|
||||||
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
void application.changeAndSaveItem(uiFeature.asComponent, (mutator) => {
|
||||||
mutator.conflictOf = undefined
|
mutator.conflictOf = undefined
|
||||||
|
@ -66,7 +66,7 @@ const NewNotePreferences: FunctionComponent<Props> = ({
|
|||||||
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('')
|
const [customNoteTitleFormat, setCustomNoteTitleFormat] = useState('')
|
||||||
|
|
||||||
const getGlobalEditorDefaultIdentifier = useCallback((): string => {
|
const getGlobalEditorDefaultIdentifier = useCallback((): string => {
|
||||||
return application.geDefaultEditorIdentifier()
|
return application.componentManager.getDefaultEditorIdentifier()
|
||||||
}, [application])
|
}, [application])
|
||||||
|
|
||||||
const reloadPreferences = useCallback(() => {
|
const reloadPreferences = useCallback(() => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { WebApplication } from '@/Application/WebApplication'
|
import { WebApplication } from '@/Application/WebApplication'
|
||||||
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
import { QuickSettingsController } from '@/Controllers/QuickSettingsController'
|
||||||
import { ComponentOrNativeFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
import { UIFeature, GetDarkThemeFeature } from '@standardnotes/snjs'
|
||||||
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
import { TOGGLE_DARK_MODE_COMMAND } from '@standardnotes/ui-services'
|
||||||
import { classNames } from '@standardnotes/utils'
|
import { classNames } from '@standardnotes/utils'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
@ -25,7 +25,7 @@ const QuickSettingsButton = ({ application, isOpen, toggleMenu, quickSettingsMen
|
|||||||
return commandService.addCommandHandler({
|
return commandService.addCommandHandler({
|
||||||
command: TOGGLE_DARK_MODE_COMMAND,
|
command: TOGGLE_DARK_MODE_COMMAND,
|
||||||
onKeyDown: () => {
|
onKeyDown: () => {
|
||||||
void application.componentManager.toggleTheme(new ComponentOrNativeFeature(GetDarkThemeFeature()))
|
void application.componentManager.toggleTheme(new UIFeature(GetDarkThemeFeature()))
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}, [application, commandService])
|
}, [application, commandService])
|
||||||
|
@ -33,7 +33,6 @@ describe('note view controller', () => {
|
|||||||
application.sync.sync = jest.fn().mockReturnValue(Promise.resolve())
|
application.sync.sync = jest.fn().mockReturnValue(Promise.resolve())
|
||||||
|
|
||||||
componentManager = {} as jest.Mocked<SNComponentManager>
|
componentManager = {} as jest.Mocked<SNComponentManager>
|
||||||
componentManager.legacyGetDefaultEditor = jest.fn()
|
|
||||||
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
Object.defineProperty(application, 'componentManager', { value: componentManager })
|
||||||
|
|
||||||
const mutator = {} as jest.Mocked<MutatorClientInterface>
|
const mutator = {} as jest.Mocked<MutatorClientInterface>
|
||||||
@ -41,7 +40,7 @@ describe('note view controller', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should create notes with plaintext note type', async () => {
|
it('should create notes with plaintext note type', async () => {
|
||||||
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
||||||
|
|
||||||
const controller = new NoteViewController(application)
|
const controller = new NoteViewController(application)
|
||||||
await controller.initialize()
|
await controller.initialize()
|
||||||
@ -54,15 +53,15 @@ describe('note view controller', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should create notes with markdown note type', async () => {
|
it('should create notes with markdown note type', async () => {
|
||||||
componentManager.legacyGetDefaultEditor = jest.fn().mockReturnValue({
|
application.items.getDisplayableComponents = jest.fn().mockReturnValue([
|
||||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
{
|
||||||
} as SNComponent)
|
identifier: FeatureIdentifier.MarkdownProEditor,
|
||||||
|
} as SNComponent,
|
||||||
|
])
|
||||||
|
|
||||||
componentManager.componentOrNativeFeatureForIdentifier = jest.fn().mockReturnValue({
|
application.componentManager.getDefaultEditorIdentifier = jest
|
||||||
identifier: FeatureIdentifier.MarkdownProEditor,
|
.fn()
|
||||||
} as SNComponent)
|
.mockReturnValue(FeatureIdentifier.MarkdownProEditor)
|
||||||
|
|
||||||
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.MarkdownProEditor)
|
|
||||||
|
|
||||||
const controller = new NoteViewController(application)
|
const controller = new NoteViewController(application)
|
||||||
await controller.initialize()
|
await controller.initialize()
|
||||||
@ -75,7 +74,7 @@ describe('note view controller', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should add tag to note if default tag is set', async () => {
|
it('should add tag to note if default tag is set', async () => {
|
||||||
application.geDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
application.componentManager.getDefaultEditorIdentifier = jest.fn().mockReturnValue(FeatureIdentifier.PlainEditor)
|
||||||
|
|
||||||
const tag = {
|
const tag = {
|
||||||
uuid: 'tag-uuid',
|
uuid: 'tag-uuid',
|
||||||
|
@ -94,7 +94,7 @@ export class NoteViewController implements ItemViewControllerInterface {
|
|||||||
if (!this.item) {
|
if (!this.item) {
|
||||||
log(LoggingDomain.NoteView, 'Initializing as template note')
|
log(LoggingDomain.NoteView, 'Initializing as template note')
|
||||||
|
|
||||||
const editorIdentifier = this.application.geDefaultEditorIdentifier(this.defaultTag)
|
const editorIdentifier = this.application.componentManager.getDefaultEditorIdentifier(this.defaultTag)
|
||||||
|
|
||||||
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
|
const noteType = noteTypeForEditorIdentifier(editorIdentifier)
|
||||||
|
|
||||||
|
@ -13,12 +13,12 @@ import {
|
|||||||
ApplicationEvent,
|
ApplicationEvent,
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentInterface,
|
ComponentInterface,
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
ComponentViewerInterface,
|
ComponentViewerInterface,
|
||||||
ContentType,
|
ContentType,
|
||||||
EditorLineWidth,
|
EditorLineWidth,
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
isIframeUIFeature,
|
isUIFeatureAnIframeFeature,
|
||||||
isPayloadSourceInternalChange,
|
isPayloadSourceInternalChange,
|
||||||
isPayloadSourceRetrieved,
|
isPayloadSourceRetrieved,
|
||||||
NoteType,
|
NoteType,
|
||||||
@ -456,7 +456,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private createComponentViewer(component: ComponentOrNativeFeature<IframeComponentFeatureDescription>) {
|
private createComponentViewer(component: UIFeature<IframeComponentFeatureDescription>) {
|
||||||
if (!component) {
|
if (!component) {
|
||||||
throw Error('Cannot create component viewer for undefined component')
|
throw Error('Cannot create component viewer for undefined component')
|
||||||
}
|
}
|
||||||
@ -516,7 +516,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
const newUIFeature = this.application.componentManager.editorForNote(this.note)
|
const newUIFeature = this.application.componentManager.editorForNote(this.note)
|
||||||
|
|
||||||
/** Component editors cannot interact with template notes so the note must be inserted */
|
/** Component editors cannot interact with template notes so the note must be inserted */
|
||||||
if (isIframeUIFeature(newUIFeature) && this.controller.isTemplateNote) {
|
if (isUIFeatureAnIframeFeature(newUIFeature) && this.controller.isTemplateNote) {
|
||||||
await this.controller.insertTemplatedNote()
|
await this.controller.insertTemplatedNote()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -529,7 +529,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isIframeUIFeature(newUIFeature)) {
|
if (isUIFeatureAnIframeFeature(newUIFeature)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
editorComponentViewer: this.createComponentViewer(newUIFeature),
|
editorComponentViewer: this.createComponentViewer(newUIFeature),
|
||||||
editorStateDidLoad: true,
|
editorStateDidLoad: true,
|
||||||
@ -767,7 +767,7 @@ class NoteView extends AbstractComponent<NoteViewProps, State> {
|
|||||||
for (const component of needsNewViewer) {
|
for (const component of needsNewViewer) {
|
||||||
newViewers.push(
|
newViewers.push(
|
||||||
this.application.componentManager.createComponentViewer(
|
this.application.componentManager.createComponentViewer(
|
||||||
new ComponentOrNativeFeature<IframeComponentFeatureDescription>(component),
|
new UIFeature<IframeComponentFeatureDescription>(component),
|
||||||
{
|
{
|
||||||
uuid: this.note.uuid,
|
uuid: this.note.uuid,
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ContentType, NoteContent, NoteType, SNNote, classNames, isIframeUIFeature } from '@standardnotes/snjs'
|
import { ContentType, NoteContent, NoteType, SNNote, classNames, isUIFeatureAnIframeFeature } from '@standardnotes/snjs'
|
||||||
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
|
import { UIEventHandler, useEffect, useMemo, useRef } from 'react'
|
||||||
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
import { MutuallyExclusiveMediaQueryBreakpoints, useMediaQuery } from '@/Hooks/useMediaQuery'
|
||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
@ -31,7 +31,7 @@ export const ReadonlyNoteContent = ({
|
|||||||
|
|
||||||
const componentViewer = useMemo(() => {
|
const componentViewer = useMemo(() => {
|
||||||
const editorForCurrentNote = application.componentManager.editorForNote(note)
|
const editorForCurrentNote = application.componentManager.editorForNote(note)
|
||||||
if (!isIframeUIFeature(editorForCurrentNote)) {
|
if (!isUIFeatureAnIframeFeature(editorForCurrentNote)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
import {
|
import { UIFeature, EditorFeatureDescription, IframeComponentFeatureDescription } from '@standardnotes/snjs'
|
||||||
ComponentOrNativeFeature,
|
|
||||||
EditorFeatureDescription,
|
|
||||||
IframeComponentFeatureDescription,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
|
|
||||||
export type EditorMenuItem = {
|
export type EditorMenuItem = {
|
||||||
uiFeature: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
uiFeature: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||||
isEntitled: boolean
|
isEntitled: boolean
|
||||||
isLabs?: boolean
|
isLabs?: boolean
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { FunctionComponent } from 'react'
|
import { FunctionComponent } from 'react'
|
||||||
import {
|
import { UIFeature, EditorFeatureDescription, IframeComponentFeatureDescription, SNNote } from '@standardnotes/snjs'
|
||||||
ComponentOrNativeFeature,
|
|
||||||
EditorFeatureDescription,
|
|
||||||
IframeComponentFeatureDescription,
|
|
||||||
SNNote,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
import { NotesController } from '@/Controllers/NotesController/NotesController'
|
||||||
import { iconClass } from './ClassNames'
|
import { iconClass } from './ClassNames'
|
||||||
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
import MenuSwitchButtonItem from '../Menu/MenuSwitchButtonItem'
|
||||||
|
|
||||||
export const SpellcheckOptions: FunctionComponent<{
|
export const SpellcheckOptions: FunctionComponent<{
|
||||||
editorForNote: ComponentOrNativeFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
editorForNote: UIFeature<EditorFeatureDescription | IframeComponentFeatureDescription>
|
||||||
notesController: NotesController
|
notesController: NotesController
|
||||||
note: SNNote
|
note: SNNote
|
||||||
}> = ({ editorForNote, notesController, note }) => {
|
}> = ({ editorForNote, notesController, note }) => {
|
||||||
|
@ -24,7 +24,7 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="Activate Component"
|
title="Activate Plugin"
|
||||||
close={deny}
|
close={deny}
|
||||||
actions={[
|
actions={[
|
||||||
{ label: 'Cancel', onClick: deny, type: 'cancel', mobileSlot: 'left' },
|
{ label: 'Cancel', onClick: deny, type: 'cancel', mobileSlot: 'left' },
|
||||||
@ -52,10 +52,7 @@ const PermissionsModal = ({ callback, component, dismiss, permissionsString }: P
|
|||||||
</div>
|
</div>
|
||||||
<div className="sk-panel-row [word-break:break-word]">
|
<div className="sk-panel-row [word-break:break-word]">
|
||||||
<p className="sk-p">
|
<p className="sk-p">
|
||||||
Components use an offline messaging system to communicate. Learn more at{' '}
|
Plugins use an offline messaging system to communicate and can only access the current note.
|
||||||
<a href="https://standardnotes.com/permissions" rel="noopener" target="_blank" className="sk-a info">
|
|
||||||
https://standardnotes.com/permissions.
|
|
||||||
</a>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,11 +20,7 @@ const PermissionsModalWrapper: FunctionComponent<Props> = ({ application }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onAppStart = useCallback(() => {
|
const onAppStart = useCallback(() => {
|
||||||
application.componentManager.presentPermissionsDialog = presentPermissionsDialog
|
application.componentManager.setPermissionDialogUIHandler(presentPermissionsDialog)
|
||||||
|
|
||||||
return () => {
|
|
||||||
;(application.componentManager.presentPermissionsDialog as unknown) = undefined
|
|
||||||
}
|
|
||||||
}, [application, presentPermissionsDialog])
|
}, [application, presentPermissionsDialog])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
ComponentInterface,
|
ComponentInterface,
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
ContentType,
|
ContentType,
|
||||||
FeatureIdentifier,
|
FeatureIdentifier,
|
||||||
PreferencesServiceEvent,
|
PreferencesServiceEvent,
|
||||||
@ -31,7 +31,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ quickSettingsMenuCont
|
|||||||
|
|
||||||
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
|
const { focusModeEnabled, setFocusModeEnabled } = application.paneController
|
||||||
const { closeQuickSettingsMenu } = quickSettingsMenuController
|
const { closeQuickSettingsMenu } = quickSettingsMenuController
|
||||||
const [themes, setThemes] = useState<ComponentOrNativeFeature<ThemeFeatureDescription>[]>([])
|
const [themes, setThemes] = useState<UIFeature<ThemeFeatureDescription>[]>([])
|
||||||
const [editorStackComponents, setEditorStackComponents] = useState<ComponentInterface[]>([])
|
const [editorStackComponents, setEditorStackComponents] = useState<ComponentInterface[]>([])
|
||||||
|
|
||||||
const activeThemes = application.componentManager.getActiveThemes()
|
const activeThemes = application.componentManager.getActiveThemes()
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
import {
|
import { UIFeature, FeatureIdentifier, FeatureStatus, ThemeFeatureDescription } from '@standardnotes/snjs'
|
||||||
ComponentOrNativeFeature,
|
|
||||||
FeatureIdentifier,
|
|
||||||
FeatureStatus,
|
|
||||||
ThemeFeatureDescription,
|
|
||||||
} from '@standardnotes/snjs'
|
|
||||||
import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react'
|
import { FunctionComponent, MouseEventHandler, useCallback, useMemo } from 'react'
|
||||||
import Icon from '@/Components/Icon/Icon'
|
import Icon from '@/Components/Icon/Icon'
|
||||||
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
import { usePremiumModal } from '@/Hooks/usePremiumModal'
|
||||||
@ -18,7 +13,7 @@ import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/Keyboard
|
|||||||
import { useApplication } from '../ApplicationProvider'
|
import { useApplication } from '../ApplicationProvider'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
uiFeature: ComponentOrNativeFeature<ThemeFeatureDescription>
|
uiFeature: UIFeature<ThemeFeatureDescription>
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
const ThemesMenuButton: FunctionComponent<Props> = ({ uiFeature }) => {
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
NoteContent,
|
NoteContent,
|
||||||
NoteType,
|
NoteType,
|
||||||
SNNote,
|
SNNote,
|
||||||
isIframeUIFeature,
|
isUIFeatureAnIframeFeature,
|
||||||
spaceSeparatedStrings,
|
spaceSeparatedStrings,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { useCallback, useEffect, useMemo } from 'react'
|
import { useCallback, useEffect, useMemo } from 'react'
|
||||||
@ -61,7 +61,7 @@ const SuperNoteConverter = ({
|
|||||||
}, [format, note])
|
}, [format, note])
|
||||||
|
|
||||||
const componentViewer = useMemo(() => {
|
const componentViewer = useMemo(() => {
|
||||||
if (!uiFeature || !isIframeUIFeature(uiFeature)) {
|
if (!uiFeature || !isUIFeatureAnIframeFeature(uiFeature)) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
import { ComponentOrNativeFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
|
import { UIFeature, FeatureIdentifier, ThemeFeatureDescription } from '@standardnotes/snjs'
|
||||||
|
|
||||||
const isDarkModeTheme = (theme: ComponentOrNativeFeature<ThemeFeatureDescription>) =>
|
const isDarkModeTheme = (theme: UIFeature<ThemeFeatureDescription>) =>
|
||||||
theme.featureIdentifier === FeatureIdentifier.DarkTheme
|
theme.featureIdentifier === FeatureIdentifier.DarkTheme
|
||||||
|
|
||||||
export const sortThemes = (
|
export const sortThemes = (a: UIFeature<ThemeFeatureDescription>, b: UIFeature<ThemeFeatureDescription>) => {
|
||||||
a: ComponentOrNativeFeature<ThemeFeatureDescription>,
|
|
||||||
b: ComponentOrNativeFeature<ThemeFeatureDescription>,
|
|
||||||
) => {
|
|
||||||
const aIsLayerable = a.layerable
|
const aIsLayerable = a.layerable
|
||||||
const bIsLayerable = b.layerable
|
const bIsLayerable = b.layerable
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
GetIframeAndNativeEditors,
|
GetIframeAndNativeEditors,
|
||||||
ComponentArea,
|
ComponentArea,
|
||||||
GetSuperNoteFeature,
|
GetSuperNoteFeature,
|
||||||
ComponentOrNativeFeature,
|
UIFeature,
|
||||||
IframeComponentFeatureDescription,
|
IframeComponentFeatureDescription,
|
||||||
} from '@standardnotes/snjs'
|
} from '@standardnotes/snjs'
|
||||||
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
import { EditorMenuGroup } from '@/Components/NotesOptions/EditorMenuGroup'
|
||||||
@ -30,7 +30,7 @@ const insertNativeEditorsInMap = (map: NoteTypeToEditorRowsMap, application: Web
|
|||||||
const noteType = editorFeature.note_type
|
const noteType = editorFeature.note_type
|
||||||
map[noteType].push({
|
map[noteType].push({
|
||||||
isEntitled: application.features.getFeatureStatus(editorFeature.identifier) === FeatureStatus.Entitled,
|
isEntitled: application.features.getFeatureStatus(editorFeature.identifier) === FeatureStatus.Entitled,
|
||||||
uiFeature: new ComponentOrNativeFeature(editorFeature),
|
uiFeature: new UIFeature(editorFeature),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,7 +51,7 @@ const insertInstalledComponentsInMap = (map: NoteTypeToEditorRowsMap, applicatio
|
|||||||
const noteType = editor.noteType
|
const noteType = editor.noteType
|
||||||
|
|
||||||
const editorItem: EditorMenuItem = {
|
const editorItem: EditorMenuItem = {
|
||||||
uiFeature: new ComponentOrNativeFeature<IframeComponentFeatureDescription>(editor),
|
uiFeature: new UIFeature<IframeComponentFeatureDescription>(editor),
|
||||||
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
|
isEntitled: application.features.getFeatureStatus(editor.identifier) === FeatureStatus.Entitled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user