fix(codev): boundaries D3 + matching rebuildLinks + couleurs + bulles toggle + FAB +
This commit is contained in:
@@ -37,9 +37,11 @@ const props = withDefaults(defineProps<{
|
|||||||
fiches: CodevFiche[]
|
fiches: CodevFiche[]
|
||||||
matches?: CodevMatch[]
|
matches?: CodevMatch[]
|
||||||
mode?: 'none' | 'solution' | 'alliance' | 'surprise'
|
mode?: 'none' | 'solution' | 'alliance' | 'surprise'
|
||||||
|
showLabels?: boolean
|
||||||
}>(), {
|
}>(), {
|
||||||
matches: () => [],
|
matches: () => [],
|
||||||
mode: 'none',
|
mode: 'none',
|
||||||
|
showLabels: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -146,18 +148,15 @@ function rebuildLinks() {
|
|||||||
currentLinks = buildLinks(currentNodes)
|
currentLinks = buildLinks(currentNodes)
|
||||||
if (!gLinks || !simulation) return
|
if (!gLinks || !simulation) return
|
||||||
|
|
||||||
const linkSel = gLinks
|
// .join() moderne D3 pour garantir le re-rendu complet
|
||||||
|
gLinks
|
||||||
.selectAll<SVGLineElement, SimLink>('line')
|
.selectAll<SVGLineElement, SimLink>('line')
|
||||||
.data(currentLinks, (d: SimLink) => {
|
.data(currentLinks)
|
||||||
const s = d.source as SimNode
|
.join(
|
||||||
const t = d.target as SimNode
|
enter => enter.append('line'),
|
||||||
return `${s.id}-${t.id}-${d.mode}`
|
update => update,
|
||||||
})
|
exit => exit.remove()
|
||||||
|
)
|
||||||
linkSel.exit().remove()
|
|
||||||
|
|
||||||
linkSel.enter()
|
|
||||||
.append('line')
|
|
||||||
.attr('stroke', d => linkColor(d.mode))
|
.attr('stroke', d => linkColor(d.mode))
|
||||||
.attr('stroke-width', d => 1 + d.score * 3)
|
.attr('stroke-width', d => 1 + d.score * 3)
|
||||||
.attr('stroke-opacity', 0.7)
|
.attr('stroke-opacity', 0.7)
|
||||||
@@ -223,12 +222,12 @@ function render() {
|
|||||||
.attr('stroke', '#fff')
|
.attr('stroke', '#fff')
|
||||||
.attr('stroke-width', 1.5)
|
.attr('stroke-width', 1.5)
|
||||||
|
|
||||||
// Pastille besoin (bas-droite, orange)
|
// Pastille besoin (bas-droite, bleu)
|
||||||
nodeGroups.append('circle')
|
nodeGroups.append('circle')
|
||||||
.attr('r', 6)
|
.attr('r', 6)
|
||||||
.attr('cx', r * 0.65)
|
.attr('cx', r * 0.65)
|
||||||
.attr('cy', r * 0.65)
|
.attr('cy', r * 0.65)
|
||||||
.attr('fill', '#f97316')
|
.attr('fill', '#3b82f6')
|
||||||
.attr('stroke', '#fff')
|
.attr('stroke', '#fff')
|
||||||
.attr('stroke-width', 1.5)
|
.attr('stroke-width', 1.5)
|
||||||
|
|
||||||
@@ -236,6 +235,57 @@ function render() {
|
|||||||
nodeGroups.append('title')
|
nodeGroups.append('title')
|
||||||
.text(d => `${d.nom}\nOffre : ${d.offre}\nBesoin : ${d.besoin}`)
|
.text(d => `${d.nom}\nOffre : ${d.offre}\nBesoin : ${d.besoin}`)
|
||||||
|
|
||||||
|
// Groupe label bulle (affiche si showLabels)
|
||||||
|
const labelGroups = nodeGroups.append('g')
|
||||||
|
.attr('class', 'label-bubble')
|
||||||
|
.attr('visibility', props.showLabels ? 'visible' : 'hidden')
|
||||||
|
|
||||||
|
// Fond bulle besoin (dessous du noeud)
|
||||||
|
labelGroups.append('rect')
|
||||||
|
.attr('class', 'bubble-besoin-bg')
|
||||||
|
.attr('x', -(r + 50))
|
||||||
|
.attr('y', r + 4)
|
||||||
|
.attr('width', 100)
|
||||||
|
.attr('height', 28)
|
||||||
|
.attr('rx', 6)
|
||||||
|
.attr('fill', '#eff6ff')
|
||||||
|
.attr('stroke', '#3b82f6')
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
|
||||||
|
// Texte besoin
|
||||||
|
labelGroups.append('text')
|
||||||
|
.attr('class', 'bubble-besoin-txt')
|
||||||
|
.attr('x', -(r) + 50)
|
||||||
|
.attr('y', r + 22)
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('font-size', 9)
|
||||||
|
.attr('fill', '#1e40af')
|
||||||
|
.attr('pointer-events', 'none')
|
||||||
|
.text(d => truncate(d.besoin, 18))
|
||||||
|
|
||||||
|
// Fond bulle offre (dessus du noeud)
|
||||||
|
labelGroups.append('rect')
|
||||||
|
.attr('class', 'bubble-offre-bg')
|
||||||
|
.attr('x', -(r + 50))
|
||||||
|
.attr('y', -(r + 32))
|
||||||
|
.attr('width', 100)
|
||||||
|
.attr('height', 28)
|
||||||
|
.attr('rx', 6)
|
||||||
|
.attr('fill', '#f0fdf4')
|
||||||
|
.attr('stroke', '#22c55e')
|
||||||
|
.attr('stroke-width', 1)
|
||||||
|
|
||||||
|
// Texte offre
|
||||||
|
labelGroups.append('text')
|
||||||
|
.attr('class', 'bubble-offre-txt')
|
||||||
|
.attr('x', -(r) + 50)
|
||||||
|
.attr('y', -(r + 14))
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('font-size', 9)
|
||||||
|
.attr('fill', '#166534')
|
||||||
|
.attr('pointer-events', 'none')
|
||||||
|
.text(d => truncate(d.offre, 18))
|
||||||
|
|
||||||
// Simulation
|
// Simulation
|
||||||
simulation = d3.forceSimulation<SimNode, SimLink>(currentNodes)
|
simulation = d3.forceSimulation<SimNode, SimLink>(currentNodes)
|
||||||
.force('link', d3.forceLink<SimNode, SimLink>(currentLinks)
|
.force('link', d3.forceLink<SimNode, SimLink>(currentLinks)
|
||||||
@@ -245,6 +295,8 @@ function render() {
|
|||||||
.force('charge', d3.forceManyBody<SimNode>().strength(-400))
|
.force('charge', d3.forceManyBody<SimNode>().strength(-400))
|
||||||
.force('center', d3.forceCenter(width.value / 2, height.value / 2))
|
.force('center', d3.forceCenter(width.value / 2, height.value / 2))
|
||||||
.force('collide', d3.forceCollide<SimNode>().radius(r + 12))
|
.force('collide', d3.forceCollide<SimNode>().radius(r + 12))
|
||||||
|
.force('x', d3.forceX(width.value / 2).strength(0.05))
|
||||||
|
.force('y', d3.forceY(height.value / 2).strength(0.05))
|
||||||
.alphaDecay(0.02)
|
.alphaDecay(0.02)
|
||||||
.on('tick', tick)
|
.on('tick', tick)
|
||||||
|
|
||||||
@@ -254,16 +306,21 @@ function render() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function tick() {
|
function tick() {
|
||||||
|
const r = nodeRadius.value
|
||||||
if (!gLinks || !gNodes) return
|
if (!gLinks || !gNodes) return
|
||||||
|
|
||||||
gLinks.selectAll<SVGLineElement, SimLink>('line')
|
gLinks.selectAll<SVGLineElement, SimLink>('line')
|
||||||
.attr('x1', d => (d.source as SimNode).x ?? 0)
|
.attr('x1', d => Math.max(r, Math.min(width.value - r, (d.source as SimNode).x ?? 0)))
|
||||||
.attr('y1', d => (d.source as SimNode).y ?? 0)
|
.attr('y1', d => Math.max(r, Math.min(height.value - r, (d.source as SimNode).y ?? 0)))
|
||||||
.attr('x2', d => (d.target as SimNode).x ?? 0)
|
.attr('x2', d => Math.max(r, Math.min(width.value - r, (d.target as SimNode).x ?? 0)))
|
||||||
.attr('y2', d => (d.target as SimNode).y ?? 0)
|
.attr('y2', d => Math.max(r, Math.min(height.value - r, (d.target as SimNode).y ?? 0)))
|
||||||
|
|
||||||
gNodes.selectAll<SVGGElement, SimNode>('g.node')
|
gNodes.selectAll<SVGGElement, SimNode>('g.node')
|
||||||
.attr('transform', d => `translate(${d.x ?? 0},${d.y ?? 0})`)
|
.attr('transform', d => {
|
||||||
|
const x = Math.max(r, Math.min(width.value - r, d.x ?? 0))
|
||||||
|
const y = Math.max(r, Math.min(height.value - r, d.y ?? 0))
|
||||||
|
return `translate(${x},${y})`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Watch matches/mode (hook pour M4) ─────────────────────────────────────
|
// ── Watch matches/mode (hook pour M4) ─────────────────────────────────────
|
||||||
@@ -271,13 +328,21 @@ function tick() {
|
|||||||
watch(() => [props.matches, props.mode] as const, () => {
|
watch(() => [props.matches, props.mode] as const, () => {
|
||||||
if (!simulation) return
|
if (!simulation) return
|
||||||
rebuildLinks()
|
rebuildLinks()
|
||||||
simulation.force('link', d3.forceLink<SimNode, SimLink>(currentLinks)
|
const newForce = d3.forceLink<SimNode, SimLink>(currentLinks)
|
||||||
.id(d => d.id)
|
.id(d => String(d.id))
|
||||||
.distance(120)
|
.distance(120)
|
||||||
.strength(0.3))
|
.strength(0.5)
|
||||||
simulation.alpha(0.5).restart()
|
simulation.force('link', newForce)
|
||||||
|
simulation.alpha(0.8).restart()
|
||||||
}, { deep: true })
|
}, { deep: true })
|
||||||
|
|
||||||
|
// ── Watch showLabels ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
watch(() => props.showLabels, (val) => {
|
||||||
|
if (!svgEl.value) return
|
||||||
|
d3.select(svgEl.value).selectAll('.label-bubble').attr('visibility', val ? 'visible' : 'hidden')
|
||||||
|
})
|
||||||
|
|
||||||
// ── Watch fiches (re-render si nouvelles fiches) ───────────────────────────
|
// ── Watch fiches (re-render si nouvelles fiches) ───────────────────────────
|
||||||
|
|
||||||
watch(() => props.fiches, () => {
|
watch(() => props.fiches, () => {
|
||||||
|
|||||||
@@ -11,11 +11,22 @@
|
|||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div class="show-labels-bar">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
:class="{ active: showLabels }"
|
||||||
|
@click="showLabels = !showLabels"
|
||||||
|
>
|
||||||
|
{{ showLabels ? 'Masquer besoins/offres' : 'Montrer besoins/offres' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
<CodevGraph
|
<CodevGraph
|
||||||
:fiches="fiches"
|
:fiches="fiches"
|
||||||
:matches="matches"
|
:matches="matches"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
|
:show-labels="showLabels"
|
||||||
@select-fiche="onSelectFiche"
|
@select-fiche="onSelectFiche"
|
||||||
/>
|
/>
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
@@ -71,6 +82,11 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- FAB ajouter une fiche -->
|
||||||
|
<NuxtLink to="/codev/fiche" class="fab-add" title="Ajouter ma fiche" aria-label="Ajouter une fiche">
|
||||||
|
+
|
||||||
|
</NuxtLink>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,6 +101,7 @@ const fiches = computed(() => data.value?.list ?? [])
|
|||||||
|
|
||||||
const matches = ref<CodevMatch[]>([])
|
const matches = ref<CodevMatch[]>([])
|
||||||
const mode = ref<'none' | 'solution' | 'alliance' | 'surprise'>('none')
|
const mode = ref<'none' | 'solution' | 'alliance' | 'surprise'>('none')
|
||||||
|
const showLabels = ref(false)
|
||||||
|
|
||||||
const MODE_LABELS: Record<string, string> = {
|
const MODE_LABELS: Record<string, string> = {
|
||||||
solution: 'Solution',
|
solution: 'Solution',
|
||||||
@@ -256,6 +273,59 @@ function onSelectFiche(id: number) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Toggle besoins/offres ── */
|
||||||
|
|
||||||
|
.show-labels-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-labels-bar button {
|
||||||
|
border: 1px solid #d0d4dc;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: white;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #374151;
|
||||||
|
transition: background 0.15s, color 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.show-labels-bar button.active {
|
||||||
|
background: #1B4436;
|
||||||
|
color: white;
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── FAB ajouter ── */
|
||||||
|
|
||||||
|
.fab-add {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 80px;
|
||||||
|
right: 16px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1B4436;
|
||||||
|
color: white;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-decoration: none;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
|
||||||
|
z-index: 100;
|
||||||
|
transition: transform 0.15s, opacity 0.15s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fab-add:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Mobile ── */
|
/* ── Mobile ── */
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<!-- En-tête -->
|
<!-- En-tête -->
|
||||||
<div class="fiche-header">
|
<div class="fiche-header">
|
||||||
<h1>Ma fiche</h1>
|
<h1>{{ isEdit ? 'Modifier ma fiche' : 'Ma fiche' }}</h1>
|
||||||
<p class="fiche-lead">3 lignes pour te présenter. Le reste se passe entre nous.</p>
|
<p class="fiche-lead">3 lignes pour te présenter. Le reste se passe entre nous.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
<!-- Bouton -->
|
<!-- Bouton -->
|
||||||
<button type="submit" class="submit-btn" :disabled="loading">
|
<button type="submit" class="submit-btn" :disabled="loading">
|
||||||
{{ loading ? 'Envoi en cours...' : 'Ajouter ma fiche' }}
|
{{ isEdit ? (loading ? 'Modification...' : 'Enregistrer les modifications') : (loading ? 'Envoi en cours...' : 'Ajouter ma fiche') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
@@ -115,12 +115,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
const route = useRoute()
|
||||||
|
const editId = computed(() => route.query.id ? Number(route.query.id) : null)
|
||||||
|
const isEdit = computed(() => editId.value !== null)
|
||||||
|
|
||||||
const form = ref({ nom: '', besoin: '', offre: '', hashtagsRaw: '' })
|
const form = ref({ nom: '', besoin: '', offre: '', hashtagsRaw: '' })
|
||||||
const error = ref('')
|
const error = ref('')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const activeTip = ref<'besoin' | 'offre' | null>(null)
|
const activeTip = ref<'besoin' | 'offre' | null>(null)
|
||||||
|
|
||||||
useHead({ title: 'Ma fiche — Co-développement' })
|
useHead({ title: computed(() => isEdit.value ? 'Modifier ma fiche — Co-développement' : 'Ma fiche — Co-développement') })
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (!isEdit.value) return
|
||||||
|
try {
|
||||||
|
const fiche = await $fetch<any>(`/api/codev/fiches/${editId.value}`)
|
||||||
|
form.value.nom = fiche.nom
|
||||||
|
form.value.besoin = fiche.besoin
|
||||||
|
form.value.offre = fiche.offre
|
||||||
|
form.value.hashtagsRaw = fiche.hashtags.join(', ')
|
||||||
|
} catch {
|
||||||
|
error.value = 'Impossible de charger la fiche, elle a peut-etre ete supprimee.'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
function toggleTip(field: 'besoin' | 'offre') {
|
function toggleTip(field: 'besoin' | 'offre') {
|
||||||
activeTip.value = activeTip.value === field ? null : field
|
activeTip.value = activeTip.value === field ? null : field
|
||||||
@@ -136,15 +153,18 @@ async function submit() {
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.slice(0, 3)
|
.slice(0, 3)
|
||||||
|
|
||||||
await $fetch('/api/codev/fiches', {
|
const payload = {
|
||||||
method: 'POST',
|
nom: form.value.nom,
|
||||||
body: {
|
besoin: form.value.besoin,
|
||||||
nom: form.value.nom,
|
offre: form.value.offre,
|
||||||
besoin: form.value.besoin,
|
hashtags,
|
||||||
offre: form.value.offre,
|
}
|
||||||
hashtags,
|
|
||||||
},
|
if (isEdit.value) {
|
||||||
})
|
await $fetch(`/api/codev/fiches/${editId.value}`, { method: 'PATCH', body: payload })
|
||||||
|
} else {
|
||||||
|
await $fetch('/api/codev/fiches', { method: 'POST', body: payload })
|
||||||
|
}
|
||||||
await navigateTo('/codev/carto')
|
await navigateTo('/codev/carto')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
error.value = e?.data?.message || e?.statusMessage || 'Erreur, reessaie'
|
error.value = e?.data?.message || e?.statusMessage || 'Erreur, reessaie'
|
||||||
|
|||||||
34
server/api/codev/fiches/[id].get.ts
Normal file
34
server/api/codev/fiches/[id].get.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CodevFiche } from '~/types/codev'
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event): Promise<CodevFiche> => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const tableId = config.codevTableId
|
||||||
|
const baseId = config.codevBaseId || 'pipilvsi7dibo80'
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
|
||||||
|
if (!tableId || !id) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Parametre manquant' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}/${id}`
|
||||||
|
|
||||||
|
const r: any = await $fetch(url, {
|
||||||
|
headers: { 'xc-token': config.nocodbToken },
|
||||||
|
}).catch(() => null)
|
||||||
|
|
||||||
|
if (!r) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Fiche introuvable' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: r.Id ?? r.id,
|
||||||
|
nom: r.nom || '',
|
||||||
|
besoin: r.besoin || '',
|
||||||
|
offre: r.offre || '',
|
||||||
|
hashtags: (r.hashtags || '')
|
||||||
|
.split(',')
|
||||||
|
.map((h: string) => h.trim().toLowerCase().replace(/^#/, ''))
|
||||||
|
.filter(Boolean),
|
||||||
|
created_at: r.created_at || r.CreatedAt || '',
|
||||||
|
}
|
||||||
|
})
|
||||||
59
server/api/codev/fiches/[id].patch.ts
Normal file
59
server/api/codev/fiches/[id].patch.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
const PatchSchema = z.object({
|
||||||
|
nom: z.string().min(2).max(50).trim(),
|
||||||
|
besoin: z.string().min(5).max(300).trim(),
|
||||||
|
offre: z.string().min(5).max(300).trim(),
|
||||||
|
hashtags: z.array(z.string().max(30)).max(3).default([]),
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
const config = useRuntimeConfig()
|
||||||
|
const tableId = config.codevTableId
|
||||||
|
const baseId = config.codevBaseId || 'pipilvsi7dibo80'
|
||||||
|
const id = getRouterParam(event, 'id')
|
||||||
|
const body = await readBody(event)
|
||||||
|
|
||||||
|
if (!tableId || !id) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Parametre manquant' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = PatchSchema.safeParse(body)
|
||||||
|
if (!parsed.success) {
|
||||||
|
throw createError({
|
||||||
|
statusCode: 422,
|
||||||
|
statusMessage: 'Validation echouee',
|
||||||
|
data: parsed.error.flatten().fieldErrors,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
nom: parsed.data.nom,
|
||||||
|
besoin: parsed.data.besoin,
|
||||||
|
offre: parsed.data.offre,
|
||||||
|
hashtags: parsed.data.hashtags
|
||||||
|
.map((h) => h.trim().toLowerCase().replace(/^#/, ''))
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 3)
|
||||||
|
.join(','),
|
||||||
|
}
|
||||||
|
|
||||||
|
// NocoDB v1 PATCH par Id
|
||||||
|
const url = `${config.nocodbUrl}/api/v1/db/data/noco/${baseId}/${tableId}/${id}`
|
||||||
|
|
||||||
|
try {
|
||||||
|
await $fetch(url, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: {
|
||||||
|
'xc-token': config.nocodbToken,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error('[codev/fiches.patch] NocoDB patch error:', e?.message ?? e)
|
||||||
|
throw createError({ statusCode: 502, statusMessage: 'Erreur serveur' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: 200, ok: true }
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user