Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ et le versionnement sur [Semantic Versioning](http://semver.org/).

### Changed

- Formulaires mis en forme : champs en grille, sections titrées dans l'onglet « Modèle », contenu des panneaux dépliés sur fond clair, messages sous les champs lisibles. Titre du modèle dans l'en-tête de sa vue. Bandeau du modèle réduit aux problèmes ; identifiant du modèle, chemin du fichier en bas de vue, tuiles et types de fichiers retirés. « Monter » et « Descendre » retirés des colonnes. Carte d'un problème et fenêtre des corrections proposées mises en forme (gravité en pastille, conseil encadré, correction « sûre » signalée).
- Clés étrangères et conditions affichées comme du SQL (mots-clés en bleu gras, chaînes en vert), modifiables par un crayon. Badges colorés : présence des fichiers attendus (obligatoire en rouge, recommandé en orange, facultatif en vert), contraintes des colonnes (valeur obligatoire, colonne obligatoire, unique). « Monter » et « Descendre » retirés des fichiers attendus. Bouton « Équivalents » renommé « Appliquer aux colonnes / fichiers / listes semblables ».
- Arborescence des fichiers attendus (arbre et vue du modèle) : à chaque niveau, les fichiers d'abord, puis les dossiers. Regex mises en valeur : encadrées en police à chasse fixe, variables de `variables.json` en bleu gras, variables facultatives en jaune. Accueil de l'éditeur en ligne : pleine largeur, sans les paragraphes de confidentialité et de commandes.
- Vue d'un modèle en onglets « Modèle », « Fichiers », « Tables » (nouveau : modèles de table du modèle avec présence, nombre de colonnes et problèmes, tables non référencées) et « Listes de codes » ; l'onglet suit l'adresse (fichier de l'arbre, problème cliqué). Bandeau de synthèse en tuiles qui ouvrent l'onglet correspondant, types de fichiers au pluriel. Bouton « Listes de codes (N) » retiré. « Télécharger le JSON » (ou le CSV) à côté des onglets Vue / Source, et non plus dans le titre.
Expand Down
6 changes: 4 additions & 2 deletions src/ui/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ const online = import.meta.env.MODE === 'pages'
// Site en lecture seule produit par `vce site` (013) : instantané statique, champs en lecture seule.
const site = isSite()
const siteSource = site ? new SiteSource() : undefined
const READ_ONLY = { VTextField: { readonly: true }, VTextarea: { readonly: true }, VSelect: { readonly: true }, VCombobox: { readonly: true }, VCheckbox: { readonly: true } }
// Champs : pas d'espace réservé sous un champ sans message ; en lecture seule (013), non modifiables.
const FIELDS = { VTextField: { hideDetails: 'auto' }, VTextarea: { hideDetails: 'auto' }, VSelect: { hideDetails: 'auto' }, VCombobox: { hideDetails: 'auto' } }
const READ_ONLY = { VTextField: { hideDetails: 'auto', readonly: true }, VTextarea: { hideDetails: 'auto', readonly: true }, VSelect: { hideDetails: 'auto', readonly: true }, VCombobox: { hideDetails: 'auto', readonly: true }, VCheckbox: { readonly: true } }
const showHome = ref(false)
watch(() => store.session, () => (showHome.value = false))

Expand Down Expand Up @@ -119,7 +121,7 @@ const view = computed(() => VIEWS[kindOf(route.value.file)])
<strong>Lecture seule</strong> — pour modifier cette configuration, ouvrez son dépôt avec <code>vce serve</code> ou l'éditeur en ligne.
<span v-if="siteSource?.version" class="text-medium-emphasis"> Éditeur v{{ siteSource.version }}.</span>
</v-alert>
<v-defaults-provider :defaults="store.readOnly ? READ_ONLY : undefined">
<v-defaults-provider :defaults="store.readOnly ? READ_ONLY : FIELDS">
<v-container fluid>
<v-alert v-if="store.error" type="error" class="mb-4">{{ store.error }}</v-alert>
<v-progress-linear v-if="store.loading && !store.result" indeterminate aria-label="Chargement de la configuration" />
Expand Down
2 changes: 0 additions & 2 deletions src/ui/components/FileFrame.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,6 @@ watch(tab, highlight)
</div>
<slot v-if="tab === 'view' || !source" />
<SourceView v-else :file="source" />
<!-- Chemin du fichier : discret, en bas de la vue (et au survol du titre). -->
<p class="text-caption text-medium-emphasis mt-6" data-testid="frame-file-path">Fichier : <code>{{ file }}</code></p>
</section>
</template>

