Merge branch 'backups' into 'develop'

Restore zip backups, export CSV, import CSV

Closes #1264

See merge request soapbox-pub/soapbox!2158
This commit is contained in:
Alex Gleason 2023-01-13 19:41:01 +00:00
commit 21f864c8a6
9 changed files with 50 additions and 51 deletions

View File

@ -11,11 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- UI: added backdrop blur behind modals. - UI: added backdrop blur behind modals.
- Admin: let admins configure media preview for attachment thumbnails. - Admin: let admins configure media preview for attachment thumbnails.
- Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`. - Login: accept `?server` param in external login, eg `fe.soapbox.pub/login/external?server=gleasonator.com`.
- Backups: restored Pleroma backups functionality.
- Export: restored "Export data" to CSV.
### Changed ### Changed
- Posts: letterbox images to 19:6 again. - Posts: letterbox images to 19:6 again.
- Status Info: moved context (repost, pinned) to improve UX. - Status Info: moved context (repost, pinned) to improve UX.
- Posts: remove file icon from empty link previews. - Posts: remove file icon from empty link previews.
- Settings: moved "Import data" under settings.
### Fixed ### Fixed
- Layout: use accent color for "floating action button" (mobile compose button). - Layout: use accent color for "floating action button" (mobile compose button).

View File

@ -28,7 +28,6 @@ const messages = defineMessages({
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
soapboxConfig: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' }, soapboxConfig: { id: 'navigation_bar.soapbox_config', defaultMessage: 'Soapbox config' },
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
accountMigration: { id: 'navigation_bar.account_migration', defaultMessage: 'Move account' }, accountMigration: { id: 'navigation_bar.account_migration', defaultMessage: 'Move account' },
accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' }, accountAliases: { id: 'navigation_bar.account_aliases', defaultMessage: 'Account aliases' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
@ -305,15 +304,6 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
/> />
)} )}
{features.import && (
<SidebarLink
to='/settings/import'
icon={require('@tabler/icons/cloud-upload.svg')}
text={intl.formatMessage(messages.importData)}
onClick={onClose}
/>
)}
<Divider /> <Divider />
<SidebarLink <SidebarLink

View File

@ -1,10 +1,9 @@
import classNames from 'clsx';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { fetchBackups, createBackup } from 'soapbox/actions/backups'; import { fetchBackups, createBackup } from 'soapbox/actions/backups';
import ScrollableList from 'soapbox/components/scrollable-list'; import ScrollableList from 'soapbox/components/scrollable-list';
import { Column } from 'soapbox/components/ui'; import { Button, Column, FormActions, Text } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks'; import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
const messages = defineMessages({ const messages = defineMessages({
@ -23,22 +22,14 @@ const Backups = () => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const handleCreateBackup: React.MouseEventHandler<HTMLAnchorElement> = e => { const handleCreateBackup: React.MouseEventHandler = e => {
dispatch(createBackup()); dispatch(createBackup());
e.preventDefault(); e.preventDefault();
}; };
const makeColumnMenu = () => {
return [{
text: intl.formatMessage(messages.create),
action: handleCreateBackup,
icon: require('@tabler/icons/plus.svg'),
}];
};
useEffect(() => { useEffect(() => {
dispatch(fetchBackups()).then(() => { dispatch(fetchBackups()).then(() => {
setIsLoading(true); setIsLoading(false);
}).catch(() => {}); }).catch(() => {});
}, []); }, []);
@ -46,16 +37,14 @@ const Backups = () => {
const emptyMessageAction = ( const emptyMessageAction = (
<a href='#' onClick={handleCreateBackup}> <a href='#' onClick={handleCreateBackup}>
{intl.formatMessage(messages.emptyMessageAction)} <Text tag='span' theme='primary' size='sm' className='hover:underline'>
{intl.formatMessage(messages.emptyMessageAction)}
</Text>
</a> </a>
); );
return ( return (
<Column <Column label={intl.formatMessage(messages.heading)}>
label={intl.formatMessage(messages.heading)}
// @ts-ignore FIXME: make this menu available.
menu={makeColumnMenu()}
>
<ScrollableList <ScrollableList
isLoading={isLoading} isLoading={isLoading}
showLoading={showLoading} showLoading={showLoading}
@ -64,16 +53,22 @@ const Backups = () => {
> >
{backups.map((backup) => ( {backups.map((backup) => (
<div <div
className={classNames('backup', { 'backup--pending': !backup.processed })} className='p-4'
key={backup.id} key={backup.id}
> >
{backup.processed {backup.processed
? <a href={backup.url} target='_blank'>{backup.inserted_at}</a> ? <a href={backup.url} target='_blank'>{backup.inserted_at}</a>
: <div>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</div> : <Text theme='subtle'>{intl.formatMessage(messages.pending)}: {backup.inserted_at}</Text>
} }
</div> </div>
))} ))}
</ScrollableList> </ScrollableList>
<FormActions>
<Button theme='primary' disabled={isLoading} onClick={handleCreateBackup}>
{intl.formatMessage(messages.create)}
</Button>
</FormActions>
</Column> </Column>
); );
}; };

View File

@ -27,6 +27,9 @@ const messages = defineMessages({
other: { id: 'settings.other', defaultMessage: 'Other options' }, other: { id: 'settings.other', defaultMessage: 'Other options' },
mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' }, mfaEnabled: { id: 'mfa.enabled', defaultMessage: 'Enabled' },
mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' }, mfaDisabled: { id: 'mfa.disabled', defaultMessage: 'Disabled' },
backups: { id: 'column.backups', defaultMessage: 'Backups' },
importData: { id: 'navigation_bar.import_data', defaultMessage: 'Import data' },
exportData: { id: 'column.export_data', defaultMessage: 'Export data' },
}); });
/** User settings page. */ /** User settings page. */
@ -47,6 +50,9 @@ const Settings = () => {
const navigateToDeleteAccount = () => history.push('/settings/account'); const navigateToDeleteAccount = () => history.push('/settings/account');
const navigateToMoveAccount = () => history.push('/settings/migration'); const navigateToMoveAccount = () => history.push('/settings/migration');
const navigateToAliases = () => history.push('/settings/aliases'); const navigateToAliases = () => history.push('/settings/aliases');
const navigateToBackups = () => history.push('/settings/backups');
const navigateToImportData = () => history.push('/settings/import');
const navigateToExportData = () => history.push('/settings/export');
const isMfaEnabled = mfa.getIn(['settings', 'totp']); const isMfaEnabled = mfa.getIn(['settings', 'totp']);
@ -130,6 +136,18 @@ const Settings = () => {
<CardBody> <CardBody>
<List> <List>
{features.importData && (
<ListItem label={intl.formatMessage(messages.importData)} onClick={navigateToImportData} />
)}
{features.exportData && (
<ListItem label={intl.formatMessage(messages.exportData)} onClick={navigateToExportData} />
)}
{features.backups && (
<ListItem label={intl.formatMessage(messages.backups)} onClick={navigateToBackups} />
)}
{features.federating && (features.accountMoving ? ( {features.federating && (features.accountMoving ? (
<ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} /> <ListItem label={intl.formatMessage(messages.accountMigration)} onClick={navigateToMoveAccount} />
) : features.accountAliases && ( ) : features.accountAliases && (

View File

@ -57,9 +57,6 @@ const LinkFooter: React.FC = (): JSX.Element => {
{account.locked && ( {account.locked && (
<FooterLink to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></FooterLink> <FooterLink to='/follow_requests'><FormattedMessage id='navigation_bar.follow_requests' defaultMessage='Follow requests' /></FooterLink>
)} )}
{features.import && (
<FooterLink to='/settings/import'><FormattedMessage id='navigation_bar.import_data' defaultMessage='Import data' /></FooterLink>
)}
<FooterLink to='/logout' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink> <FooterLink to='/logout' onClick={onClickLogOut}><FormattedMessage id='navigation_bar.logout' defaultMessage='Logout' /></FooterLink>
</>} </>}
</div> </div>

View File

@ -76,9 +76,9 @@ import {
EmailConfirmation, EmailConfirmation,
DeleteAccount, DeleteAccount,
SoapboxConfig, SoapboxConfig,
// ExportData, ExportData,
ImportData, ImportData,
// Backups, Backups,
MfaForm, MfaForm,
ChatIndex, ChatIndex,
ChatWidget, ChatWidget,
@ -277,11 +277,11 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
{features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />} {features.scheduledStatuses && <WrappedRoute path='/scheduled_statuses' page={DefaultPage} component={ScheduledStatuses} content={children} />}
<WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} /> <WrappedRoute path='/settings/profile' page={DefaultPage} component={EditProfile} content={children} />
{/* FIXME: this could DDoS our API? :\ */} {features.exportData && <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} />}
{/* <WrappedRoute path='/settings/export' page={DefaultPage} component={ExportData} content={children} /> */}
{features.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />} {features.importData && <WrappedRoute path='/settings/import' page={DefaultPage} component={ImportData} content={children} />}
{features.accountAliases && <WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />} {features.accountAliases && <WrappedRoute path='/settings/aliases' page={DefaultPage} component={Aliases} content={children} />}
{features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />} {features.accountMoving && <WrappedRoute path='/settings/migration' page={DefaultPage} component={Migration} content={children} />}
{features.backups && <WrappedRoute path='/settings/backups' page={DefaultPage} component={Backups} content={children} />}
<WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} /> <WrappedRoute path='/settings/email' page={DefaultPage} component={EditEmail} content={children} />
<WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} /> <WrappedRoute path='/settings/password' page={DefaultPage} component={EditPassword} content={children} />
<WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} /> <WrappedRoute path='/settings/account' page={DefaultPage} component={DeleteAccount} content={children} />
@ -289,7 +289,6 @@ const SwitchingColumnsArea: React.FC<ISwitchingColumnsArea> = ({ children }) =>
<WrappedRoute path='/settings/mfa' page={DefaultPage} component={MfaForm} exact /> <WrappedRoute path='/settings/mfa' page={DefaultPage} component={MfaForm} exact />
<WrappedRoute path='/settings/tokens' page={DefaultPage} component={AuthTokenList} content={children} /> <WrappedRoute path='/settings/tokens' page={DefaultPage} component={AuthTokenList} content={children} />
<WrappedRoute path='/settings' page={DefaultPage} component={Settings} content={children} /> <WrappedRoute path='/settings' page={DefaultPage} component={Settings} content={children} />
{/* <WrappedRoute path='/backups' page={DefaultPage} component={Backups} content={children} /> */}
<WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} /> <WrappedRoute path='/soapbox/config' adminOnly page={DefaultPage} component={SoapboxConfig} content={children} />
<WrappedRoute path='/soapbox/admin' staffOnly page={AdminPage} component={Dashboard} content={children} exact /> <WrappedRoute path='/soapbox/admin' staffOnly page={AdminPage} component={Dashboard} content={children} exact />

View File

@ -178,6 +178,13 @@ const getInstanceFeatures = (instance: Instance) => {
*/ */
announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'), announcementsReactions: v.software === MASTODON && gte(v.compatVersion, '3.1.0'),
/**
* Pleroma backups.
* @see GET /api/v1/pleroma/backups
* @see POST /api/v1/pleroma/backups
*/
backups: v.software === PLEROMA,
/** /**
* Set your birthday and view upcoming birthdays. * Set your birthday and view upcoming birthdays.
* @see GET /api/v1/pleroma/birthdays * @see GET /api/v1/pleroma/birthdays
@ -381,6 +388,9 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === TRUTHSOCIAL, v.software === TRUTHSOCIAL,
]), ]),
/** Whether to allow exporting follows/blocks/mutes to CSV by paginating the API. */
exportData: true,
/** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */ /** Whether the accounts who favourited or emoji-reacted to a status can be viewed through the API. */
exposableReactions: any([ exposableReactions: any([
v.software === MASTODON, v.software === MASTODON,

View File

@ -43,7 +43,6 @@
@import 'components/video-player'; @import 'components/video-player';
@import 'components/audio-player'; @import 'components/audio-player';
@import 'components/filters'; @import 'components/filters';
@import 'components/backups';
@import 'components/crypto-donate'; @import 'components/crypto-donate';
@import 'components/aliases'; @import 'components/aliases';
@import 'components/icon'; @import 'components/icon';

View File

@ -1,12 +0,0 @@
.backup {
padding: 15px;
border-bottom: 1px solid var(--brand-color--faint);
a {
color: var(--brand-color--hicontrast);
}
&--pending {
@apply text-gray-400 italic;
}
}