Skip to content

Commit

Permalink
feat: extension manager in preferences (#670)
Browse files Browse the repository at this point in the history
* feat: add extensions pane

* fix: rename extensions folder for MacOS compatibility

* feat: extension toggles and uninstall

* feat: implement extension renaming, activation, deactivation and UI/UX fixes

* feat(preferences): improve extension item design

* feat(preferences): hide custom extension input when installation confirmed
  • Loading branch information
gorjan5sk authored Oct 8, 2021
1 parent 92699d2 commit 7b6c99d
Show file tree
Hide file tree
Showing 6 changed files with 395 additions and 1 deletion.
4 changes: 3 additions & 1 deletion app/assets/javascripts/preferences/PreferencesMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const PREFERENCE_IDS = [
'account',
'appearance',
'security',
'extensions',
'listed',
'shortcuts',
'accessibility',
Expand All @@ -28,6 +29,7 @@ const PREFERENCES_MENU_ITEMS: PreferencesMenuItem[] = [
{ id: 'account', label: 'Account', icon: 'user' },
{ id: 'appearance', label: 'Appearance', icon: 'themes' },
{ id: 'security', label: 'Security', icon: 'security' },
{ id: 'extensions', label: 'Extensions', icon: 'tune' },
{ id: 'listed', label: 'Listed', icon: 'listed' },
{ id: 'shortcuts', label: 'Shortcuts', icon: 'keyboard' },
{ id: 'accessibility', label: 'Accessibility', icon: 'accessibility' },
Expand Down Expand Up @@ -65,7 +67,7 @@ export class PreferencesMenu {
);
}

selectPane(key: PreferenceId) {
selectPane(key: PreferenceId): void {
this._selectedPane = key;
}
}
3 changes: 3 additions & 0 deletions app/assets/javascripts/preferences/PreferencesView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { WebApplication } from '@/ui_models/application';
import { MfaProps } from './panes/two-factor-auth/MfaProps';
import { AppState } from '@/ui_models/app_state';
import { useEffect } from 'preact/hooks';
import { Extensions } from './panes/Extensions';

interface PreferencesProps extends MfaProps {
application: WebApplication;
Expand Down Expand Up @@ -40,6 +41,8 @@ const PaneSelector: FunctionComponent<
application={props.application}
/>
);
case 'extensions':
return <Extensions application={props.application} />;
case 'listed':
return <Listed application={props.application} />;
case 'shortcuts':
Expand Down
112 changes: 112 additions & 0 deletions app/assets/javascripts/preferences/panes/Extensions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { ContentType, SNComponent } from '@standardnotes/snjs';
import { Button } from '@/components/Button';
import { DecoratedInput } from '@/components/DecoratedInput';
import { WebApplication } from '@/ui_models/application';
import { FunctionComponent } from 'preact';
import {
Title,
PreferencesGroup,
PreferencesPane,
PreferencesSegment,
} from '../components';
import { ConfirmCustomExtension, ExtensionItem } from './extensions-segments';
import { useEffect, useRef, useState } from 'preact/hooks';

const loadExtensions = (application: WebApplication) => application.getItems([
ContentType.ActionsExtension,
ContentType.Component,
ContentType.Theme,
]) as SNComponent[];

export const Extensions: FunctionComponent<{
application: WebApplication
}> = ({ application }) => {

const [customUrl, setCustomUrl] = useState('');
const [confirmableExtension, setConfirmableExtension] = useState<SNComponent | undefined>(undefined);
const [extensions, setExtensions] = useState(loadExtensions(application));

const confirmableEnd = useRef<HTMLDivElement>(null);

useEffect(() => {
if (confirmableExtension) {
confirmableEnd.current.scrollIntoView({ behavior: 'smooth' });
}
}, [confirmableExtension, confirmableEnd]);

const uninstallExtension = async (extension: SNComponent) => {
await application.deleteItem(extension);
setExtensions(loadExtensions(application));
};

const submitExtensionUrl = async (url: string) => {
const component = await application.downloadExternalFeature(url);
if (component) {
setConfirmableExtension(component);
}
};

const handleConfirmExtensionSubmit = async (confirm: boolean) => {
if (confirm) {
confirmExtension();
}
setConfirmableExtension(undefined);
setCustomUrl('');
};

const confirmExtension = async () => {
await application.insertItem(confirmableExtension as SNComponent);
setExtensions(loadExtensions(application));
};

const toggleActivateExtension = (extension: SNComponent) => {
application.toggleComponent(extension);
setExtensions(loadExtensions(application));
};

return (
<PreferencesPane>
<PreferencesGroup>
{
extensions
.filter(extension => extension.package_info.identifier !== 'org.standardnotes.extensions-manager')
.sort((e1, e2) => e1.name.toLowerCase().localeCompare(e2.name.toLowerCase()))
.map((extension, i) => (
<ExtensionItem application={application} extension={extension}
first={i === 0} uninstall={uninstallExtension} toggleActivate={toggleActivateExtension} />
))
}
</PreferencesGroup>

<PreferencesGroup>
{!confirmableExtension &&
<PreferencesSegment>
<Title>Install Custom Extension</Title>
<div className="min-h-2" />
<DecoratedInput
placeholder={'Enter Extension URL'}
text={customUrl}
onChange={(value) => { setCustomUrl(value); }}
/>
<div className="min-h-1" />
<Button
type="primary"
label="Install"
onClick={() => submitExtensionUrl(customUrl)}
/>

</PreferencesSegment>
}
{confirmableExtension &&
<PreferencesSegment>
<ConfirmCustomExtension
component={confirmableExtension}
callback={handleConfirmExtensionSubmit}
/>
<div ref={confirmableEnd} />
</PreferencesSegment>
}
</PreferencesGroup>
</PreferencesPane>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { displayStringForContentType, SNComponent } from '@standardnotes/snjs';
import { Button } from '@/components/Button';
import { FunctionComponent } from 'preact';
import {
Title,
Text,
Subtitle,
PreferencesSegment,
} from '../../components';

export const ConfirmCustomExtension: FunctionComponent<{
component: SNComponent,
callback: (confirmed: boolean) => void
}> = ({ component, callback }) => {

const fields = [
{
label: 'Name',
value: component.package_info.name
},
{
label: 'Description',
value: component.package_info.description
},
{
label: 'Version',
value: component.package_info.version
},
{
label: 'Hosted URL',
value: component.package_info.url
},
{
label: 'Download URL',
value: component.package_info.download_url
},
{
label: 'Extension Type',
value: displayStringForContentType(component.content_type)
},
];

return (
<PreferencesSegment>
<Title>Confirm Extension</Title>

{fields.map((field) => {
if (!field.value) { return undefined; }
return (
<>
<Subtitle>{field.label}</Subtitle>
<Text>{field.value}</Text>
<div className="min-h-2" />
</>
);
})}

<div className="min-h-3" />

<div className="flex flex-row">
<Button
className="min-w-20"
type="primary"
label="Install"
onClick={() => callback(true)}
/>

<div className="min-w-3" />

<Button
className="min-w-20"
type="primary"
label="Cancel"
onClick={() => callback(false)}
/>
</div>

</PreferencesSegment>
);
};
Loading

0 comments on commit 7b6c99d

Please sign in to comment.