Expand Down
58 changes: 53 additions & 5 deletions src/ui/components/FixMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,62 @@ const fixes = computed(() => store.fixes(props.problem))
</script>

<template>
<v-menu v-if="fixes.length" location="bottom start">
<v-menu v-if="fixes.length" location="bottom end" max-width="30rem">
<template #activator="{ props: menu }">
<v-btn v-bind="menu" :prepend-icon="mdiAutoFix" size="small" variant="tonal" color="primary" class="align-self-start mt-1" data-testid="fix">
<v-btn v-bind="menu" :prepend-icon="mdiAutoFix" size="small" variant="flat" color="primary" data-testid="fix">
Corriger
</v-btn>
</template>
<v-list density="compact" max-width="28rem">
<v-list-item v-for="(fix, i) in fixes" :key="i" :title="fix.label" lines="three" data-testid="fix-option" @click="store.applyFix(fix)" />
</v-list>
<v-card class="fixes" data-testid="fix-menu">
<v-card-item>
<v-card-title class="text-subtitle-1 font-weight-bold">Corrections proposées</v-card-title>
<v-card-subtitle class="subtitle">{{ problem.message }}</v-card-subtitle>
</v-card-item>
<v-divider />
<v-list density="comfortable" class="py-0">
<v-list-item v-for="(fix, i) in fixes" :key="i" class="fix" data-testid="fix-option" @click="store.applyFix(fix)">
<template #prepend><v-icon :icon="mdiAutoFix" color="primary" aria-hidden="true" /></template>
<span class="label">{{ fix.label }}</span>
<template #append>
<span v-if="fix.safe" class="safe" title="Correction sans ambiguïté, applicable en groupe">sûre</span>
</template>
</v-list-item>
</v-list>
<v-divider />
<p class="note">Une seule étape, annulable (Ctrl+Z).</p>
</v-card>
</v-menu>
</template>

<style scoped>
.subtitle {
white-space: normal;
opacity: 1;
color: rgb(var(--v-theme-secondary));
}
.fix {
border-bottom: 1px solid rgb(var(--v-theme-divider));
}
.fix:last-child {
border-bottom: none;
}
.label {
white-space: normal;
line-height: 1.35;
}
.safe {
margin-left: 8px;
padding: 0 6px;
border-radius: 10px;
font-size: 0.72rem;
font-weight: 700;
color: #1e5631;
background: #e3f4e8;
}
.note {
margin: 0;
padding: 6px 16px 8px;
font-size: 0.8rem;
color: rgb(var(--v-theme-secondary));
}
</style>
111 changes: 18 additions & 93 deletions src/ui/components/ModelSummary.vue
Original file line number Diff line number Diff line change
@@ -1,118 +1,43 @@
<script setup lang="ts">
import { mdiAlertCircleOutline, mdiFileTreeOutline, mdiFormatListBulleted, mdiTable } from '@mdi/js'
import { mdiAlertCircleOutline, mdiCheckCircleOutline } from '@mdi/js'
import { computed } from 'vue'
import { fileTypeLabel } from '@core/labels'
import type { DocumentModel } from '@core/model/types'
import { useProject } from '../stores/project'

// Bandeau de synthèse d'un modèle (004 US4) : fichiers par type, tables, listes de codes,
// problèmes ; chaque tuile ouvre l'onglet correspondant de la vue.
const props = defineProps<{ name: string; doc: DocumentModel }>()
const emit = defineEmits<{ select: [tab: 'files' | 'tables' | 'codes'] }>()
// Problèmes d'un modèle (004 US4) : bandeau coloré selon l'état, lien vers le panneau filtré.
const props = defineProps<{ name: string }>()
const store = useProject()
const loaded = computed(() => store.result!.project.models.get(props.name))
const byType = computed(() => {
const counts = new Map<string, number>()
for (const f of props.doc.files) counts.set(f.type, (counts.get(f.type) ?? 0) + 1)
return [...counts].map(([type, n]) => ({ type, label: `${n} ${(n > 1 ? PLURALS[type] : undefined) ?? fileTypeLabel(type).replace(/^(?!PDF)./, (c) => c.toLowerCase())}` }))
})
// Libellés au pluriel des types de fichiers attendus.
const PLURALS: Record<string, string> = { table: 'tables', directory: 'dossiers', metadata: 'fiches de métadonnées', pdf: 'PDF', multi_table: 'bases multi-tables' }
const counts = computed(() => store.countsByModel.get(props.name) ?? { errors: 0, warnings: 0 })
const plural = (n: number, word: string) => `${n} ${word}${n > 1 ? 's' : ''}`
const tiles = computed(() => [
{ tab: 'files' as const, icon: mdiFileTreeOutline, value: props.doc.files.length, label: props.doc.files.length > 1 ? 'fichiers attendus' : 'fichier attendu' },
{ tab: 'tables' as const, icon: mdiTable, value: loaded.value?.tables.size ?? 0, label: (loaded.value?.tables.size ?? 0) > 1 ? 'modèles de table' : 'modèle de table' },
{ tab: 'codes' as const, icon: mdiFormatListBulleted, value: props.doc.codes?.length ?? 0, label: (props.doc.codes?.length ?? 0) > 1 ? 'listes de codes' : 'liste de codes' },
])
const tone = computed(() => (counts.value.errors ? 'error' : counts.value.warnings ? 'warning' : 'ok'))
</script>

