ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
102 lines
4.4 KiB
TypeScript
102 lines
4.4 KiB
TypeScript
import type { components } from '@pipu/api-client';
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { useErrorText } from '../../shared/lib/errors';
|
|
import { Button, Field, FormError } from '../../shared/ui/ui';
|
|
import { useUploadsApi, useUploadSources, useUploadSettings } from './queries';
|
|
import s from '../../pages/page.module.css';
|
|
import u from '../../pages/uploads/uploads.module.css';
|
|
|
|
type Settings = components['schemas']['UploadSettings'];
|
|
const GB = 1024 ** 3;
|
|
|
|
/** Настройки очереди: общий лимит, лимит на модератора, размер файла, хранилище по умолчанию. */
|
|
export function UploadSettingsSection() {
|
|
const { t } = useTranslation(['admin', 'common']);
|
|
const api = useUploadsApi();
|
|
const queryClient = useQueryClient();
|
|
const errorText = useErrorText();
|
|
const settings = useUploadSettings();
|
|
const sources = useUploadSources();
|
|
const [form, setForm] = useState<{ queue: string; perUser: string; maxFile: string; source: string; upgrade: boolean } | null>(null);
|
|
useEffect(() => {
|
|
if (settings.data) setForm(toForm(settings.data));
|
|
}, [settings.data]);
|
|
|
|
const save = useMutation({
|
|
mutationFn: async () => {
|
|
const f = form!;
|
|
const gb = (v: string) => Math.round(Number.parseFloat(v.replace(',', '.')) * GB);
|
|
return api.updateSettings({
|
|
queue_limit_bytes: gb(f.queue), max_file_bytes: gb(f.maxFile),
|
|
per_user_limit_bytes: f.perUser.trim() ? gb(f.perUser) : null,
|
|
default_source_id: f.source || null, allow_quality_upgrade: f.upgrade,
|
|
});
|
|
},
|
|
onSuccess: async (data) => {
|
|
queryClient.setQueryData(['upload-settings'], data);
|
|
await queryClient.invalidateQueries({ queryKey: ['upload-quota'] });
|
|
},
|
|
});
|
|
if (!form) return null;
|
|
const valid = [form.queue, form.maxFile].every((v) => Number.parseFloat(v.replace(',', '.')) > 0) &&
|
|
(!form.perUser.trim() || Number.parseFloat(form.perUser.replace(',', '.')) > 0);
|
|
const dirty = settings.data ? JSON.stringify(toForm(settings.data)) !== JSON.stringify(form) : false;
|
|
return (
|
|
<section className={s.section}>
|
|
<h2>{t('uploads.settings_title')}</h2>
|
|
<div className={u.settingsGrid}>
|
|
<Field label={t('uploads.set_queue')} value={form.queue} onChange={(v) => setForm({ ...form, queue: v })} inputMode="decimal" />
|
|
<Field
|
|
label={t('uploads.set_per_user')}
|
|
value={form.perUser}
|
|
hint={t('uploads.set_per_user_hint')}
|
|
onChange={(v) => setForm({ ...form, perUser: v })}
|
|
inputMode="decimal"
|
|
/>
|
|
<Field label={t('uploads.set_max_file')} value={form.maxFile} onChange={(v) => setForm({ ...form, maxFile: v })} inputMode="decimal" />
|
|
<label>
|
|
<span className={u.settingsLabel}>{t('uploads.set_source')}</span>
|
|
<select className={s.select} value={form.source} onChange={(e) => setForm({ ...form, source: e.target.value })}>
|
|
<option value="">{t('uploads.set_source_auto')}</option>
|
|
{sources.data
|
|
?.filter((x) => x.accepts_uploads)
|
|
.map((x) => (
|
|
<option key={x.id} value={x.id}>
|
|
{x.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<div>
|
|
<label className={u.settingsCheck}>
|
|
<input type="checkbox" checked={form.upgrade} onChange={(e) => setForm({ ...form, upgrade: e.target.checked })} />
|
|
<span>{t('uploads.set_upgrade')}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<p className={s.note}>{t('uploads.set_note')}</p>
|
|
<div className={s.toolbar}>
|
|
<span style={{ flex: 1 }} />
|
|
<Button variant="primary" busy={save.isPending} disabled={!dirty || !valid} onClick={() => save.mutate()}>
|
|
{t('uploads.save')}
|
|
</Button>
|
|
</div>
|
|
{save.isSuccess && !dirty && <span className={s.ok}>{t('uploads.saved')}</span>}
|
|
<FormError>{save.error ? errorText(save.error) : null}</FormError>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function toForm(st: Settings) {
|
|
const gb = (n: number) => String(Math.round((n / GB) * 100) / 100);
|
|
return {
|
|
queue: gb(st.queue_limit_bytes),
|
|
perUser: st.per_user_limit_bytes === null ? '' : gb(st.per_user_limit_bytes),
|
|
maxFile: gb(st.max_file_bytes),
|
|
source: st.default_source_id ?? '',
|
|
upgrade: st.allow_quality_upgrade,
|
|
};
|
|
}
|