feat: Add Listed pane in preferences (#651)
* feat: Add Listed pane in preferences * feat: Add list of blogs in Listed preferences feat: Allow custom classnames in LinkButton feat: Add mt-0 class * fix: Don't show non-Listed Action Extensions * fix: Use streamItems() * fix: Re-render UI when item is deleted * feat: Remove hardcoded margin-top for LinkButton * fix: Fix ESLint exhaustive-deps error * fix: Use useCallback hook feat: Disconnect shows state "Disconnecting..." when deleting item * fix: Remove unused imports * fix: Simplify disconnect function fix: Use key in the correct place * feat: Add confirmation dialog when deleting a blog feat: Show Blog/Blogs in the title depending on the number of items * style: Revert file to original formatting * refactor: Use preact instead of react refactor: Use FunctionalComponent type * feat: Show alert when disconnecting errors out fix: Set state to false even if errors refactor: Use ternary operator for Getting Started section * feat: Load Listed blog actions asynchronously * feat: Only fetch actions if not already available * refactor: Use async/await for disconnecting Co-authored-by: Mo Bitar <mo@standardnotes.org>
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { RoundIconButton } from '@/components/RoundIconButton';
|
import { RoundIconButton } from '@/components/RoundIconButton';
|
||||||
import { TitleBar, Title } from '@/components/TitleBar';
|
import { TitleBar, Title } from '@/components/TitleBar';
|
||||||
import { FunctionComponent } from 'preact';
|
import { FunctionComponent } from 'preact';
|
||||||
import { AccountPreferences, General, HelpAndFeedback, Security } from './panes';
|
import { AccountPreferences, HelpAndFeedback, Listed, General, Security } 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';
|
||||||
@@ -41,7 +41,7 @@ const PaneSelector: FunctionComponent<
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'listed':
|
case 'listed':
|
||||||
return null;
|
return <Listed application={props.application} />;
|
||||||
case 'shortcuts':
|
case 'shortcuts':
|
||||||
return null;
|
return null;
|
||||||
case 'accessibility':
|
case 'accessibility':
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ export const Text: FunctionComponent<{ className?: string }> = ({
|
|||||||
}) => <p className={`${className} text-xs`}>{children}</p>;
|
}) => <p className={`${className} text-xs`}>{children}</p>;
|
||||||
|
|
||||||
const buttonClasses = `block bg-default color-text rounded border-solid \
|
const buttonClasses = `block bg-default color-text rounded border-solid \
|
||||||
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content mt-3 \
|
border-1 border-gray-300 px-4 py-1.75 font-bold text-sm fit-content \
|
||||||
focus:bg-contrast hover:bg-contrast `;
|
focus:bg-contrast hover:bg-contrast `;
|
||||||
|
|
||||||
export const LinkButton: FunctionComponent<{ label: string; link: string }> = ({
|
export const LinkButton: FunctionComponent<{
|
||||||
label,
|
label: string;
|
||||||
link,
|
link: string;
|
||||||
}) => (
|
className?: string;
|
||||||
<a target="_blank" className={buttonClasses} href={link}>
|
}> = ({ label, link, className }) => (
|
||||||
|
<a target="_blank" className={`${className} ${buttonClasses}`} href={link}>
|
||||||
{label}
|
{label}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
|||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
<PreferencesSegment>
|
<PreferencesSegment>
|
||||||
<Subtitle>Can’t find your question here?</Subtitle>
|
<Subtitle>Can’t find your question here?</Subtitle>
|
||||||
<LinkButton label="Open FAQ" link="https://standardnotes.com/help" />
|
<LinkButton
|
||||||
|
className="mt-3"
|
||||||
|
label="Open FAQ"
|
||||||
|
link="https://standardnotes.com/help"
|
||||||
|
/>
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
<PreferencesGroup>
|
<PreferencesGroup>
|
||||||
@@ -68,6 +72,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
|||||||
before advocating for a feature request.
|
before advocating for a feature request.
|
||||||
</Text>
|
</Text>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
className="mt-3"
|
||||||
label="Go to the forum"
|
label="Go to the forum"
|
||||||
link="https://forum.standardnotes.org/"
|
link="https://forum.standardnotes.org/"
|
||||||
/>
|
/>
|
||||||
@@ -82,6 +87,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
|||||||
group for discussions on security, themes, editors and more.
|
group for discussions on security, themes, editors and more.
|
||||||
</Text>
|
</Text>
|
||||||
<LinkButton
|
<LinkButton
|
||||||
|
className="mt-3"
|
||||||
link="https://standardnotes.com/slack"
|
link="https://standardnotes.com/slack"
|
||||||
label="Join our Slack group"
|
label="Join our Slack group"
|
||||||
/>
|
/>
|
||||||
@@ -93,7 +99,7 @@ export const HelpAndFeedback: FunctionComponent = () => (
|
|||||||
<Text>
|
<Text>
|
||||||
Send an email to help@standardnotes.com and we’ll sort it out.
|
Send an email to help@standardnotes.com and we’ll sort it out.
|
||||||
</Text>
|
</Text>
|
||||||
<LinkButton link="mailto: help@standardnotes.com" label="Email us" />
|
<LinkButton className="mt-3" link="mailto: help@standardnotes.com" label="Email us" />
|
||||||
</PreferencesSegment>
|
</PreferencesSegment>
|
||||||
</PreferencesGroup>
|
</PreferencesGroup>
|
||||||
</PreferencesPane>
|
</PreferencesPane>
|
||||||
|
|||||||
116
app/assets/javascripts/preferences/panes/Listed.tsx
Normal file
116
app/assets/javascripts/preferences/panes/Listed.tsx
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
PreferencesGroup,
|
||||||
|
PreferencesPane,
|
||||||
|
PreferencesSegment,
|
||||||
|
Title,
|
||||||
|
Subtitle,
|
||||||
|
Text,
|
||||||
|
LinkButton,
|
||||||
|
} from '../components';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import { ContentType, SNComponent } from '@standardnotes/snjs';
|
||||||
|
import { SNItem } from '@standardnotes/snjs/dist/@types/models/core/item';
|
||||||
|
import { useCallback, useEffect, useState } from 'preact/hooks';
|
||||||
|
import { BlogItem } from './listed/BlogItem';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
application: WebApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Listed = observer(({ application }: Props) => {
|
||||||
|
const [items, setItems] = useState<SNComponent[]>([]);
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
|
||||||
|
const reloadItems = useCallback(() => {
|
||||||
|
const components = application
|
||||||
|
.getItems(ContentType.ActionsExtension)
|
||||||
|
.filter(
|
||||||
|
(item) => (item as SNComponent).package_info?.name === 'Listed'
|
||||||
|
) as SNComponent[];
|
||||||
|
setItems(components);
|
||||||
|
}, [application]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
reloadItems();
|
||||||
|
}, [reloadItems]);
|
||||||
|
|
||||||
|
const disconnectListedBlog = (item: SNItem) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
setIsDeleting(true);
|
||||||
|
application
|
||||||
|
.deleteItem(item)
|
||||||
|
.then(() => {
|
||||||
|
reloadItems();
|
||||||
|
setIsDeleting(false);
|
||||||
|
resolve(true);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
application.alertService.alert(err);
|
||||||
|
setIsDeleting(false);
|
||||||
|
console.error(err);
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PreferencesPane>
|
||||||
|
{items.length > 0 && (
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>
|
||||||
|
Your {items.length === 1 ? 'Blog' : 'Blogs'} on Listed
|
||||||
|
</Title>
|
||||||
|
<div className="h-2 w-full" />
|
||||||
|
{items.map((item, index, array) => {
|
||||||
|
return (
|
||||||
|
<BlogItem
|
||||||
|
item={item}
|
||||||
|
showSeparator={index !== array.length - 1}
|
||||||
|
disabled={isDeleting}
|
||||||
|
disconnect={disconnectListedBlog}
|
||||||
|
key={item.uuid}
|
||||||
|
application={application}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</PreferencesSegment>
|
||||||
|
</PreferencesGroup>
|
||||||
|
)}
|
||||||
|
<PreferencesGroup>
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Title>About Listed</Title>
|
||||||
|
<div className="h-2 w-full" />
|
||||||
|
<Subtitle>What is Listed?</Subtitle>
|
||||||
|
<Text>
|
||||||
|
Listed is a free blogging platform that allows you to create a
|
||||||
|
public journal published directly from your notes.{' '}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href="https://listed.to"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
Learn more
|
||||||
|
</a>
|
||||||
|
</Text>
|
||||||
|
</PreferencesSegment>
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<PreferencesSegment>
|
||||||
|
<Subtitle>How to get started?</Subtitle>
|
||||||
|
<Text>
|
||||||
|
First, you’ll need to sign up for Listed. Once you have your
|
||||||
|
Listed account, follow the instructions to connect it with your
|
||||||
|
Standard Notes account.
|
||||||
|
</Text>
|
||||||
|
<LinkButton
|
||||||
|
className="min-w-20 mt-3"
|
||||||
|
link="https://listed.to"
|
||||||
|
label="Get started"
|
||||||
|
/>
|
||||||
|
</PreferencesSegment>
|
||||||
|
) : null}
|
||||||
|
</PreferencesGroup>
|
||||||
|
</PreferencesPane>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './HelpFeedback';
|
export * from './HelpFeedback';
|
||||||
export * from './Security';
|
export * from './Security';
|
||||||
export * from './AccountPreferences';
|
export * from './AccountPreferences';
|
||||||
|
export * from './Listed';
|
||||||
export * from './General';
|
export * from './General';
|
||||||
|
|||||||
110
app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Normal file
110
app/assets/javascripts/preferences/panes/listed/BlogItem.tsx
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Button } from '@/components/Button';
|
||||||
|
import { HorizontalSeparator } from '@/components/shared/HorizontalSeparator';
|
||||||
|
import { LinkButton, Subtitle } from '@/preferences/components';
|
||||||
|
import { WebApplication } from '@/ui_models/application';
|
||||||
|
import {
|
||||||
|
Action,
|
||||||
|
ButtonType,
|
||||||
|
SNActionsExtension,
|
||||||
|
SNComponent,
|
||||||
|
SNItem,
|
||||||
|
} from '@standardnotes/snjs';
|
||||||
|
import { FunctionalComponent } from 'preact';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
item: SNComponent;
|
||||||
|
showSeparator: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
disconnect: (item: SNItem) => Promise<unknown>;
|
||||||
|
application: WebApplication;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BlogItem: FunctionalComponent<Props> = ({
|
||||||
|
item,
|
||||||
|
showSeparator,
|
||||||
|
disabled,
|
||||||
|
disconnect,
|
||||||
|
application,
|
||||||
|
}) => {
|
||||||
|
const [actions, setActions] = useState<Action[] | undefined>([]);
|
||||||
|
const [isLoadingActions, setIsLoadingActions] = useState(false);
|
||||||
|
const [isDisconnecting, setIsDisconnecting] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadActions = async () => {
|
||||||
|
setIsLoadingActions(true);
|
||||||
|
application.actionsManager
|
||||||
|
.loadExtensionInContextOfItem(item as SNActionsExtension, item)
|
||||||
|
.then((extension) => {
|
||||||
|
setActions(extension?.actions);
|
||||||
|
})
|
||||||
|
.catch((err) => application.alertService.alert(err))
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoadingActions(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (!actions || actions.length === 0) loadActions();
|
||||||
|
}, [application.actionsManager, application.alertService, item, actions]);
|
||||||
|
|
||||||
|
const handleDisconnect = () => {
|
||||||
|
setIsDisconnecting(true);
|
||||||
|
application.alertService
|
||||||
|
.confirm(
|
||||||
|
'Disconnecting will result in loss of access to your blog. Ensure your Listed author key is backed up before uninstalling.',
|
||||||
|
`Disconnect blog "${item?.name}"?`,
|
||||||
|
'Disconnect',
|
||||||
|
ButtonType.Danger
|
||||||
|
)
|
||||||
|
.then(async (shouldDisconnect) => {
|
||||||
|
if (shouldDisconnect) {
|
||||||
|
await disconnect(item as SNItem);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
application.alertService.alert(err);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsDisconnecting(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Subtitle>{item?.name}</Subtitle>
|
||||||
|
<div className="flex">
|
||||||
|
{isLoadingActions ? (
|
||||||
|
<div className="sk-spinner small info"></div>
|
||||||
|
) : null}
|
||||||
|
{actions && actions?.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<LinkButton
|
||||||
|
className="mr-2"
|
||||||
|
label="Open Blog"
|
||||||
|
link={
|
||||||
|
actions?.find((action: Action) => action.label === 'Open Blog')
|
||||||
|
?.url || ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<LinkButton
|
||||||
|
className="mr-2"
|
||||||
|
label="Settings"
|
||||||
|
link={
|
||||||
|
actions?.find((action: Action) => action.label === 'Settings')
|
||||||
|
?.url || ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="danger"
|
||||||
|
label={isDisconnecting ? 'Disconnecting...' : 'Disconnect'}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={handleDisconnect}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{showSeparator && <HorizontalSeparator classes="mt-5 mb-3" />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user