<template>
<div class="summary mb-4" data-testid="model-summary">
<button v-for="t in tiles" :key="t.tab" type="button" class="tile" :data-testid="`summary-${t.tab}`" @click="emit('select', t.tab)">
<v-icon :icon="t.icon" color="primary" aria-hidden="true" />
<span class="value">{{ t.value }}</span>
<span class="label">{{ t.label }}</span>
</button>
<div class="tile problems" :class="{ error: counts.errors, warning: !counts.errors && counts.warnings }" data-testid="summary-problems">
<v-icon :icon="mdiAlertCircleOutline" :color="counts.errors ? 'error' : counts.warnings ? 'warning' : 'success'" aria-hidden="true" />
<span class="value">{{ counts.errors + counts.warnings }}</span>
<span class="label">{{ counts.errors + counts.warnings ? `${plural(counts.errors, 'erreur')}, ${plural(counts.warnings, 'avertissement')}` : 'aucun problème' }}</span>
<v-btn v-if="counts.errors + counts.warnings" size="small" variant="text" color="primary" class="px-1" data-testid="show-model-problems"
@click="store.problemModel = name">Voir les problèmes</v-btn>
</div>
<div class="types" aria-label="Fichiers attendus par type">
<span v-for="t in byType" :key="t.type" class="type">{{ t.label }}</span>
</div>
<div class="summary mb-4" :class="tone" data-testid="model-summary">
<v-icon :icon="tone === 'ok' ? mdiCheckCircleOutline : mdiAlertCircleOutline" :color="tone === 'ok' ? 'success' : tone" aria-hidden="true" />
<span v-if="tone === 'ok'">Aucun problème</span>
<span v-else><strong>{{ plural(counts.errors, 'erreur') }}</strong>, {{ plural(counts.warnings, 'avertissement') }}</span>
<v-btn v-if="tone !== 'ok'" size="small" variant="text" color="primary" data-testid="show-model-problems" @click="store.problemModel = name">
Voir les problèmes
</v-btn>
</div>
</template>

