fix(uploads): refresh destination previews and improve queue layout
ci / api / api (push) Successful in 50s
ci / db / db (push) Successful in 56s
ci / module boundaries / boundaries (push) Successful in 8s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s
ci / ts / ts (packages + web) (push) Failing after 4m28s
ci / api / api (push) Successful in 50s
ci / db / db (push) Successful in 56s
ci / module boundaries / boundaries (push) Successful in 8s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m15s
ci / ts / ts (packages + web) (push) Failing after 4m28s
This commit is contained in:
1 parent
eb75437708
commit
30c3c85f89
18 files changed
+603
-402
No files matched your search
@@ -1,4 +1,4 @@
|
||||
.page { max-width: 1050px; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
|
||||
.page { width: 100%; min-width: 0; margin: 0 auto; display: flex; flex-direction: column; gap: 24px; }
|
||||
.header, .cardHead { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 16px; }
|
||||
.header h1 { margin: 6px 0; font-family: var(--font-display); font-size: 30px; }
|
||||
.header p, .note { color: var(--text2); font-size: 13px; line-height: 1.6; margin: 0; }
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { formatBytes, formatDateTime, formatDuration } from '../../shared/lib/format';
|
||||
import { editIcon, refreshIcon } from '../../shared/ui/icons';
|
||||
import { chevronRightIcon, editIcon, refreshIcon } from '../../shared/ui/icons';
|
||||
import { useId, useState } from 'react';
|
||||
import { needsFormatAttention } from './files';
|
||||
import { UploadStatusBadge, useReasonText, type UploadFile } from './common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@@ -24,6 +25,8 @@ export function BatchCard({
|
||||
onRetry: (id: string) => void;
|
||||
}) {
|
||||
const { t, i18n } = useTranslation('admin');
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const contentId = useId();
|
||||
const first = files[0]!;
|
||||
const ids = files.map((f) => f.id);
|
||||
const all = ids.every((id) => selected.has(id));
|
||||
@@ -31,31 +34,36 @@ export function BatchCard({
|
||||
return (
|
||||
<section className={u.batch} aria-label={t('uploads.batch_of', { author: first.author.username })}>
|
||||
<div className={u.batchHead}>
|
||||
<Button variant="ghost" className={u.batchToggle} aria-label={t(collapsed ? 'uploads.expand_batch' : 'uploads.collapse_batch')} title={t(collapsed ? 'uploads.expand_batch' : 'uploads.collapse_batch')} aria-expanded={!collapsed} aria-controls={contentId} onClick={() => setCollapsed((value) => !value)}>
|
||||
<Icon src={chevronRightIcon} size={20} className={!collapsed ? u.batchExpanded : undefined} />
|
||||
</Button>
|
||||
{selectable && (
|
||||
<input type="checkbox" aria-label={t('uploads.select_batch')} checked={all} onChange={(e) => onToggle(ids, e.target.checked)} />
|
||||
)}
|
||||
<b>{first.author.username}</b>
|
||||
<span>{formatDateTime(first.created_at, i18n.language)}</span>
|
||||
<span>{t('uploads.batch_size', { count: files.length, size: formatBytes(size, i18n.language) })}</span>
|
||||
{first.batch_comment && <span className={u.comment}>«{first.batch_comment}»</span>}
|
||||
<span style={{ flex: 1 }} />
|
||||
{selectable && (
|
||||
<Button variant="ghost" onClick={() => onEditBatch(first.batch_id)}>
|
||||
{t('uploads.edit_batch')}
|
||||
</Button>
|
||||
)}
|
||||
{first.batch_comment && <span className={u.comment}>«{first.batch_comment}»</span>}
|
||||
</div>
|
||||
<div id={contentId} hidden={collapsed}>
|
||||
{files.map((f) => (
|
||||
<QueueRow
|
||||
key={f.id}
|
||||
file={f}
|
||||
selectable={selectable}
|
||||
checked={selected.has(f.id)}
|
||||
onCheck={(on) => onToggle([f.id], on)}
|
||||
onEdit={() => onEdit(f)}
|
||||
onRetry={() => onRetry(f.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{files.map((f) => (
|
||||
<QueueRow
|
||||
key={f.id}
|
||||
file={f}
|
||||
selectable={selectable}
|
||||
checked={selected.has(f.id)}
|
||||
onCheck={(on) => onToggle([f.id], on)}
|
||||
onEdit={() => onEdit(f)}
|
||||
onRetry={() => onRetry(f.id)}
|
||||
/>
|
||||
))}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -155,6 +163,10 @@ function QueueRow({
|
||||
<>
|
||||
<code className={u.target}>{f.target_rel_path}</code>
|
||||
<span className={u.sub}>{t('uploads.target_path_hint')}</span>
|
||||
{f.target_storage_path && <>
|
||||
<span className={u.placementLabel}>{t('uploads.target_full_path')}</span>
|
||||
<code className={u.target}>{f.target_storage_path}</code>
|
||||
</>}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -83,7 +83,7 @@ export function PatternsPage({ id }: { id: string }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`${s.page} ${s.narrow}`} style={{ maxWidth: 1100 }}>
|
||||
<div className={s.page}>
|
||||
<div className={s.head}>
|
||||
<div>
|
||||
<h1>{t('patterns.title', { name: source.data?.name ?? '…' })}</h1>
|
||||
|
||||
@@ -72,6 +72,7 @@ export function WriteTemplateSection({ source }: { source: Source }) {
|
||||
onSuccess: async (src) => {
|
||||
queryClient.setQueryData(['source', id], src);
|
||||
await queryClient.invalidateQueries({ queryKey: ['sources'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-uploads'] });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.page {
|
||||
max-width: 1100px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -179,6 +179,9 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.batchToggle { min-width: 44px; min-height: 44px; padding: 8px; }
|
||||
.batchExpanded { transform: rotate(90deg); }
|
||||
|
||||
.comment {
|
||||
flex-basis: 100%;
|
||||
color: var(--text);
|
||||
|
||||
Reference in new issue
Block a user