-
-
Notifications
You must be signed in to change notification settings - Fork 423
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: extension manager in preferences (#670)
* 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
Showing
6 changed files
with
395 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
112 changes: 112 additions & 0 deletions
112
app/assets/javascripts/preferences/panes/Extensions.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
80 changes: 80 additions & 0 deletions
80
app/assets/javascripts/preferences/panes/extensions-segments/ConfirmCustomExtension.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
Oops, something went wrong.