<style scoped>
.summary {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
gap: 10px;
}
.tile {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
column-gap: 10px;
display: flex;
align-items: center;
padding: 10px 14px;
flex-wrap: wrap;
gap: 8px;
padding: 8px 12px;
border: 1px solid rgb(var(--v-theme-divider));
border-left: 4px solid rgb(var(--v-theme-primary));
border-left: 4px solid rgb(var(--v-theme-success));
border-radius: 6px;
background: rgb(var(--v-theme-surface-light));
color: rgb(var(--v-theme-on-surface));
text-align: left;
font: inherit;
}
button.tile {
cursor: pointer;
}
button.tile:hover,
button.tile:focus-visible {
background: rgba(var(--v-theme-primary), 0.1);
}
.tile .v-icon {
grid-row: span 2;
}
.value {
font-family: var(--v-font-heading);
font-size: 1.6rem;
font-weight: 600;
line-height: 1.1;
}
.label {
grid-column: 2;
color: rgb(var(--v-theme-secondary));
font-size: 0.875rem;
}
.problems {
border-left-color: rgb(var(--v-theme-success));
}
.problems.error {
.summary.error {
border-left-color: rgb(var(--v-theme-error));
}
.problems.warning {
.summary.warning {
border-left-color: rgb(var(--v-theme-warning));
}
.problems .v-btn {
grid-column: 2;
justify-self: start;
}
.types {
grid-column: 1 / -1;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.type {
padding: 1px 8px;
border: 1px solid rgb(var(--v-theme-divider));
border-radius: 12px;
font-size: 0.8rem;
color: rgb(var(--v-theme-secondary));
}
</style>
19 changes: 10 additions & 9 deletions src/ui/components/ModelView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function newCode() {
</script>

<template>
<FileFrame :title="`Modèle ${name}`" :file="path ?? `${name}/`" :source="path">
<FileFrame :title="`Modèle ${doc?.title || name}`" :file="path ?? `${name}/`" :source="path">
<template #chips>
<v-chip v-if="doc?.abstract" size="small" color="secondary">abstrait</v-chip>
<v-btn :prepend-icon="mdiFolderDownload" size="small" variant="tonal" data-testid="download-model" @click="store.download({ kind: 'model', model: name })">
Expand All @@ -154,24 +154,26 @@ function newCode() {
<NotConformant v-else-if="!doc" />

<template v-else>
<ModelSummary :name="name" :doc="doc" @select="(t: ModelTab) => (modelTab = t)" />
<ModelSummary :name="name" />
<v-tabs v-model="modelTab" color="primary" class="model-tabs mb-4" data-testid="model-tabs">
<v-tab value="model" data-testid="model-tab-model">Modèle</v-tab>
<v-tab value="files" data-testid="model-tab-files">Fichiers ({{ doc.files.length }})</v-tab>
<v-tab value="tables" data-testid="model-tab-tables">Tables ({{ tables.length }})</v-tab>
<v-tab value="codes" data-testid="model-tab-codes">Listes de codes ({{ doc.codes?.length ?? 0 }})</v-tab>
</v-tabs>

<section v-if="modelTab === 'model'" data-pointer="">
<div class="d-flex flex-wrap ga-4">
<section v-if="modelTab === 'model'" data-pointer="" class="form-section">
<h3 class="form-heading">Identification</h3>
<div class="form-grid">
<v-text-field label="Nom" :model-value="doc.name" density="compact" variant="outlined" class="field" data-testid="model-name"
@update:model-value="(v: string) => setProp('/name', v, true)" />
<v-text-field label="Titre" :model-value="doc.title" density="compact" variant="outlined" class="field"
@update:model-value="(v: string) => setProp('/title', v)" />
</div>
<v-textarea label="Description" :model-value="doc.description" density="compact" variant="outlined" rows="1" auto-grow
@update:model-value="(v: string) => setProp('/description', v)" />
<div class="d-flex flex-wrap ga-4">
<h3 class="form-heading">Dossier du document</h3>
<div class="form-grid">
<v-text-field label="Nom du dossier (expression régulière)" :model-value="doc.constraints?.folderName" density="compact" variant="outlined" class="field mono"
:hint="store.regexHint(doc.constraints?.folderName, name)" persistent-hint data-testid="model-folderName"
@update:model-value="(v: string) => setProp('/constraints/folderName', store.regexInput(v, name))">
Expand All @@ -180,7 +182,6 @@ function newCode() {
<v-text-field label="Spécification des métadonnées" :model-value="doc.constraints?.metadataSpecification" density="compact" variant="outlined" class="field"
data-testid="metadata-specification" @update:model-value="(v: string) => setProp('/constraints/metadataSpecification', v)" />
</div>
<p v-if="doc.id" class="text-body-2">Identifiant : <code>{{ doc.id }}</code></p>
</section>

<template v-if="modelTab === 'files'">
Expand All @@ -204,13 +205,13 @@ function newCode() {
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<div class="d-flex flex-wrap ga-4">
<div class="form-grid">
<v-text-field label="Nom" :model-value="f.name" density="compact" variant="outlined" class="field"
@update:model-value="(v: string) => setProp(pointer('files', i, 'name'), v, true)" />
<v-text-field label="Titre" :model-value="f.title" density="compact" variant="outlined" class="field"
@update:model-value="(v: string) => setProp(pointer('files', i, 'title'), v)" />
</div>
<div class="d-flex flex-wrap ga-4">
<div class="form-grid">
<v-select label="Type" :items="FILE_TYPES" :model-value="f.type" density="compact" variant="outlined" class="field" data-testid="file-type"
@update:model-value="(v: string) => setProp(pointer('files', i, 'type'), v, true)" />
<v-select label="Présence" :items="MANDATORY" :model-value="f.mandatory ?? 'WARN'" density="compact" variant="outlined" class="field" data-testid="file-mandatory"
Expand Down Expand Up @@ -316,7 +317,7 @@ function newCode() {
min-width: 12rem;
}
.field {
min-width: 16rem;
min-width: 0;
flex: 1 1 16rem;
}
.mono :deep(input) {
Expand Down
Loading
Loading