Merge branch 'develop' of github.com:standardnotes/web into develop
This commit is contained in:
3
.github/workflows/beta.yml
vendored
3
.github/workflows/beta.yml
vendored
@@ -31,6 +31,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Copy robots.txt
|
||||||
|
run: cp public/robots.txt.development public/robots.txt
|
||||||
|
|
||||||
- name: Publish to Registry
|
- name: Publish to Registry
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
uses: elgohr/Publish-Docker-Github-Action@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.github/workflows/dev.yml
vendored
3
.github/workflows/dev.yml
vendored
@@ -35,6 +35,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Copy robots.txt
|
||||||
|
run: cp public/robots.txt.development public/robots.txt
|
||||||
|
|
||||||
- name: Publish to Registry
|
- name: Publish to Registry
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
uses: elgohr/Publish-Docker-Github-Action@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.github/workflows/prod.yml
vendored
3
.github/workflows/prod.yml
vendored
@@ -35,6 +35,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Copy robots.txt
|
||||||
|
run: cp public/robots.txt.production public/robots.txt
|
||||||
|
|
||||||
- name: Publish to Registry
|
- name: Publish to Registry
|
||||||
uses: elgohr/Publish-Docker-Github-Action@master
|
uses: elgohr/Publish-Docker-Github-Action@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -32,6 +32,9 @@ dump.rdb
|
|||||||
# Ignore compiled assets
|
# Ignore compiled assets
|
||||||
/public/assets
|
/public/assets
|
||||||
|
|
||||||
|
# Ignore robots.txt - generated before image creation
|
||||||
|
/public/robots.txt
|
||||||
|
|
||||||
# Ignore user uploads
|
# Ignore user uploads
|
||||||
/public/uploads/*
|
/public/uploads/*
|
||||||
!/public/uploads/.keep
|
!/public/uploads/.keep
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
FROM ruby:2.7.1-alpine3.12
|
FROM ruby:2.7.4-alpine3.14
|
||||||
|
|
||||||
RUN apk add --update --no-cache \
|
RUN apk add --update --no-cache \
|
||||||
alpine-sdk \
|
alpine-sdk \
|
||||||
nodejs-current \
|
nodejs-current \
|
||||||
python2 \
|
python3 \
|
||||||
git \
|
git \
|
||||||
nodejs-npm \
|
|
||||||
yarn \
|
yarn \
|
||||||
tzdata
|
tzdata
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ COPY package.json yarn.lock Gemfile Gemfile.lock /app/
|
|||||||
|
|
||||||
COPY vendor /app/vendor
|
COPY vendor /app/vendor
|
||||||
|
|
||||||
RUN yarn install --pure-lockfile --ignore-engines
|
RUN yarn install --pure-lockfile
|
||||||
|
|
||||||
RUN gem install bundler && bundle install
|
RUN gem install bundler && bundle install
|
||||||
|
|
||||||
|
|||||||
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 {
|
import {
|
||||||
ActionsMenu,
|
ActionsMenu,
|
||||||
ComponentModal,
|
ComponentModal,
|
||||||
ComponentView,
|
|
||||||
EditorMenu,
|
EditorMenu,
|
||||||
InputModal,
|
InputModal,
|
||||||
MenuRow,
|
MenuRow,
|
||||||
@@ -76,6 +75,7 @@ import { AppVersion, IsWebPlatform } from '@/version';
|
|||||||
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
|
import { NotesListOptionsDirective } from './components/NotesListOptionsMenu';
|
||||||
import { PurchaseFlowDirective } from './purchaseFlow';
|
import { PurchaseFlowDirective } from './purchaseFlow';
|
||||||
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
import { QuickSettingsMenuDirective } from './components/QuickSettingsMenu';
|
||||||
|
import { ComponentViewDirective } from '@/components/ComponentView';
|
||||||
|
|
||||||
function reloadHiddenFirefoxTab(): boolean {
|
function reloadHiddenFirefoxTab(): boolean {
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +154,7 @@ const startApplication: StartApplication = async function startApplication(
|
|||||||
.directive('actionsMenu', () => new ActionsMenu())
|
.directive('actionsMenu', () => new ActionsMenu())
|
||||||
.directive('challengeModal', () => new ChallengeModal())
|
.directive('challengeModal', () => new ChallengeModal())
|
||||||
.directive('componentModal', () => new ComponentModal())
|
.directive('componentModal', () => new ComponentModal())
|
||||||
.directive('componentView', () => new ComponentView())
|
.directive('componentView', ComponentViewDirective)
|
||||||
.directive('editorMenu', () => new EditorMenu())
|
.directive('editorMenu', () => new EditorMenu())
|
||||||
.directive('inputModal', () => new InputModal())
|
.directive('inputModal', () => new InputModal())
|
||||||
.directive('menuRow', () => new MenuRow())
|
.directive('menuRow', () => new MenuRow())
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ const AccountMenu: FunctionComponent<Props> = observer(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sn-component">
|
<div className='sn-component'>
|
||||||
<div
|
<div
|
||||||
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
className={`sn-menu-border sn-account-menu sn-dropdown ${
|
||||||
shouldAnimateCloseMenu
|
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: () => 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()}>
|
||||||
|
<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: () => 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
360
app/assets/javascripts/components/ComponentView/index.tsx
Normal file
360
app/assets/javascripts/components/ComponentView/index.tsx
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
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 { 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;
|
||||||
|
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,
|
||||||
|
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(() => {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
setIsDeprecated(component.isDeprecated);
|
||||||
|
setDeprecationMessage(component.package_info.deprecation_message);
|
||||||
|
}, [application.componentManager, 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: '=',
|
||||||
|
manualDealloc: '='
|
||||||
|
});
|
||||||
@@ -50,6 +50,7 @@ import EyeOffIcon from '../../icons/ic-eye-off.svg';
|
|||||||
import LockIcon from '../../icons/ic-lock.svg';
|
import LockIcon from '../../icons/ic-lock.svg';
|
||||||
import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg';
|
import ArrowsSortUpIcon from '../../icons/ic-arrows-sort-up.svg';
|
||||||
import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg';
|
import ArrowsSortDownIcon from '../../icons/ic-arrows-sort-down.svg';
|
||||||
|
import WindowIcon from '../../icons/ic-window.svg';
|
||||||
|
|
||||||
import { toDirective } from './utils';
|
import { toDirective } from './utils';
|
||||||
import { FunctionalComponent } from 'preact';
|
import { FunctionalComponent } from 'preact';
|
||||||
@@ -106,6 +107,7 @@ const ICONS = {
|
|||||||
'check-bold': CheckBoldIcon,
|
'check-bold': CheckBoldIcon,
|
||||||
'account-circle': AccountCircleIcon,
|
'account-circle': AccountCircleIcon,
|
||||||
'menu-arrow-down': MenuArrowDownIcon,
|
'menu-arrow-down': MenuArrowDownIcon,
|
||||||
|
window: WindowIcon
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IconType = keyof typeof ICONS;
|
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 { ActionsMenu } from './actionsMenu';
|
||||||
export { ComponentModal } from './componentModal';
|
export { ComponentModal } from './componentModal';
|
||||||
export { ComponentView } from './componentView';
|
|
||||||
export { EditorMenu } from './editorMenu';
|
export { EditorMenu } from './editorMenu';
|
||||||
export { InputModal } from './inputModal';
|
export { InputModal } from './inputModal';
|
||||||
export { MenuRow } from './menuRow';
|
export { MenuRow } from './menuRow';
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
export enum RootScopeMessages {
|
export enum RootScopeMessages {
|
||||||
ReloadExtendedData = 'reload-ext-data',
|
|
||||||
NewUpdateAvailable = 'new-update-available'
|
NewUpdateAvailable = 'new-update-available'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { IconType } from '@/components/Icon';
|
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 = [
|
const PREFERENCE_IDS = [
|
||||||
'general',
|
'general',
|
||||||
@@ -16,11 +21,15 @@ const PREFERENCE_IDS = [
|
|||||||
|
|
||||||
export type PreferenceId = typeof PREFERENCE_IDS[number];
|
export type PreferenceId = typeof PREFERENCE_IDS[number];
|
||||||
interface PreferencesMenuItem {
|
interface PreferencesMenuItem {
|
||||||
readonly id: PreferenceId;
|
readonly id: PreferenceId | FeatureIdentifier;
|
||||||
readonly icon: IconType;
|
readonly icon: IconType;
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SelectableMenuItem extends PreferencesMenuItem {
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Items are in order of appearance
|
* Items are in order of appearance
|
||||||
*/
|
*/
|
||||||
@@ -46,38 +55,93 @@ const READY_PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export class PreferencesMenu {
|
export class PreferencesMenu {
|
||||||
private _selectedPane: PreferenceId = 'account';
|
private _selectedPane: PreferenceId | FeatureIdentifier = 'account';
|
||||||
|
private _extensionPanes: SNComponent[] = [];
|
||||||
private _menu: PreferencesMenuItem[];
|
private _menu: PreferencesMenuItem[];
|
||||||
|
private _extensionLatestVersions: ExtensionsLatestVersions = new ExtensionsLatestVersions(new Map());
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
private application: WebApplication,
|
||||||
private readonly _enableUnfinishedFeatures: boolean,
|
private readonly _enableUnfinishedFeatures: boolean,
|
||||||
) {
|
) {
|
||||||
this._menu = this._enableUnfinishedFeatures ? PREFERENCES_MENU_ITEMS : READY_PREFERENCES_MENU_ITEMS;
|
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,
|
this,
|
||||||
{
|
{
|
||||||
_twoFactorAuth: observable,
|
_twoFactorAuth: observable,
|
||||||
_selectedPane: observable,
|
_selectedPane: observable,
|
||||||
|
_extensionPanes: observable.ref,
|
||||||
|
_extensionLatestVersions: observable.ref,
|
||||||
|
loadLatestVersions: action,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get menuItems(): (PreferencesMenuItem & {
|
private loadLatestVersions(): void {
|
||||||
selected: boolean;
|
ExtensionsLatestVersions.load(this.application).then(versions => {
|
||||||
})[] {
|
this._extensionLatestVersions = versions;
|
||||||
return this._menu.map((p) => ({
|
});
|
||||||
...p,
|
}
|
||||||
selected: p.id === this._selectedPane,
|
|
||||||
|
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 {
|
get selectedMenuItem(): PreferencesMenuItem | undefined {
|
||||||
return (
|
return this._menu.find((item) => item.id === this._selectedPane);
|
||||||
this._menu.find((item) => item.id === this._selectedPane)?.id ?? 'account'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
this._selectedPane = key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Security,
|
Security,
|
||||||
} from './panes';
|
} from './panes';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
import { PreferencesMenu } from './PreferencesMenu';
|
import { PreferencesMenu } from './PreferencesMenu';
|
||||||
import { PreferencesMenuView } from './PreferencesMenuView';
|
import { PreferencesMenuView } from './PreferencesMenuView';
|
||||||
import { WebApplication } from '@/ui_models/application';
|
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 { AppState } from '@/ui_models/app_state';
|
||||||
import { useEffect, useMemo } from 'preact/hooks';
|
import { useEffect, useMemo } from 'preact/hooks';
|
||||||
import { Extensions } from './panes/Extensions';
|
import { Extensions } from './panes/Extensions';
|
||||||
|
import { ExtensionPane } from './panes/ExtensionPane';
|
||||||
|
|
||||||
interface PreferencesProps extends MfaProps {
|
interface PreferencesProps extends MfaProps {
|
||||||
application: WebApplication;
|
application: WebApplication;
|
||||||
@@ -25,44 +27,64 @@ interface PreferencesProps extends MfaProps {
|
|||||||
|
|
||||||
const PaneSelector: FunctionComponent<
|
const PaneSelector: FunctionComponent<
|
||||||
PreferencesProps & { menu: PreferencesMenu }
|
PreferencesProps & { menu: PreferencesMenu }
|
||||||
> = observer((props) => {
|
> = observer(
|
||||||
switch (props.menu.selectedPaneId) {
|
({
|
||||||
case 'general':
|
menu,
|
||||||
return (
|
appState,
|
||||||
<General appState={props.appState} application={props.application} />
|
application,
|
||||||
);
|
mfaProvider,
|
||||||
case 'account':
|
userProvider
|
||||||
return (
|
}) => {
|
||||||
<AccountPreferences
|
switch (menu.selectedPaneId) {
|
||||||
application={props.application}
|
case 'general':
|
||||||
appState={props.appState}
|
return (
|
||||||
/>
|
<General appState={appState} application={application} />
|
||||||
);
|
);
|
||||||
case 'appearance':
|
case 'account':
|
||||||
return null;
|
return (
|
||||||
case 'security':
|
<AccountPreferences
|
||||||
return (
|
application={application}
|
||||||
<Security
|
appState={appState}
|
||||||
mfaProvider={props.mfaProvider}
|
/>
|
||||||
userProvider={props.userProvider}
|
);
|
||||||
appState={props.appState}
|
case 'appearance':
|
||||||
application={props.application}
|
return null;
|
||||||
/>
|
case 'security':
|
||||||
);
|
return (
|
||||||
case 'extensions':
|
<Security
|
||||||
return <Extensions application={props.application} />;
|
mfaProvider={mfaProvider}
|
||||||
case 'listed':
|
userProvider={userProvider}
|
||||||
return <Listed application={props.application} />;
|
appState={appState}
|
||||||
case 'shortcuts':
|
application={application}
|
||||||
return null;
|
/>
|
||||||
case 'accessibility':
|
);
|
||||||
return null;
|
case 'extensions':
|
||||||
case 'get-free-month':
|
return <Extensions application={application} extensionsLatestVersions={menu.extensionsLatestVersions} />;
|
||||||
return null;
|
case 'listed':
|
||||||
case 'help-feedback':
|
return <Listed application={application} />;
|
||||||
return <HelpAndFeedback />;
|
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<
|
const PreferencesCanvas: FunctionComponent<
|
||||||
PreferencesProps & { menu: PreferencesMenu }
|
PreferencesProps & { menu: PreferencesMenu }
|
||||||
@@ -75,9 +97,9 @@ const PreferencesCanvas: FunctionComponent<
|
|||||||
|
|
||||||
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
export const PreferencesView: FunctionComponent<PreferencesProps> = observer(
|
||||||
(props) => {
|
(props) => {
|
||||||
const menu = useMemo(() => new PreferencesMenu(props.appState.enableUnfinishedFeatures), [
|
const menu = useMemo(
|
||||||
props.appState.enableUnfinishedFeatures
|
() => new PreferencesMenu(props.application, props.appState.enableUnfinishedFeatures),
|
||||||
]);
|
[props.appState.enableUnfinishedFeatures, props.application]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
menu.selectPane(props.appState.preferences.currentPane);
|
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,
|
PreferencesPane,
|
||||||
PreferencesSegment,
|
PreferencesSegment,
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments';
|
import { ConfirmCustomExtension, ExtensionItem, ExtensionsLatestVersions } from './extensions-segments';
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import { FeatureDescription } from '@standardnotes/features';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
|
||||||
const loadExtensions = (application: WebApplication) => application.getItems([
|
const loadExtensions = (application: WebApplication) => application.getItems([
|
||||||
ContentType.ActionsExtension,
|
ContentType.ActionsExtension,
|
||||||
@@ -19,30 +19,14 @@ const loadExtensions = (application: WebApplication) => application.getItems([
|
|||||||
ContentType.Theme,
|
ContentType.Theme,
|
||||||
]) as SNComponent[];
|
]) 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<{
|
export const Extensions: FunctionComponent<{
|
||||||
application: WebApplication
|
application: WebApplication
|
||||||
}> = ({ application }) => {
|
extensionsLatestVersions: ExtensionsLatestVersions,
|
||||||
|
}> = observer(({ application, extensionsLatestVersions }) => {
|
||||||
|
|
||||||
const [customUrl, setCustomUrl] = useState('');
|
const [customUrl, setCustomUrl] = useState('');
|
||||||
const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>(undefined);
|
const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>(undefined);
|
||||||
const [extensions, setExtensions] = useState(loadExtensions(application));
|
const [extensions, setExtensions] = useState(loadExtensions(application));
|
||||||
const [latestVersions, setLatestVersions] = useState<Map<string, string> | undefined>(undefined);
|
|
||||||
|
|
||||||
const confirmableEnd = useRef<HTMLDivElement>(null);
|
const confirmableEnd = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -52,12 +36,6 @@ export const Extensions: FunctionComponent<{
|
|||||||
}
|
}
|
||||||
}, [confirmableExtension, confirmableEnd]);
|
}, [confirmableExtension, confirmableEnd]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!latestVersions) {
|
|
||||||
loadLatestVersions(application).then(versions => setLatestVersions(versions));
|
|
||||||
}
|
|
||||||
}, [latestVersions, application]);
|
|
||||||
|
|
||||||
const uninstallExtension = async (extension: SNComponent) => {
|
const uninstallExtension = async (extension: SNComponent) => {
|
||||||
await application.deleteItem(extension);
|
await application.deleteItem(extension);
|
||||||
setExtensions(loadExtensions(application));
|
setExtensions(loadExtensions(application));
|
||||||
@@ -94,12 +72,13 @@ export const Extensions: FunctionComponent<{
|
|||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
{
|
{
|
||||||
extensions
|
extensions
|
||||||
|
.filter(extension => !['modal', 'rooms'].includes(extension.area))
|
||||||
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
|
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
|
||||||
.map((extension, i) => (
|
.map((extension, i) => (
|
||||||
<ExtensionItem
|
<ExtensionItem
|
||||||
application={application}
|
application={application}
|
||||||
extension={extension}
|
extension={extension}
|
||||||
latestVersion={latestVersions?.get(extension.package_info.identifier)}
|
latestVersion={extensionsLatestVersions.getVersion(extension)}
|
||||||
first={i === 0}
|
first={i === 0}
|
||||||
uninstall={uninstallExtension}
|
uninstall={uninstallExtension}
|
||||||
toggleActivate={toggleActivateExtension} />
|
toggleActivate={toggleActivateExtension} />
|
||||||
@@ -140,4 +119,4 @@ export const Extensions: FunctionComponent<{
|
|||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Switch } from "@/components/Switch";
|
|||||||
import { WebApplication } from "@/ui_models/application";
|
import { WebApplication } from "@/ui_models/application";
|
||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
|
import { RenameExtension } from "./RenameExtension";
|
||||||
|
|
||||||
const ExtensionVersions: FunctionComponent<{
|
const ExtensionVersions: FunctionComponent<{
|
||||||
installedVersion: string,
|
installedVersion: string,
|
||||||
@@ -37,161 +38,108 @@ const UseHosted: FunctionComponent<{
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const RenameExtension: FunctionComponent<{
|
export interface ExtensionItemProps {
|
||||||
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<{
|
|
||||||
application: WebApplication,
|
application: WebApplication,
|
||||||
extension: SNComponent,
|
extension: SNComponent,
|
||||||
first: boolean,
|
first: boolean,
|
||||||
latestVersion: string | undefined,
|
latestVersion: string | undefined,
|
||||||
uninstall: (extension: SNComponent) => void,
|
uninstall: (extension: SNComponent) => void,
|
||||||
toggleActivate: (extension: SNComponent) => void,
|
toggleActivate?: (extension: SNComponent) => void,
|
||||||
}> = ({ application, extension, first, uninstall, toggleActivate, latestVersion }) => {
|
}
|
||||||
|
|
||||||
|
export const ExtensionItem: FunctionComponent<ExtensionItemProps> =
|
||||||
|
({ application, extension, first, uninstall, toggleActivate, latestVersion }) => {
|
||||||
const [autoupdateDisabled, setAutoupdateDisabled] = useState(extension.autoupdateDisabled ?? false);
|
const [autoupdateDisabled, setAutoupdateDisabled] = useState(extension.autoupdateDisabled ?? false);
|
||||||
const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false);
|
const [offlineOnly, setOfflineOnly] = useState(extension.offlineOnly ?? false);
|
||||||
const [extensionName, setExtensionName] = useState(extension.name);
|
const [extensionName, setExtensionName] = useState(extension.name);
|
||||||
|
|
||||||
const toggleAutoupdate = () => {
|
const toggleAutoupdate = () => {
|
||||||
const newAutoupdateValue = !autoupdateDisabled;
|
const newAutoupdateValue = !autoupdateDisabled;
|
||||||
setAutoupdateDisabled(newAutoupdateValue);
|
setAutoupdateDisabled(newAutoupdateValue);
|
||||||
application
|
application
|
||||||
.changeAndSaveItem(extension.uuid, (m: any) => {
|
.changeAndSaveItem(extension.uuid, (m: any) => {
|
||||||
if (m.content == undefined) m.content = {};
|
if (m.content == undefined) m.content = {};
|
||||||
m.content.autoupdateDisabled = newAutoupdateValue;
|
m.content.autoupdateDisabled = newAutoupdateValue;
|
||||||
})
|
})
|
||||||
.then((item) => {
|
.then((item) => {
|
||||||
const component = (item as SNComponent);
|
const component = (item as SNComponent);
|
||||||
setAutoupdateDisabled(component.autoupdateDisabled);
|
setAutoupdateDisabled(component.autoupdateDisabled);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
console.error(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 './ConfirmCustomExtension';
|
||||||
export * from './ExtensionItem';
|
export * from './ExtensionItem';
|
||||||
|
export * from './ExtensionsLatestVersions';
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export class AccountMenuState {
|
|||||||
setOtherSessionsSignOut: action,
|
setOtherSessionsSignOut: action,
|
||||||
setCurrentPane: action,
|
setCurrentPane: action,
|
||||||
setEnableServerOption: action,
|
setEnableServerOption: action,
|
||||||
|
setServer: action,
|
||||||
|
|
||||||
notesAndTagsCount: computed,
|
notesAndTagsCount: computed,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export enum AppStateEvent {
|
|||||||
BeganBackupDownload,
|
BeganBackupDownload,
|
||||||
EndedBackupDownload,
|
EndedBackupDownload,
|
||||||
WindowDidFocus,
|
WindowDidFocus,
|
||||||
WindowDidBlur,
|
WindowDidBlur
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PanelResizedData = {
|
export type PanelResizedData = {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class PreferencesState {
|
|||||||
this.currentPane = 'account';
|
this.currentPane = 'account';
|
||||||
};
|
};
|
||||||
|
|
||||||
get isOpen() {
|
get isOpen(): boolean {
|
||||||
return this._open;
|
return this._open;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,6 +112,7 @@
|
|||||||
ng-if='self.state.editorComponent && !self.state.editorUnloading',
|
ng-if='self.state.editorComponent && !self.state.editorUnloading',
|
||||||
on-load='self.onEditorLoad',
|
on-load='self.onEditorLoad',
|
||||||
application='self.application'
|
application='self.application'
|
||||||
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
textarea#note-text-editor.editable.font-editor(
|
textarea#note-text-editor.editable.font-editor(
|
||||||
dir='auto',
|
dir='auto',
|
||||||
@@ -168,4 +169,5 @@
|
|||||||
manual-dealloc='true',
|
manual-dealloc='true',
|
||||||
ng-show='!self.stackComponentHidden(component)',
|
ng-show='!self.stackComponentHidden(component)',
|
||||||
application='self.application'
|
application='self.application'
|
||||||
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
component-view.component-view(
|
component-view.component-view(
|
||||||
component-uuid='self.component.uuid',
|
component-uuid='self.component.uuid',
|
||||||
application='self.application'
|
application='self.application'
|
||||||
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
#tags-content.content(ng-if='!(self.component && self.component.active)')
|
#tags-content.content(ng-if='!(self.component && self.component.active)')
|
||||||
.tags-title-section.section-title-bar
|
.tags-title-section.section-title-bar
|
||||||
|
|||||||
@@ -44,3 +44,9 @@
|
|||||||
@extend .color-info;
|
@extend .color-info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preferences-extension-pane {
|
||||||
|
iframe {
|
||||||
|
height: 60vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -282,10 +282,18 @@
|
|||||||
width: 6.5rem;
|
width: 6.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-200 {
|
||||||
|
max-width: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-92 {
|
.w-92 {
|
||||||
width: 23rem;
|
width: 23rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-200 {
|
||||||
|
width: 50rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-1 {
|
.min-w-1 {
|
||||||
min-width: 0.25rem;
|
min-width: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,4 +14,5 @@
|
|||||||
ng-if='ctrl.component.active'
|
ng-if='ctrl.component.active'
|
||||||
component-uuid="ctrl.component.uuid",
|
component-uuid="ctrl.component.uuid",
|
||||||
application='ctrl.application'
|
application='ctrl.application'
|
||||||
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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,5 @@
|
|||||||
ng-if="ctrl.state.editor",
|
ng-if="ctrl.state.editor",
|
||||||
template-component="ctrl.state.editor",
|
template-component="ctrl.state.editor",
|
||||||
application='ctrl.application'
|
application='ctrl.application'
|
||||||
|
app-state='self.appState'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ case "$COMMAND" in
|
|||||||
echo "Prestart Step 2/5 - Cleaning assets"
|
echo "Prestart Step 2/5 - Cleaning assets"
|
||||||
bundle exec rails assets:clobber
|
bundle exec rails assets:clobber
|
||||||
echo "Prestart Step 3/5 - Installing dependencies"
|
echo "Prestart Step 3/5 - Installing dependencies"
|
||||||
npm install
|
yarn install --pure-lockfile
|
||||||
echo "Prestart Step 4/5 - Creating Webpack bundle"
|
echo "Prestart Step 4/5 - Creating Webpack bundle"
|
||||||
npm run bundle
|
yarn run bundle
|
||||||
echo "Prestart Step 5/5 - Compiling assets"
|
echo "Prestart Step 5/5 - Compiling assets"
|
||||||
bundle exec rails assets:precompile
|
bundle exec rails assets:precompile
|
||||||
echo "Starting Server..."
|
echo "Starting Server..."
|
||||||
|
|||||||
@@ -70,9 +70,9 @@
|
|||||||
"@reach/checkbox": "^0.16.0",
|
"@reach/checkbox": "^0.16.0",
|
||||||
"@reach/dialog": "^0.16.2",
|
"@reach/dialog": "^0.16.2",
|
||||||
"@reach/listbox": "^0.16.2",
|
"@reach/listbox": "^0.16.2",
|
||||||
"@standardnotes/features": "1.7.2",
|
"@standardnotes/features": "1.7.3",
|
||||||
"@standardnotes/sncrypto-web": "^1.5.3",
|
"@standardnotes/sncrypto-web": "1.5.3",
|
||||||
"@standardnotes/snjs": "2.16.0",
|
"@standardnotes/snjs": "2.16.2",
|
||||||
"mobx": "^6.3.5",
|
"mobx": "^6.3.5",
|
||||||
"mobx-react-lite": "^3.2.1",
|
"mobx-react-lite": "^3.2.1",
|
||||||
"preact": "^10.5.15",
|
"preact": "^10.5.15",
|
||||||
|
|||||||
2
public/robots.txt.development
Normal file
2
public/robots.txt.development
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
84
yarn.lock
84
yarn.lock
@@ -2147,55 +2147,43 @@
|
|||||||
prop-types "^15.7.2"
|
prop-types "^15.7.2"
|
||||||
tslib "^2.3.0"
|
tslib "^2.3.0"
|
||||||
|
|
||||||
"@standardnotes/auth@3.7.2":
|
"@standardnotes/auth@^3.8.1":
|
||||||
version "3.7.2"
|
version "3.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.2.tgz#de553ca38c64ae76b3ee3a3aa12ea20311030adb"
|
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.8.1.tgz#4197fb2f7e223c6bd13a870a3feac3c73294fb3c"
|
||||||
integrity sha512-YED+iWX1FxMpn4UJ0Yo37/K0Py/xNYoqcFSlgEcXNorNllRHpLXGXKZ3ILAQVRa0R1oYXpmsthx4bjg2JSptiA==
|
integrity sha512-Q2/81dgFGIGuYlQ4VnSjGRsDB0Qw0tQP/qsiuV+DQj+wdp5Wy5WX3Q4g+p2PNvoyEAYgbuduEHZfWuTLAaIdyw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.1.0"
|
"@standardnotes/common" "^1.2.1"
|
||||||
|
|
||||||
"@standardnotes/auth@^3.7.0":
|
"@standardnotes/common@^1.2.1":
|
||||||
version "3.7.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/auth/-/auth-3.7.1.tgz#d0b1eb63f605e04ecb077fdb5ef83e3fe6db33f9"
|
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.1.tgz#9db212db86ccbf08b347da02549b3dbe4bedbb02"
|
||||||
integrity sha512-xtjAvtikLW3Xv75X/kYA1KTm8FJVPPlXvl+ofnrf/ijkIaRkbUW/3TUhMES+G5CMiG2TZv6uVn32GqJipqgQQQ==
|
integrity sha512-HilBxS50CBlC6TJvy1mrnhGVDzOH63M/Jf+hyMxQ0Vt1nYzpd0iyxVEUrgMh7ZiyO1b9CLnCDED99Jy9rnZWVQ==
|
||||||
|
|
||||||
|
"@standardnotes/domain-events@^2.5.1":
|
||||||
|
version "2.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.5.1.tgz#e6433e940ae616683d1c24f76133c70755504c44"
|
||||||
|
integrity sha512-p0VB4Al/ZcVqcj9ztU7TNqzc3jjjG6/U7x9lBW/QURHxpB+PnwJq3kFU5V5JA9QpCOYlXLT71CMERMf/O5QX6g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/common" "^1.1.0"
|
"@standardnotes/auth" "^3.8.1"
|
||||||
|
|
||||||
"@standardnotes/common@1.2.0":
|
"@standardnotes/features@1.7.3", "@standardnotes/features@^1.7.3":
|
||||||
version "1.2.0"
|
version "1.7.3"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.2.0.tgz#949c9d384c54fbabeacca9ea3f6485cbc78da4bf"
|
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.7.3.tgz#4872c837fd11d069a8a41941bb3e5f294fb13d9c"
|
||||||
integrity sha512-QiOAG858BcXUGSRjsmtk854/4OLyGkdcbvixia8Xcfv4d76iL/pQf7JFTDbanr9Ygodrc6B+h+NuzliO41COcg==
|
integrity sha512-G9NACv8pfVOB9O9L1C+Yoh25vMWVFLfF0FKSK5jjm/THm/w3SiQ2K82BIGgoQGpVGGAPEPa3Ru+OCBs3w8u+Jg==
|
||||||
|
|
||||||
"@standardnotes/common@^1.1.0":
|
|
||||||
version "1.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/common/-/common-1.1.0.tgz#5ffb0a50f9947471e236bb66d097f153ad9a148f"
|
|
||||||
integrity sha512-Nm2IFWbMSfZDD7cnKtN+Gjic0f+PhPq/da/o4eOoUKg21VeOaQkTn+jlQKraKIs6Lmf+w9mmPNAgMc5o4hj7Lg==
|
|
||||||
|
|
||||||
"@standardnotes/domain-events@2.1.0":
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/domain-events/-/domain-events-2.1.0.tgz#a5c4379983a728a738f145aa9e76f7640c7283a2"
|
|
||||||
integrity sha512-8bCQk2V2fyWKalVWC9L8cuj2kuKLe+bTTp0xBVTDpDhWrGFzXfsI79AzWbOl/CLHJU/PWrXf1lvUgQwPwT+RlA==
|
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "^3.7.0"
|
"@standardnotes/common" "^1.2.1"
|
||||||
|
|
||||||
"@standardnotes/features@1.7.2":
|
"@standardnotes/settings@^1.2.1":
|
||||||
version "1.7.2"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/features/-/features-1.7.2.tgz#7a45a947f56c55d191614f7293af553c5209705a"
|
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.1.tgz#4c7656ea86d784a2f77c70acc89face5d28da024"
|
||||||
integrity sha512-zFTHzYAC+08Lbeni5x3RalR5FT8qVORgv3T/z6/Ye4mGvDyXSAddgDPn+o/NmzirwBTpaF6ogSzwZocsElm8zg==
|
integrity sha512-EhCDtQKcVzY6cJ6qXCkAiA3sJ3Wj/q0L0ZVYq+tCXd0jaxmZ8fSk5YNqdwJfjmNXsqtuh7xq6eA2dcXd1fD9VQ==
|
||||||
dependencies:
|
|
||||||
"@standardnotes/common" "^1.1.0"
|
|
||||||
|
|
||||||
"@standardnotes/settings@1.2.0":
|
"@standardnotes/sncrypto-common@^1.5.2":
|
||||||
version "1.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/settings/-/settings-1.2.0.tgz#d7936c788138265b0720085ca9e63358d3092459"
|
|
||||||
integrity sha512-7ikL9BfgXPcLsTJKgCNuRCJN/rFeWreXNxC8M/rxGY+Yk0694WXYyM6jFY8Ry6yV9vLaVukS7Ov6acf+D4wrFg==
|
|
||||||
|
|
||||||
"@standardnotes/sncrypto-common@1.5.2", "@standardnotes/sncrypto-common@^1.5.2":
|
|
||||||
version "1.5.2"
|
version "1.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd"
|
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-common/-/sncrypto-common-1.5.2.tgz#be9404689d94f953c68302609a4f76751eaa82cd"
|
||||||
integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw==
|
integrity sha512-+OQ6gajTcVSHruw33T52MHyBDKL1vRCfQBXQn4tt4+bCfBAe+PFLkEQMHp35bg5twCfg9+wUf2KhmNNSNyBBZw==
|
||||||
|
|
||||||
"@standardnotes/sncrypto-web@^1.5.3":
|
"@standardnotes/sncrypto-web@1.5.3":
|
||||||
version "1.5.3"
|
version "1.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.5.3.tgz#b055bcac553914cbeebfa10e45f46fff817116c3"
|
resolved "https://registry.yarnpkg.com/@standardnotes/sncrypto-web/-/sncrypto-web-1.5.3.tgz#b055bcac553914cbeebfa10e45f46fff817116c3"
|
||||||
integrity sha512-thyFc71cTJTfmLNPgT1hDMiMefZ1bgN0eTa22GEJSp4T41J/X9MldyP2dTmc7sHNM95TJlwzlIJ0iQtxFUE50w==
|
integrity sha512-thyFc71cTJTfmLNPgT1hDMiMefZ1bgN0eTa22GEJSp4T41J/X9MldyP2dTmc7sHNM95TJlwzlIJ0iQtxFUE50w==
|
||||||
@@ -2204,17 +2192,17 @@
|
|||||||
buffer "^6.0.3"
|
buffer "^6.0.3"
|
||||||
libsodium-wrappers "^0.7.9"
|
libsodium-wrappers "^0.7.9"
|
||||||
|
|
||||||
"@standardnotes/snjs@2.16.0":
|
"@standardnotes/snjs@2.16.2":
|
||||||
version "2.16.0"
|
version "2.16.2"
|
||||||
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.16.0.tgz#af1e427d8ed7f71b5019f7c8cca349422a885b02"
|
resolved "https://registry.yarnpkg.com/@standardnotes/snjs/-/snjs-2.16.2.tgz#2958ec0a2f1724343de82204f905311d7c3ffb65"
|
||||||
integrity sha512-dAvFFRu9PuIbW4fF1hZEfh/DwADO2bZP1s24biYY1bfMOr9Z0131n2xIN/yUsA7zw0Sch6m1Nof8OCEZntX5xQ==
|
integrity sha512-G9HNu1TsAnK0OeRo6IYvmIR/huKoNkB+qWDPuh2+p/pJjLrtO6SGrOD4cm4Mg/63t29g8wW8Za/6/tPJHZOFCg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@standardnotes/auth" "3.7.2"
|
"@standardnotes/auth" "^3.8.1"
|
||||||
"@standardnotes/common" "1.2.0"
|
"@standardnotes/common" "^1.2.1"
|
||||||
"@standardnotes/domain-events" "2.1.0"
|
"@standardnotes/domain-events" "^2.5.1"
|
||||||
"@standardnotes/features" "1.7.2"
|
"@standardnotes/features" "^1.7.3"
|
||||||
"@standardnotes/settings" "1.2.0"
|
"@standardnotes/settings" "^1.2.1"
|
||||||
"@standardnotes/sncrypto-common" "1.5.2"
|
"@standardnotes/sncrypto-common" "^1.5.2"
|
||||||
|
|
||||||
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
"@svgr/babel-plugin-add-jsx-attribute@^5.4.0":
|
||||||
version "5.4.0"
|
version "5.4.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user