|
|
|
@ -6,6 +6,7 @@
|
|
|
|
import AddFieldDrawer from './AddFieldDrawer.svelte';
|
|
|
|
import AddFieldDrawer from './AddFieldDrawer.svelte';
|
|
|
|
import {makeLingRecord} from "@/ling/lib";
|
|
|
|
import {makeLingRecord} from "@/ling/lib";
|
|
|
|
import { flip } from 'svelte/animate';
|
|
|
|
import { flip } from 'svelte/animate';
|
|
|
|
|
|
|
|
import AddFieldMenu from "@/mtas/svelte/components/AddFieldMenu.svelte";
|
|
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
let {
|
|
|
|
value = $bindable<Metadata>({}),
|
|
|
|
value = $bindable<Metadata>({}),
|
|
|
|
@ -13,6 +14,7 @@
|
|
|
|
defaultLocale = 'es',
|
|
|
|
defaultLocale = 'es',
|
|
|
|
dark = false,
|
|
|
|
dark = false,
|
|
|
|
attributes = [],
|
|
|
|
attributes = [],
|
|
|
|
|
|
|
|
pagination = true,
|
|
|
|
onUpdate,
|
|
|
|
onUpdate,
|
|
|
|
}: {
|
|
|
|
}: {
|
|
|
|
value?: Metadata;
|
|
|
|
value?: Metadata;
|
|
|
|
@ -20,6 +22,7 @@
|
|
|
|
defaultLocale?: string;
|
|
|
|
defaultLocale?: string;
|
|
|
|
dark?: boolean;
|
|
|
|
dark?: boolean;
|
|
|
|
attributes?: string[];
|
|
|
|
attributes?: string[];
|
|
|
|
|
|
|
|
pagination?: boolean;
|
|
|
|
onUpdate?: (metadata: Metadata) => void;
|
|
|
|
onUpdate?: (metadata: Metadata) => void;
|
|
|
|
} = $props();
|
|
|
|
} = $props();
|
|
|
|
|
|
|
|
|
|
|
|
@ -44,10 +47,8 @@
|
|
|
|
let rangeStart = $derived(pageSize === 0 ? 1 : (page - 1) * pageSize + 1);
|
|
|
|
let rangeStart = $derived(pageSize === 0 ? 1 : (page - 1) * pageSize + 1);
|
|
|
|
let rangeEnd = $derived(pageSize === 0 ? keyOrder.length : Math.min(page * pageSize, keyOrder.length));
|
|
|
|
let rangeEnd = $derived(pageSize === 0 ? keyOrder.length : Math.min(page * pageSize, keyOrder.length));
|
|
|
|
|
|
|
|
|
|
|
|
// Mantener page dentro de los límites
|
|
|
|
|
|
|
|
$effect(() => { if (page > totalPages) page = Math.max(1, totalPages); });
|
|
|
|
$effect(() => { if (page > totalPages) page = Math.max(1, totalPages); });
|
|
|
|
|
|
|
|
|
|
|
|
// Páginas a mostrar en la barra (con elipsis = -1)
|
|
|
|
|
|
|
|
let paginationPages = $derived((() => {
|
|
|
|
let paginationPages = $derived((() => {
|
|
|
|
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
|
|
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1);
|
|
|
|
const pages: number[] = [1];
|
|
|
|
const pages: number[] = [1];
|
|
|
|
@ -60,21 +61,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
let validation = $derived(validateMetadata(value));
|
|
|
|
let validation = $derived(validateMetadata(value));
|
|
|
|
|
|
|
|
|
|
|
|
const typeLabels: Record<MetadataFieldType, string> = {
|
|
|
|
|
|
|
|
string: 'Texto', number: 'Número', boolean: 'Bool', ling: 'i18n',
|
|
|
|
|
|
|
|
date: 'Fecha', datetime: 'DateTime', url: 'URL', currency: 'Moneda',
|
|
|
|
|
|
|
|
image: 'Imagen', video: 'Vídeo', audio: 'Audio', pdf: 'PDF',
|
|
|
|
|
|
|
|
file: 'Fichero', html: 'HTML', group: 'Grupo',
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const typeGroups: { label: string; types: MetadataFieldType[] }[] = [
|
|
|
|
|
|
|
|
{ label: 'Texto', types: ['string', 'ling', 'html', 'url'] },
|
|
|
|
|
|
|
|
{ label: 'Número', types: ['number', 'currency'] },
|
|
|
|
|
|
|
|
{ label: 'Fecha', types: ['date', 'datetime'] },
|
|
|
|
|
|
|
|
{ label: 'Lógico', types: ['boolean'] },
|
|
|
|
|
|
|
|
{ label: 'Media', types: ['image', 'video', 'audio', 'pdf', 'file'] },
|
|
|
|
|
|
|
|
{ label: 'Grupo', types: ['group'] },
|
|
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function emit(): void { onUpdate?.(value); }
|
|
|
|
function emit(): void { onUpdate?.(value); }
|
|
|
|
|
|
|
|
|
|
|
|
@ -142,7 +129,6 @@
|
|
|
|
newOrder.splice(newIdx, 0, key);
|
|
|
|
newOrder.splice(newIdx, 0, key);
|
|
|
|
value = { ...value, order: newOrder };
|
|
|
|
value = { ...value, order: newOrder };
|
|
|
|
emit();
|
|
|
|
emit();
|
|
|
|
// Seguir al campo si cruza un límite de página
|
|
|
|
|
|
|
|
if (pageSize > 0) {
|
|
|
|
if (pageSize > 0) {
|
|
|
|
const newPage = Math.floor(newIdx / pageSize) + 1;
|
|
|
|
const newPage = Math.floor(newIdx / pageSize) + 1;
|
|
|
|
if (newPage !== page) page = newPage;
|
|
|
|
if (newPage !== page) page = newPage;
|
|
|
|
@ -150,49 +136,34 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<div data-theme={dark ? 'dark' : 'light'}>
|
|
|
|
<div data-theme={dark ? 'dark' : 'light'} class="h-full flex flex-col min-h-0">
|
|
|
|
<div class="card bg-base-100 border border-base-200 shadow-sm w-full">
|
|
|
|
<div class="card bg-base-100 border border-base-200 shadow-sm w-full flex flex-col flex-1 min-h-0">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── Header ──────────────────────────────────────────────────── -->
|
|
|
|
<!-- ── Header ──────────────────────────────────────────────────── -->
|
|
|
|
<div class="card-body gap-4 pb-3 border-b border-base-200">
|
|
|
|
<div class="flex items-center gap-2 px-5 py-3 border-b border-base-200 shrink-0">
|
|
|
|
<div class="flex items-center gap-3">
|
|
|
|
<div role="img"
|
|
|
|
<h2 class="card-title">Metadata</h2>
|
|
|
|
aria-label={validation.valid ? 'Válido' : 'Con errores'}
|
|
|
|
<span class="text-base-content/50">
|
|
|
|
title={validation.valid ? 'Válido' : `${validation.errors.length} errores`}
|
|
|
|
{keyOrder.length} {keyOrder.length === 1 ? 'campo' : 'campos'}
|
|
|
|
class={['h-2 w-2 shrink-0 rounded-full', validation.valid ? 'bg-success' : 'bg-error'].join(' ')}
|
|
|
|
</span>
|
|
|
|
></div>
|
|
|
|
<div class={['badge badge-sm badge-soft ml-auto', validation.valid ? 'badge-success' : 'badge-error'].join(' ')}>
|
|
|
|
<h2 class="font-semibold text-base">Metadata</h2>
|
|
|
|
<div class={['h-1.5 w-1.5 rounded-full mr-1', validation.valid ? 'bg-success' : 'bg-error'].join(' ')}></div>
|
|
|
|
<span class="text-base-content/40 text-sm">
|
|
|
|
{validation.valid ? 'Válido' : `${validation.errors.length} errores`}
|
|
|
|
{keyOrder.length} {keyOrder.length === 1 ? 'campo' : 'campos'}
|
|
|
|
</div>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
<div class="ml-auto">
|
|
|
|
|
|
|
|
<AddFieldMenu onSelect={openDrawer} />
|
|
|
|
<div class="flex flex-wrap gap-x-4 gap-y-2">
|
|
|
|
|
|
|
|
{#each typeGroups as group}
|
|
|
|
|
|
|
|
<div class="flex items-center gap-1">
|
|
|
|
|
|
|
|
<span class="text-base-content/30 text-xs">{group.label}</span>
|
|
|
|
|
|
|
|
{#each group.types as type}
|
|
|
|
|
|
|
|
<button type="button"
|
|
|
|
|
|
|
|
onclick={() => openDrawer(type)}
|
|
|
|
|
|
|
|
class="btn btn-ghost btn-xs gap-1 px-1.5"
|
|
|
|
|
|
|
|
title="Añadir {typeLabels[type]}">
|
|
|
|
|
|
|
|
<FieldTypeIcon {type} size={14} />
|
|
|
|
|
|
|
|
<span class="text-xs">{typeLabels[type]}</span>
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── Field list ─────────────────────────────────────────────── -->
|
|
|
|
<!-- ── Field list ─────────────────────────────────────────────── -->
|
|
|
|
<div class="card-body gap-2 pt-3">
|
|
|
|
<div class="flex flex-col gap-2 p-4 overflow-y-auto flex-1 min-h-0">
|
|
|
|
{#if keyOrder.length === 0}
|
|
|
|
{#if keyOrder.length === 0}
|
|
|
|
<div class="rounded-box flex flex-col items-center justify-center gap-3 border-2 border-dashed border-base-300 py-16">
|
|
|
|
<div class="rounded-box flex flex-col items-center justify-center gap-3 border-2 border-dashed border-base-300 py-16">
|
|
|
|
<svg class="h-10 w-10 text-base-content/10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
<svg class="h-10 w-10 text-base-content/10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
|
|
<rect x="3" y="3" width="18" height="18" rx="4"/>
|
|
|
|
<rect x="3" y="3" width="18" height="18" rx="4"/>
|
|
|
|
<path d="M12 8v8M8 12h8"/>
|
|
|
|
<path d="M12 8v8M8 12h8"/>
|
|
|
|
</svg>
|
|
|
|
</svg>
|
|
|
|
<p class="text-base-content/30">Añade campos usando los botones de arriba</p>
|
|
|
|
<p class="text-base-content/30">Usa el botón Añadir para crear campos</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
{#each pageKeys as key (key)}
|
|
|
|
{#each pageKeys as key (key)}
|
|
|
|
@ -217,7 +188,7 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{#if !validation.valid}
|
|
|
|
{#if !validation.valid}
|
|
|
|
<div class="card-body pt-0">
|
|
|
|
<div class="px-4 pb-3">
|
|
|
|
<div class="alert alert-error">
|
|
|
|
<div class="alert alert-error">
|
|
|
|
<div class="flex flex-col gap-1">
|
|
|
|
<div class="flex flex-col gap-1">
|
|
|
|
<span>Errores de validación</span>
|
|
|
|
<span>Errores de validación</span>
|
|
|
|
@ -230,16 +201,16 @@
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ── Footer: paginación ──────────────────────────────────────── -->
|
|
|
|
<!-- ── Footer: paginación ──────────────────────────────────────── -->
|
|
|
|
{#if keyOrder.length > 0}
|
|
|
|
{#if pagination && keyOrder.length > 0}
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-200 px-5 py-3 bg-amber-50 border-gray-300">
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3 border-t border-base-200 px-5 py-3">
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Filas por página -->
|
|
|
|
<!-- Filas por página -->
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
<div class="flex items-center gap-2">
|
|
|
|
<span class="text-xs text-base-content/40">Filas por página</span>
|
|
|
|
<span class="text-xs text-base-content/40">Filas por página</span>
|
|
|
|
<select
|
|
|
|
<select
|
|
|
|
class="select select-bordered select-xs"
|
|
|
|
class="select select-bordered select-xs"
|
|
|
|
value={pageSize}
|
|
|
|
bind:value={pageSize}
|
|
|
|
onchange={(e) => { pageSize = parseInt((e.currentTarget as HTMLSelectElement).value); page = 1; }}
|
|
|
|
onchange={() => { page = 1; }}
|
|
|
|
>
|
|
|
|
>
|
|
|
|
{#each PAGE_SIZES as s}
|
|
|
|
{#each PAGE_SIZES as s}
|
|
|
|
<option value={s}>{s}</option>
|
|
|
|
<option value={s}>{s}</option>
|
|
|
|
@ -251,20 +222,13 @@
|
|
|
|
<!-- Barra de páginas -->
|
|
|
|
<!-- Barra de páginas -->
|
|
|
|
{#if totalPages > 1}
|
|
|
|
{#if totalPages > 1}
|
|
|
|
<div class="join">
|
|
|
|
<div class="join">
|
|
|
|
<button
|
|
|
|
<button type="button" class="join-item btn btn-xs" disabled={page === 1}
|
|
|
|
type="button"
|
|
|
|
onclick={() => page--} aria-label="Página anterior">‹</button>
|
|
|
|
class="join-item btn btn-xs"
|
|
|
|
|
|
|
|
disabled={page === 1}
|
|
|
|
|
|
|
|
onclick={() => page--}
|
|
|
|
|
|
|
|
aria-label="Página anterior"
|
|
|
|
|
|
|
|
>‹</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{#each paginationPages as p (p === -1 ? `ellipsis-${p}` : p)}
|
|
|
|
{#each paginationPages as p (p === -1 ? `ellipsis-${p}` : p)}
|
|
|
|
{#if p === -1}
|
|
|
|
{#if p === -1}
|
|
|
|
<button type="button" class="join-item btn btn-xs btn-disabled" tabindex="-1">…</button>
|
|
|
|
<button type="button" class="join-item btn btn-xs btn-disabled" tabindex="-1">…</button>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
<button
|
|
|
|
<button type="button"
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class={['join-item btn btn-xs', p === page ? 'btn-active' : ''].join(' ')}
|
|
|
|
class={['join-item btn btn-xs', p === page ? 'btn-active' : ''].join(' ')}
|
|
|
|
onclick={() => page = p}
|
|
|
|
onclick={() => page = p}
|
|
|
|
aria-label="Página {p}"
|
|
|
|
aria-label="Página {p}"
|
|
|
|
@ -272,14 +236,8 @@
|
|
|
|
>{p}</button>
|
|
|
|
>{p}</button>
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
<button type="button" class="join-item btn btn-xs" disabled={page === totalPages}
|
|
|
|
<button
|
|
|
|
onclick={() => page++} aria-label="Página siguiente">›</button>
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class="join-item btn btn-xs"
|
|
|
|
|
|
|
|
disabled={page === totalPages}
|
|
|
|
|
|
|
|
onclick={() => page++}
|
|
|
|
|
|
|
|
aria-label="Página siguiente"
|
|
|
|
|
|
|
|
>›</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|