feat(preferences): extension modals into extension panes (#683)
* feat(preferences): show inline extensions in extensions pane * wip * wip * refactor: convert ComponentView to React component * refactor: convert ComponentView to React component * chore: fix merge conflicts * feat: don't show features whose `area` is "room", update modal items' icons in Preferences menu * chore: fix TS error * feat: don't show 2FA Manager in modal-based component * feat: remove `ExtendedDataReloadComplete` event, since Extensions Manger is being removed from the app * chore: avoid hardcoded values in svg image, optimize `if` condition * chore: remove remnant comment * fix: fix typescript errors Co-authored-by: vardanhakobyan <vardan_live@live.com>
This commit is contained in:
3
app/assets/icons/ic-window.svg
Normal file
3
app/assets/icons/ic-window.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3.33325 3.33325H16.6666V16.6666H3.33325V3.33325ZM4.99992 6.66658V14.9999H14.9999V6.66658H4.99992Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 191 B |
@@ -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())
|
||||
|
||||
@@ -105,7 +105,7 @@ const AccountMenu: FunctionComponent<Props> = observer(
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="sn-component">
|
||||
<div className='sn-component'>
|
||||
<div
|
||||
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
||||
shouldAnimateCloseMenu
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
deprecationMessage: string | undefined;
|
||||
dismissDeprecationMessage: () => void;
|
||||
}
|
||||
|
||||
export const IsDeprecated: FunctionalComponent<IProps> = ({
|
||||
deprecationMessage,
|
||||
dismissDeprecationMessage
|
||||
}) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
<div className={'left'}>
|
||||
<div className={'sk-app-bar-item'}>
|
||||
<div className={'sk-label warning'}>
|
||||
{deprecationMessage || 'This extension is deprecated.'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<div className={'sk-app-bar-item'} onClick={dismissDeprecationMessage}>
|
||||
<button className={'sn-button small info'}>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
expiredDate: string;
|
||||
reloadStatus: (doManualReload?: boolean) => void;
|
||||
}
|
||||
|
||||
export const IsExpired: FunctionalComponent<IProps> = ({
|
||||
expiredDate,
|
||||
reloadStatus
|
||||
}) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
<div className={'left'}>
|
||||
<div className={'sk-app-bar-item'}>
|
||||
<div className={'sk-app-bar-item-column'}>
|
||||
<div className={'sk-circle danger small'} />
|
||||
</div>
|
||||
<div className={'sk-app-bar-item-column'}>
|
||||
<div>
|
||||
<a
|
||||
className={'sk-label sk-base'}
|
||||
href={'https://dashboard.standardnotes.com'}
|
||||
rel={'noopener'}
|
||||
target={'_blank'}
|
||||
>
|
||||
Your Extended subscription expired on {expiredDate}
|
||||
</a>
|
||||
<div className={'sk-p'}>
|
||||
Extensions are in a read-only state.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<div className={'sk-app-bar-item'} onClick={() => reloadStatus(true)}>
|
||||
<button className={'sn-button small info'}>Reload</button>
|
||||
</div>
|
||||
<div className={'sk-app-bar-item'}>
|
||||
<div className={'sk-app-bar-item-column'}>
|
||||
<a
|
||||
className={'sn-button small warning'}
|
||||
href={'https://standardnotes.com/help/41/my-extensions-appear-as-expired-even-though-my-subscription-is-still-valid'}
|
||||
rel={'noopener'}
|
||||
target={'_blank'}
|
||||
>
|
||||
Help
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
componentName: string;
|
||||
reloadIframe: () => void;
|
||||
}
|
||||
|
||||
export const IssueOnLoading: FunctionalComponent<IProps> = ({
|
||||
componentName,
|
||||
reloadIframe
|
||||
}) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-app-bar no-edges no-top-edge dynamic-height'}>
|
||||
<div className={'left'}>
|
||||
<div className={'sk-app-bar-item'}>
|
||||
<div className={'sk-label.warning'}>
|
||||
There was an issue loading {componentName}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'right'}>
|
||||
<div className={'sk-app-bar-item'} onClick={reloadIframe}>
|
||||
<button className={'sn-button small info'}>Reload</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
isReloading: boolean;
|
||||
reloadStatus: (doManualReload?: boolean) => void;
|
||||
}
|
||||
|
||||
export const OfflineRestricted: FunctionalComponent<IProps> = ({
|
||||
isReloading,
|
||||
reloadStatus
|
||||
}) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-panel static'}>
|
||||
<div className={'sk-panel-content'}>
|
||||
<div className={'sk-panel-section stretch'}>
|
||||
<div className={'sk-panel-column'} />
|
||||
<div className={'sk-h1 sk-bold'}>
|
||||
You have restricted this extension to be used offline only.
|
||||
</div>
|
||||
<div className={'sk-subtitle'}>
|
||||
Offline extensions are not available in the Web app.
|
||||
</div>
|
||||
<div className={'sk-panel-row'} />
|
||||
<div className={'sk-panel-row'}>
|
||||
<div className={'sk-panel-column'}>
|
||||
<div className={'sk-p'}>
|
||||
You can either:
|
||||
</div>
|
||||
<ul>
|
||||
<li className={'sk-p'}>
|
||||
<span className={'font-bold'}>
|
||||
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.
|
||||
</span>
|
||||
</li>
|
||||
<li className={'sk-p'}>
|
||||
<span className={'font-bold'}>Use the Desktop application.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'sk-panel-row'}>
|
||||
{isReloading ?
|
||||
<div className={'sk-spinner info small'} />
|
||||
:
|
||||
<button className={'sn-button small info'} onClick={() => reloadStatus()}>
|
||||
Reload
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { FunctionalComponent } from 'preact';
|
||||
|
||||
interface IProps {
|
||||
componentName: string;
|
||||
}
|
||||
|
||||
export const UrlMissing: FunctionalComponent<IProps> = ({ componentName }) => {
|
||||
return (
|
||||
<div className={'sn-component'}>
|
||||
<div className={'sk-panel static'}>
|
||||
<div className={'sk-panel-content'}>
|
||||
<div className={'sk-panel-section stretch'}>
|
||||
<div className={'sk-panel-section-title'}>
|
||||
This extension is not installed correctly.
|
||||
</div>
|
||||
<p>Please uninstall {componentName}, then re-install it.</p>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
367
app/assets/javascripts/components/ComponentView/index.tsx
Normal file
367
app/assets/javascripts/components/ComponentView/index.tsx
Normal file
@@ -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<IProps> = observer(
|
||||
({
|
||||
application,
|
||||
appState,
|
||||
onLoad,
|
||||
componentUuid,
|
||||
templateComponent,
|
||||
broadcast,
|
||||
manualDealloc = false
|
||||
}) => {
|
||||
const liveComponentRef = useRef<LiveItem<SNComponent> | null>(null);
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const [isIssueOnLoading, setIsIssueOnLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isReloading, setIsReloading] = useState(false);
|
||||
const [loadTimeout, setLoadTimeout] = useState<number | undefined>(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<string | undefined>(undefined);
|
||||
const [isDeprecationMessageDismissed, setIsDeprecationMessageDismissed] = useState(false);
|
||||
const [didAttemptReload, setDidAttemptReload] = useState(false);
|
||||
const [component, setComponent] = useState<SNComponent | undefined>(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 && (
|
||||
<IssueOnLoading
|
||||
componentName={component.name}
|
||||
reloadIframe={reloadIframe}
|
||||
/>
|
||||
)}
|
||||
{isExpired && (
|
||||
<IsExpired expiredDate={expiredDate} reloadStatus={reloadStatus} />
|
||||
)}
|
||||
{isDeprecated && !isDeprecationMessageDismissed && (
|
||||
<IsDeprecated
|
||||
deprecationMessage={deprecationMessage}
|
||||
dismissDeprecationMessage={dismissDeprecationMessage}
|
||||
/>
|
||||
)}
|
||||
{error == 'offline-restricted' && (
|
||||
<OfflineRestricted isReloading={isReloading} reloadStatus={reloadStatus} />
|
||||
)}
|
||||
{error == 'url-missing' && (
|
||||
<UrlMissing componentName={component.name} />
|
||||
)}
|
||||
{component.uuid && !isReloading && isComponentValid && (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
data-component-id={component.uuid}
|
||||
frameBorder={0}
|
||||
data-attr-id={`component-iframe-${component.uuid}`}
|
||||
src={getUrl()}
|
||||
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
|
||||
</iframe>
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className={'loading-overlay'} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
export const ComponentViewDirective = toDirective<IProps>(ComponentView, {
|
||||
onLoad: '=',
|
||||
componentUuid: '=',
|
||||
templateComponent: '=',
|
||||
broadcast: '=',
|
||||
manualDealloc: '='
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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<SNComponent>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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<PreferencesMenu, '_selectedPane' | '_twoFactorAuth'>(
|
||||
|
||||
this.loadExtensionsPanes();
|
||||
this.loadLatestVersions();
|
||||
|
||||
makeAutoObservable<PreferencesMenu,
|
||||
'_selectedPane' | '_twoFactorAuth' | '_extensionPanes' | '_extensionLatestVersions' | 'loadLatestVersions'
|
||||
>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<General appState={props.appState} application={props.application} />
|
||||
);
|
||||
case 'account':
|
||||
return (
|
||||
<AccountPreferences
|
||||
application={props.application}
|
||||
appState={props.appState}
|
||||
/>
|
||||
);
|
||||
case 'appearance':
|
||||
return null;
|
||||
case 'security':
|
||||
return (
|
||||
<Security
|
||||
mfaProvider={props.mfaProvider}
|
||||
userProvider={props.userProvider}
|
||||
appState={props.appState}
|
||||
application={props.application}
|
||||
/>
|
||||
);
|
||||
case 'extensions':
|
||||
return <Extensions application={props.application} />;
|
||||
case 'listed':
|
||||
return <Listed application={props.application} />;
|
||||
case 'shortcuts':
|
||||
return null;
|
||||
case 'accessibility':
|
||||
return null;
|
||||
case 'get-free-month':
|
||||
return null;
|
||||
case 'help-feedback':
|
||||
return <HelpAndFeedback />;
|
||||
}
|
||||
});
|
||||
> = observer(
|
||||
({
|
||||
menu,
|
||||
appState,
|
||||
application,
|
||||
mfaProvider,
|
||||
userProvider
|
||||
}) => {
|
||||
switch (menu.selectedPaneId) {
|
||||
case 'general':
|
||||
return (
|
||||
<General appState={appState} application={application} />
|
||||
);
|
||||
case 'account':
|
||||
return (
|
||||
<AccountPreferences
|
||||
application={application}
|
||||
appState={appState}
|
||||
/>
|
||||
);
|
||||
case 'appearance':
|
||||
return null;
|
||||
case 'security':
|
||||
return (
|
||||
<Security
|
||||
mfaProvider={mfaProvider}
|
||||
userProvider={userProvider}
|
||||
appState={appState}
|
||||
application={application}
|
||||
/>
|
||||
);
|
||||
case 'extensions':
|
||||
return <Extensions application={application} extensionsLatestVersions={menu.extensionsLatestVersions} />;
|
||||
case 'listed':
|
||||
return <Listed application={application} />;
|
||||
case 'shortcuts':
|
||||
return null;
|
||||
case 'accessibility':
|
||||
return null;
|
||||
case 'get-free-month':
|
||||
return null;
|
||||
case 'help-feedback':
|
||||
return <HelpAndFeedback />;
|
||||
default:
|
||||
if (menu.selectedExtension != undefined) {
|
||||
return (
|
||||
<ExtensionPane
|
||||
application={application}
|
||||
appState={appState}
|
||||
extension={menu.selectedExtension}
|
||||
preferencesMenu={menu}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return <General appState={appState} application={application} />;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const PreferencesCanvas: FunctionComponent<
|
||||
PreferencesProps & { menu: PreferencesMenu }
|
||||
@@ -75,9 +97,9 @@ const PreferencesCanvas: FunctionComponent<
|
||||
|
||||
export const PreferencesView: FunctionComponent<PreferencesProps> = 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);
|
||||
|
||||
47
app/assets/javascripts/preferences/panes/ExtensionPane.tsx
Normal file
47
app/assets/javascripts/preferences/panes/ExtensionPane.tsx
Normal file
@@ -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<IProps> = observer(
|
||||
({ extension, application, appState, preferencesMenu }) => {
|
||||
const latestVersion = preferencesMenu.extensionsLatestVersions.getVersion(extension);
|
||||
|
||||
return (
|
||||
<div className="preferences-extension-pane color-foreground flex-grow flex flex-row overflow-y-auto min-h-0">
|
||||
<div className="flex-grow flex flex-col py-6 items-center">
|
||||
<div className="w-200 max-w-200 flex flex-col">
|
||||
<PreferencesGroup>
|
||||
<ExtensionItem
|
||||
application={application}
|
||||
extension={extension}
|
||||
first={false}
|
||||
uninstall={() => application.deleteItem(extension).then(() => preferencesMenu.loadExtensionsPanes())}
|
||||
toggleActivate={() => application.toggleComponent(extension).then(() => preferencesMenu.loadExtensionsPanes())}
|
||||
latestVersion={latestVersion}
|
||||
/>
|
||||
<PreferencesSegment>
|
||||
<ComponentView
|
||||
application={application}
|
||||
appState={appState}
|
||||
componentUuid={extension.uuid}
|
||||
/>
|
||||
</PreferencesSegment>
|
||||
</PreferencesGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -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<string, string>) {
|
||||
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<string, string> = 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<SNComponent | undefined>(undefined);
|
||||
const [extensions, setExtensions] = useState(loadExtensions(application));
|
||||
const [latestVersions, setLatestVersions] = useState<Map<string, string> | undefined>(undefined);
|
||||
|
||||
const confirmableEnd = useRef<HTMLDivElement>(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<{
|
||||
<PreferencesGroup>
|
||||
{
|
||||
extensions
|
||||
.filter(extension => !['modal', 'rooms'].includes(extension.area))
|
||||
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
|
||||
.map((extension, i) => (
|
||||
<ExtensionItem
|
||||
application={application}
|
||||
extension={extension}
|
||||
latestVersion={latestVersions?.get(extension.package_info.identifier)}
|
||||
latestVersion={extensionsLatestVersions.getVersion(extension)}
|
||||
first={i === 0}
|
||||
uninstall={uninstallExtension}
|
||||
toggleActivate={toggleActivateExtension} />
|
||||
@@ -140,4 +119,4 @@ export const Extensions: FunctionComponent<{
|
||||
</PreferencesGroup>
|
||||
</PreferencesPane>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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<{
|
||||
</div>
|
||||
);
|
||||
|
||||
const RenameExtension: FunctionComponent<{
|
||||
extensionName: string, changeName: (newName: string) => void
|
||||
}> = ({ extensionName, changeName }) => {
|
||||
const [isRenaming, setIsRenaming] = useState(false);
|
||||
const [newExtensionName, setNewExtensionName] = useState<string>(extensionName);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="flex flex-row mr-3 items-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
disabled={!isRenaming}
|
||||
autocomplete='off'
|
||||
className="flex-grow text-base font-bold no-border bg-default px-0 color-text"
|
||||
type="text"
|
||||
value={newExtensionName}
|
||||
onChange={({ target: input }) => setNewExtensionName((input as HTMLInputElement)?.value)}
|
||||
/>
|
||||
<div className="min-w-3" />
|
||||
{isRenaming ?
|
||||
<>
|
||||
<a className="pt-1 cursor-pointer" onClick={confirmRename}>Confirm</a>
|
||||
<div className="min-w-3" />
|
||||
<a className="pt-1 cursor-pointer" onClick={cancelRename}>Cancel</a>
|
||||
</> :
|
||||
<a className="pt-1 cursor-pointer" onClick={startRenaming}>Rename</a>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<ExtensionItemProps> =
|
||||
({ 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 (
|
||||
<PreferencesSegment>
|
||||
{first && <>
|
||||
<Title>Extensions</Title>
|
||||
<div className="w-full min-h-3" />
|
||||
</>}
|
||||
|
||||
<RenameExtension extensionName={extensionName} changeName={changeExtensionName} />
|
||||
<div className="min-h-2" />
|
||||
|
||||
<ExtensionVersions installedVersion={installedVersion} latestVersion={latestVersion} />
|
||||
|
||||
{localInstallable && <AutoUpdateLocal autoupdateDisabled={autoupdateDisabled} toggleAutoupdate={toggleAutoupdate} />}
|
||||
{localInstallable && <UseHosted offlineOnly={offlineOnly} toggleOfllineOnly={toggleOffllineOnly} />}
|
||||
|
||||
{isEditorOrTags || isExternal &&
|
||||
<>
|
||||
<div className="min-h-2" />
|
||||
<div className="flex flex-row">
|
||||
{isEditorOrTags && toggleActivate != undefined && (
|
||||
<>
|
||||
{extension.active ?
|
||||
<Button className="min-w-20" type="normal" label="Deactivate" onClick={() => toggleActivate(extension)} /> :
|
||||
<Button className="min-w-20" type="normal" label="Activate" onClick={() => toggleActivate(extension)} />
|
||||
}
|
||||
<div className="min-w-3" />
|
||||
</>
|
||||
)}
|
||||
{isExternal && <Button className="min-w-20" type="normal" label="Uninstall" onClick={() => uninstall(extension)} />}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</PreferencesSegment >
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<PreferencesSegment>
|
||||
{first && <>
|
||||
<Title>Extensions</Title>
|
||||
<div className="w-full min-h-3" />
|
||||
</>}
|
||||
|
||||
<RenameExtension extensionName={extensionName} changeName={changeExtensionName} />
|
||||
<div className="min-h-2" />
|
||||
|
||||
<ExtensionVersions installedVersion={installedVersion} latestVersion={latestVersion} />
|
||||
|
||||
{localInstallable && <AutoUpdateLocal autoupdateDisabled={autoupdateDisabled} toggleAutoupdate={toggleAutoupdate} />}
|
||||
{localInstallable && <UseHosted offlineOnly={offlineOnly} toggleOfllineOnly={toggleOffllineOnly} />}
|
||||
|
||||
{isEditorOrTags || isExternal &&
|
||||
<>
|
||||
<div className="min-h-2" />
|
||||
<div className="flex flex-row">
|
||||
{isEditorOrTags && (
|
||||
<>
|
||||
{extension.active ?
|
||||
<Button className="min-w-20" type="normal" label="Deactivate" onClick={() => toggleActivate(extension)} /> :
|
||||
<Button className="min-w-20" type="normal" label="Activate" onClick={() => toggleActivate(extension)} />
|
||||
}
|
||||
<div className="min-w-3" />
|
||||
</>
|
||||
)}
|
||||
{isExternal && <Button className="min-w-20" type="normal" label="Uninstall" onClick={() => uninstall(extension)} />}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</PreferencesSegment >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<ExtensionsLatestVersions> {
|
||||
const map = await application.getAvailableSubscriptions()
|
||||
.then(subscriptions => {
|
||||
const versionMap: Map<string, string> = 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<string, string>) {
|
||||
makeAutoObservable<ExtensionsLatestVersions, 'latestVersionsMap'>(
|
||||
this, { latestVersionsMap: observable.ref });
|
||||
}
|
||||
|
||||
getVersion(extension: SNComponent): string | undefined {
|
||||
return this.latestVersionsMap.get(extension.package_info.identifier);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function collectFeatures(features: FeatureDescription[] | undefined, versionMap: Map<string, string>) {
|
||||
if (features == undefined) return;
|
||||
for (const feature of features) {
|
||||
versionMap.set(feature.identifier, feature.version);
|
||||
}
|
||||
}
|
||||
@@ -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<string>(extensionName);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(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 (
|
||||
<div className="flex flex-row mr-3 items-center">
|
||||
<input
|
||||
ref={inputRef}
|
||||
disabled={!isRenaming}
|
||||
autocomplete='off'
|
||||
className="flex-grow text-base font-bold no-border bg-default px-0 color-text"
|
||||
type="text"
|
||||
value={newExtensionName}
|
||||
onChange={({ target: input }) => setNewExtensionName((input as HTMLInputElement)?.value)}
|
||||
/>
|
||||
<div className="min-w-3" />
|
||||
{isRenaming ?
|
||||
<>
|
||||
<a className="pt-1 cursor-pointer" onClick={confirmRename}>Confirm</a>
|
||||
<div className="min-w-3" />
|
||||
<a className="pt-1 cursor-pointer" onClick={cancelRename}>Cancel</a>
|
||||
</> :
|
||||
<a className="pt-1 cursor-pointer" onClick={startRenaming}>Rename</a>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './ConfirmCustomExtension';
|
||||
export * from './ExtensionItem';
|
||||
export * from './ExtensionsLatestVersions';
|
||||
|
||||
@@ -34,7 +34,7 @@ export enum AppStateEvent {
|
||||
BeganBackupDownload,
|
||||
EndedBackupDownload,
|
||||
WindowDidFocus,
|
||||
WindowDidBlur,
|
||||
WindowDidBlur
|
||||
}
|
||||
|
||||
export type PanelResizedData = {
|
||||
|
||||
@@ -29,7 +29,7 @@ export class PreferencesState {
|
||||
this.currentPane = 'account';
|
||||
};
|
||||
|
||||
get isOpen() {
|
||||
get isOpen(): boolean {
|
||||
return this._open;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,3 +44,9 @@
|
||||
@extend .color-info;
|
||||
}
|
||||
}
|
||||
|
||||
.preferences-extension-pane {
|
||||
iframe {
|
||||
height: 60vh;
|
||||
}
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -14,4 +14,6 @@
|
||||
ng-if='ctrl.component.active'
|
||||
component-uuid="ctrl.component.uuid",
|
||||
application='ctrl.application'
|
||||
app-state='self.appState'
|
||||
broadcast='$broadcast'
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
@@ -33,4 +33,6 @@
|
||||
ng-if="ctrl.state.editor",
|
||||
template-component="ctrl.state.editor",
|
||||
application='ctrl.application'
|
||||
app-state='self.appState'
|
||||
broadcast='$broadcast'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user