Add relation manager: link/unlink records directly in modal

- RelationManager component: shows linked items as removable tags,
  live search to find and add new links (×-button to unlink)
- tables.js: full fetchRelated config with linkEndpoint + searchEndpoint
  for words↔pictures, words↔categories, objects↔words, objects↔pictures
- api.js: add apiLink, apiUnlink, apiDelete helpers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 22:43:48 +02:00
parent a8ff541117
commit a02705734d
3 changed files with 202 additions and 49 deletions

View File

@@ -73,6 +73,18 @@ export async function apiFetchOne(path) {
return apiFetch(path);
}
export async function apiDelete(endpoint, id) {
return apiFetch(`${endpoint}/${id}`, { method: 'DELETE' });
}
export async function apiLink(path) {
return apiFetch(path, { method: 'POST', body: '{}' });
}
export async function apiUnlink(path) {
return apiFetch(path, { method: 'DELETE' });
}
// Multipart upload — does NOT set Content-Type (browser sets boundary automatically)
export async function apiUpload(path, formData) {
const token = getToken();

View File

@@ -10,10 +10,32 @@ export const TABLES = {
editableFields: {
titel_de: { type: 'text' },
titel_en: { type: 'text' },
titel_sv: { type: 'text' },
status: { type: 'select', options: ['published', 'blocked', 'draft', 'requested', 'translated'] },
difficulty_level:{ type: 'number', min: 1, max: 10 },
},
fetchRelated: [],
fetchRelated: [
{
key: 'pictures',
label: 'Bilder',
endpoint: id => `/words/${id}/pictures`,
display: p => p.design || p.id,
targetTable: 'pictures',
linkEndpoint: (id, targetId) => `/words/${id}/pictures/${targetId}`,
searchEndpoint: '/pictures',
searchLabel: p => p.design || p.id,
},
{
key: 'categories',
label: 'Kategorien',
endpoint: id => `/words/${id}/categories`,
display: c => c.name_de || c.id,
targetTable: 'categories',
linkEndpoint: (id, targetId) => `/words/${id}/categories/${targetId}`,
searchEndpoint: '/categories',
searchLabel: c => c.name_de || c.id,
},
],
},
pictures: {
label: 'Bilder',
@@ -30,7 +52,16 @@ export const TABLES = {
blurhash: { type: 'text' },
},
fetchRelated: [
{ key: 'words', label: 'Wörter', endpoint: id => `/pictures/${id}/words`, display: w => w.titel_de || w.id },
{
key: 'words',
label: 'Wörter',
endpoint: id => `/pictures/${id}/words`,
display: w => w.titel_de || w.id,
targetTable: 'words',
linkEndpoint: (id, targetId) => `/pictures/${id}/words/${targetId}`,
searchEndpoint: '/words',
searchLabel: w => w.titel_de || w.id,
},
],
},
objects: {
@@ -45,7 +76,28 @@ export const TABLES = {
notes: { type: 'textarea' },
status: { type: 'select', options: ['published', 'blocked', 'draft'] },
},
fetchRelated: [],
fetchRelated: [
{
key: 'words',
label: 'Wörter',
endpoint: id => `/objects/${id}/words`,
display: w => w.titel_de || w.id,
targetTable: 'words',
linkEndpoint: (id, targetId) => `/objects/${id}/words/${targetId}`,
searchEndpoint: '/words',
searchLabel: w => w.titel_de || w.id,
},
{
key: 'pictures',
label: 'Bilder',
endpoint: id => `/objects/${id}/pictures`,
display: p => p.design || p.id,
targetTable: 'pictures',
linkEndpoint: (id, targetId) => `/objects/${id}/pictures/${targetId}`,
searchEndpoint: '/pictures',
searchLabel: p => p.design || p.id,
},
],
},
pairs: {
label: 'Pairs',