feat(b5): M2b — légende des cinq types et double type pour les indécis
Deux chips maximum : le premier alimente submission_type, le second s'écrit en tête de description_user (« Type suggéré : X ou Y »), sans ALTER NocoDB. Le marqueur « Type : non précisé » n'est posé que sans aucun type (contrat worker/enrich.js inchangé). ?categorie= présélectionne un seul chip. Tests : 16/16. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvxdoEGWjCTSEJDmraYW96
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
043e2e2348
commit
a1edcf3093
@@ -38,17 +38,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group">
|
<div class="field-group">
|
||||||
<label id="libre-type-label">Type suggéré <span class="label-hint">(optionnel — ça nous aide à trier)</span></label>
|
<label id="libre-type-label">Type suggéré <span class="label-hint">(optionnel — un ou deux si tu hésites)</span></label>
|
||||||
<ChipGroup aria-label="Type suggéré">
|
<ChipGroup aria-label="Type suggéré" aria-describedby="libre-type-hint">
|
||||||
<Chip
|
<Chip
|
||||||
v-for="t in TYPES_SUGGERES"
|
v-for="t in TYPES_SUGGERES"
|
||||||
:key="t"
|
:key="t"
|
||||||
:label="TYPES_SUGGERES_LABELS[t]"
|
:label="TYPES_SUGGERES_LABELS[t]"
|
||||||
variant="fonction"
|
variant="fonction"
|
||||||
:active="typeSuggere === t"
|
:active="typeSuggere === t || typeSecondaire === t"
|
||||||
@toggle="toggleType(t)"
|
@toggle="toggleType(t)"
|
||||||
/>
|
/>
|
||||||
</ChipGroup>
|
</ChipGroup>
|
||||||
|
<span id="libre-type-hint" class="label-hint">
|
||||||
|
Écosystème : {{ TYPES_SUGGERES_HINTS.ecosysteme }} · Réseau : {{ TYPES_SUGGERES_HINTS.reseau }} · Jobs : {{ TYPES_SUGGERES_HINTS.jobs }} · Outils : {{ TYPES_SUGGERES_HINTS.outils }} · Références : {{ TYPES_SUGGERES_HINTS.references }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field-group" :class="{ 'field-error': errors.submitted_by_email }">
|
<div class="field-group" :class="{ 'field-error': errors.submitted_by_email }">
|
||||||
@@ -84,7 +87,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LibreSubmitSchema, TYPES_SUGGERES, TYPES_SUGGERES_LABELS, type TypeSuggere } from '~/utils/submitLibre'
|
import { LibreSubmitSchema, TYPES_SUGGERES, TYPES_SUGGERES_LABELS, TYPES_SUGGERES_HINTS, type TypeSuggere } from '~/utils/submitLibre'
|
||||||
|
|
||||||
const props = defineProps<{ presetType?: TypeSuggere }>()
|
const props = defineProps<{ presetType?: TypeSuggere }>()
|
||||||
const emit = defineEmits<{ success: [] }>()
|
const emit = defineEmits<{ success: [] }>()
|
||||||
@@ -93,6 +96,8 @@ const liens = reactive<string[]>([''])
|
|||||||
const texte = ref('')
|
const texte = ref('')
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const typeSuggere = ref<TypeSuggere | undefined>(props.presetType)
|
const typeSuggere = ref<TypeSuggere | undefined>(props.presetType)
|
||||||
|
// Deuxième chip optionnelle (B5-M2b) — pour les indécis entre deux types.
|
||||||
|
const typeSecondaire = ref<TypeSuggere | undefined>()
|
||||||
const siteWeb = ref('') // honeypot
|
const siteWeb = ref('') // honeypot
|
||||||
|
|
||||||
const errors = reactive<Record<string, string>>({})
|
const errors = reactive<Record<string, string>>({})
|
||||||
@@ -108,8 +113,29 @@ function removeLien(i: number) {
|
|||||||
if (liens.length === 0) liens.push('')
|
if (liens.length === 0) liens.push('')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deux chips maximum. Premier clic = principal, deuxième = secondaire.
|
||||||
|
* Décocher le principal alors qu'un secondaire existe promeut ce dernier.
|
||||||
|
* Troisième clic sur un chip inactif quand deux sont déjà actifs : remplace
|
||||||
|
* le secondaire par le nouveau chip (choix le plus simple — pas de file d'attente).
|
||||||
|
*/
|
||||||
function toggleType(t: TypeSuggere) {
|
function toggleType(t: TypeSuggere) {
|
||||||
typeSuggere.value = typeSuggere.value === t ? undefined : t
|
if (typeSuggere.value === t) {
|
||||||
|
typeSuggere.value = typeSecondaire.value
|
||||||
|
typeSecondaire.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeSecondaire.value === t) {
|
||||||
|
typeSecondaire.value = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (typeSuggere.value === undefined) {
|
||||||
|
typeSuggere.value = t
|
||||||
|
} else if (typeSecondaire.value === undefined) {
|
||||||
|
typeSecondaire.value = t
|
||||||
|
} else {
|
||||||
|
typeSecondaire.value = t
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentPayload() {
|
function currentPayload() {
|
||||||
@@ -118,6 +144,7 @@ function currentPayload() {
|
|||||||
texte: texte.value,
|
texte: texte.value,
|
||||||
submitted_by_email: email.value || undefined,
|
submitted_by_email: email.value || undefined,
|
||||||
type_suggere: typeSuggere.value,
|
type_suggere: typeSuggere.value,
|
||||||
|
type_secondaire: typeSecondaire.value,
|
||||||
site_web: siteWeb.value,
|
site_web: siteWeb.value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import { dirname, join } from 'node:path'
|
|||||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
const jiti = createJiti(import.meta.url)
|
const jiti = createJiti(import.meta.url)
|
||||||
|
|
||||||
const { LibreSubmitSchema } = await jiti.import(join(__dirname, '..', 'utils', 'submitLibre.ts'))
|
const { LibreSubmitSchema, toOrgaPayload, toReferencePayload } = await jiti.import(
|
||||||
|
join(__dirname, '..', 'utils', 'submitLibre.ts'),
|
||||||
|
)
|
||||||
|
|
||||||
function baseInput(overrides) {
|
function baseInput(overrides) {
|
||||||
return { liens: [], texte: '', site_web: '', ...overrides }
|
return { liens: [], texte: '', site_web: '', ...overrides }
|
||||||
@@ -50,6 +52,26 @@ const cases = [
|
|||||||
input: baseInput({ liens: ['ftp://exemple.fr/fichier.pdf'] }),
|
input: baseInput({ liens: ['ftp://exemple.fr/fichier.pdf'] }),
|
||||||
expectOk: false,
|
expectOk: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'double type valide (jobs + outils) → ok',
|
||||||
|
input: baseInput({ liens: ['https://exemple.fr'], type_suggere: 'jobs', type_secondaire: 'outils' }),
|
||||||
|
expectOk: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type_secondaire sans type_suggere → refus',
|
||||||
|
input: baseInput({ liens: ['https://exemple.fr'], type_secondaire: 'outils' }),
|
||||||
|
expectOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type_secondaire = type_suggere → refus',
|
||||||
|
input: baseInput({ liens: ['https://exemple.fr'], type_suggere: 'jobs', type_secondaire: 'jobs' }),
|
||||||
|
expectOk: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'type inconnu → refus',
|
||||||
|
input: baseInput({ liens: ['https://exemple.fr'], type_suggere: 'inconnu' }),
|
||||||
|
expectOk: false,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let failures = 0
|
let failures = 0
|
||||||
@@ -62,5 +84,57 @@ for (const c of cases) {
|
|||||||
if (!pass) failures++
|
if (!pass) failures++
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n${cases.length - failures}/${cases.length} tests passés`)
|
// ── Mappings (toOrgaPayload / toReferencePayload) ──────────────────────────
|
||||||
|
function checkMapping(name, ok) {
|
||||||
|
console.log(`${ok ? 'PASS' : 'FAIL'} — ${name}`)
|
||||||
|
if (!ok) failures++
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = LibreSubmitSchema.parse(baseInput({ liens: ['https://exemple.fr'] }))
|
||||||
|
const payload = toOrgaPayload(data)
|
||||||
|
checkMapping(
|
||||||
|
'aucun type → description_user commence par « Type : non précisé »',
|
||||||
|
payload.description_user.startsWith('Type : non précisé'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = LibreSubmitSchema.parse(baseInput({ liens: ['https://exemple.fr'], type_suggere: 'jobs' }))
|
||||||
|
const payload = toOrgaPayload(data)
|
||||||
|
checkMapping(
|
||||||
|
'un type → pas de marqueur ni de ligne « Type suggéré »',
|
||||||
|
!payload.description_user.startsWith('Type : non précisé') && !payload.description_user.includes('Type suggéré'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = LibreSubmitSchema.parse(
|
||||||
|
baseInput({ liens: ['https://exemple.fr'], type_suggere: 'jobs', type_secondaire: 'outils' }),
|
||||||
|
)
|
||||||
|
const payload = toOrgaPayload(data)
|
||||||
|
checkMapping(
|
||||||
|
'double type → description_user commence par « Type suggéré : Jobs ou Outils »',
|
||||||
|
payload.description_user.startsWith('Type suggéré : Jobs ou Outils'),
|
||||||
|
)
|
||||||
|
checkMapping('double type → submission_type = le premier (job)', payload.submission_type === 'job')
|
||||||
|
checkMapping(
|
||||||
|
'double type → ne commence PAS par « Type : non précisé »',
|
||||||
|
!payload.description_user.startsWith('Type : non précisé'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const data = LibreSubmitSchema.parse(
|
||||||
|
baseInput({ liens: ['https://exemple.fr'], type_suggere: 'references', type_secondaire: 'outils' }),
|
||||||
|
)
|
||||||
|
const payload = toReferencePayload(data)
|
||||||
|
checkMapping(
|
||||||
|
'premier = Références + second → description commence par « Type suggéré : Références ou Outils »',
|
||||||
|
payload.description.startsWith('Type suggéré : Références ou Outils'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMappingChecks = 6
|
||||||
|
console.log(`\n${failures === 0 ? 'PASS' : 'FAIL'} — ${cases.length + totalMappingChecks - failures}/${cases.length + totalMappingChecks} tests passés`)
|
||||||
process.exit(failures > 0 ? 1 : 0)
|
process.exit(failures > 0 ? 1 : 0)
|
||||||
|
|||||||
+44
-5
@@ -25,6 +25,20 @@ export const TYPES_SUGGERES_LABELS: Record<TypeSuggere, string> = {
|
|||||||
references: 'Références',
|
references: 'Références',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Légende courte sous les chips — définitions tirées des textes existants du site :
|
||||||
|
* ecosysteme/jobs/outils/references reprennent la tête d'onglet (`TABS`, pages/proposer.vue)
|
||||||
|
* et/ou la phrase de l'échelle qui pointe vers ce type (utils/echelles.ts) ; reseau reprend
|
||||||
|
* la tête d'onglet « Réseau AEP » et les champs projet/lien de FormReseau.vue.
|
||||||
|
*/
|
||||||
|
export const TYPES_SUGGERES_HINTS: Record<TypeSuggere, string> = {
|
||||||
|
ecosysteme: 'entraide, mutualisation',
|
||||||
|
reseau: 'acteurs et projets AEP',
|
||||||
|
jobs: 'offres, plateformes d\'emploi',
|
||||||
|
outils: 'ressources, méthodes',
|
||||||
|
references: 'bibliothèque, ouvrages',
|
||||||
|
}
|
||||||
|
|
||||||
/** URL http(s) uniquement — un `ftp://` ou autre schéma est invalide. */
|
/** URL http(s) uniquement — un `ftp://` ou autre schéma est invalide. */
|
||||||
const httpUrl = z
|
const httpUrl = z
|
||||||
.string()
|
.string()
|
||||||
@@ -42,6 +56,8 @@ export const LibreSubmitSchema = z
|
|||||||
.or(z.literal(''))
|
.or(z.literal(''))
|
||||||
.transform((v) => v || undefined),
|
.transform((v) => v || undefined),
|
||||||
type_suggere: z.enum(TYPES_SUGGERES).optional().or(z.literal('')).transform((v) => (v ? v : undefined)),
|
type_suggere: z.enum(TYPES_SUGGERES).optional().or(z.literal('')).transform((v) => (v ? v : undefined)),
|
||||||
|
// Deuxième chip optionnelle (B5-M2b) — indécis entre deux types.
|
||||||
|
type_secondaire: z.enum(TYPES_SUGGERES).optional().or(z.literal('')).transform((v) => (v ? v : undefined)),
|
||||||
// Honeypot anti-spam : champ caché, doit rester vide pour un humain.
|
// Honeypot anti-spam : champ caché, doit rester vide pour un humain.
|
||||||
site_web: z.string().optional().or(z.literal('')),
|
site_web: z.string().optional().or(z.literal('')),
|
||||||
})
|
})
|
||||||
@@ -49,6 +65,14 @@ export const LibreSubmitSchema = z
|
|||||||
message: 'Ajoute au moins un lien ou quelques mots.',
|
message: 'Ajoute au moins un lien ou quelques mots.',
|
||||||
path: ['texte'],
|
path: ['texte'],
|
||||||
})
|
})
|
||||||
|
.refine((data) => !data.type_secondaire || !!data.type_suggere, {
|
||||||
|
message: 'Choisis d\'abord un type principal.',
|
||||||
|
path: ['type_secondaire'],
|
||||||
|
})
|
||||||
|
.refine((data) => !data.type_secondaire || data.type_secondaire !== data.type_suggere, {
|
||||||
|
message: 'Le second type doit être différent du premier.',
|
||||||
|
path: ['type_secondaire'],
|
||||||
|
})
|
||||||
|
|
||||||
export type LibreSubmitInput = z.infer<typeof LibreSubmitSchema>
|
export type LibreSubmitInput = z.infer<typeof LibreSubmitSchema>
|
||||||
|
|
||||||
@@ -75,26 +99,41 @@ function nomSuggere(data: LibreSubmitInput): string {
|
|||||||
return `[à qualifier] ${base || fallback || 'sans titre'}`
|
return `[à qualifier] ${base || fallback || 'sans titre'}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function descriptionAvecLiens(texte: string, liens: string[], typeNonPrecise: boolean): string {
|
/**
|
||||||
|
* `prefixLine` est soit le marqueur worker « Type : non précisé » (aucun type choisi),
|
||||||
|
* soit la ligne « Type suggéré : X ou Y » (double type) — jamais les deux à la fois,
|
||||||
|
* et toujours en toute première position quand présent (contrat worker/enrich.js:562).
|
||||||
|
*/
|
||||||
|
function descriptionAvecLiens(texte: string, liens: string[], prefixLine?: string): string {
|
||||||
const parts: string[] = []
|
const parts: string[] = []
|
||||||
if (typeNonPrecise) parts.push('Type : non précisé')
|
if (prefixLine) parts.push(prefixLine)
|
||||||
if (texte.trim()) parts.push(texte.trim())
|
if (texte.trim()) parts.push(texte.trim())
|
||||||
if (liens.length) parts.push(`Liens :\n${liens.join('\n')}`)
|
if (liens.length) parts.push(`Liens :\n${liens.join('\n')}`)
|
||||||
return parts.join('\n\n')
|
return parts.join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ligneTypeSuggere(data: LibreSubmitInput): string | undefined {
|
||||||
|
if (!data.type_secondaire) return undefined
|
||||||
|
// Le schéma garantit type_suggere défini dès que type_secondaire l'est.
|
||||||
|
return `Type suggéré : ${TYPES_SUGGERES_LABELS[data.type_suggere!]} ou ${TYPES_SUGGERES_LABELS[data.type_secondaire]}`
|
||||||
|
}
|
||||||
|
|
||||||
/** Mapping vers la table orgas existante (submission_type ecosysteme/reseau/job/outil). */
|
/** Mapping vers la table orgas existante (submission_type ecosysteme/reseau/job/outil). */
|
||||||
export function toOrgaPayload(data: LibreSubmitInput) {
|
export function toOrgaPayload(data: LibreSubmitInput) {
|
||||||
const typeNonPrecise = !data.type_suggere || data.type_suggere === 'references'
|
const hasSecondaire = !!data.type_secondaire
|
||||||
|
// Marqueur worker posé uniquement quand aucun type exploitable n'a été choisi du tout —
|
||||||
|
// un double type ne le pose jamais (contrat worker/enrich.js:562).
|
||||||
|
const typeNonPrecise = !hasSecondaire && (!data.type_suggere || data.type_suggere === 'references')
|
||||||
const submissionType = data.type_suggere && data.type_suggere !== 'references'
|
const submissionType = data.type_suggere && data.type_suggere !== 'references'
|
||||||
? TYPE_TO_SUBMISSION_TYPE[data.type_suggere]
|
? TYPE_TO_SUBMISSION_TYPE[data.type_suggere]
|
||||||
: 'ecosysteme'
|
: 'ecosysteme'
|
||||||
|
const prefixLine = typeNonPrecise ? 'Type : non précisé' : ligneTypeSuggere(data)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
submission_type: submissionType,
|
submission_type: submissionType,
|
||||||
nom: nomSuggere(data),
|
nom: nomSuggere(data),
|
||||||
url: data.liens[0] || null,
|
url: data.liens[0] || null,
|
||||||
description_user: descriptionAvecLiens(data.texte, data.liens, typeNonPrecise),
|
description_user: descriptionAvecLiens(data.texte, data.liens, prefixLine),
|
||||||
submitted_by_email: data.submitted_by_email || null,
|
submitted_by_email: data.submitted_by_email || null,
|
||||||
moderation_status: 'pending' as const,
|
moderation_status: 'pending' as const,
|
||||||
ai_processed: false,
|
ai_processed: false,
|
||||||
@@ -109,7 +148,7 @@ export function toReferencePayload(data: LibreSubmitInput) {
|
|||||||
titre: nomSuggere(data),
|
titre: nomSuggere(data),
|
||||||
auteur: 'non précisé',
|
auteur: 'non précisé',
|
||||||
url: data.liens[0] || null,
|
url: data.liens[0] || null,
|
||||||
description: descriptionAvecLiens(data.texte, data.liens, false),
|
description: descriptionAvecLiens(data.texte, data.liens, ligneTypeSuggere(data)),
|
||||||
pertinence_rag: null,
|
pertinence_rag: null,
|
||||||
hashtags: null,
|
hashtags: null,
|
||||||
submitted_by_email: data.submitted_by_email || null,
|
submitted_by_email: data.submitted_by_email || null,
|
||||||
|
|||||||
Reference in New Issue
Block a user