import type { components } from '@pipu/api-client'; import { unwrap } from '@pipu/core'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useSession } from '../../app/session'; import { useErrorText, useFieldError } from '../../shared/lib/errors'; import { Button, cx, Field, FormError } from '../../shared/ui/ui'; import s from '../page.module.css'; import l from './library.module.css'; type Source = components['schemas']['Source']; type Check = components['schemas']['WriteTemplateCheck']; /** * Приём загрузок и шаблон пути новых файлов (docs/uploads.md#7): автоподбор по существующей структуре * и проверка обратимости регэкспами хранилища. Сохранить можно и с расхождениями — админ видит предупреждение. */ export function WriteTemplateSection({ source }: { source: Source }) { const { t } = useTranslation(['admin', 'common']); const session = useSession(); const queryClient = useQueryClient(); const errorText = useErrorText(); const fieldError = useFieldError(); const id = source.id; const [accepts, setAccepts] = useState(source.accepts_uploads); const [template, setTemplate] = useState(source.write_template ?? ''); const [check, setCheck] = useState(null); useEffect(() => { setAccepts(source.accepts_uploads); setTemplate(source.write_template ?? ''); }, [source.accepts_uploads, source.write_template]); const runCheck = useMutation({ mutationFn: async (tpl: string) => unwrap( await session.client.POST('/api/v1/admin/sources/{id}/write-template/check', { params: { path: { id } }, body: { template: tpl }, }), ), onSuccess: setCheck, }); const suggest = useMutation({ mutationFn: async () => unwrap(await session.client.POST('/api/v1/admin/sources/{id}/write-template/suggest', { params: { path: { id } } })) .template, onSuccess: (tpl) => { setTemplate(tpl); runCheck.mutate(tpl); }, }); const save = useMutation({ mutationFn: async () => unwrap( await session.client.PATCH('/api/v1/admin/sources/{id}', { params: { path: { id } }, body: { accepts_uploads: accepts, write_template: template }, }), ), onSuccess: async (src) => { queryClient.setQueryData(['source', id], src); await queryClient.invalidateQueries({ queryKey: ['sources'] }); }, }); const err = save.error ?? suggest.error ?? runCheck.error; const tplError = fieldError(err, 'write_template') ?? fieldError(err, 'template'); const dirty = accepts !== source.accepts_uploads || template !== (source.write_template ?? ''); return (

{t('write_template.title')}

{t('write_template.intro')}

{accepts && source.type === 'nfs' &&

{t('write_template.nfs_rw_hint')}

} { setTemplate(v); setCheck(null); save.reset(); }} hint={t('write_template.hint')} error={tplError} mono />
{accepts && !template.trim() &&

{t('write_template.required')}

} {save.isSuccess && !dirty && {t('write_template.saved')}} {err && !tplError ? errorText(err) : null} {check && }
); } function CheckResult({ check }: { check: Check }) { const { t } = useTranslation('admin'); if (check.total === 0) return

{t('write_template.no_tracks')}

; return ( <>

{t(check.ok ? 'write_template.check_ok' : 'write_template.check_warn', { round_trip: check.round_trip, same_path: check.same_path, total: check.total, })}

{check.mismatches.length > 0 && (
{check.mismatches.map((m) => ( ))}
{t('write_template.col_real')} {t('write_template.col_rendered')} {t('write_template.col_problem')}
{m.rel_path} {m.rendered ?? '—'} {m.error ?? (m.fields.length ? m.fields.map((f) => (f === 'pattern' ? t('write_template.no_pattern') : f)).join(', ') : t('write_template.other_path'))}
)} ); }