mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
feat(r5): bibliothèque-corpus en tableau (arbitrage de recette du référent)
- La liste des documents passe en tableau, conforme à l'écran 5 de maquette-r5 (la bibliothèque sera surtout des PDF) : Document, Rattaché à, Indexation, Corpus (ASSETS.edit), actions. - Les vignettes restent aux cartes « Documents » des fiches OT/ascenseur (photos) ; StatutCorpus/InterrupteurCorpus extraits en composants. - Une image non indexable affiche son interrupteur éteint quel que soit l'état stocké — l'interrupteur montre la réalité du corpus. - e2e adapté aux lignes de tableau, 16/16 Playwright rejoués ; artefact de revue pixel mis à jour (arbitrages rendus). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -65,7 +65,7 @@ test('recette R5 : corpus → réindexation → assistant sourcé → refus honn
|
||||
),
|
||||
});
|
||||
await page.getByRole('button', { name: 'Téléverser', exact: true }).last().click();
|
||||
const vignette = page.locator('.doc', { hasText: fichier });
|
||||
const vignette = page.locator('tr', { hasText: fichier }); // tableau du corpus (arbitrage référent)
|
||||
await expect(vignette).toBeVisible();
|
||||
await expect(vignette.locator('.st')).toHaveText('à indexer'); // jamais indexé à la naissance
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ function ApercuVignette({ doc }: { doc: DocumentDto }) {
|
||||
}
|
||||
|
||||
/** Statut corpus (R5, D3) : lisible d'un coup d'œil, jamais ambigu. */
|
||||
function StatutCorpus({ doc }: { doc: DocumentDto }) {
|
||||
export function StatutCorpus({ doc }: { doc: DocumentDto }) {
|
||||
if (doc.contentType !== 'application/pdf') {
|
||||
return <span className="st exclu">image — non indexable</span>;
|
||||
}
|
||||
@@ -84,42 +84,42 @@ function StatutCorpus({ doc }: { doc: DocumentDto }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Interrupteur d'inclusion au corpus (R5, D3) — PDF seulement, réversible.
|
||||
* Une image non indexable s'affiche ÉTEINTE quel que soit l'état stocké :
|
||||
* l'interrupteur montre la réalité du corpus, pas une colonne de base. */
|
||||
export function InterrupteurCorpus({ doc }: { doc: DocumentDto }) {
|
||||
const bascule = useSetDocumentCorpus();
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={doc.contentType === 'application/pdf' && doc.inCorpus}
|
||||
aria-label={doc.inCorpus ? 'Exclure du corpus' : 'Inclure au corpus'}
|
||||
title={
|
||||
doc.contentType !== 'application/pdf'
|
||||
? 'Seuls les PDF sont indexables'
|
||||
: doc.inCorpus
|
||||
? 'Exclure du corpus (effectif à la prochaine réindexation)'
|
||||
: 'Inclure au corpus (effectif à la prochaine réindexation)'
|
||||
}
|
||||
className="interrupteur corpus"
|
||||
disabled={bascule.isPending || doc.contentType !== 'application/pdf'}
|
||||
onClick={() => bascule.mutate({ id: doc.id, inCorpus: !doc.inCorpus })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function VignetteDoc({
|
||||
doc,
|
||||
corpus,
|
||||
surSuppression,
|
||||
}: {
|
||||
doc: DocumentDto;
|
||||
/** true = l'utilisateur administre le corpus (statut + interrupteur visibles). */
|
||||
corpus?: boolean;
|
||||
surSuppression?: (id: string) => void;
|
||||
}) {
|
||||
const bascule = useSetDocumentCorpus();
|
||||
return (
|
||||
<div className="doc">
|
||||
<ApercuVignette doc={doc} />
|
||||
<span className={CLASSE_TYPE[doc.kind] ?? 'type-doc'}>{DOCUMENT_KIND_LABELS[doc.kind]}</span>
|
||||
{corpus ? (
|
||||
<span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<StatutCorpus doc={doc} />
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={doc.inCorpus}
|
||||
aria-label={doc.inCorpus ? 'Exclure du corpus' : 'Inclure au corpus'}
|
||||
title={
|
||||
doc.contentType !== 'application/pdf'
|
||||
? 'Seuls les PDF sont indexables'
|
||||
: doc.inCorpus
|
||||
? 'Exclure du corpus (effectif à la prochaine réindexation)'
|
||||
: 'Inclure au corpus (effectif à la prochaine réindexation)'
|
||||
}
|
||||
className="interrupteur corpus"
|
||||
disabled={bascule.isPending || doc.contentType !== 'application/pdf'}
|
||||
onClick={() => bascule.mutate({ id: doc.id, inCorpus: !doc.inCorpus })}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
<b>{doc.fileName}</b>
|
||||
<span className="meta">
|
||||
{doc.assetReference ? `Asc. ${doc.assetReference} · ` : ''}
|
||||
|
||||
@@ -6,10 +6,16 @@ import {
|
||||
type DocumentKind,
|
||||
} from '@siop/shared';
|
||||
import { useReindexAssistant } from '@/api/assistant';
|
||||
import { useDeleteDocument, useDocuments, useUploadDocument } from '@/api/gestion';
|
||||
import {
|
||||
ouvrirDocument,
|
||||
telechargerDocument,
|
||||
useDeleteDocument,
|
||||
useDocuments,
|
||||
useUploadDocument,
|
||||
} from '@/api/gestion';
|
||||
import { useAssetOptions } from '@/api/referentiel';
|
||||
import { usePermissions } from '@/auth/use-permissions';
|
||||
import { tailleLisible, VignetteDoc } from '@/components/carte-documents';
|
||||
import { InterrupteurCorpus, StatutCorpus, tailleLisible } from '@/components/carte-documents';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Modale } from '@/components/ui/modale';
|
||||
|
||||
@@ -121,15 +127,81 @@ export default function PageBibliotheque() {
|
||||
</button>
|
||||
) : null}
|
||||
{documents?.length ? (
|
||||
<div className="docs">
|
||||
{documents.map((d) => (
|
||||
<VignetteDoc
|
||||
key={d.id}
|
||||
doc={d}
|
||||
corpus={administreCorpus}
|
||||
surSuppression={peutEditer ? (id) => suppression.mutate(id) : undefined}
|
||||
/>
|
||||
))}
|
||||
<div className="carte" style={{ padding: 0 }}>
|
||||
<div className="table">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Document</th>
|
||||
<th>Rattaché à</th>
|
||||
<th>Indexation</th>
|
||||
{administreCorpus ? <th>Corpus</th> : null}
|
||||
<th aria-label="Actions" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{documents.map((d) => (
|
||||
<tr key={d.id}>
|
||||
<td>
|
||||
<b>{d.fileName}</b>
|
||||
<span className="sous">
|
||||
{DOCUMENT_KIND_LABELS[d.kind]} · {tailleLisible(d.size)} ·{' '}
|
||||
{new Intl.DateTimeFormat('fr-FR', { dateStyle: 'medium' }).format(
|
||||
new Date(d.createdAt),
|
||||
)}
|
||||
{d.uploadedByName ? ` · ${d.uploadedByName}` : ''}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{d.assetReference ? `Asc. ${d.assetReference}` : null}
|
||||
{d.assetReference && d.workOrderReference ? ' · ' : null}
|
||||
{d.workOrderReference ?? null}
|
||||
{!d.assetReference && !d.workOrderReference ? '—' : null}
|
||||
</td>
|
||||
<td>
|
||||
<StatutCorpus doc={d} />
|
||||
</td>
|
||||
{administreCorpus ? (
|
||||
<td>
|
||||
<InterrupteurCorpus doc={d} />
|
||||
</td>
|
||||
) : null}
|
||||
<td style={{ whiteSpace: 'nowrap', textAlign: 'right' }}>
|
||||
<span style={{ display: 'inline-flex', gap: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void ouvrirDocument(d.id)}
|
||||
style={{ color: 'var(--primaire)', fontWeight: 600, fontSize: 12.5 }}
|
||||
>
|
||||
Ouvrir
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void telechargerDocument(d.id, d.fileName)}
|
||||
style={{ color: 'var(--primaire)', fontWeight: 600, fontSize: 12.5 }}
|
||||
>
|
||||
Télécharger
|
||||
</button>
|
||||
{peutEditer ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (window.confirm(`Supprimer « ${d.fileName} » ?`)) {
|
||||
suppression.mutate(d.id);
|
||||
}
|
||||
}}
|
||||
style={{ color: 'var(--danger)', fontWeight: 600, fontSize: 12.5 }}
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
) : null}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="carte" style={{ color: 'var(--encre-2)' }}>Aucun document.</div>
|
||||
|
||||
Reference in New Issue
Block a user