ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m17s
ci / ts / ts (packages + web) (push) Successful in 3m13s
ci / api / api (push) Successful in 59s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m31s
ci / db / db (push) Successful in 2m50s
69 lines
3.3 KiB
TypeScript
69 lines
3.3 KiB
TypeScript
import { unwrap } from '@pipu/core';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import { useSession } from '../../app/session';
|
|
import { useErrorText } from '../../shared/lib/errors';
|
|
import { Button, Dialog, FormError } from '../../shared/ui/ui';
|
|
import s from '../../pages/page.module.css';
|
|
import { downloadUserArchive, MAX_ARCHIVE_BYTES, parseUserArchive, type UserArchive } from './transfer';
|
|
|
|
export function UserTransfer() {
|
|
const { t } = useTranslation(['admin', 'common']);
|
|
const session = useSession();
|
|
const queryClient = useQueryClient();
|
|
const errorText = useErrorText();
|
|
const [open, setOpen] = useState(false);
|
|
const [archive, setArchive] = useState<UserArchive | null>(null);
|
|
const [fileError, setFileError] = useState(false);
|
|
const [reading, setReading] = useState(false);
|
|
const exporting = useMutation({
|
|
mutationFn: async () => downloadUserArchive(unwrap(await session.client.GET('/api/v1/admin/users/export'))),
|
|
});
|
|
const importing = useMutation({
|
|
mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/users/import', { body: archive! })),
|
|
onSuccess: () => { setArchive(null); void queryClient.invalidateQueries({ queryKey: ['admin-users'] }); },
|
|
});
|
|
const busy = importing.isPending || reading;
|
|
const close = () => { if (!busy) { setOpen(false); setArchive(null); } };
|
|
|
|
const read = async (file: File | undefined) => {
|
|
setArchive(null);
|
|
setFileError(false);
|
|
importing.reset();
|
|
if (!file) return;
|
|
setReading(true);
|
|
try {
|
|
if (file.size > MAX_ARCHIVE_BYTES) throw new Error('size');
|
|
setArchive(parseUserArchive(await file.text()));
|
|
} catch { setFileError(true); }
|
|
finally { setReading(false); }
|
|
};
|
|
|
|
return <>
|
|
<Button busy={exporting.isPending} onClick={() => exporting.mutate()}>{t('transfer.export')}</Button>
|
|
<Button onClick={() => { importing.reset(); setFileError(false); setOpen(true); }}>{t('transfer.import')}</Button>
|
|
<FormError>{exporting.error ? errorText(exporting.error) : null}</FormError>
|
|
{open && <Dialog title={t('transfer.import')} onClose={close} footer={<>
|
|
<Button variant="ghost" disabled={busy} onClick={close}>{t('common:form.close')}</Button>
|
|
<Button variant="primary" busy={importing.isPending} disabled={!archive || busy} onClick={() => importing.mutate()}>
|
|
{t('transfer.apply')}
|
|
</Button>
|
|
</>}>
|
|
<p className={s.note}>{t('transfer.description')}</p>
|
|
<p className={s.note}>{t('transfer.sensitive')}</p>
|
|
<label className={s.sessions}>
|
|
<span>{t('transfer.file')}</span>
|
|
<input type="file" accept=".json,application/json" disabled={busy} onChange={(e) => void read(e.target.files?.[0])} />
|
|
</label>
|
|
{archive && <p className={s.note}>{t('transfer.preview', { count: archive.users.length })}</p>}
|
|
<FormError>{fileError ? t('transfer.invalid') : importing.error ? errorText(importing.error) : null}</FormError>
|
|
{importing.data && <div role="status">
|
|
<p>{t('transfer.result', { count: importing.data.created, skipped: importing.data.skipped.length })}</p>
|
|
{importing.data.skipped.length > 0 && <p className={s.note}>{importing.data.skipped.join(', ')}</p>}
|
|
</div>}
|
|
</Dialog>}
|
|
</>;
|
|
}
|