diff --git a/app/assets/icons/ic-window.svg b/app/assets/icons/ic-window.svg new file mode 100644 index 000000000..c94c68f8d --- /dev/null +++ b/app/assets/icons/ic-window.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/assets/javascripts/app.ts b/app/assets/javascripts/app.ts index ee83819e6..4ea77716b 100644 --- a/app/assets/javascripts/app.ts +++ b/app/assets/javascripts/app.ts @@ -42,7 +42,6 @@ import { import { ActionsMenu, ComponentModal, - ComponentView, EditorMenu, InputModal, MenuRow, @@ -76,6 +75,7 @@ import { AppVersion, IsWebPlatform } from '@/version'; import { NotesListOptionsDirective } from './components/NotesListOptionsMenu'; import { PurchaseFlowDirective } from './purchaseFlow'; import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu'; +import { ComponentViewDirective } from '@/components/ComponentView'; function reloadHiddenFirefoxTab(): boolean { /** @@ -154,7 +154,7 @@ const startApplication: StartApplication = async function startApplication( .directive('actionsMenu', () => new ActionsMenu()) .directive('challengeModal', () => new ChallengeModal()) .directive('componentModal', () => new ComponentModal()) - .directive('componentView', () => new ComponentView()) + .directive('componentView', ComponentViewDirective) .directive('editorMenu', () => new EditorMenu()) .directive('inputModal', () => new InputModal()) .directive('menuRow', () => new MenuRow()) diff --git a/app/assets/javascripts/components/AccountMenu/index.tsx b/app/assets/javascripts/components/AccountMenu/index.tsx index 54de2a9ab..12f6a12b0 100644 --- a/app/assets/javascripts/components/AccountMenu/index.tsx +++ b/app/assets/javascripts/components/AccountMenu/index.tsx @@ -105,7 +105,7 @@ const AccountMenu: FunctionComponent = observer( }; return ( -
+
void; +} + +export const IsDeprecated: FunctionalComponent = ({ + deprecationMessage, + dismissDeprecationMessage + }) => { + return ( +
+
+
+
+
+ {deprecationMessage || 'This extension is deprecated.'} +
+
+
+
+
+ +
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/ComponentView/IsExpired.tsx b/app/assets/javascripts/components/ComponentView/IsExpired.tsx new file mode 100644 index 000000000..730e880c7 --- /dev/null +++ b/app/assets/javascripts/components/ComponentView/IsExpired.tsx @@ -0,0 +1,57 @@ +import { FunctionalComponent } from 'preact'; + +interface IProps { + expiredDate: string; + reloadStatus: (doManualReload?: boolean) => void; +} + +export const IsExpired: FunctionalComponent = ({ + expiredDate, + reloadStatus + }) => { + return ( +
+
+
+
+
+
+
+
+
+ + Your Extended subscription expired on {expiredDate} + +
+ Extensions are in a read-only state. +
+
+
+
+
+
+
reloadStatus(true)}> + +
+
+ +
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx b/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx new file mode 100644 index 000000000..3bec4eab7 --- /dev/null +++ b/app/assets/javascripts/components/ComponentView/IssueOnLoading.tsx @@ -0,0 +1,30 @@ +import { FunctionalComponent } from 'preact'; + +interface IProps { + componentName: string; + reloadIframe: () => void; +} + +export const IssueOnLoading: FunctionalComponent = ({ + componentName, + reloadIframe + }) => { + return ( +
+
+
+
+
+ There was an issue loading {componentName} +
+
+
+
+
+ +
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx new file mode 100644 index 000000000..df2c04787 --- /dev/null +++ b/app/assets/javascripts/components/ComponentView/OfflineRestricted.tsx @@ -0,0 +1,58 @@ +import { FunctionalComponent } from 'preact'; + +interface IProps { + isReloading: boolean; + reloadStatus: (doManualReload?: boolean) => void; +} + +export const OfflineRestricted: FunctionalComponent = ({ + isReloading, + reloadStatus + }) => { + return ( +
+
+
+
+
+
+ You have restricted this extension to be used offline only. +
+
+ Offline extensions are not available in the Web app. +
+
+
+
+
+ You can either: +
+
    +
  • + + Enable the Hosted option for this extension by opening the 'Extensions' menu and{' '} + toggling 'Use hosted when local is unavailable' under this extension's options.{' '} + Then press Reload below. + +
  • +
  • + Use the Desktop application. +
  • +
+
+
+
+ {isReloading ? +
+ : + + } +
+
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/ComponentView/UrlMissing.tsx b/app/assets/javascripts/components/ComponentView/UrlMissing.tsx new file mode 100644 index 000000000..c2dd6072c --- /dev/null +++ b/app/assets/javascripts/components/ComponentView/UrlMissing.tsx @@ -0,0 +1,26 @@ +import { FunctionalComponent } from 'preact'; + +interface IProps { + componentName: string; +} + +export const UrlMissing: FunctionalComponent = ({ componentName }) => { + return ( +
+
+
+
+
+ This extension is not installed correctly. +
+

Please uninstall {componentName}, then re-install it.

+

+ This issue can occur if you access Standard Notes using an older version of the app.{' '} + Ensure you are running at least version 2.1 on all platforms. +

+
+
+
+
+ ); +}; diff --git a/app/assets/javascripts/components/ComponentView/index.tsx b/app/assets/javascripts/components/ComponentView/index.tsx new file mode 100644 index 000000000..50d972785 --- /dev/null +++ b/app/assets/javascripts/components/ComponentView/index.tsx @@ -0,0 +1,367 @@ +import { ComponentAction, LiveItem, SNComponent } from '@node_modules/@standardnotes/snjs'; +import { WebApplication } from '@/ui_models/application'; +import { FunctionalComponent } from 'preact'; +import { toDirective } from '@/components/utils'; +import { useCallback, useEffect, useRef, useState } from 'preact/hooks'; +import { observer } from 'mobx-react-lite'; +import { isDesktopApplication } from '@/utils'; +import { RootScopeMessages } from '@/messages'; +import { OfflineRestricted } from '@/components/ComponentView/OfflineRestricted'; +import { UrlMissing } from '@/components/ComponentView/UrlMissing'; +import { IsDeprecated } from '@/components/ComponentView/IsDeprecated'; +import { IsExpired } from '@/components/ComponentView/IsExpired'; +import { IssueOnLoading } from '@/components/ComponentView/IssueOnLoading'; +import { AppState } from '@/ui_models/app_state'; +import { ComponentArea } from '@node_modules/@standardnotes/features'; + +interface IProps { + application: WebApplication; + appState: AppState; + componentUuid: string; + onLoad?: (component: SNComponent) => void; + templateComponent?: SNComponent; + broadcast?: (...args: unknown[]) => unknown; + manualDealloc?: boolean; +} + +/** + * The maximum amount of time we'll wait for a component + * to load before displaying error + */ +const MaxLoadThreshold = 4000; +const VisibilityChangeKey = 'visibilitychange'; +const avoidFlickerTimeout = 7; + +export const ComponentView: FunctionalComponent = observer( + ({ + application, + appState, + onLoad, + componentUuid, + templateComponent, + broadcast, + manualDealloc = false + }) => { + const liveComponentRef = useRef | null>(null); + const iframeRef = useRef(null); + + const [isIssueOnLoading, setIsIssueOnLoading] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isReloading, setIsReloading] = useState(false); + const [loadTimeout, setLoadTimeout] = useState(undefined); + const [isExpired, setIsExpired] = useState(false); + const [isComponentValid, setIsComponentValid] = useState(true); + const [error, setError] = useState<'offline-restricted' | 'url-missing' | undefined>(undefined); + const [isDeprecated, setIsDeprecated] = useState(false); + const [deprecationMessage, setDeprecationMessage] = useState(undefined); + const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false); + const [didAttemptReload, setDidAttemptReload] = useState(false); + const [component, setComponent] = useState(undefined); + + const getComponent = useCallback((): SNComponent => { + return (templateComponent || liveComponentRef.current?.item) as SNComponent; + }, [templateComponent]); + + const reloadIframe = () => { + setTimeout(() => { + setIsReloading(true); + setTimeout(() => { + setIsReloading(false); + }); + }); + }; + + const reloadStatus = useCallback((doManualReload = true) => { + if (!component) { + return; + } + + const offlineRestricted = component.offlineOnly && !isDesktopApplication(); + const hasUrlError = function() { + if (isDesktopApplication()) { + return !component.local_url && !component.hasValidHostedUrl(); + } else { + return !component.hasValidHostedUrl(); + } + }(); + + setIsExpired(component.valid_until && component.valid_until <= new Date()); + + const readonlyState = application.componentManager!.getReadonlyStateForComponent(component); + + if (!readonlyState.lockReadonly) { + application.componentManager!.setReadonlyStateForComponent(component, isExpired); + } + setIsComponentValid(!offlineRestricted && !hasUrlError); + + if (!isComponentValid) { + setIsLoading(false); + } + + if (offlineRestricted) { + setError('offline-restricted'); + } else if (hasUrlError) { + setError('url-missing'); + } else { + setError(undefined); + } + if (isExpired && doManualReload) { + broadcast?.(RootScopeMessages.ReloadExtendedData); + } + setIsDeprecated(component.isDeprecated); + setDeprecationMessage(component.package_info.deprecation_message); + }, [application.componentManager, broadcast, component, isComponentValid, isExpired]); + + const dismissDeprecationMessage = () => { + setTimeout(() => { + setIsDeprecationMessageDismissed(true); + }); + }; + + const onVisibilityChange = useCallback(() => { + if (document.visibilityState === 'hidden') { + return; + } + if (isIssueOnLoading) { + reloadIframe(); + } + }, [isIssueOnLoading]); + + const handleIframeLoadTimeout =useCallback(async () => { + if (isLoading) { + setIsLoading(false); + setIsIssueOnLoading(true); + + if (!didAttemptReload) { + setDidAttemptReload(true); + reloadIframe(); + } else { + document.addEventListener( + VisibilityChangeKey, + onVisibilityChange + ); + } + } + }, [didAttemptReload, isLoading, onVisibilityChange]); + + const handleIframeLoad = useCallback(async (iframe: HTMLIFrameElement) => { + if (!component) { + return; + } + + let desktopError = false; + if (isDesktopApplication()) { + try { + /** Accessing iframe.contentWindow.origin only allowed in desktop app. */ + if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') { + desktopError = true; + } + // eslint-disable-next-line no-empty + } catch (e) { + } + } + clearTimeout(loadTimeout); + await application.componentManager!.registerComponentWindow( + component, + iframe.contentWindow! + ); + + setTimeout(() => { + setIsLoading(false); + setIsIssueOnLoading(desktopError ? true : false); + onLoad?.(component!); + }, avoidFlickerTimeout); + }, [application.componentManager, component, loadTimeout, onLoad]); + + const loadComponent = useCallback(() => { + if (!component) { + throw Error('Component view is missing component'); + } + + if (!component.active && !component.isEditor() && component.area !== ComponentArea.Modal) { + /** Editors don't need to be active to be displayed */ + throw Error('Component view component must be active'); + } + + setIsLoading(true); + if (loadTimeout) { + clearTimeout(loadTimeout); + } + const timeoutHandler = setTimeout(() => { + handleIframeLoadTimeout(); + }, MaxLoadThreshold); + + setLoadTimeout(timeoutHandler); + }, [component, handleIframeLoadTimeout, loadTimeout]); + + useEffect(() => { + if (!iframeRef.current) { + return; + } + + iframeRef.current.onload = () => { + if (!component) { + return; + } + + const iframe = application.componentManager!.iframeForComponent( + component.uuid + ); + if (!iframe) { + return; + } + + setTimeout(() => { + loadComponent(); + reloadStatus(); + handleIframeLoad(iframe); + }); + }; + }, [application.componentManager, component, handleIframeLoad, loadComponent, reloadStatus]); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const expiredDate = isExpired ? component.dateToLocalizedString(component.valid_until) : ''; + + const getUrl = () => { + const url = component ? application.componentManager!.urlForComponent(component) : ''; + return url as string; + }; + + useEffect(() => { + if (componentUuid) { + liveComponentRef.current = new LiveItem(componentUuid, application); + } else { + application.componentManager.addTemporaryTemplateComponent(templateComponent as SNComponent); + } + + return () => { + if (application.componentManager) { + /** Component manager Can be destroyed already via locking */ + if (component) { + application.componentManager.onComponentIframeDestroyed(component.uuid); + } + if (templateComponent) { + application.componentManager.removeTemporaryTemplateComponent(templateComponent); + } + } + + if (liveComponentRef.current) { + liveComponentRef.current.deinit(); + } + + document.removeEventListener( + VisibilityChangeKey, + onVisibilityChange + ); + }; + }, [appState, application, component, componentUuid, onVisibilityChange, reloadStatus, templateComponent]); + + useEffect(() => { + // Set/update `component` based on `componentUuid` prop. + // It's a hint that the props were changed and we should rerender this component (and particularly, the iframe). + if (!component || component.uuid && componentUuid && component.uuid !== componentUuid) { + const latestComponentValue = getComponent(); + setComponent(latestComponentValue); + } + }, [component, componentUuid, getComponent]); + + useEffect(() => { + if (!component) { + return; + } + + const unregisterComponentHandler = application.componentManager!.registerHandler({ + identifier: 'component-view-' + Math.random(), + areas: [component.area], + actionHandler: (component, action, data) => { + switch (action) { + case (ComponentAction.SetSize): + application.componentManager!.handleSetSizeEvent(component, data); + break; + case (ComponentAction.KeyDown): + application.io.handleComponentKeyDown(data.keyboardModifier); + break; + case (ComponentAction.KeyUp): + application.io.handleComponentKeyUp(data.keyboardModifier); + break; + case (ComponentAction.Click): + application.getAppState().notes.setContextMenuOpen(false); + break; + default: + return; + } + } + }); + + return () => { + unregisterComponentHandler(); + }; + }, [application, component]); + + useEffect(() => { + const unregisterDesktopObserver = application.getDesktopService() + .registerUpdateObserver((component: SNComponent) => { + if (component.uuid === component.uuid && component.active) { + reloadIframe(); + } + }); + + return () => { + unregisterDesktopObserver(); + }; + }, [application]); + + if (!component) { + return null; + } + + return ( + <> + {isIssueOnLoading && ( + + )} + {isExpired && ( + + )} + {isDeprecated && !isDeprecationMessageDismissed && ( + + )} + {error == 'offline-restricted' && ( + + )} + {error == 'url-missing' && ( + + )} + {component.uuid && !isReloading && isComponentValid && ( + + )} + {isLoading && ( +
+ )} + + ); + }); + +export const ComponentViewDirective = toDirective(ComponentView, { + onLoad: '=', + componentUuid: '=', + templateComponent: '=', + broadcast: '=', + manualDealloc: '=' +}); diff --git a/app/assets/javascripts/components/Icon.tsx b/app/assets/javascripts/components/Icon.tsx index 0ccc69b60..f21c0137f 100644 --- a/app/assets/javascripts/components/Icon.tsx +++ b/app/assets/javascripts/components/Icon.tsx @@ -50,6 +50,7 @@ import EyeOffIcon from '../../icons/ic-eye-off.svg'; import LockIcon from '../../icons/ic-lock.svg'; import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg'; import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg'; +import WindowIcon from '../../icons/ic-window.svg'; import { toDirective } from './utils'; import { FunctionalComponent } from 'preact'; @@ -106,6 +107,7 @@ const ICONS = { 'check-bold': CheckBoldIcon, 'account-circle': AccountCircleIcon, 'menu-arrow-down': MenuArrowDownIcon, + window: WindowIcon }; export type IconType = keyof typeof ICONS; diff --git a/app/assets/javascripts/directives/views/componentView.ts b/app/assets/javascripts/directives/views/componentView.ts deleted file mode 100644 index 67392b335..000000000 --- a/app/assets/javascripts/directives/views/componentView.ts +++ /dev/null @@ -1,294 +0,0 @@ -import { RootScopeMessages } from './../../messages'; -import { WebApplication } from '@/ui_models/application'; -import { SNComponent, ComponentAction, LiveItem } from '@standardnotes/snjs'; -import { WebDirective } from './../../types'; -import template from '%/directives/component-view.pug'; -import { isDesktopApplication } from '../../utils'; -/** - * The maximum amount of time we'll wait for a component - * to load before displaying error - */ -const MaxLoadThreshold = 4000; -const VisibilityChangeKey = 'visibilitychange'; - -interface ComponentViewScope { - componentUuid: string - onLoad?: (component: SNComponent) => void - application: WebApplication -} - -class ComponentViewCtrl implements ComponentViewScope { - - /** @scope */ - onLoad?: (component: SNComponent) => void - componentUuid!: string - templateComponent!: SNComponent - application!: WebApplication - liveComponent!: LiveItem - - private $rootScope: ng.IRootScopeService - private $timeout: ng.ITimeoutService - private componentValid = true - private cleanUpOn: () => void - private unregisterComponentHandler!: () => void - private unregisterDesktopObserver!: () => void - private issueLoading = false - private isDeprecated = false - private deprecationMessage: string | undefined = undefined - private deprecationMessageDismissed = false - public reloading = false - private expired = false - private loading = false - private didAttemptReload = false - public error: 'offline-restricted' | 'url-missing' | undefined - private loadTimeout: any - - /* @ngInject */ - constructor( - $scope: ng.IScope, - $rootScope: ng.IRootScopeService, - $timeout: ng.ITimeoutService, - ) { - this.$rootScope = $rootScope; - this.$timeout = $timeout; - this.cleanUpOn = $scope.$on('ext-reload-complete', () => { - this.reloadStatus(false); - }); - /** To allow for registering events */ - this.onVisibilityChange = this.onVisibilityChange.bind(this); - } - - $onDestroy() { - if(this.application.componentManager) { - /** Component manager Can be destroyed already via locking */ - this.application.componentManager.onComponentIframeDestroyed(this.component.uuid); - if(this.templateComponent) { - this.application.componentManager.removeTemporaryTemplateComponent(this.templateComponent); - } - } - if(this.liveComponent) { - this.liveComponent.deinit(); - } - this.cleanUpOn(); - (this.cleanUpOn as any) = undefined; - this.unregisterComponentHandler(); - (this.unregisterComponentHandler as any) = undefined; - this.unregisterDesktopObserver(); - (this.unregisterDesktopObserver as any) = undefined; - (this.templateComponent as any) = undefined; - (this.liveComponent as any) = undefined; - (this.application as any) = undefined; - (this.onVisibilityChange as any) = undefined; - this.onLoad = undefined; - document.removeEventListener( - VisibilityChangeKey, - this.onVisibilityChange - ); - } - - $onInit() { - if(this.componentUuid) { - this.liveComponent = new LiveItem(this.componentUuid, this.application); - } else { - this.application.componentManager.addTemporaryTemplateComponent(this.templateComponent); - } - this.registerComponentHandlers(); - this.registerPackageUpdateObserver(); - } - - get component() { - return this.templateComponent || this.liveComponent?.item; - } - - /** @template */ - public onIframeInit() { - /** Perform in timeout required so that dynamic iframe id is set (based on ctrl values) */ - this.$timeout(() => { - this.loadComponent(); - }); - } - - private loadComponent() { - if (!this.component) { - throw Error('Component view is missing component'); - } - if (!this.component.active && !this.component.isEditor()) { - /** Editors don't need to be active to be displayed */ - throw Error('Component view component must be active'); - } - const iframe = this.application.componentManager!.iframeForComponent( - this.component.uuid - ); - if (!iframe) { - return; - } - this.loading = true; - if (this.loadTimeout) { - this.$timeout.cancel(this.loadTimeout); - } - this.loadTimeout = this.$timeout(() => { - this.handleIframeLoadTimeout(); - }, MaxLoadThreshold); - iframe.onload = () => { - this.reloadStatus(); - this.handleIframeLoad(iframe); - }; - } - - private registerPackageUpdateObserver() { - this.unregisterDesktopObserver = this.application.getDesktopService() - .registerUpdateObserver((component: SNComponent) => { - if (component.uuid === this.component.uuid && component.active) { - this.reloadIframe(); - } - }); - } - - private registerComponentHandlers() { - this.unregisterComponentHandler = this.application.componentManager!.registerHandler({ - identifier: 'component-view-' + Math.random(), - areas: [this.component.area], - actionHandler: (component, action, data) => { - switch (action) { - case (ComponentAction.SetSize): - this.application.componentManager!.handleSetSizeEvent(component, data); - break; - case (ComponentAction.KeyDown): - this.application.io.handleComponentKeyDown(data.keyboardModifier); - break; - case (ComponentAction.KeyUp): - this.application.io.handleComponentKeyUp(data.keyboardModifier); - break; - case (ComponentAction.Click): - this.application.getAppState().notes.setContextMenuOpen(false); - break; - default: - return; - } - } - }); - } - - private reloadIframe() { - this.$timeout(() => { - this.reloading = true; - this.$timeout(() => { - this.reloading = false; - }); - }); - } - - private dismissDeprecationMessage() { - this.$timeout(() => { - this.deprecationMessageDismissed = true; - }); - } - - private onVisibilityChange() { - if (document.visibilityState === 'hidden') { - return; - } - if (this.issueLoading) { - this.reloadIframe(); - } - } - - public reloadStatus(doManualReload = true) { - const component = this.component; - const offlineRestricted = component.offlineOnly && !isDesktopApplication(); - const hasUrlError = function () { - if (isDesktopApplication()) { - return !component.local_url && !component.hasValidHostedUrl(); - } else { - return !component.hasValidHostedUrl(); - } - }(); - this.expired = component.valid_until && component.valid_until <= new Date(); - const readonlyState = this.application.componentManager! - .getReadonlyStateForComponent(component); - if (!readonlyState.lockReadonly) { - this.application.componentManager! - .setReadonlyStateForComponent(component, this.expired); - } - this.componentValid = !offlineRestricted && !hasUrlError; - if (!this.componentValid) { - this.loading = false; - } - if (offlineRestricted) { - this.error = 'offline-restricted'; - } else if (hasUrlError) { - this.error = 'url-missing'; - } else { - this.error = undefined; - } - if (this.expired && doManualReload) { - this.$rootScope.$broadcast(RootScopeMessages.ReloadExtendedData); - } - this.isDeprecated = component.isDeprecated; - this.deprecationMessage = component.package_info.deprecation_message; - } - - private async handleIframeLoadTimeout() { - if (this.loading) { - this.loading = false; - this.issueLoading = true; - if (!this.didAttemptReload) { - this.didAttemptReload = true; - this.reloadIframe(); - } else { - document.addEventListener( - VisibilityChangeKey, - this.onVisibilityChange - ); - } - } - } - - private async handleIframeLoad(iframe: HTMLIFrameElement) { - let desktopError = false; - if (isDesktopApplication()) { - try { - /** Accessing iframe.contentWindow.origin only allowed in desktop app. */ - if (!iframe.contentWindow!.origin || iframe.contentWindow!.origin === 'null') { - desktopError = true; - } - // eslint-disable-next-line no-empty - } catch (e) { } - } - this.$timeout.cancel(this.loadTimeout); - await this.application.componentManager!.registerComponentWindow( - this.component, - iframe.contentWindow! - ); - const avoidFlickerTimeout = 7; - this.$timeout(() => { - this.loading = false; - // eslint-disable-next-line no-unneeded-ternary - this.issueLoading = desktopError ? true : false; - this.onLoad && this.onLoad(this.component!); - }, avoidFlickerTimeout); - } - - /** @template */ - public getUrl() { - const url = this.application.componentManager!.urlForComponent(this.component); - return url; - } -} - -export class ComponentView extends WebDirective { - constructor() { - super(); - this.restrict = 'E'; - this.template = template; - this.scope = { - componentUuid: '=', - templateComponent: '=?', - onLoad: '=?', - application: '=' - }; - this.controller = ComponentViewCtrl; - this.controllerAs = 'ctrl'; - this.bindToController = true; - } -} diff --git a/app/assets/javascripts/directives/views/index.ts b/app/assets/javascripts/directives/views/index.ts index 273a3e944..ee69ce822 100644 --- a/app/assets/javascripts/directives/views/index.ts +++ b/app/assets/javascripts/directives/views/index.ts @@ -1,6 +1,5 @@ export { ActionsMenu } from './actionsMenu'; export { ComponentModal } from './componentModal'; -export { ComponentView } from './componentView'; export { EditorMenu } from './editorMenu'; export { InputModal } from './inputModal'; export { MenuRow } from './menuRow'; diff --git a/app/assets/javascripts/preferences/PreferencesMenu.ts b/app/assets/javascripts/preferences/PreferencesMenu.ts index fd07570ee..729dec93b 100644 --- a/app/assets/javascripts/preferences/PreferencesMenu.ts +++ b/app/assets/javascripts/preferences/PreferencesMenu.ts @@ -1,5 +1,10 @@ import { IconType } from '@/components/Icon'; -import { makeAutoObservable, observable } from 'mobx'; +import { action, makeAutoObservable, observable } from 'mobx'; +import { ExtensionsLatestVersions } from '@/preferences/panes/extensions-segments'; +import { ContentType, SNComponent } from '@node_modules/@standardnotes/snjs'; +import { WebApplication } from '@/ui_models/application'; +import { FeatureIdentifier } from '@node_modules/@standardnotes/features/dist/Domain/Feature/FeatureIdentifier'; +import { ComponentArea } from '@standardnotes/snjs'; const PREFERENCE_IDS = [ 'general', @@ -16,11 +21,15 @@ const PREFERENCE_IDS = [ export type PreferenceId = typeof PREFERENCE_IDS[number]; interface PreferencesMenuItem { - readonly id: PreferenceId; + readonly id: PreferenceId | FeatureIdentifier; readonly icon: IconType; readonly label: string; } +interface SelectableMenuItem extends PreferencesMenuItem { + selected: boolean; +} + /** * Items are in order of appearance */ @@ -46,38 +55,93 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [ ]; export class PreferencesMenu { - private _selectedPane: PreferenceId = 'account'; + private _selectedPane: PreferenceId | FeatureIdentifier = 'account'; + private _extensionPanes: SNComponent[] = []; private _menu: PreferencesMenuItem[]; + private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map()); constructor( + private application: WebApplication, private readonly _enableUnfinishedFeatures: boolean, ) { this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS; - makeAutoObservable( + + this.loadExtensionsPanes(); + this.loadLatestVersions(); + + makeAutoObservable( this, { _twoFactorAuth: observable, _selectedPane: observable, + _extensionPanes: observable.ref, + _extensionLatestVersions: observable.ref, + loadLatestVersions: action, } ); } - get menuItems(): (PreferencesMenuItem & { - selected: boolean; - })[] { - return this._menu.map((p) => ({ - ...p, - selected: p.id === this._selectedPane, + private loadLatestVersions(): void { + ExtensionsLatestVersions.load(this.application).then(versions => { + this._extensionLatestVersions = versions; + }); + } + + get extensionsLatestVersions(): ExtensionsLatestVersions { + return this._extensionLatestVersions; + } + + loadExtensionsPanes(): void { + this._extensionPanes = (this.application.getItems([ + ContentType.ActionsExtension, + ContentType.Component, + ContentType.Theme, + ]) as SNComponent[]) + .filter(extension => extension.area === ComponentArea.Modal && extension.package_info.identifier !== FeatureIdentifier.TwoFactorAuthManager); + } + + get menuItems(): SelectableMenuItem[] { + const menuItems = this._menu.map((preference) => ({ + ...preference, + selected: preference.id === this._selectedPane, })); + const extensionsMenuItems: SelectableMenuItem[] = this._extensionPanes + .map(extension => { + return { + icon: 'window', + id: extension.package_info.identifier, + label: extension.name, + selected: extension.package_info.identifier === this._selectedPane + }; + }); + + return menuItems.concat(extensionsMenuItems); } - get selectedPaneId(): PreferenceId { - return ( - this._menu.find((item) => item.id === this._selectedPane)?.id ?? 'account' - ); + get selectedMenuItem(): PreferencesMenuItem | undefined { + return this._menu.find((item) => item.id === this._selectedPane); } - selectPane(key: PreferenceId): void { + get selectedExtension(): SNComponent | undefined { + return this._extensionPanes.find((extension) => + extension.package_info.identifier === this._selectedPane); + } + + get selectedPaneId(): PreferenceId | FeatureIdentifier { + if (this.selectedMenuItem != undefined) { + return this.selectedMenuItem.id; + } + + if (this.selectedExtension != undefined) { + return this.selectedExtension.package_info.identifier; + } + + return 'account'; + } + + selectPane(key: PreferenceId | FeatureIdentifier): void { this._selectedPane = key; } } diff --git a/app/assets/javascripts/preferences/PreferencesView.tsx b/app/assets/javascripts/preferences/PreferencesView.tsx index 561ff44d7..3bdb64e1b 100644 --- a/app/assets/javascripts/preferences/PreferencesView.tsx +++ b/app/assets/javascripts/preferences/PreferencesView.tsx @@ -9,6 +9,7 @@ import { Security, } from './panes'; import { observer } from 'mobx-react-lite'; + import { PreferencesMenu } from './PreferencesMenu'; import { PreferencesMenuView } from './PreferencesMenuView'; import { WebApplication } from '@/ui_models/application'; @@ -16,6 +17,7 @@ import { MfaProps } from './panes/two-factor-auth/MfaProps'; import { AppState } from '@/ui_models/app_state'; import { useEffect, useMemo } from 'preact/hooks'; import { Extensions } from './panes/Extensions'; +import { ExtensionPane } from './panes/ExtensionPane'; interface PreferencesProps extends MfaProps { application: WebApplication; @@ -25,44 +27,64 @@ interface PreferencesProps extends MfaProps { const PaneSelector: FunctionComponent< PreferencesProps & { menu: PreferencesMenu } -> = observer((props) => { - switch (props.menu.selectedPaneId) { - case 'general': - return ( - - ); - case 'account': - return ( - - ); - case 'appearance': - return null; - case 'security': - return ( - - ); - case 'extensions': - return ; - case 'listed': - return ; - case 'shortcuts': - return null; - case 'accessibility': - return null; - case 'get-free-month': - return null; - case 'help-feedback': - return ; - } -}); +> = observer( + ({ + menu, + appState, + application, + mfaProvider, + userProvider + }) => { + switch (menu.selectedPaneId) { + case 'general': + return ( + + ); + case 'account': + return ( + + ); + case 'appearance': + return null; + case 'security': + return ( + + ); + case 'extensions': + return ; + case 'listed': + return ; + case 'shortcuts': + return null; + case 'accessibility': + return null; + case 'get-free-month': + return null; + case 'help-feedback': + return ; + default: + if (menu.selectedExtension != undefined) { + return ( + + ); + } else { + return ; + } + } + }); const PreferencesCanvas: FunctionComponent< PreferencesProps & { menu: PreferencesMenu } @@ -75,9 +97,9 @@ const PreferencesCanvas: FunctionComponent< export const PreferencesView: FunctionComponent = observer( (props) => { - const menu = useMemo(() => new PreferencesMenu(props.appState.enableUnfinishedFeatures), [ - props.appState.enableUnfinishedFeatures - ]); + const menu = useMemo( + () => new PreferencesMenu(props.application, props.appState.enableUnfinishedFeatures), + [props.appState.enableUnfinishedFeatures, props.application]); useEffect(() => { menu.selectPane(props.appState.preferences.currentPane); diff --git a/app/assets/javascripts/preferences/panes/ExtensionPane.tsx b/app/assets/javascripts/preferences/panes/ExtensionPane.tsx new file mode 100644 index 000000000..ed8db4f35 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/ExtensionPane.tsx @@ -0,0 +1,47 @@ +import { PreferencesGroup, PreferencesSegment } from "@/preferences/components"; +import { WebApplication } from "@/ui_models/application"; +import { SNComponent } from "@standardnotes/snjs/dist/@types"; +import { observer } from "mobx-react-lite"; +import { FunctionComponent } from "preact"; +import { ExtensionItem } from "./extensions-segments"; +import { ComponentView } from '@/components/ComponentView'; +import { AppState } from '@/ui_models/app_state'; +import { PreferencesMenu } from '@/preferences/PreferencesMenu'; + +interface IProps { + application: WebApplication; + appState: AppState; + extension: SNComponent; + preferencesMenu: PreferencesMenu; +} + +export const ExtensionPane: FunctionComponent = observer( + ({ extension, application, appState, preferencesMenu }) => { + const latestVersion = preferencesMenu.extensionsLatestVersions.getVersion(extension); + + return ( +
+
+
+ + application.deleteItem(extension).then(() => preferencesMenu.loadExtensionsPanes())} + toggleActivate={() => application.toggleComponent(extension).then(() => preferencesMenu.loadExtensionsPanes())} + latestVersion={latestVersion} + /> + + + + +
+
+
+ ); + }); diff --git a/app/assets/javascripts/preferences/panes/Extensions.tsx b/app/assets/javascripts/preferences/panes/Extensions.tsx index 7ce4a6295..3e51b1d39 100644 --- a/app/assets/javascripts/preferences/panes/Extensions.tsx +++ b/app/assets/javascripts/preferences/panes/Extensions.tsx @@ -9,9 +9,9 @@ import { PreferencesPane, PreferencesSegment, } from '../components'; -import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments'; +import { ConfirmCustomExtension, ExtensionItem, ExtensionsLatestVersions } from './extensions-segments'; import { useEffect, useRef, useState } from 'preact/hooks'; -import { FeatureDescription } from '@standardnotes/features'; +import { observer } from 'mobx-react-lite'; const loadExtensions = (application: WebApplication) => application.getItems([ ContentType.ActionsExtension, @@ -19,30 +19,14 @@ const loadExtensions = (application: WebApplication) => application.getItems([ ContentType.Theme, ]) as SNComponent[]; -function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map) { - if (features == undefined) return; - for (const feature of features) { - versionMap.set(feature.identifier, feature.version); - } -} - -const loadLatestVersions = (application: WebApplication) => application.getAvailableSubscriptions() - .then(subscriptions => { - const versionMap: Map = new Map(); - collectFeatures(subscriptions?.CORE_PLAN?.features, versionMap); - collectFeatures(subscriptions?.PLUS_PLAN?.features, versionMap); - collectFeatures(subscriptions?.PRO_PLAN?.features, versionMap); - return versionMap; - }); - export const Extensions: FunctionComponent<{ application: WebApplication -}> = ({ application }) => { + extensionsLatestVersions: ExtensionsLatestVersions, +}> = observer(({ application, extensionsLatestVersions }) => { const [customUrl, setCustomUrl] = useState(''); const [confirmableExtension, setConfirmableExtension] = useState(undefined); const [extensions, setExtensions] = useState(loadExtensions(application)); - const [latestVersions, setLatestVersions] = useState | undefined>(undefined); const confirmableEnd = useRef(null); @@ -52,12 +36,6 @@ export const Extensions: FunctionComponent<{ } }, [confirmableExtension, confirmableEnd]); - useEffect(() => { - if (!latestVersions) { - loadLatestVersions(application).then(versions => setLatestVersions(versions)); - } - }, [latestVersions, application]); - const uninstallExtension = async (extension: SNComponent) => { await application.deleteItem(extension); setExtensions(loadExtensions(application)); @@ -94,12 +72,13 @@ export const Extensions: FunctionComponent<{ { extensions + .filter(extension => !['modal', 'rooms'].includes(extension.area)) .sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase())) .map((extension, i) => ( @@ -140,4 +119,4 @@ export const Extensions: FunctionComponent<{ ); -}; +}); diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx index 15f6b7731..a3f0bd9fb 100644 --- a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx +++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionItem.tsx @@ -6,6 +6,7 @@ import { Switch } from "@/components/Switch"; import { WebApplication } from "@/ui_models/application"; import { useEffect, useRef, useState } from "preact/hooks"; import { Button } from "@/components/Button"; +import { RenameExtension } from "./RenameExtension"; const ExtensionVersions: FunctionComponent<{ installedVersion: string, @@ -37,161 +38,108 @@ const UseHosted: FunctionComponent<{
); -const RenameExtension: FunctionComponent<{ - extensionName: string, changeName: (newName: string) => void -}> = ({ extensionName, changeName }) => { - const [isRenaming, setIsRenaming] = useState(false); - const [newExtensionName, setNewExtensionName] = useState(extensionName); - - const inputRef = useRef(null); - - useEffect(() => { - if (isRenaming) { - inputRef.current!.focus(); - } - }, [inputRef, isRenaming]); - - const startRenaming = () => { - setNewExtensionName(extensionName); - setIsRenaming(true); - }; - - const cancelRename = () => { - setNewExtensionName(extensionName); - setIsRenaming(false); - }; - - const confirmRename = () => { - if (newExtensionName == undefined || newExtensionName === '') { - return; - } - changeName(newExtensionName); - setIsRenaming(false); - }; - - return ( -
- setNewExtensionName((input as HTMLInputElement)?.value)} - /> -
- {isRenaming ? - <> - Confirm -
- Cancel - : - Rename - } -
- ); -}; - -export const ExtensionItem: FunctionComponent<{ +export interface ExtensionItemProps { application: WebApplication, extension: SNComponent, first: boolean, latestVersion: string | undefined, uninstall: (extension: SNComponent) => void, - toggleActivate: (extension: SNComponent) => void, -}> = ({ application, extension, first, uninstall, toggleActivate, latestVersion }) => { + toggleActivate?: (extension: SNComponent) => void, +} + +export const ExtensionItem: FunctionComponent = + ({ application, extension, first, uninstall, toggleActivate, latestVersion }) => { const [autoupdateDisabled, setAutoupdateDisabled] = useState(extension.autoupdateDisabled ?? false); const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false); const [extensionName, setExtensionName] = useState(extension.name); - const toggleAutoupdate = () => { - const newAutoupdateValue = !autoupdateDisabled; - setAutoupdateDisabled(newAutoupdateValue); - application - .changeAndSaveItem(extension.uuid, (m: any) => { - if (m.content == undefined) m.content = {}; - m.content.autoupdateDisabled = newAutoupdateValue; - }) - .then((item) => { - const component = (item as SNComponent); - setAutoupdateDisabled(component.autoupdateDisabled); - }) - .catch(e => { - console.error(e); - }); + const toggleAutoupdate = () => { + const newAutoupdateValue = !autoupdateDisabled; + setAutoupdateDisabled(newAutoupdateValue); + application + .changeAndSaveItem(extension.uuid, (m: any) => { + if (m.content == undefined) m.content = {}; + m.content.autoupdateDisabled = newAutoupdateValue; + }) + .then((item) => { + const component = (item as SNComponent); + setAutoupdateDisabled(component.autoupdateDisabled); + }) + .catch(e => { + console.error(e); + }); + }; + + const toggleOffllineOnly = () => { + const newOfflineOnly = !offlineOnly; + setOfflineOnly(newOfflineOnly); + application + .changeAndSaveItem(extension.uuid, (m: any) => { + if (m.content == undefined) m.content = {}; + m.content.offlineOnly = newOfflineOnly; + }) + .then((item) => { + const component = (item as SNComponent); + setOfflineOnly(component.offlineOnly); + }) + .catch(e => { + console.error(e); + }); + }; + + const changeExtensionName = (newName: string) => { + setExtensionName(newName); + application + .changeAndSaveItem(extension.uuid, (m: any) => { + if (m.content == undefined) m.content = {}; + m.content.name = newName; + }) + .then((item) => { + const component = (item as SNComponent); + setExtensionName(component.name); + }); + }; + + const localInstallable = extension.package_info.download_url; + + const isExternal = !extension.package_info.identifier.startsWith('org.standardnotes.'); + + const installedVersion = extension.package_info.version; + + const isEditorOrTags = ['editor-stack', 'tags-list'].includes(extension.area); + + return ( + + {first && <> + Extensions +
+ } + + +
+ + + + {localInstallable && } + {localInstallable && } + + {isEditorOrTags || isExternal && + <> +
+
+ {isEditorOrTags && toggleActivate != undefined && ( + <> + {extension.active ? +
+ + } + + ); }; - - const toggleOffllineOnly = () => { - const newOfflineOnly = !offlineOnly; - setOfflineOnly(newOfflineOnly); - application - .changeAndSaveItem(extension.uuid, (m: any) => { - if (m.content == undefined) m.content = {}; - m.content.offlineOnly = newOfflineOnly; - }) - .then((item) => { - const component = (item as SNComponent); - setOfflineOnly(component.offlineOnly); - }) - .catch(e => { - console.error(e); - }); - }; - - const changeExtensionName = (newName: string) => { - setExtensionName(newName); - application - .changeAndSaveItem(extension.uuid, (m: any) => { - if (m.content == undefined) m.content = {}; - m.content.name = newName; - }) - .then((item) => { - const component = (item as SNComponent); - setExtensionName(component.name); - }); - }; - - const localInstallable = extension.package_info.download_url; - - const isExternal = !extension.package_info.identifier.startsWith('org.standardnotes.'); - - const installedVersion = extension.package_info.version; - - const isEditorOrTags = ['editor-stack', 'tags-list'].includes(extension.area); - - return ( - - {first && <> - Extensions -
- } - - -
- - - - {localInstallable && } - {localInstallable && } - - {isEditorOrTags || isExternal && - <> -
-
- {isEditorOrTags && ( - <> - {extension.active ? -
- - } - - ); -}; diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts new file mode 100644 index 000000000..0075d69e2 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/extensions-segments/ExtensionsLatestVersions.ts @@ -0,0 +1,35 @@ +import { WebApplication } from "@/ui_models/application"; +import { FeatureDescription } from "@standardnotes/features"; +import { SNComponent } from "@standardnotes/snjs/dist/@types"; +import { makeAutoObservable, observable } from "mobx"; + +export class ExtensionsLatestVersions { + static async load(application: WebApplication): Promise { + const map = await application.getAvailableSubscriptions() + .then(subscriptions => { + const versionMap: Map = new Map(); + collectFeatures(subscriptions?.CORE_PLAN?.features as FeatureDescription[], versionMap); + collectFeatures(subscriptions?.PLUS_PLAN?.features as FeatureDescription[], versionMap); + collectFeatures(subscriptions?.PRO_PLAN?.features as FeatureDescription[], versionMap); + return versionMap; + }); + return new ExtensionsLatestVersions(map); + } + + constructor(private readonly latestVersionsMap: Map) { + makeAutoObservable( + this, { latestVersionsMap: observable.ref }); + } + + getVersion(extension: SNComponent): string | undefined { + return this.latestVersionsMap.get(extension.package_info.identifier); + } + +} + +function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map) { + if (features == undefined) return; + for (const feature of features) { + versionMap.set(feature.identifier, feature.version); + } +} diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx b/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx new file mode 100644 index 000000000..7356f7ad4 --- /dev/null +++ b/app/assets/javascripts/preferences/panes/extensions-segments/RenameExtension.tsx @@ -0,0 +1,58 @@ +import { FunctionComponent } from "preact"; +import { useState, useRef, useEffect } from "preact/hooks"; + +export const RenameExtension: FunctionComponent<{ + extensionName: string, changeName: (newName: string) => void +}> = ({ extensionName, changeName }) => { + const [isRenaming, setIsRenaming] = useState(false); + const [newExtensionName, setNewExtensionName] = useState(extensionName); + + const inputRef = useRef(null); + + useEffect(() => { + if (isRenaming) { + inputRef.current!.focus(); + } + }, [inputRef, isRenaming]); + + const startRenaming = () => { + setNewExtensionName(extensionName); + setIsRenaming(true); + }; + + const cancelRename = () => { + setNewExtensionName(extensionName); + setIsRenaming(false); + }; + + const confirmRename = () => { + if (!newExtensionName) { + return; + } + changeName(newExtensionName); + setIsRenaming(false); + }; + + return ( +
+ setNewExtensionName((input as HTMLInputElement)?.value)} + /> +
+ {isRenaming ? + <> + Confirm +
+ Cancel + : + Rename + } +
+ ); +}; diff --git a/app/assets/javascripts/preferences/panes/extensions-segments/index.ts b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts index 20694952c..ada8156e9 100644 --- a/app/assets/javascripts/preferences/panes/extensions-segments/index.ts +++ b/app/assets/javascripts/preferences/panes/extensions-segments/index.ts @@ -1,2 +1,3 @@ export * from './ConfirmCustomExtension'; export * from './ExtensionItem'; +export * from './ExtensionsLatestVersions'; diff --git a/app/assets/javascripts/ui_models/app_state/app_state.ts b/app/assets/javascripts/ui_models/app_state/app_state.ts index f486cdf28..d82f1df5d 100644 --- a/app/assets/javascripts/ui_models/app_state/app_state.ts +++ b/app/assets/javascripts/ui_models/app_state/app_state.ts @@ -34,7 +34,7 @@ export enum AppStateEvent { BeganBackupDownload, EndedBackupDownload, WindowDidFocus, - WindowDidBlur, + WindowDidBlur } export type PanelResizedData = { diff --git a/app/assets/javascripts/ui_models/app_state/preferences_state.ts b/app/assets/javascripts/ui_models/app_state/preferences_state.ts index 9a4a0a961..1c556254d 100644 --- a/app/assets/javascripts/ui_models/app_state/preferences_state.ts +++ b/app/assets/javascripts/ui_models/app_state/preferences_state.ts @@ -29,7 +29,7 @@ export class PreferencesState { this.currentPane = 'account'; }; - get isOpen() { + get isOpen(): boolean { return this._open; } } diff --git a/app/assets/javascripts/views/editor/editor-view.pug b/app/assets/javascripts/views/editor/editor-view.pug index 8c7724f55..18154258e 100644 --- a/app/assets/javascripts/views/editor/editor-view.pug +++ b/app/assets/javascripts/views/editor/editor-view.pug @@ -112,6 +112,8 @@ ng-if='self.state.editorComponent && !self.state.editorUnloading', on-load='self.onEditorLoad', application='self.application' + app-state='self.appState' + broadcast='$broadcast' ) textarea#note-text-editor.editable.font-editor( dir='auto', @@ -168,4 +170,6 @@ manual-dealloc='true', ng-show='!self.stackComponentHidden(component)', application='self.application' + app-state='self.appState' + broadcast='$broadcast' ) diff --git a/app/assets/javascripts/views/tags/tags-view.pug b/app/assets/javascripts/views/tags/tags-view.pug index 600ab2760..8af6838af 100644 --- a/app/assets/javascripts/views/tags/tags-view.pug +++ b/app/assets/javascripts/views/tags/tags-view.pug @@ -3,6 +3,8 @@ component-view.component-view( component-uuid='self.component.uuid', application='self.application' + app-state='self.appState' + broadcast='$broadcast' ) #tags-content.content(ng-if='!(self.component && self.component.active)') .tags-title-section.section-title-bar diff --git a/app/assets/stylesheets/_preferences.scss b/app/assets/stylesheets/_preferences.scss index bb9a3342f..6bddccf02 100644 --- a/app/assets/stylesheets/_preferences.scss +++ b/app/assets/stylesheets/_preferences.scss @@ -44,3 +44,9 @@ @extend .color-info; } } + +.preferences-extension-pane { + iframe { + height: 60vh; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/_sn.scss b/app/assets/stylesheets/_sn.scss index 259767b39..8ac9088df 100644 --- a/app/assets/stylesheets/_sn.scss +++ b/app/assets/stylesheets/_sn.scss @@ -282,10 +282,18 @@ width: 6.5rem; } +.max-w-200 { + max-width: 50rem; +} + .w-92 { width: 23rem; } +.w-200 { + width: 50rem; +} + .min-w-1 { min-width: 0.25rem; } @@ -316,7 +324,7 @@ .min-w-70 { min-width: 17.5rem; -} +} .min-w-24 { min-width: 6rem; @@ -328,7 +336,7 @@ .min-w-90 { min-width: 22.5rem; -} +} .min-h-1px { min-height: 1px; @@ -549,7 +557,7 @@ .-z-index-1 { z-index: -1; } - + .sn-component .btn-w-full { width: 100%; } diff --git a/app/assets/templates/directives/component-modal.pug b/app/assets/templates/directives/component-modal.pug index 150af89e4..6b7300364 100644 --- a/app/assets/templates/directives/component-modal.pug +++ b/app/assets/templates/directives/component-modal.pug @@ -14,4 +14,6 @@ ng-if='ctrl.component.active' component-uuid="ctrl.component.uuid", application='ctrl.application' + app-state='self.appState' + broadcast='$broadcast' ) diff --git a/app/assets/templates/directives/component-view.pug b/app/assets/templates/directives/component-view.pug deleted file mode 100644 index 917f44a66..000000000 --- a/app/assets/templates/directives/component-view.pug +++ /dev/null @@ -1,90 +0,0 @@ -.sn-component(ng-if='ctrl.issueLoading') - .sk-app-bar.no-edges.no-top-edge.dynamic-height - .left - .sk-app-bar-item - .sk-label.warning There was an issue loading {{ctrl.component.name}}. - .right - .sk-app-bar-item(ng-click='ctrl.reloadIframe()') - button.sn-button.small.info Reload -.sn-component(ng-if='ctrl.expired') - .sk-app-bar.no-edges.no-top-edge.dynamic-height - .left - .sk-app-bar-item - .sk-app-bar-item-column - .sk-circle.danger.small - .sk-app-bar-item-column - div - a.sk-label.sk-base( - href='https://dashboard.standardnotes.com', - rel='noopener', - target='_blank' - ) - | Your Extended subscription expired on - | {{ctrl.component.dateToLocalizedString(ctrl.component.valid_until)}}. - .sk-p - | Extensions are in a read-only state. - .right - .sk-app-bar-item(ng-click='ctrl.reloadStatus(true)') - button.sn-button.small.info Reload - .sk-app-bar-item - .sk-app-bar-item-column - a.sn-button.small.warning( - href='https://standardnotes.com/help/41/expired', - rel='noopener', - target='_blank' - ) Help -.sn-component(ng-if='ctrl.isDeprecated && !ctrl.deprecationMessageDismissed') - .sk-app-bar.no-edges.no-top-edge.dynamic-height - .left - .sk-app-bar-item - .sk-label.warning {{ctrl.deprecationMessage || 'This extension is deprecated.'}} - .right - .sk-app-bar-item(ng-click='ctrl.dismissDeprecationMessage()') - button.sn-button.small.info Dismiss - -.sn-component(ng-if="ctrl.error == 'offline-restricted'") - .sk-panel.static - .sk-panel-content - .sk-panel-section.stretch - .sk-panel-column - .sk-h1.sk-bold You have restricted this extension to be used offline only. - .sk-subtitle Offline extensions are not available in the Web app. - .sk-panel-row - .sk-panel-row - .sk-panel-column - .sk-p You can either: - ul - li.sk-p - strong Enable the Hosted option - | for this extension by opening the 'Extensions' menu and - | toggling 'Use hosted when local is unavailable' under this - | extension's options. Then press Reload below. - li.sk-p - strong Use the Desktop application. - .sk-panel-row - button.sn-button.small.info( - ng-click='ctrl.reloadStatus()', - ng-if='!ctrl.reloading' - ) Reload - .sk-spinner.info.small(ng-if='ctrl.reloading') -.sn-component(ng-if="ctrl.error == 'url-missing'") - .sk-panel.static - .sk-panel-content - .sk-panel-section.stretch - .sk-panel-section-title This extension is not installed correctly. - p Please uninstall {{ctrl.component.name}}, then re-install it. - p - | This issue can occur if you access Standard Notes using an older - | version of the app. - | Ensure you are running at least version 2.1 on all platforms. -iframe( - data-component-id='{{ctrl.component.uuid}}', - frameborder='0', - ng-init='ctrl.onIframeInit()' - ng-attr-id='component-iframe-{{ctrl.component.uuid}}', - ng-if='ctrl.component.uuid && !ctrl.reloading && ctrl.componentValid', - ng-src='{{ctrl.getUrl() | trusted}}', - sandbox='allow-scripts allow-top-navigation-by-user-activation allow-popups allow-popups-to-escape-sandbox allow-same-origin allow-modals allow-forms allow-downloads' - ) - | Loading -.loading-overlay(ng-if='ctrl.loading') diff --git a/app/assets/templates/directives/revision-preview-modal.pug b/app/assets/templates/directives/revision-preview-modal.pug index ee0895408..91fbbe011 100644 --- a/app/assets/templates/directives/revision-preview-modal.pug +++ b/app/assets/templates/directives/revision-preview-modal.pug @@ -33,4 +33,6 @@ ng-if="ctrl.state.editor", template-component="ctrl.state.editor", application='ctrl.application' + app-state='self.appState' + broadcast='$broadcast' )