@tinya-code/gas-admin-ui 0.2.0-alpha.1 → 0.2.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -1
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs → tinya-code-gas-admin-ui-calendar-view-CquWGzw7.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map → tinya-code-gas-admin-ui-calendar-view-CquWGzw7.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs → tinya-code-gas-admin-ui-condition-evaluator-DmvlAP9z.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DmvlAP9z.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs → tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map → tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs → tinya-code-gas-admin-ui-error-mapper.service-dKQNLNRJ.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-dKQNLNRJ.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs → tinya-code-gas-admin-ui-file-CKh878UH.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map → tinya-code-gas-admin-ui-file-CKh878UH.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs → tinya-code-gas-admin-ui-form-view-_LpdZb15.mjs} +9 -9
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map → tinya-code-gas-admin-ui-form-view-_LpdZb15.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs → tinya-code-gas-admin-ui-image-BPfPNwru.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs.map → tinya-code-gas-admin-ui-image-BPfPNwru.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs → tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs.map → tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs → tinya-code-gas-admin-ui-list-BKr33pde.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs.map → tinya-code-gas-admin-ui-list-BKr33pde.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs → tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs.map → tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs → tinya-code-gas-admin-ui-relation-WCflawIU.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs.map → tinya-code-gas-admin-ui-relation-WCflawIU.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs → tinya-code-gas-admin-ui-render-registry-ghv7Mb1x.mjs} +10 -10
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs.map → tinya-code-gas-admin-ui-render-registry-ghv7Mb1x.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs → tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs} +53 -4
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs → tinya-code-gas-admin-ui-seed-BnwHE1Tw.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs.map → tinya-code-gas-admin-ui-seed-BnwHE1Tw.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-select-CL2kj7pM.mjs → tinya-code-gas-admin-ui-select-BaF4kSXl.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-select-CL2kj7pM.mjs.map → tinya-code-gas-admin-ui-select-BaF4kSXl.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs → tinya-code-gas-admin-ui-upload.service-C7GzSmuL.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs.map → tinya-code-gas-admin-ui-upload.service-C7GzSmuL.mjs.map} +1 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +8 -8
- package/package.json +1 -1
- package/types/tinya-code-gas-admin-ui.d.ts +96 -32
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { y as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs";
|
|
3
3
|
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
4
4
|
import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
|
|
5
|
-
import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-
|
|
5
|
+
import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-C7GzSmuL.mjs";
|
|
6
6
|
import * as i0 from "@angular/core";
|
|
7
7
|
import { Component, DestroyRef, computed, inject, input, model, output, signal, viewChild } from "@angular/core";
|
|
8
8
|
import { HttpEventType } from "@angular/common/http";
|
|
@@ -413,4 +413,4 @@ function acceptMatches(accept, file) {
|
|
|
413
413
|
}
|
|
414
414
|
export { image_exports as n, FieldImage as t };
|
|
415
415
|
|
|
416
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-image-
|
|
416
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-image-BPfPNwru.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-image-lUe8nrUf.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/image/image.ts"],"sourcesContent":["import { HttpEventType } from '@angular/common/http';\nimport {\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport type { ImageField } from '../../core/models/schema.model';\nimport { ApiError } from '../../core/models/api.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { UploadService } from '../../core/services/upload.service';\n\n/**\n * Carga de imagen (base.md §7): el valor es SOLO la URL pública.\n *\n * Control propio `FormValueControl<string>`: el nodo llega con `[formField]`\n * desde el despachador y el `id` lo pone el host sobre este elemento (mismo\n * patrón que `multiselect`), para que el `<label for>` del campo exista.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con imagen (preview + Reemplazar /\n * Quitar / Ver en grande, y drop sobre el preview para reemplazar) y error\n * (conserva la imagen anterior).\n */\n@Component({\n selector: 'app-field-image',\n imports: [Button],\n template: ` @if (error(); as message) {\n <p\n class=\"mb-2 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-xs text-danger\"\n role=\"alert\"\n >\n {{ message }}\n </p>\n }\n @if (uploading()) {\n <div class=\"rounded-xl border border-neutral/20 bg-surface p-3\" data-testid=\"image-uploading\">\n <div class=\"h-1.5 w-full overflow-hidden rounded-full bg-neutral/20\">\n <div\n class=\"h-full rounded-full bg-primary transition-all\"\n [style.width.%]=\"progress()\"\n ></div>\n </div>\n <p class=\"mt-1 text-xs text-neutral\">Subiendo… {{ progress() }}%</p>\n </div>\n } @else if (value()) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n data-testid=\"image-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <img\n [src]=\"value()\"\n [alt]=\"''\"\n [class]=\"previewClass()\"\n [style.aspect-ratio]=\"aspectRatio()\"\n />\n <div class=\"mt-2 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Reemplazar\n </button>\n @if (!field().required) {\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"clear()\"\n >\n Quitar\n </button>\n }\n <a\n class=\"text-sm font-medium text-primary hover:underline\"\n [href]=\"value()\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Ver en grande\n </a>\n </div>\n </div>\n } @else {\n <div\n class=\"rounded-xl border-2 border-dashed border-neutral/30 p-6 text-center\"\n data-testid=\"image-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá una imagen acá</p>\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n class=\"mt-2\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Subir imagen\n </button>\n </div>\n }\n\n <input\n #fileInput\n type=\"file\"\n class=\"sr-only\"\n [accept]=\"acceptAttr()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n />`,\n})\nexport class FieldImage implements FormValueControl<string> {\n readonly field = input.required<ImageField>();\n /** Recurso dueño de la imagen (firma de subida: carpeta de Cloudinary, §11.2). */\n readonly resource = input('');\n\n /** Sincronizado por el directive `FormField` del host. */\n readonly value = model.required<string>();\n readonly disabled = input(false);\n /** Marca el campo como tocado (upload/clear/error), como el `blur` nativo. */\n readonly touch = output<void>();\n\n private readonly upload = inject(UploadService);\n private readonly notifications = inject(NotificationService);\n private readonly destroyRef = inject(DestroyRef);\n\n protected readonly uploading = signal(false);\n protected readonly progress = signal(0);\n protected readonly error = signal<string | null>(null);\n\n /** Literal en el código: Tailwind v4 genera las clases al escanearlo. */\n protected readonly previewClass = computed(\n () => `${this.field().previewSize ?? 'w-48'} rounded-lg object-cover`,\n );\n protected readonly aspectRatio = computed(() => this.field().aspectRatio ?? null);\n protected readonly acceptAttr = computed(() => this.field().accept ?? 'image/*');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (el campo `fileInput` quedaba `undefined` y\n * `pickFile()` no hacía nada — el selector no se abría). Igual que en\n * `modal.ts`: el tipo se expresa como genérico (`ElementRef<T>`).\n */\n private readonly fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');\n private uploadSub?: { unsubscribe(): void };\n\n protected pickFile(): void {\n this.fileInput().nativeElement.click();\n }\n\n protected clear(): void {\n this.value.set('');\n this.error.set(null);\n this.touch.emit();\n }\n\n protected onFileSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n // Permite volver a elegir el mismo archivo tras un error.\n input.value = '';\n if (file) {\n this.startUpload(file);\n }\n }\n\n protected onDrop(event: Event): void {\n event.preventDefault();\n const file = (event as DragEvent).dataTransfer?.files?.[0];\n if (file) {\n this.startUpload(file);\n }\n }\n\n /** Valida en cliente (accept + maxSizeMB) y sube con progreso. */\n private startUpload(file: File): void {\n if (this.disabled()) {\n return;\n }\n const problem = this.validate(file);\n if (problem !== null) {\n this.error.set(problem);\n this.touch.emit();\n return;\n }\n this.error.set(null);\n this.uploading.set(true);\n this.progress.set(0);\n this.touch.emit();\n\n this.uploadSub?.unsubscribe();\n // El nombre del campo viaja siempre: si declara `formats`, el backend\n // firma ESA lista; sin formatos firma la lista global (R9.5.3).\n this.uploadSub = this.upload\n .uploadWithProgress(file, this.resource(), this.field().key)\n .subscribe({\n next: (event) => {\n if (event.type === HttpEventType.UploadProgress) {\n if (event.total) {\n this.progress.set(Math.round((event.loaded / event.total) * 100));\n }\n return;\n }\n if (event.type === HttpEventType.Response) {\n const url = event.body ?? '';\n this.uploading.set(false);\n if (url === '') {\n this.error.set('No se pudo subir la imagen.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Imagen subida.');\n }\n },\n error: (err: unknown) => {\n this.uploading.set(false);\n // El backend explica el motivo (firma inválida, 429 rate-limit…).\n this.error.set(\n err instanceof ApiError\n ? err.message\n : 'No se pudo subir la imagen. Revisa la conexión e inténtalo de nuevo.',\n );\n },\n });\n this.destroyRef.onDestroy(() => this.uploadSub?.unsubscribe());\n }\n\n private validate(file: File): string | null {\n if (!file.type.startsWith('image/')) {\n return 'El archivo debe ser una imagen.';\n }\n const accept = this.field().accept;\n if (accept !== undefined && !acceptMatches(accept, file)) {\n return 'Formato no permitido para este campo.';\n }\n const maxSizeMB = this.field().maxSizeMB;\n if (maxSizeMB !== undefined && file.size > maxSizeMB * 1024 * 1024) {\n return `La imagen supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `image/*` o extensión `.png`. */\nfunction acceptMatches(accept: string, file: File): boolean {\n for (const raw of accept.split(',')) {\n const entry = raw.trim().toLowerCase();\n if (entry === '') {\n continue;\n }\n if (entry.startsWith('.')) {\n if (file.name.toLowerCase().endsWith(entry)) {\n return true;\n }\n } else if (entry.endsWith('/*')) {\n if (file.type.startsWith(entry.slice(0, -1))) {\n return true;\n }\n } else if (file.type === entry) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-image-BPfPNwru.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/image/image.ts"],"sourcesContent":["import { HttpEventType } from '@angular/common/http';\nimport {\n Component,\n computed,\n DestroyRef,\n ElementRef,\n inject,\n input,\n model,\n output,\n signal,\n viewChild,\n} from '@angular/core';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport type { ImageField } from '../../core/models/schema.model';\nimport { ApiError } from '../../core/models/api.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { UploadService } from '../../core/services/upload.service';\n\n/**\n * Carga de imagen (base.md §7): el valor es SOLO la URL pública.\n *\n * Control propio `FormValueControl<string>`: el nodo llega con `[formField]`\n * desde el despachador y el `id` lo pone el host sobre este elemento (mismo\n * patrón que `multiselect`), para que el `<label for>` del campo exista.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con imagen (preview + Reemplazar /\n * Quitar / Ver en grande, y drop sobre el preview para reemplazar) y error\n * (conserva la imagen anterior).\n */\n@Component({\n selector: 'app-field-image',\n imports: [Button],\n template: ` @if (error(); as message) {\n <p\n class=\"mb-2 rounded-lg border border-danger/30 bg-danger/10 px-3 py-2 text-xs text-danger\"\n role=\"alert\"\n >\n {{ message }}\n </p>\n }\n @if (uploading()) {\n <div class=\"rounded-xl border border-neutral/20 bg-surface p-3\" data-testid=\"image-uploading\">\n <div class=\"h-1.5 w-full overflow-hidden rounded-full bg-neutral/20\">\n <div\n class=\"h-full rounded-full bg-primary transition-all\"\n [style.width.%]=\"progress()\"\n ></div>\n </div>\n <p class=\"mt-1 text-xs text-neutral\">Subiendo… {{ progress() }}%</p>\n </div>\n } @else if (value()) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n data-testid=\"image-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <img\n [src]=\"value()\"\n [alt]=\"''\"\n [class]=\"previewClass()\"\n [style.aspect-ratio]=\"aspectRatio()\"\n />\n <div class=\"mt-2 flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Reemplazar\n </button>\n @if (!field().required) {\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n [disabled]=\"disabled()\"\n (click)=\"clear()\"\n >\n Quitar\n </button>\n }\n <a\n class=\"text-sm font-medium text-primary hover:underline\"\n [href]=\"value()\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Ver en grande\n </a>\n </div>\n </div>\n } @else {\n <div\n class=\"rounded-xl border-2 border-dashed border-neutral/30 p-6 text-center\"\n data-testid=\"image-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá una imagen acá</p>\n <button\n type=\"button\"\n app-button\n variant=\"outline\"\n class=\"mt-2\"\n [disabled]=\"disabled()\"\n (click)=\"pickFile()\"\n >\n Subir imagen\n </button>\n </div>\n }\n\n <input\n #fileInput\n type=\"file\"\n class=\"sr-only\"\n [accept]=\"acceptAttr()\"\n [disabled]=\"disabled()\"\n (change)=\"onFileSelected($event)\"\n />`,\n})\nexport class FieldImage implements FormValueControl<string> {\n readonly field = input.required<ImageField>();\n /** Recurso dueño de la imagen (firma de subida: carpeta de Cloudinary, §11.2). */\n readonly resource = input('');\n\n /** Sincronizado por el directive `FormField` del host. */\n readonly value = model.required<string>();\n readonly disabled = input(false);\n /** Marca el campo como tocado (upload/clear/error), como el `blur` nativo. */\n readonly touch = output<void>();\n\n private readonly upload = inject(UploadService);\n private readonly notifications = inject(NotificationService);\n private readonly destroyRef = inject(DestroyRef);\n\n protected readonly uploading = signal(false);\n protected readonly progress = signal(0);\n protected readonly error = signal<string | null>(null);\n\n /** Literal en el código: Tailwind v4 genera las clases al escanearlo. */\n protected readonly previewClass = computed(\n () => `${this.field().previewSize ?? 'w-48'} rounded-lg object-cover`,\n );\n protected readonly aspectRatio = computed(() => this.field().aspectRatio ?? null);\n protected readonly acceptAttr = computed(() => this.field().accept ?? 'image/*');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (el campo `fileInput` quedaba `undefined` y\n * `pickFile()` no hacía nada — el selector no se abría). Igual que en\n * `modal.ts`: el tipo se expresa como genérico (`ElementRef<T>`).\n */\n private readonly fileInput = viewChild.required<ElementRef<HTMLInputElement>>('fileInput');\n private uploadSub?: { unsubscribe(): void };\n\n protected pickFile(): void {\n this.fileInput().nativeElement.click();\n }\n\n protected clear(): void {\n this.value.set('');\n this.error.set(null);\n this.touch.emit();\n }\n\n protected onFileSelected(event: Event): void {\n const input = event.target as HTMLInputElement;\n const file = input.files?.[0];\n // Permite volver a elegir el mismo archivo tras un error.\n input.value = '';\n if (file) {\n this.startUpload(file);\n }\n }\n\n protected onDrop(event: Event): void {\n event.preventDefault();\n const file = (event as DragEvent).dataTransfer?.files?.[0];\n if (file) {\n this.startUpload(file);\n }\n }\n\n /** Valida en cliente (accept + maxSizeMB) y sube con progreso. */\n private startUpload(file: File): void {\n if (this.disabled()) {\n return;\n }\n const problem = this.validate(file);\n if (problem !== null) {\n this.error.set(problem);\n this.touch.emit();\n return;\n }\n this.error.set(null);\n this.uploading.set(true);\n this.progress.set(0);\n this.touch.emit();\n\n this.uploadSub?.unsubscribe();\n // El nombre del campo viaja siempre: si declara `formats`, el backend\n // firma ESA lista; sin formatos firma la lista global (R9.5.3).\n this.uploadSub = this.upload\n .uploadWithProgress(file, this.resource(), this.field().key)\n .subscribe({\n next: (event) => {\n if (event.type === HttpEventType.UploadProgress) {\n if (event.total) {\n this.progress.set(Math.round((event.loaded / event.total) * 100));\n }\n return;\n }\n if (event.type === HttpEventType.Response) {\n const url = event.body ?? '';\n this.uploading.set(false);\n if (url === '') {\n this.error.set('No se pudo subir la imagen.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Imagen subida.');\n }\n },\n error: (err: unknown) => {\n this.uploading.set(false);\n // El backend explica el motivo (firma inválida, 429 rate-limit…).\n this.error.set(\n err instanceof ApiError\n ? err.message\n : 'No se pudo subir la imagen. Revisa la conexión e inténtalo de nuevo.',\n );\n },\n });\n this.destroyRef.onDestroy(() => this.uploadSub?.unsubscribe());\n }\n\n private validate(file: File): string | null {\n if (!file.type.startsWith('image/')) {\n return 'El archivo debe ser una imagen.';\n }\n const accept = this.field().accept;\n if (accept !== undefined && !acceptMatches(accept, file)) {\n return 'Formato no permitido para este campo.';\n }\n const maxSizeMB = this.field().maxSizeMB;\n if (maxSizeMB !== undefined && file.size > maxSizeMB * 1024 * 1024) {\n return `La imagen supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `image/*` o extensión `.png`. */\nfunction acceptMatches(accept: string, file: File): boolean {\n for (const raw of accept.split(',')) {\n const entry = raw.trim().toLowerCase();\n if (entry === '') {\n continue;\n }\n if (entry.startsWith('.')) {\n if (file.name.toLowerCase().endsWith(entry)) {\n return true;\n }\n } else if (entry.endsWith('/*')) {\n if (file.type.startsWith(entry.slice(0, -1))) {\n return true;\n }\n } else if (file.type === entry) {\n return true;\n }\n }\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-
|
|
1
|
+
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs";
|
|
2
2
|
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
3
|
-
import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-
|
|
4
|
-
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-
|
|
3
|
+
import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs";
|
|
4
|
+
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-dKQNLNRJ.mjs";
|
|
5
5
|
import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
|
|
6
6
|
import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
|
|
7
7
|
import * as i0 from "@angular/core";
|
|
@@ -436,4 +436,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
436
436
|
});
|
|
437
437
|
export { KanbanView };
|
|
438
438
|
|
|
439
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-kanban-view-
|
|
439
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/views/kanban-view/kanban-model.ts","../../../projects/gas-admin-ui/src/lib/views/kanban-view/kanban-view.ts"],"sourcesContent":["import type { KanbanViewConfig, ResourceSchema } from '../../core/models/schema.model';\nimport type { SourceRow } from '../../core/services/data-source.service';\n\n/**\n * Pure grouping + card-text model for the generic kanban view (R9.8.4).\n *\n * No Angular imports: everything here is a plain function over rows, so the\n * rules below are testable without a TestBed.\n *\n * Rules (decided · R9.8.1 read-only, R9.8.4 visibility):\n * - Column order is `config.columns` order; a declared column ALWAYS exists,\n * even with zero rows (an empty column is a visible «Sin tarjetas», never\n * a missing column).\n * - A row whose `groupField` value is empty/undefined goes to `unknown` —\n * surfaced, never silently dropped.\n * - A value that is not declared gets its own column appended AFTER the\n * declared ones: undeclared values are VISIBLE, not hidden.\n * - Order inside a column is the row insertion order (stable, deterministic).\n */\n\n/** One column of the board: declared in the config or discovered in the rows. */\nexport interface KanbanColumnBucket {\n /** Raw `groupField` value that owns the bucket (unique across the board). */\n value: string;\n /** Visible label: the declared column's label, or the raw value. */\n label: string;\n /** Rows of the bucket, in insertion order. */\n rows: SourceRow[];\n}\n\n/** Result of grouping raw rows onto board columns (R9.8.4). */\nexport interface KanbanMapping {\n /** Declared columns (in declared order) + one bucket per undeclared value. */\n columns: KanbanColumnBucket[];\n /** Rows whose `groupField` value is empty — visible, never dropped. */\n unknown: SourceRow[];\n}\n\n/** Options of the grouping (all optional; the defaults are the decided rules). */\nexport interface KanbanMapOptions {\n /**\n * Label of buckets whose value is NOT declared in `config.columns`.\n * Default: the raw value itself, so undeclared values stay verbatim and\n * visible. The view uses the default; the hook exists for callers that\n * want to decorate the label without touching the grouping rules.\n */\n labelFor?: (value: string) => string;\n}\n\n/** Title of a card whose every candidate is empty/unreadable (never blank). */\nexport const CARD_NO_TITLE = 'Sin título';\n\n/** Object cells: the readable keys, in preference order. */\nconst OBJECT_VALUE_KEYS = ['label', 'value', 'id'] as const;\n\n/** Printable text of a scalar cell; `null` when it is empty or not scalar. */\nfunction scalarText(value: unknown): string | null {\n if (typeof value === 'string') {\n const text = value.trim();\n return text === '' ? null : text;\n }\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n return null;\n}\n\n/**\n * Printable text of ANY cell: scalars as-is (trimmed), objects reduced to\n * their first readable `label`/`value`/`id`. `null` when there is nothing\n * readable — the caller decides (bucket → `unknown`, card → next fallback).\n *\n * Never returns `undefined` nor `[object Object]`: that is the whole point.\n */\nfunction cellText(value: unknown): string | null {\n const scalar = scalarText(value);\n if (scalar !== null) return scalar;\n if (value !== null && typeof value === 'object') {\n const cell = value as Record<string, unknown>;\n for (const key of OBJECT_VALUE_KEYS) {\n const text = scalarText(cell[key]);\n if (text !== null) return text;\n }\n }\n return null;\n}\n\n/**\n * Pure row → board grouping (R9.8.4): the single place that decides which\n * column owns each row, which rows are «unknown» and in what order the\n * columns are painted.\n *\n * Deterministic: declared columns keep their declared order (even when\n * empty), undeclared values are appended in first-appearance order, and the\n * rows inside each column keep their insertion order.\n */\nexport function mapKanbanRows(\n rows: readonly SourceRow[],\n config: KanbanViewConfig,\n opts: KanbanMapOptions = {},\n): KanbanMapping {\n const columns: KanbanColumnBucket[] = [];\n const byValue = new Map<string, KanbanColumnBucket>();\n\n // Declared columns first — always visible, even with zero rows.\n const declared = Array.isArray(config.columns) ? config.columns : [];\n for (const column of declared) {\n const value = typeof column?.value === 'string' ? column.value : '';\n // Contract (R9.8.2): `value` obligatorio y único — a duplicated or empty\n // declaration cannot own a bucket, the first valid one wins.\n if (value === '' || byValue.has(value)) continue;\n const label = typeof column.label === 'string' && column.label !== '' ? column.label : value;\n const bucket: KanbanColumnBucket = { value, label, rows: [] };\n byValue.set(value, bucket);\n columns.push(bucket);\n }\n\n const unknown: SourceRow[] = [];\n for (const row of rows) {\n const value = cellText(row[config.groupField]);\n if (value === null) {\n // Empty/undefined (or unreadable object): visible bucket, never dropped.\n unknown.push(row);\n continue;\n }\n let bucket = byValue.get(value);\n if (bucket === undefined) {\n // Undeclared value: its own column, appended AFTER the declared ones.\n const label = opts.labelFor !== undefined ? opts.labelFor(value) : value;\n bucket = { value, label, rows: [] };\n byValue.set(value, bucket);\n columns.push(bucket);\n }\n bucket.rows.push(row);\n }\n\n return { columns, unknown };\n}\n\n/**\n * Title of a card: `config.titleField` → `schema.titleField` →\n * `schema.keyField`, first candidate with readable text wins. An unreadable\n * value (empty, `undefined`, an object with no label) never reaches the DOM:\n * the chain falls through to the key, and to a visible «Sin título» when not\n * even the key has text.\n */\nexport function cardTitle(\n row: SourceRow,\n config: KanbanViewConfig,\n schema: ResourceSchema,\n): string {\n const candidates = [config.titleField, schema.titleField, schema.keyField];\n for (const field of candidates) {\n if (typeof field !== 'string' || field === '') continue;\n const text = cellText(row[field]);\n if (text !== null) return text;\n }\n return CARD_NO_TITLE;\n}\n\n/**\n * Second line of a card: the `config.subtitleField` value when the config\n * declares one, `''` otherwise (the schema has no notion of subtitle).\n */\nexport function cardSubtitle(\n row: SourceRow,\n config: KanbanViewConfig,\n _schema: ResourceSchema,\n): string {\n const field = config.subtitleField;\n if (typeof field !== 'string' || field === '') return '';\n return cellText(row[field]) ?? '';\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute } from '@angular/router';\n\nimport type {\n KanbanViewConfig,\n ResourceSchema,\n SelectionBinding,\n} from '../../core/models/schema.model';\nimport { ErrorMapperService } from '../../core/services/error-mapper.service';\nimport { DataSourceService, type SourceRow } from '../../core/services/data-source.service';\nimport type { SelectionEmitter } from '../composer/view-context';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Button } from '../../shared/components/button/button';\nimport { EmptyState } from '../../shared/components/empty-state/empty-state';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport {\n cardSubtitle,\n cardTitle,\n mapKanbanRows,\n type KanbanColumnBucket,\n type KanbanMapping,\n} from './kanban-model';\n\n/**\n * Narrowing helper: `UiViewBase` keeps `type: string` open, so comparing\n * `view.type === 'kanban'` alone does not remove the base member from the\n * union. Requiring `config` in the value is what actually proves the view is\n * a `UiViewKanban` at runtime.\n */\nfunction kanbanConfigOf(view: unknown): KanbanViewConfig | undefined {\n if (!view || typeof view !== 'object') return undefined;\n const candidate = view as { type?: unknown; config?: unknown };\n return candidate.type === 'kanban' && candidate.config !== undefined\n ? (candidate.config as KanbanViewConfig)\n : undefined;\n}\n\ntype Status = 'loading' | 'ready' | 'error';\n\n/** Label of the bucket for rows without a group value (Spanish UI copy). */\nconst UNKNOWN_LABEL = 'Sin valor';\n\n/**\n * Synthetic `value` of that bucket: a real group value can never collide\n * with it (a declared `value` is non-empty and the grouping never produces\n * this literal), so `@for` tracking stays unique.\n */\nconst UNKNOWN_VALUE = '__sin_valor__';\n\n/**\n * Generic kanban view (R9.8.4): rows of the declared\n * `KanbanViewConfig.source` (or the resource's own listing) grouped by\n * `groupField` into the declared columns, client-side.\n *\n * Data flow: ONE fetcher (`DataSourceService.rows`) — there is no dynamic\n * context here, so `ctx` is `undefined`; the 30 s cache of the service is\n * what keeps repeated visits free.\n *\n * Read-only by design (R9.8.1): no drag-and-drop, no move buttons, no\n * mutation path at all. Colour never carries meaning alone — every count is\n * text — and an empty column is a visible «Sin tarjetas», never a gap.\n */\n@Component({\n selector: 'app-kanban-view',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [Button, EmptyState, Skeleton],\n template: `\n @if (config(); as cfg) {\n <section class=\"space-y-4\" aria-label=\"Tablero\" [attr.aria-busy]=\"status() === 'loading'\">\n <header class=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 class=\"font-display text-xl font-semibold\">Tablero</h2>\n <p class=\"text-sm text-neutral\">\n Agrupado por <code>{{ cfg.groupField }}</code>\n </p>\n </header>\n\n @switch (status()) {\n @case ('loading') {\n <app-skeleton [lines]=\"6\" [height]=\"32\" />\n }\n @case ('error') {\n <app-empty-state title=\"No se pudo cargar el tablero\" [message]=\"errorMessage()\">\n <button app-button variant=\"outline\" type=\"button\" (click)=\"retry()\">\n Reintentar\n </button>\n </app-empty-state>\n }\n @case ('ready') {\n <div class=\"flex gap-3 overflow-x-auto pb-2\">\n @for (column of columns(); track column.value) {\n <section\n class=\"flex w-64 shrink-0 flex-col gap-3 rounded-xl border border-neutral/20 bg-surface p-3\"\n [attr.data-column]=\"column.value\"\n [attr.aria-label]=\"columnAria(column)\"\n >\n <header class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-sm font-semibold\">{{ column.label }}</h3>\n <span class=\"rounded-full bg-neutral/10 px-2 py-0.5 text-xs text-neutral\">\n {{ column.rows.length }}<span class=\"sr-only\"> tarjetas</span>\n </span>\n </header>\n @if (column.rows.length === 0) {\n <p class=\"text-sm text-neutral\">Sin tarjetas</p>\n } @else {\n <ul class=\"flex flex-col gap-2\">\n @for (row of column.rows; track $index) {\n <li\n class=\"rounded-lg border border-neutral/15 p-2\"\n [attr.tabindex]=\"seleccionable() ? '0' : null\"\n [attr.role]=\"seleccionable() ? 'button' : null\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(row)\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(row)\"\n (keydown.enter)=\"activar($event, row)\"\n (keydown.space)=\"activar($event, row)\"\n >\n <p class=\"text-sm font-medium\">{{ cardTitleOf(row) }}</p>\n @if (cardSubtitleOf(row); as subtitle) {\n <p class=\"mt-0.5 text-xs text-neutral\">{{ subtitle }}</p>\n }\n </li>\n }\n </ul>\n }\n </section>\n }\n </div>\n }\n }\n </section>\n } @else {\n <app-empty-state\n title=\"Configuración inválida\"\n message=\"Esta vista no declara una configuración de kanban válida.\"\n />\n }\n `,\n})\nexport class KanbanView {\n private readonly route = inject(ActivatedRoute);\n // The shell mounts views via `loadComponent` (no input bindings), so the\n // schema is resolved from route data exactly like CalendarView does.\n private readonly routeData = toSignal(this.route.data, { requireSync: true });\n private readonly dataSource = inject(DataSourceService);\n private readonly errorMapper = inject(ErrorMapperService);\n\n /**\n * Resource schema for this view. The shell mounts views via\n * `loadComponent` (no input bindings), so the schema is resolved from\n * route data: either an explicit `schema` object (fixtures), or a\n * `resourceId` looked up in the registry (the production route shape).\n */\n readonly schema = computed<ResourceSchema | undefined>(() => {\n const data = this.routeData();\n const explicit = data['schema'];\n if (explicit !== undefined) return explicit as ResourceSchema;\n const resourceId = data['resourceId'];\n return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;\n });\n\n /**\n * Config resolved from `schema.uiViews[<route uiView>]`; `undefined` when\n * the route does not name a kanban view with a usable config — the\n * template then renders the invalid-config empty state and NOTHING is\n * fetched (the effect below bails out on `undefined`).\n */\n readonly config = computed<KanbanViewConfig | undefined>(() => {\n const schema = this.schema();\n const viewName = this.routeData()['uiView'];\n if (schema === undefined || typeof viewName !== 'string') return undefined;\n const cfg = kanbanConfigOf(schema.uiViews?.[viewName]);\n if (cfg === undefined) return undefined;\n // Usable = the two references the board cannot render without: a\n // non-empty `groupField` and at least one column with a non-empty value.\n const usable =\n typeof cfg.groupField === 'string' &&\n cfg.groupField !== '' &&\n Array.isArray(cfg.columns) &&\n cfg.columns.length > 0 &&\n cfg.columns.every((column) => typeof column?.value === 'string' && column.value !== '');\n return usable ? cfg : undefined;\n });\n\n readonly status = signal<Status>('loading');\n readonly rows = signal<SourceRow[]>([]);\n readonly errorMessage = signal('');\n /** Clave seleccionada localmente cuando el binding declara `mode:'inline'`. */\n readonly seleccionada = signal<string | null>(null);\n\n /** Guards against a slow response overwriting a newer load's rows. */\n private loadEpoch = 0;\n\n constructor() {\n effect(() => {\n const cfg = this.config();\n if (cfg === undefined) return;\n void this.load(cfg);\n });\n }\n\n /** Rows grouped onto columns + the visible `unknown` bucket (pure model). */\n readonly mapping = computed<KanbanMapping>(() => {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return { columns: [], unknown: [] };\n return mapKanbanRows(this.rows(), cfg);\n });\n\n /**\n * Columns to paint: the model's buckets plus the `unknown` bucket at the\n * END — but only when it actually has rows, otherwise the board would show\n * an empty column nobody declared.\n */\n readonly columns = computed<readonly KanbanColumnBucket[]>(() => {\n const { columns, unknown } = this.mapping();\n if (unknown.length === 0) return columns;\n return [...columns, { value: UNKNOWN_VALUE, label: UNKNOWN_LABEL, rows: unknown }];\n });\n\n retry(): void {\n const cfg = this.config();\n if (cfg !== undefined) void this.load(cfg);\n }\n\n /** Clave estable de la fila (mismo campo que el resto del motor). */\n private keyOf(row: SourceRow): string {\n const schema = this.schema();\n const key = schema ? row[schema.keyField] : undefined;\n return key === undefined || key === null ? '' : String(key);\n }\n\n /** Binding `select` declarado por la vista (R9.9.4 · decision table). */\n private binding(): SelectionBinding | undefined {\n return this.routeData()['select'] as SelectionBinding | undefined;\n }\n\n /**\n * Clic de tarjeta. Sin declaración ⇒ inerte (el tablero no navegaba);\n * con declaración ⇒ sólo el contrato declarado corre: `route` emite al\n * dispatcher, `inline` selecciona en local, `none` es no-op explícito.\n */\n /**\n * R9.8.7 — la misma selección que el clic, operable con teclado (Enter y\n * Space sobre la tarjeta focusable). Sólo tiene sentido con `select`\n * declarado: sin binding la tarjeta no es focusable y esto nunca corre.\n */\n activar(event: Event, row: SourceRow): void {\n if (!this.seleccionable()) return;\n event.preventDefault(); // Space no debe desplazar la página\n this.seleccionar(row);\n }\n\n seleccionar(row: SourceRow): void {\n const binding = this.binding();\n if (binding === undefined) return;\n if (binding.mode === 'inline') {\n this.seleccionada.set(this.keyOf(row));\n return;\n }\n if (binding.mode === 'none') return;\n const schema = this.schema();\n const emit = this.routeData()['emit'] as SelectionEmitter | undefined;\n emit?.({\n resource: schema?.id ?? String(this.routeData()['resourceId'] ?? ''),\n key: this.keyOf(row),\n action: binding.on,\n });\n }\n\n /** El DOM declara clicable sólo si hay contrato que responder. */\n seleccionable(): boolean {\n const binding = this.binding();\n return binding !== undefined && binding.mode !== 'none';\n }\n\n esSeleccionada(row: SourceRow): boolean {\n return this.seleccionable() && this.seleccionada() === this.keyOf(row);\n }\n\n /** «Por hacer: 2 tarjetas» — the count is text, never colour alone. */\n columnAria(column: KanbanColumnBucket): string {\n const count = column.rows.length;\n return `${column.label}: ${count} ${count === 1 ? 'tarjeta' : 'tarjetas'}`;\n }\n\n cardTitleOf(row: SourceRow): string {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return '';\n return cardTitle(row, cfg, schema);\n }\n\n cardSubtitleOf(row: SourceRow): string {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return '';\n return cardSubtitle(row, cfg, schema);\n }\n\n private async load(cfg: KanbanViewConfig): Promise<void> {\n const schema = this.schema();\n if (schema === undefined) return;\n const epoch = ++this.loadEpoch;\n this.status.set('loading');\n const source = cfg.source ?? { resource: schema.id };\n try {\n const rows = await this.dataSource.rows(source, undefined);\n if (epoch !== this.loadEpoch) return;\n this.rows.set(rows);\n this.errorMessage.set('');\n this.status.set('ready');\n } catch (error) {\n if (epoch !== this.loadEpoch) return;\n this.errorMessage.set(this.errorMapper.map(error).general ?? 'No se pudo cargar el tablero.');\n this.status.set('error');\n }\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/views/kanban-view/kanban-model.ts","../../../projects/gas-admin-ui/src/lib/views/kanban-view/kanban-view.ts"],"sourcesContent":["import type { KanbanViewConfig, ResourceSchema } from '../../core/models/schema.model';\nimport type { SourceRow } from '../../core/services/data-source.service';\n\n/**\n * Pure grouping + card-text model for the generic kanban view (R9.8.4).\n *\n * No Angular imports: everything here is a plain function over rows, so the\n * rules below are testable without a TestBed.\n *\n * Rules (decided · R9.8.1 read-only, R9.8.4 visibility):\n * - Column order is `config.columns` order; a declared column ALWAYS exists,\n * even with zero rows (an empty column is a visible «Sin tarjetas», never\n * a missing column).\n * - A row whose `groupField` value is empty/undefined goes to `unknown` —\n * surfaced, never silently dropped.\n * - A value that is not declared gets its own column appended AFTER the\n * declared ones: undeclared values are VISIBLE, not hidden.\n * - Order inside a column is the row insertion order (stable, deterministic).\n */\n\n/** One column of the board: declared in the config or discovered in the rows. */\nexport interface KanbanColumnBucket {\n /** Raw `groupField` value that owns the bucket (unique across the board). */\n value: string;\n /** Visible label: the declared column's label, or the raw value. */\n label: string;\n /** Rows of the bucket, in insertion order. */\n rows: SourceRow[];\n}\n\n/** Result of grouping raw rows onto board columns (R9.8.4). */\nexport interface KanbanMapping {\n /** Declared columns (in declared order) + one bucket per undeclared value. */\n columns: KanbanColumnBucket[];\n /** Rows whose `groupField` value is empty — visible, never dropped. */\n unknown: SourceRow[];\n}\n\n/** Options of the grouping (all optional; the defaults are the decided rules). */\nexport interface KanbanMapOptions {\n /**\n * Label of buckets whose value is NOT declared in `config.columns`.\n * Default: the raw value itself, so undeclared values stay verbatim and\n * visible. The view uses the default; the hook exists for callers that\n * want to decorate the label without touching the grouping rules.\n */\n labelFor?: (value: string) => string;\n}\n\n/** Title of a card whose every candidate is empty/unreadable (never blank). */\nexport const CARD_NO_TITLE = 'Sin título';\n\n/** Object cells: the readable keys, in preference order. */\nconst OBJECT_VALUE_KEYS = ['label', 'value', 'id'] as const;\n\n/** Printable text of a scalar cell; `null` when it is empty or not scalar. */\nfunction scalarText(value: unknown): string | null {\n if (typeof value === 'string') {\n const text = value.trim();\n return text === '' ? null : text;\n }\n if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {\n return String(value);\n }\n return null;\n}\n\n/**\n * Printable text of ANY cell: scalars as-is (trimmed), objects reduced to\n * their first readable `label`/`value`/`id`. `null` when there is nothing\n * readable — the caller decides (bucket → `unknown`, card → next fallback).\n *\n * Never returns `undefined` nor `[object Object]`: that is the whole point.\n */\nfunction cellText(value: unknown): string | null {\n const scalar = scalarText(value);\n if (scalar !== null) return scalar;\n if (value !== null && typeof value === 'object') {\n const cell = value as Record<string, unknown>;\n for (const key of OBJECT_VALUE_KEYS) {\n const text = scalarText(cell[key]);\n if (text !== null) return text;\n }\n }\n return null;\n}\n\n/**\n * Pure row → board grouping (R9.8.4): the single place that decides which\n * column owns each row, which rows are «unknown» and in what order the\n * columns are painted.\n *\n * Deterministic: declared columns keep their declared order (even when\n * empty), undeclared values are appended in first-appearance order, and the\n * rows inside each column keep their insertion order.\n */\nexport function mapKanbanRows(\n rows: readonly SourceRow[],\n config: KanbanViewConfig,\n opts: KanbanMapOptions = {},\n): KanbanMapping {\n const columns: KanbanColumnBucket[] = [];\n const byValue = new Map<string, KanbanColumnBucket>();\n\n // Declared columns first — always visible, even with zero rows.\n const declared = Array.isArray(config.columns) ? config.columns : [];\n for (const column of declared) {\n const value = typeof column?.value === 'string' ? column.value : '';\n // Contract (R9.8.2): `value` obligatorio y único — a duplicated or empty\n // declaration cannot own a bucket, the first valid one wins.\n if (value === '' || byValue.has(value)) continue;\n const label = typeof column.label === 'string' && column.label !== '' ? column.label : value;\n const bucket: KanbanColumnBucket = { value, label, rows: [] };\n byValue.set(value, bucket);\n columns.push(bucket);\n }\n\n const unknown: SourceRow[] = [];\n for (const row of rows) {\n const value = cellText(row[config.groupField]);\n if (value === null) {\n // Empty/undefined (or unreadable object): visible bucket, never dropped.\n unknown.push(row);\n continue;\n }\n let bucket = byValue.get(value);\n if (bucket === undefined) {\n // Undeclared value: its own column, appended AFTER the declared ones.\n const label = opts.labelFor !== undefined ? opts.labelFor(value) : value;\n bucket = { value, label, rows: [] };\n byValue.set(value, bucket);\n columns.push(bucket);\n }\n bucket.rows.push(row);\n }\n\n return { columns, unknown };\n}\n\n/**\n * Title of a card: `config.titleField` → `schema.titleField` →\n * `schema.keyField`, first candidate with readable text wins. An unreadable\n * value (empty, `undefined`, an object with no label) never reaches the DOM:\n * the chain falls through to the key, and to a visible «Sin título» when not\n * even the key has text.\n */\nexport function cardTitle(\n row: SourceRow,\n config: KanbanViewConfig,\n schema: ResourceSchema,\n): string {\n const candidates = [config.titleField, schema.titleField, schema.keyField];\n for (const field of candidates) {\n if (typeof field !== 'string' || field === '') continue;\n const text = cellText(row[field]);\n if (text !== null) return text;\n }\n return CARD_NO_TITLE;\n}\n\n/**\n * Second line of a card: the `config.subtitleField` value when the config\n * declares one, `''` otherwise (the schema has no notion of subtitle).\n */\nexport function cardSubtitle(\n row: SourceRow,\n config: KanbanViewConfig,\n _schema: ResourceSchema,\n): string {\n const field = config.subtitleField;\n if (typeof field !== 'string' || field === '') return '';\n return cellText(row[field]) ?? '';\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute } from '@angular/router';\n\nimport type {\n KanbanViewConfig,\n ResourceSchema,\n SelectionBinding,\n} from '../../core/models/schema.model';\nimport { ErrorMapperService } from '../../core/services/error-mapper.service';\nimport { DataSourceService, type SourceRow } from '../../core/services/data-source.service';\nimport type { SelectionEmitter } from '../composer/view-context';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Button } from '../../shared/components/button/button';\nimport { EmptyState } from '../../shared/components/empty-state/empty-state';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport {\n cardSubtitle,\n cardTitle,\n mapKanbanRows,\n type KanbanColumnBucket,\n type KanbanMapping,\n} from './kanban-model';\n\n/**\n * Narrowing helper: `UiViewBase` keeps `type: string` open, so comparing\n * `view.type === 'kanban'` alone does not remove the base member from the\n * union. Requiring `config` in the value is what actually proves the view is\n * a `UiViewKanban` at runtime.\n */\nfunction kanbanConfigOf(view: unknown): KanbanViewConfig | undefined {\n if (!view || typeof view !== 'object') return undefined;\n const candidate = view as { type?: unknown; config?: unknown };\n return candidate.type === 'kanban' && candidate.config !== undefined\n ? (candidate.config as KanbanViewConfig)\n : undefined;\n}\n\ntype Status = 'loading' | 'ready' | 'error';\n\n/** Label of the bucket for rows without a group value (Spanish UI copy). */\nconst UNKNOWN_LABEL = 'Sin valor';\n\n/**\n * Synthetic `value` of that bucket: a real group value can never collide\n * with it (a declared `value` is non-empty and the grouping never produces\n * this literal), so `@for` tracking stays unique.\n */\nconst UNKNOWN_VALUE = '__sin_valor__';\n\n/**\n * Generic kanban view (R9.8.4): rows of the declared\n * `KanbanViewConfig.source` (or the resource's own listing) grouped by\n * `groupField` into the declared columns, client-side.\n *\n * Data flow: ONE fetcher (`DataSourceService.rows`) — there is no dynamic\n * context here, so `ctx` is `undefined`; the 30 s cache of the service is\n * what keeps repeated visits free.\n *\n * Read-only by design (R9.8.1): no drag-and-drop, no move buttons, no\n * mutation path at all. Colour never carries meaning alone — every count is\n * text — and an empty column is a visible «Sin tarjetas», never a gap.\n */\n@Component({\n selector: 'app-kanban-view',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [Button, EmptyState, Skeleton],\n template: `\n @if (config(); as cfg) {\n <section class=\"space-y-4\" aria-label=\"Tablero\" [attr.aria-busy]=\"status() === 'loading'\">\n <header class=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 class=\"font-display text-xl font-semibold\">Tablero</h2>\n <p class=\"text-sm text-neutral\">\n Agrupado por <code>{{ cfg.groupField }}</code>\n </p>\n </header>\n\n @switch (status()) {\n @case ('loading') {\n <app-skeleton [lines]=\"6\" [height]=\"32\" />\n }\n @case ('error') {\n <app-empty-state title=\"No se pudo cargar el tablero\" [message]=\"errorMessage()\">\n <button app-button variant=\"outline\" type=\"button\" (click)=\"retry()\">\n Reintentar\n </button>\n </app-empty-state>\n }\n @case ('ready') {\n <div class=\"flex gap-3 overflow-x-auto pb-2\">\n @for (column of columns(); track column.value) {\n <section\n class=\"flex w-64 shrink-0 flex-col gap-3 rounded-xl border border-neutral/20 bg-surface p-3\"\n [attr.data-column]=\"column.value\"\n [attr.aria-label]=\"columnAria(column)\"\n >\n <header class=\"flex items-center justify-between gap-2\">\n <h3 class=\"text-sm font-semibold\">{{ column.label }}</h3>\n <span class=\"rounded-full bg-neutral/10 px-2 py-0.5 text-xs text-neutral\">\n {{ column.rows.length }}<span class=\"sr-only\"> tarjetas</span>\n </span>\n </header>\n @if (column.rows.length === 0) {\n <p class=\"text-sm text-neutral\">Sin tarjetas</p>\n } @else {\n <ul class=\"flex flex-col gap-2\">\n @for (row of column.rows; track $index) {\n <li\n class=\"rounded-lg border border-neutral/15 p-2\"\n [attr.tabindex]=\"seleccionable() ? '0' : null\"\n [attr.role]=\"seleccionable() ? 'button' : null\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(row)\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(row)\"\n (keydown.enter)=\"activar($event, row)\"\n (keydown.space)=\"activar($event, row)\"\n >\n <p class=\"text-sm font-medium\">{{ cardTitleOf(row) }}</p>\n @if (cardSubtitleOf(row); as subtitle) {\n <p class=\"mt-0.5 text-xs text-neutral\">{{ subtitle }}</p>\n }\n </li>\n }\n </ul>\n }\n </section>\n }\n </div>\n }\n }\n </section>\n } @else {\n <app-empty-state\n title=\"Configuración inválida\"\n message=\"Esta vista no declara una configuración de kanban válida.\"\n />\n }\n `,\n})\nexport class KanbanView {\n private readonly route = inject(ActivatedRoute);\n // The shell mounts views via `loadComponent` (no input bindings), so the\n // schema is resolved from route data exactly like CalendarView does.\n private readonly routeData = toSignal(this.route.data, { requireSync: true });\n private readonly dataSource = inject(DataSourceService);\n private readonly errorMapper = inject(ErrorMapperService);\n\n /**\n * Resource schema for this view. The shell mounts views via\n * `loadComponent` (no input bindings), so the schema is resolved from\n * route data: either an explicit `schema` object (fixtures), or a\n * `resourceId` looked up in the registry (the production route shape).\n */\n readonly schema = computed<ResourceSchema | undefined>(() => {\n const data = this.routeData();\n const explicit = data['schema'];\n if (explicit !== undefined) return explicit as ResourceSchema;\n const resourceId = data['resourceId'];\n return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;\n });\n\n /**\n * Config resolved from `schema.uiViews[<route uiView>]`; `undefined` when\n * the route does not name a kanban view with a usable config — the\n * template then renders the invalid-config empty state and NOTHING is\n * fetched (the effect below bails out on `undefined`).\n */\n readonly config = computed<KanbanViewConfig | undefined>(() => {\n const schema = this.schema();\n const viewName = this.routeData()['uiView'];\n if (schema === undefined || typeof viewName !== 'string') return undefined;\n const cfg = kanbanConfigOf(schema.uiViews?.[viewName]);\n if (cfg === undefined) return undefined;\n // Usable = the two references the board cannot render without: a\n // non-empty `groupField` and at least one column with a non-empty value.\n const usable =\n typeof cfg.groupField === 'string' &&\n cfg.groupField !== '' &&\n Array.isArray(cfg.columns) &&\n cfg.columns.length > 0 &&\n cfg.columns.every((column) => typeof column?.value === 'string' && column.value !== '');\n return usable ? cfg : undefined;\n });\n\n readonly status = signal<Status>('loading');\n readonly rows = signal<SourceRow[]>([]);\n readonly errorMessage = signal('');\n /** Clave seleccionada localmente cuando el binding declara `mode:'inline'`. */\n readonly seleccionada = signal<string | null>(null);\n\n /** Guards against a slow response overwriting a newer load's rows. */\n private loadEpoch = 0;\n\n constructor() {\n effect(() => {\n const cfg = this.config();\n if (cfg === undefined) return;\n void this.load(cfg);\n });\n }\n\n /** Rows grouped onto columns + the visible `unknown` bucket (pure model). */\n readonly mapping = computed<KanbanMapping>(() => {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return { columns: [], unknown: [] };\n return mapKanbanRows(this.rows(), cfg);\n });\n\n /**\n * Columns to paint: the model's buckets plus the `unknown` bucket at the\n * END — but only when it actually has rows, otherwise the board would show\n * an empty column nobody declared.\n */\n readonly columns = computed<readonly KanbanColumnBucket[]>(() => {\n const { columns, unknown } = this.mapping();\n if (unknown.length === 0) return columns;\n return [...columns, { value: UNKNOWN_VALUE, label: UNKNOWN_LABEL, rows: unknown }];\n });\n\n retry(): void {\n const cfg = this.config();\n if (cfg !== undefined) void this.load(cfg);\n }\n\n /** Clave estable de la fila (mismo campo que el resto del motor). */\n private keyOf(row: SourceRow): string {\n const schema = this.schema();\n const key = schema ? row[schema.keyField] : undefined;\n return key === undefined || key === null ? '' : String(key);\n }\n\n /** Binding `select` declarado por la vista (R9.9.4 · decision table). */\n private binding(): SelectionBinding | undefined {\n return this.routeData()['select'] as SelectionBinding | undefined;\n }\n\n /**\n * Clic de tarjeta. Sin declaración ⇒ inerte (el tablero no navegaba);\n * con declaración ⇒ sólo el contrato declarado corre: `route` emite al\n * dispatcher, `inline` selecciona en local, `none` es no-op explícito.\n */\n /**\n * R9.8.7 — la misma selección que el clic, operable con teclado (Enter y\n * Space sobre la tarjeta focusable). Sólo tiene sentido con `select`\n * declarado: sin binding la tarjeta no es focusable y esto nunca corre.\n */\n activar(event: Event, row: SourceRow): void {\n if (!this.seleccionable()) return;\n event.preventDefault(); // Space no debe desplazar la página\n this.seleccionar(row);\n }\n\n seleccionar(row: SourceRow): void {\n const binding = this.binding();\n if (binding === undefined) return;\n if (binding.mode === 'inline') {\n this.seleccionada.set(this.keyOf(row));\n return;\n }\n if (binding.mode === 'none') return;\n const schema = this.schema();\n const emit = this.routeData()['emit'] as SelectionEmitter | undefined;\n emit?.({\n resource: schema?.id ?? String(this.routeData()['resourceId'] ?? ''),\n key: this.keyOf(row),\n action: binding.on,\n });\n }\n\n /** El DOM declara clicable sólo si hay contrato que responder. */\n seleccionable(): boolean {\n const binding = this.binding();\n return binding !== undefined && binding.mode !== 'none';\n }\n\n esSeleccionada(row: SourceRow): boolean {\n return this.seleccionable() && this.seleccionada() === this.keyOf(row);\n }\n\n /** «Por hacer: 2 tarjetas» — the count is text, never colour alone. */\n columnAria(column: KanbanColumnBucket): string {\n const count = column.rows.length;\n return `${column.label}: ${count} ${count === 1 ? 'tarjeta' : 'tarjetas'}`;\n }\n\n cardTitleOf(row: SourceRow): string {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return '';\n return cardTitle(row, cfg, schema);\n }\n\n cardSubtitleOf(row: SourceRow): string {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return '';\n return cardSubtitle(row, cfg, schema);\n }\n\n private async load(cfg: KanbanViewConfig): Promise<void> {\n const schema = this.schema();\n if (schema === undefined) return;\n const epoch = ++this.loadEpoch;\n this.status.set('loading');\n const source = cfg.source ?? { resource: schema.id };\n try {\n const rows = await this.dataSource.rows(source, undefined);\n if (epoch !== this.loadEpoch) return;\n this.rows.set(rows);\n this.errorMessage.set('');\n this.status.set('ready');\n } catch (error) {\n if (epoch !== this.loadEpoch) return;\n this.errorMessage.set(this.errorMapper.map(error).general ?? 'No se pudo cargar el tablero.');\n this.status.set('error');\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAkDA,MAAa,gBAAgB;AAG7B,MAAM,oBAAoB;CAAC;CAAS;CAAS;AAAI;AAGjD,SAAS,WAAW,OAAc;CAChC,IAAI,OAAO,UAAU,UAAU;EAC7B,MAAM,OAAO,MAAM,KAAI;EACvB,OAAO,SAAS,KAAK,OAAO;CAC9B;CACA,IAAI,OAAO,UAAU,YAAY,OAAO,UAAU,aAAa,OAAO,UAAU,UAC9E,OAAO,OAAO,KAAK;CAErB,OAAO;AACT;AASA,SAAS,SAAS,OAAc;CAC9B,MAAM,SAAS,WAAW,KAAK;CAC/B,IAAI,WAAW,MAAM,OAAO;CAC5B,IAAI,UAAU,QAAQ,OAAO,UAAU,UAAU;EAC/C,MAAM,OAAO;EACb,KAAK,MAAM,OAAO,mBAAmB;GACnC,MAAM,OAAO,WAAW,KAAK,IAAI;GACjC,IAAI,SAAS,MAAM,OAAO;EAC5B;CACF;CACA,OAAO;AACT;AAWA,SAAgB,cACd,MACA,QACA,OAAyB,CAAA,GAAE;CAE3B,MAAM,UAAgC,CAAA;CACtC,MAAM,0BAAU,IAAI,IAAG;CAGvB,MAAM,WAAW,MAAM,QAAQ,OAAO,OAAO,IAAI,OAAO,UAAU,CAAA;CAClE,KAAK,MAAM,UAAU,UAAU;EAC7B,MAAM,QAAQ,OAAO,QAAQ,UAAU,WAAW,OAAO,QAAQ;EAGjE,IAAI,UAAU,MAAM,QAAQ,IAAI,KAAK,GAAG;EAExC,MAAM,SAA6B;GAAE;GAAO,OAD9B,OAAO,OAAO,UAAU,YAAY,OAAO,UAAU,KAAK,OAAO,QAAQ;GACpC,MAAM,CAAA;EAAE;EAC3D,QAAQ,IAAI,OAAO,MAAM;EACzB,QAAQ,KAAK,MAAM;CACrB;CAEA,MAAM,UAAuB,CAAA;CAC7B,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,SAAS,IAAI,OAAO,WAAW;EAC7C,IAAI,UAAU,MAAM;GAElB,QAAQ,KAAK,GAAG;GAChB;EACF;EACA,IAAI,SAAS,QAAQ,IAAI,KAAK;EAC9B,IAAI,WAAW,QAAW;GAGxB,SAAS;IAAE;IAAO,OADJ,KAAK,aAAa,SAAY,KAAK,SAAS,KAAK,IAAI;IAC1C,MAAM,CAAA;GAAE;GACjC,QAAQ,IAAI,OAAO,MAAM;GACzB,QAAQ,KAAK,MAAM;EACrB;EACA,OAAO,KAAK,KAAK,GAAG;CACtB;CAEA,OAAO;EAAE;EAAS;CAAO;AAC3B;AASA,SAAgB,UACd,KACA,QACA,QAAsB;CAEtB,MAAM,aAAa;EAAC,OAAO;EAAY,OAAO;EAAY,OAAO;CAAQ;CACzE,KAAK,MAAM,SAAS,YAAY;EAC9B,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI;EAC/C,MAAM,OAAO,SAAS,IAAI,MAAM;EAChC,IAAI,SAAS,MAAM,OAAO;CAC5B;CACA,OAAO;AACT;AAMA,SAAgB,aACd,KACA,QACA,SAAuB;CAEvB,MAAM,QAAQ,OAAO;CACrB,IAAI,OAAO,UAAU,YAAY,UAAU,IAAI,OAAO;CACtD,OAAO,SAAS,IAAI,MAAM,KAAK;AACjC;ACvIA,SAAS,eAAe,MAAa;CACnC,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU,OAAO;CAC9C,MAAM,YAAY;CAClB,OAAO,UAAU,SAAS,YAAY,UAAU,WAAW,SACtD,UAAU,SACX;AACN;AAKA,MAAM,gBAAgB;AAOtB,MAAM,gBAAgB;AA4FtB,IAAa,aAAb,MAAa,WAAU;CACJ,QAAQ,OAAO,cAAc;CAG7B,YAAY,SAAS,KAAK,MAAM,MAAM,EAAE,aAAa,KAAI,CAAE;CAC3D,aAAa,OAAO,iBAAiB;CACrC,cAAc,OAAO,kBAAkB;CAQ/C,SAAS,eAA0C;EAC1D,MAAM,OAAO,KAAK,UAAS;EAC3B,MAAM,WAAW,KAAK;EACtB,IAAI,aAAa,QAAW,OAAO;EACnC,MAAM,aAAa,KAAK;EACxB,OAAO,OAAO,eAAe,WAAW,UAAU,UAAU,IAAI;CAClE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAQO,SAAS,eAA4C;EAC5D,MAAM,SAAS,KAAK,OAAM;EAC1B,MAAM,WAAW,KAAK,UAAS,CAAE,CAAC;EAClC,IAAI,WAAW,UAAa,OAAO,aAAa,UAAU,OAAO;EACjE,MAAM,MAAM,eAAe,OAAO,UAAU,SAAS;EACrD,IAAI,QAAQ,QAAW,OAAO;EAS9B,OALE,OAAO,IAAI,eAAe,YAC1B,IAAI,eAAe,MACnB,MAAM,QAAQ,IAAI,OAAO,KACzB,IAAI,QAAQ,SAAS,KACrB,IAAI,QAAQ,OAAO,WAAW,OAAO,QAAQ,UAAU,YAAY,OAAO,UAAU,EAAE,IACxE,MAAM;CACxB,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAEO,SAAS,OAAe,WAAU,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAClC,OAAO,OAAoB,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAC9B,eAAe,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEzB,eAAe,OAAsB,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAG3C,YAAY;CAEpB,cAAA;EACE,aAAY;GACV,MAAM,MAAM,KAAK,OAAM;GACvB,IAAI,QAAQ,QAAW;GACvB,AAAK,KAAK,KAAK,GAAG;EACpB,CAAC;CACH;CAGS,UAAU,eAA6B;EAC9C,MAAM,MAAM,KAAK,OAAM;EACvB,MAAM,SAAS,KAAK,OAAM;EAC1B,IAAI,QAAQ,UAAa,WAAW,QAAW,OAAO;GAAE,SAAS,CAAA;GAAI,SAAS,CAAA;EAAE;EAChF,OAAO,cAAc,KAAK,KAAI,GAAI,GAAG;CACvC,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAOO,UAAU,eAA6C;EAC9D,MAAM,EAAE,SAAS,YAAY,KAAK,QAAO;EACzC,IAAI,QAAQ,WAAW,GAAG,OAAO;EACjC,OAAO,CAAC,GAAG,SAAS;GAAE,OAAO;GAAe,OAAO;GAAe,MAAM;EAAO,CAAE;CACnF,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAEF,QAAK;EACH,MAAM,MAAM,KAAK,OAAM;EACvB,IAAI,QAAQ,QAAW,AAAK,KAAK,KAAK,GAAG;CAC3C;CAGQ,MAAM,KAAc;EAC1B,MAAM,SAAS,KAAK,OAAM;EAC1B,MAAM,MAAM,SAAS,IAAI,OAAO,YAAY;EAC5C,OAAO,QAAQ,UAAa,QAAQ,OAAO,KAAK,OAAO,GAAG;CAC5D;CAGQ,UAAO;EACb,OAAO,KAAK,UAAS,CAAE,CAAC;CAC1B;CAYA,QAAQ,OAAc,KAAc;EAClC,IAAI,CAAC,KAAK,cAAa,GAAI;EAC3B,MAAM,eAAc;EACpB,KAAK,YAAY,GAAG;CACtB;CAEA,YAAY,KAAc;EACxB,MAAM,UAAU,KAAK,QAAO;EAC5B,IAAI,YAAY,QAAW;EAC3B,IAAI,QAAQ,SAAS,UAAU;GAC7B,KAAK,aAAa,IAAI,KAAK,MAAM,GAAG,CAAC;GACrC;EACF;EACA,IAAI,QAAQ,SAAS,QAAQ;EAC7B,MAAM,SAAS,KAAK,OAAM;EAC1B,MAAM,OAAO,KAAK,UAAS,CAAE,CAAC;EAC9B,OAAO;GACL,UAAU,QAAQ,MAAM,OAAO,KAAK,UAAS,CAAE,CAAC,iBAAiB,EAAE;GACnE,KAAK,KAAK,MAAM,GAAG;GACnB,QAAQ,QAAQ;EACjB,CAAA;CACH;CAGA,gBAAa;EACX,MAAM,UAAU,KAAK,QAAO;EAC5B,OAAO,YAAY,UAAa,QAAQ,SAAS;CACnD;CAEA,eAAe,KAAc;EAC3B,OAAO,KAAK,cAAa,KAAM,KAAK,aAAY,MAAO,KAAK,MAAM,GAAG;CACvE;CAGA,WAAW,QAA0B;EACnC,MAAM,QAAQ,OAAO,KAAK;EAC1B,OAAO,GAAG,OAAO,MAAK,IAAK,MAAK,GAAI,UAAU,IAAI,YAAY;CAChE;CAEA,YAAY,KAAc;EACxB,MAAM,MAAM,KAAK,OAAM;EACvB,MAAM,SAAS,KAAK,OAAM;EAC1B,IAAI,QAAQ,UAAa,WAAW,QAAW,OAAO;EACtD,OAAO,UAAU,KAAK,KAAK,MAAM;CACnC;CAEA,eAAe,KAAc;EAC3B,MAAM,MAAM,KAAK,OAAM;EACvB,MAAM,SAAS,KAAK,OAAM;EAC1B,IAAI,QAAQ,UAAa,WAAW,QAAW,OAAO;EACtD,OAAO,aAAa,KAAK,KAAK,MAAM;CACtC;CAEQ,MAAM,KAAK,KAAqB;EACtC,MAAM,SAAS,KAAK,OAAM;EAC1B,IAAI,WAAW,QAAW;EAC1B,MAAM,QAAQ,EAAE,KAAK;EACrB,KAAK,OAAO,IAAI,SAAS;EACzB,MAAM,SAAS,IAAI,UAAU,EAAE,UAAU,OAAO,GAAE;EAClD,IAAI;GACF,MAAM,OAAO,MAAM,KAAK,WAAW,KAAK,QAAQ,MAAS;GACzD,IAAI,UAAU,KAAK,WAAW;GAC9B,KAAK,KAAK,IAAI,IAAI;GAClB,KAAK,aAAa,IAAI,EAAE;GACxB,KAAK,OAAO,IAAI,OAAO;EACzB,SAAS,OAAO;GACd,IAAI,UAAU,KAAK,WAAW;GAC9B,KAAK,aAAa,IAAI,KAAK,YAAY,IAAI,KAAK,CAAC,CAAC,WAAW,+BAA+B;GAC5F,KAAK,OAAO,IAAI,OAAO;EACzB;CACF;CAlLW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAU,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAV,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAU,cAAA;EAAA,UAAA;EAAA,UAAA;EAAA,UAzEX;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuET,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAxES;IAAM,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;IAAA,QAAA,CAAA,SAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;EAAA,iBAAA,GAAA,wBAAA;CAAA,CAAA;;AA0E3B,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAU,YAAA,CAAA;EA7EtB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,iBAAiB,wBAAwB;GACzC,SAAS;IAAC;IAAQ;IAAY;GAAQ;GACtC,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwEX,CAAA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
2
2
|
import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
|
|
3
3
|
import { i as listTree, r as itemTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
4
|
-
import { a as colSpanClass, i as seedFields } from "./tinya-code-gas-admin-ui-seed-
|
|
4
|
+
import { a as colSpanClass, i as seedFields } from "./tinya-code-gas-admin-ui-seed-BnwHE1Tw.mjs";
|
|
5
5
|
import { n as appendPosition, r as computeMove, t as GAP } from "./tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs";
|
|
6
6
|
import * as i0 from "@angular/core";
|
|
7
7
|
import { Component, computed, inject, input, signal } from "@angular/core";
|
|
@@ -634,4 +634,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
634
634
|
});
|
|
635
635
|
export { FieldList };
|
|
636
636
|
|
|
637
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-list-
|
|
637
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-list-BKr33pde.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-list-CH7b7dKh.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/list/list.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport { Component, computed, inject, input, signal, type TemplateRef } from '@angular/core';\nimport {\n CdkDrag,\n CdkDragDrop,\n CdkDragHandle,\n CdkDropList,\n moveItemInArray,\n} from '@angular/cdk/drag-drop';\n\nimport { LucideArrowDown, LucideArrowUp, LucideGripVertical, LucideTrash2 } from '@lucide/angular';\nimport type { ListField } from '../../core/models/schema.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { colSpanClass } from '../../shared/utils/col-span';\nimport {\n appendPosition,\n computeMove,\n GAP,\n type PositionedItem,\n} from '../../shared/utils/gap-sorting';\nimport { seedFields } from '../../shared/utils/seed';\nimport { itemTree, listTree, type ListChildContext, type RootTree } from '../field-node';\n\n/**\n * Lista de ítems (base.md §6): asa de arrastre, subir/bajar, eliminar con\n * confirmación (sólo si el ítem tiene contenido) y agregar respetando\n * `min`/`max`.\n *\n * **Los hijos los renderiza el despachador**: este componente recibe el\n * `TemplateRef` (#childTpl, declarado en el `@case` de `field-host`) y lo\n * instancia una vez por ítem con `ngTemplateOutlet`. No importa FieldHost —\n * un import mutuo revienta en runtime (ciclo-check del repo).\n *\n * El orden de visualización ES el orden del arreglo modelo; `position`\n * (gap-sorting, base.md §8) se calcula junto con cada mutación y viaja en el\n * payload. Un hueco que deja un `remove` se autocura en el siguiente `move`.\n */\n@Component({\n selector: 'app-field-list',\n imports: [\n Button,\n CdkDrag,\n CdkDragHandle,\n CdkDropList,\n LucideArrowDown,\n LucideArrowUp,\n LucideGripVertical,\n LucideTrash2,\n NgTemplateOutlet,\n ],\n template: `\n @if (items().length === 0) {\n <p\n class=\"rounded-lg border border-dashed border-neutral/30 px-4 py-6 text-center text-sm text-neutral\"\n >\n {{ field().emptyText ?? 'Sin elementos.' }}\n </p>\n } @else {\n <div\n cdkDropList\n class=\"space-y-3\"\n [cdkDropListData]=\"items()\"\n [cdkDropListDisabled]=\"!sortable()\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (item of items(); track $index; let i = $index) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n cdkDrag\n [cdkDragDisabled]=\"!sortable()\"\n >\n <div class=\"mb-2 flex items-center gap-1.5\">\n @if (sortable()) {\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n class=\"cursor-grab\"\n cdkDragHandle\n [attr.aria-label]=\"'Reordenar elemento ' + (i + 1)\"\n title=\"Arrastrar para reordenar\"\n >\n <svg lucideGripVertical size=\"16\" />\n </button>\n }\n @if (isAccordion()) {\n <button\n type=\"button\"\n class=\"flex min-w-0 items-center gap-1 text-sm font-medium\"\n [attr.aria-expanded]=\"openOf(i)\"\n (click)=\"toggle(i)\"\n >\n <span aria-hidden=\"true\">{{ openOf(i) ? '▾' : '▸' }}</span>\n <span class=\"truncate\">{{ titleOf(i) }}</span>\n </button>\n }\n @if (isPrimary(i)) {\n <span\n class=\"rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n Principal\n </span>\n }\n @if (hasProblem(i)) {\n <span class=\"text-xs font-medium text-danger\">Revisar</span>\n }\n <span class=\"ml-auto flex shrink-0 items-center gap-2 lg:gap-1\">\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"'Subir elemento ' + (i + 1)\"\n (click)=\"move(i, -1)\"\n >\n <svg lucideArrowUp size=\"16\" />\n </button>\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"i === items().length - 1\"\n [attr.aria-label]=\"'Bajar elemento ' + (i + 1)\"\n (click)=\"move(i, 1)\"\n >\n <svg lucideArrowDown size=\"16\" />\n </button>\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"!canRemove()\"\n [attr.aria-label]=\"'Eliminar elemento ' + (i + 1)\"\n (click)=\"remove(i)\"\n >\n <svg lucideTrash2 size=\"16\" />\n </button>\n </span>\n </div>\n @if (!isAccordion() || openOf(i)) {\n @if (isRow()) {\n <div class=\"flex flex-wrap items-start gap-3\">\n <ng-container\n [ngTemplateOutlet]=\"childTemplate()\"\n [ngTemplateOutletContext]=\"childContext(i)\"\n />\n </div>\n } @else {\n <div class=\"grid grid-cols-1 gap-x-4 sm:grid-cols-12\">\n <ng-container\n [ngTemplateOutlet]=\"childTemplate()\"\n [ngTemplateOutletContext]=\"childContext(i)\"\n />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <div class=\"mt-3\">\n <button app-button variant=\"outline\" type=\"button\" [disabled]=\"!canAdd()\" (click)=\"add()\">\n + {{ field().addLabel ?? 'Agregar elemento' }}\n </button>\n </div>\n `,\n})\nexport class FieldList {\n readonly field = input.required<ListField>();\n readonly tree = input.required<RootTree>();\n readonly serverErrors = input<Record<string, string>>({});\n readonly pathPrefix = input('');\n readonly exists = input(false);\n /** Template de los hijos, declarado en el `@case` del despachador. */\n readonly childTemplate = input.required<TemplateRef<ListChildContext>>();\n\n private readonly notifications = inject(NotificationService);\n\n /** Intento de apertura por ítem (`undefined` → sigue el auto-open). */\n private readonly intent = signal<Record<number, boolean>>({});\n\n private readonly itemsTree = computed(() =>\n listTree<Record<string, unknown>>(this.tree(), this.field().key),\n );\n private readonly state = computed(() => this.itemsTree()());\n protected readonly items = computed(() => this.state().value() ?? []);\n\n protected readonly sortable = computed(() => this.field().sortable ?? true);\n /** Clase fija de la barra de ícono (literal: Tailwind v4 es JIT). */\n\n protected canAdd(): boolean {\n const max = this.field().max;\n return max === undefined || this.items().length < max;\n }\n\n protected canRemove(): boolean {\n return this.items().length > (this.field().min ?? 0);\n }\n\n protected isAccordion(): boolean {\n return this.field().itemDisplay === 'accordion';\n }\n\n protected isRow(): boolean {\n return this.field().itemDisplay === 'row';\n }\n\n protected isPrimary(index: number): boolean {\n return (this.field().primaryFirst ?? false) && index === 0;\n }\n\n protected titleOf(index: number): string {\n const item = this.items()[index];\n const key = this.field().itemTitle;\n const value = key !== undefined && item !== undefined ? item[key] : undefined;\n return typeof value === 'string' && value !== '' ? value : `#${index + 1}`;\n }\n\n // ── Ítem abierto (accordion) ──\n\n protected openOf(index: number): boolean {\n const forced = this.intent()[index];\n if (forced !== undefined) {\n return forced;\n }\n return this.hasProblem(index);\n }\n\n protected toggle(index: number): void {\n const willOpen = !this.openOf(index);\n this.intent.update((map) => ({ ...map, [index]: willOpen }));\n }\n\n /** Inválido+tocado (agrega hijos) o error del backend bajo `key[i].`. */\n hasProblem(index: number): boolean {\n const state = itemTree(this.itemsTree(), index)();\n if (state.invalid() && state.touched()) {\n return true;\n }\n const prefix = `${this.pathPrefix()}${this.field().key}[${index}].`;\n return Object.keys(this.serverErrors()).some((path) => path.startsWith(prefix));\n }\n\n // ── Mutaciones (todas sobre `controlValue` → marcan `dirty` solas) ──\n\n protected add(): void {\n if (!this.canAdd()) {\n return;\n }\n const item = seedFields(this.field().itemFields);\n const positionKey = this.field().positionField;\n if (positionKey !== undefined) {\n item[positionKey] = appendPosition(this.positioned());\n }\n const newIndex = this.items().length;\n this.updateItems((next) => {\n next.push(item);\n });\n // El ítem nuevo arranca visible.\n this.intent.update((map) => ({ ...map, [newIndex]: true }));\n }\n\n protected move(index: number, direction: -1 | 1): void {\n const target = index + direction;\n if (target < 0 || target >= this.items().length) {\n return;\n }\n this.applyMove(index, target);\n }\n\n protected onDrop(event: CdkDragDrop<Record<string, unknown>[]>): void {\n if (event.previousIndex === event.currentIndex) {\n return;\n }\n this.applyMove(event.previousIndex, event.currentIndex);\n }\n\n protected async remove(index: number): Promise<void> {\n if (!this.canRemove()) {\n return;\n }\n if (this.hasContent(index)) {\n const accepted = await this.notifications.confirm({\n title: `Eliminar elemento ${index + 1}`,\n message: 'Esta acción no se puede deshacer.',\n confirmLabel: 'Eliminar',\n cancelLabel: 'Cancelar',\n });\n if (!accepted) {\n return;\n }\n }\n this.updateItems((next) => {\n next.splice(index, 1);\n });\n this.intent.set({});\n }\n\n /**\n * Contexto fresco por ítem: los nodos se atan a la identidad (NG01904).\n *\n * `$implicit` lleva el objeto completo: en el `ng-template` del host,\n * `let-child` (sin alias) lee `$implicit` — `NgTemplateOutlet` reparte el\n * contexto como propiedades top-level, no existe un alias mágico.\n */\n protected childContext(index: number): ListChildContext & { $implicit: ListChildContext } {\n const context: ListChildContext = {\n fields: this.field().itemFields,\n tree: itemTree(this.itemsTree(), index),\n prefix: `${this.pathPrefix()}${this.field().key}[${index}].`,\n errors: this.serverErrors(),\n exists: this.exists(),\n row: this.isRow(),\n colSpan: colSpanClass,\n };\n return { ...context, $implicit: context };\n }\n\n /** Ítem con algo distinto de sus `default` → pide confirmación. */\n private hasContent(index: number): boolean {\n const item = this.items()[index];\n if (item === undefined) {\n return false;\n }\n return JSON.stringify(item) !== JSON.stringify(seedFields(this.field().itemFields));\n }\n\n /**\n * Mueve el ítem y recalcula su `position` (base.md §8): los cambios de\n * posición se aplican ANTES del splice porque las claves referencian los\n * índices originales.\n */\n private applyMove(from: number, to: number): void {\n const positionKey = this.field().positionField;\n this.updateItems((next) => {\n if (positionKey !== undefined) {\n const result = computeMove(this.positioned(), String(from), to);\n for (const change of result.changes) {\n const item = next[Number(change.key)];\n if (item !== undefined) {\n item[positionKey] = change.position;\n }\n }\n }\n moveItemInArray(next, from, to);\n });\n this.intent.set({});\n }\n\n /** Posiciones actuales; sin `positionField` no se calculan. */\n private positioned(): PositionedItem[] {\n const positionKey = this.field().positionField;\n return this.items().map((item, index) => {\n const raw = positionKey !== undefined ? item[positionKey] : undefined;\n // Fallback: un registro viejo sin `position` se numera como rebalanceo.\n return {\n key: String(index),\n position: typeof raw === 'number' ? raw : (index + 1) * GAP,\n };\n });\n }\n\n private updateItems(mutator: (next: Record<string, unknown>[]) => void): void {\n this.state().controlValue.update((current) => {\n const next = [...current];\n mutator(next);\n return next;\n });\n }\n}\n"],"mappings":";;;;;;;;;;AA6KA,IAAa,YAAb,MAAa,UAAS;CACX,QAAQ,MAAM,SAAqB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACnC,OAAO,MAAM,SAAoB,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACjC,eAAe,MAA8B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAChD,aAAa,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACtB,SAAS,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAErB,gBAAgB,MAAM,SAAyC,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAEvD,gBAAgB,OAAO,mBAAmB;CAG1C,SAAS,OAAgC,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAE5C,YAAY,eAC3B,SAAkC,KAAK,KAAI,GAAI,KAAK,MAAK,CAAE,CAAC,GAAG,GAChE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CACgB,QAAQ,eAAe,KAAK,UAAS,CAAE,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,QAAQ,eAAe,KAAK,MAAK,CAAE,CAAC,MAAK,KAAM,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAElD,WAAW,eAAe,KAAK,MAAK,CAAE,CAAC,YAAY,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGjE,SAAM;EACd,MAAM,MAAM,KAAK,MAAK,CAAE,CAAC;EACzB,OAAO,QAAQ,UAAa,KAAK,MAAK,CAAE,CAAC,SAAS;CACpD;CAEU,YAAS;EACjB,OAAO,KAAK,MAAK,CAAE,CAAC,UAAU,KAAK,MAAK,CAAE,CAAC,OAAO;CACpD;CAEU,cAAW;EACnB,OAAO,KAAK,MAAK,CAAE,CAAC,gBAAgB;CACtC;CAEU,QAAK;EACb,OAAO,KAAK,MAAK,CAAE,CAAC,gBAAgB;CACtC;CAEU,UAAU,OAAa;EAC/B,QAAQ,KAAK,MAAK,CAAE,CAAC,gBAAgB,UAAU,UAAU;CAC3D;CAEU,QAAQ,OAAa;EAC7B,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC;EAC1B,MAAM,MAAM,KAAK,MAAK,CAAE,CAAC;EACzB,MAAM,QAAQ,QAAQ,UAAa,SAAS,SAAY,KAAK,OAAO;EACpE,OAAO,OAAO,UAAU,YAAY,UAAU,KAAK,QAAQ,IAAI,QAAQ;CACzE;CAIU,OAAO,OAAa;EAC5B,MAAM,SAAS,KAAK,OAAM,CAAE,CAAC;EAC7B,IAAI,WAAW,QACb,OAAO;EAET,OAAO,KAAK,WAAW,KAAK;CAC9B;CAEU,OAAO,OAAa;EAC5B,MAAM,WAAW,CAAC,KAAK,OAAO,KAAK;EACnC,KAAK,OAAO,QAAQ,SAAS;GAAE,GAAG;IAAM,QAAQ;EAAQ,EAAG;CAC7D;CAGA,WAAW,OAAa;EACtB,MAAM,QAAQ,SAAS,KAAK,UAAS,GAAI,KAAK,CAAC,CAAA;EAC/C,IAAI,MAAM,QAAO,KAAM,MAAM,QAAO,GAClC,OAAO;EAET,MAAM,SAAS,GAAG,KAAK,WAAU,IAAK,KAAK,MAAK,CAAE,CAAC,IAAG,GAAI,MAAK;EAC/D,OAAO,OAAO,KAAK,KAAK,aAAY,CAAE,CAAC,CAAC,MAAM,SAAS,KAAK,WAAW,MAAM,CAAC;CAChF;CAIU,MAAG;EACX,IAAI,CAAC,KAAK,OAAM,GACd;EAEF,MAAM,OAAO,WAAW,KAAK,MAAK,CAAE,CAAC,UAAU;EAC/C,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,IAAI,gBAAgB,QAClB,KAAK,eAAe,eAAe,KAAK,WAAU,CAAE;EAEtD,MAAM,WAAW,KAAK,MAAK,CAAE,CAAC;EAC9B,KAAK,aAAa,SAAQ;GACxB,KAAK,KAAK,IAAI;EAChB,CAAC;EAED,KAAK,OAAO,QAAQ,SAAS;GAAE,GAAG;IAAM,WAAW;EAAI,EAAG;CAC5D;CAEU,KAAK,OAAe,WAAiB;EAC7C,MAAM,SAAS,QAAQ;EACvB,IAAI,SAAS,KAAK,UAAU,KAAK,MAAK,CAAE,CAAC,QACvC;EAEF,KAAK,UAAU,OAAO,MAAM;CAC9B;CAEU,OAAO,OAA6C;EAC5D,IAAI,MAAM,kBAAkB,MAAM,cAChC;EAEF,KAAK,UAAU,MAAM,eAAe,MAAM,YAAY;CACxD;CAEU,MAAM,OAAO,OAAa;EAClC,IAAI,CAAC,KAAK,UAAS,GACjB;EAEF,IAAI,KAAK,WAAW,KAAK,GAOvB;OAAI,CAAC,MANkB,KAAK,cAAc,QAAQ;IAChD,OAAO,qBAAqB,QAAQ;IACpC,SAAS;IACT,cAAc;IACd,aAAa;GACd,CAAA,GAEC;EACF;EAEF,KAAK,aAAa,SAAQ;GACxB,KAAK,OAAO,OAAO,CAAC;EACtB,CAAC;EACD,KAAK,OAAO,IAAI,CAAA,CAAE;CACpB;CASU,aAAa,OAAa;EAClC,MAAM,UAA4B;GAChC,QAAQ,KAAK,MAAK,CAAE,CAAC;GACrB,MAAM,SAAS,KAAK,UAAS,GAAI,KAAK;GACtC,QAAQ,GAAG,KAAK,WAAU,IAAK,KAAK,MAAK,CAAE,CAAC,IAAG,GAAI,MAAK;GACxD,QAAQ,KAAK,aAAY;GACzB,QAAQ,KAAK,OAAM;GACnB,KAAK,KAAK,MAAK;GACf,SAAS;EACV;EACD,OAAO;GAAE,GAAG;GAAS,WAAW;EAAO;CACzC;CAGQ,WAAW,OAAa;EAC9B,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC;EAC1B,IAAI,SAAS,QACX,OAAO;EAET,OAAO,KAAK,UAAU,IAAI,MAAM,KAAK,UAAU,WAAW,KAAK,MAAK,CAAE,CAAC,UAAU,CAAC;CACpF;CAOQ,UAAU,MAAc,IAAU;EACxC,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,KAAK,aAAa,SAAQ;GACxB,IAAI,gBAAgB,QAAW;IAC7B,MAAM,SAAS,YAAY,KAAK,WAAU,GAAI,OAAO,IAAI,GAAG,EAAE;IAC9D,KAAK,MAAM,UAAU,OAAO,SAAS;KACnC,MAAM,OAAO,KAAK,OAAO,OAAO,GAAG;KACnC,IAAI,SAAS,QACX,KAAK,eAAe,OAAO;IAE/B;GACF;GACA,gBAAgB,MAAM,MAAM,EAAE;EAChC,CAAC;EACD,KAAK,OAAO,IAAI,CAAA,CAAE;CACpB;CAGQ,aAAU;EAChB,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,OAAO,KAAK,MAAK,CAAE,CAAC,KAAK,MAAM,UAAS;GACtC,MAAM,MAAM,gBAAgB,SAAY,KAAK,eAAe;GAE5D,OAAO;IACL,KAAK,OAAO,KAAK;IACjB,UAAU,OAAO,QAAQ,WAAW,OAAO,QAAQ,KAAK;GACzD;EACH,CAAC;CACH;CAEQ,YAAY,SAAkD;EACpE,KAAK,MAAK,CAAE,CAAC,aAAa,QAAQ,YAAW;GAC3C,MAAM,OAAO,CAAC,GAAG,OAAO;GACxB,QAAQ,IAAI;GACZ,OAAO;EACT,CAAC;CACH;CAzMW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAS,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAT,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAS,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UA1HV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwHT,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAlIC;IAAM,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACN;IAAO,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACP;IAAa,UAAA;IAAA,QAAA,CAAA,uBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAW,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,aAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACX;IAAe,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACf;IAAa,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAkB,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAClB;IAAY,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACZ;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;CAAA,CAAA;;AA4HP,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAS,YAAA,CAAA;EAvIrB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACD;GACD,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyHX,CAAA"}
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-list-BKr33pde.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/list/list.ts"],"sourcesContent":["import { NgTemplateOutlet } from '@angular/common';\nimport { Component, computed, inject, input, signal, type TemplateRef } from '@angular/core';\nimport {\n CdkDrag,\n CdkDragDrop,\n CdkDragHandle,\n CdkDropList,\n moveItemInArray,\n} from '@angular/cdk/drag-drop';\n\nimport { LucideArrowDown, LucideArrowUp, LucideGripVertical, LucideTrash2 } from '@lucide/angular';\nimport type { ListField } from '../../core/models/schema.model';\nimport { NotificationService } from '../../core/services/notification.service';\nimport { Button } from '../../shared/components/button/button';\nimport { colSpanClass } from '../../shared/utils/col-span';\nimport {\n appendPosition,\n computeMove,\n GAP,\n type PositionedItem,\n} from '../../shared/utils/gap-sorting';\nimport { seedFields } from '../../shared/utils/seed';\nimport { itemTree, listTree, type ListChildContext, type RootTree } from '../field-node';\n\n/**\n * Lista de ítems (base.md §6): asa de arrastre, subir/bajar, eliminar con\n * confirmación (sólo si el ítem tiene contenido) y agregar respetando\n * `min`/`max`.\n *\n * **Los hijos los renderiza el despachador**: este componente recibe el\n * `TemplateRef` (#childTpl, declarado en el `@case` de `field-host`) y lo\n * instancia una vez por ítem con `ngTemplateOutlet`. No importa FieldHost —\n * un import mutuo revienta en runtime (ciclo-check del repo).\n *\n * El orden de visualización ES el orden del arreglo modelo; `position`\n * (gap-sorting, base.md §8) se calcula junto con cada mutación y viaja en el\n * payload. Un hueco que deja un `remove` se autocura en el siguiente `move`.\n */\n@Component({\n selector: 'app-field-list',\n imports: [\n Button,\n CdkDrag,\n CdkDragHandle,\n CdkDropList,\n LucideArrowDown,\n LucideArrowUp,\n LucideGripVertical,\n LucideTrash2,\n NgTemplateOutlet,\n ],\n template: `\n @if (items().length === 0) {\n <p\n class=\"rounded-lg border border-dashed border-neutral/30 px-4 py-6 text-center text-sm text-neutral\"\n >\n {{ field().emptyText ?? 'Sin elementos.' }}\n </p>\n } @else {\n <div\n cdkDropList\n class=\"space-y-3\"\n [cdkDropListData]=\"items()\"\n [cdkDropListDisabled]=\"!sortable()\"\n (cdkDropListDropped)=\"onDrop($event)\"\n >\n @for (item of items(); track $index; let i = $index) {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-3\"\n cdkDrag\n [cdkDragDisabled]=\"!sortable()\"\n >\n <div class=\"mb-2 flex items-center gap-1.5\">\n @if (sortable()) {\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n class=\"cursor-grab\"\n cdkDragHandle\n [attr.aria-label]=\"'Reordenar elemento ' + (i + 1)\"\n title=\"Arrastrar para reordenar\"\n >\n <svg lucideGripVertical size=\"16\" />\n </button>\n }\n @if (isAccordion()) {\n <button\n type=\"button\"\n class=\"flex min-w-0 items-center gap-1 text-sm font-medium\"\n [attr.aria-expanded]=\"openOf(i)\"\n (click)=\"toggle(i)\"\n >\n <span aria-hidden=\"true\">{{ openOf(i) ? '▾' : '▸' }}</span>\n <span class=\"truncate\">{{ titleOf(i) }}</span>\n </button>\n }\n @if (isPrimary(i)) {\n <span\n class=\"rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary\"\n >\n Principal\n </span>\n }\n @if (hasProblem(i)) {\n <span class=\"text-xs font-medium text-danger\">Revisar</span>\n }\n <span class=\"ml-auto flex shrink-0 items-center gap-2 lg:gap-1\">\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"i === 0\"\n [attr.aria-label]=\"'Subir elemento ' + (i + 1)\"\n (click)=\"move(i, -1)\"\n >\n <svg lucideArrowUp size=\"16\" />\n </button>\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"i === items().length - 1\"\n [attr.aria-label]=\"'Bajar elemento ' + (i + 1)\"\n (click)=\"move(i, 1)\"\n >\n <svg lucideArrowDown size=\"16\" />\n </button>\n <button\n type=\"button\"\n app-button\n variant=\"ghost\"\n size=\"icon\"\n [disabled]=\"!canRemove()\"\n [attr.aria-label]=\"'Eliminar elemento ' + (i + 1)\"\n (click)=\"remove(i)\"\n >\n <svg lucideTrash2 size=\"16\" />\n </button>\n </span>\n </div>\n @if (!isAccordion() || openOf(i)) {\n @if (isRow()) {\n <div class=\"flex flex-wrap items-start gap-3\">\n <ng-container\n [ngTemplateOutlet]=\"childTemplate()\"\n [ngTemplateOutletContext]=\"childContext(i)\"\n />\n </div>\n } @else {\n <div class=\"grid grid-cols-1 gap-x-4 sm:grid-cols-12\">\n <ng-container\n [ngTemplateOutlet]=\"childTemplate()\"\n [ngTemplateOutletContext]=\"childContext(i)\"\n />\n </div>\n }\n }\n </div>\n }\n </div>\n }\n\n <div class=\"mt-3\">\n <button app-button variant=\"outline\" type=\"button\" [disabled]=\"!canAdd()\" (click)=\"add()\">\n + {{ field().addLabel ?? 'Agregar elemento' }}\n </button>\n </div>\n `,\n})\nexport class FieldList {\n readonly field = input.required<ListField>();\n readonly tree = input.required<RootTree>();\n readonly serverErrors = input<Record<string, string>>({});\n readonly pathPrefix = input('');\n readonly exists = input(false);\n /** Template de los hijos, declarado en el `@case` del despachador. */\n readonly childTemplate = input.required<TemplateRef<ListChildContext>>();\n\n private readonly notifications = inject(NotificationService);\n\n /** Intento de apertura por ítem (`undefined` → sigue el auto-open). */\n private readonly intent = signal<Record<number, boolean>>({});\n\n private readonly itemsTree = computed(() =>\n listTree<Record<string, unknown>>(this.tree(), this.field().key),\n );\n private readonly state = computed(() => this.itemsTree()());\n protected readonly items = computed(() => this.state().value() ?? []);\n\n protected readonly sortable = computed(() => this.field().sortable ?? true);\n /** Clase fija de la barra de ícono (literal: Tailwind v4 es JIT). */\n\n protected canAdd(): boolean {\n const max = this.field().max;\n return max === undefined || this.items().length < max;\n }\n\n protected canRemove(): boolean {\n return this.items().length > (this.field().min ?? 0);\n }\n\n protected isAccordion(): boolean {\n return this.field().itemDisplay === 'accordion';\n }\n\n protected isRow(): boolean {\n return this.field().itemDisplay === 'row';\n }\n\n protected isPrimary(index: number): boolean {\n return (this.field().primaryFirst ?? false) && index === 0;\n }\n\n protected titleOf(index: number): string {\n const item = this.items()[index];\n const key = this.field().itemTitle;\n const value = key !== undefined && item !== undefined ? item[key] : undefined;\n return typeof value === 'string' && value !== '' ? value : `#${index + 1}`;\n }\n\n // ── Ítem abierto (accordion) ──\n\n protected openOf(index: number): boolean {\n const forced = this.intent()[index];\n if (forced !== undefined) {\n return forced;\n }\n return this.hasProblem(index);\n }\n\n protected toggle(index: number): void {\n const willOpen = !this.openOf(index);\n this.intent.update((map) => ({ ...map, [index]: willOpen }));\n }\n\n /** Inválido+tocado (agrega hijos) o error del backend bajo `key[i].`. */\n hasProblem(index: number): boolean {\n const state = itemTree(this.itemsTree(), index)();\n if (state.invalid() && state.touched()) {\n return true;\n }\n const prefix = `${this.pathPrefix()}${this.field().key}[${index}].`;\n return Object.keys(this.serverErrors()).some((path) => path.startsWith(prefix));\n }\n\n // ── Mutaciones (todas sobre `controlValue` → marcan `dirty` solas) ──\n\n protected add(): void {\n if (!this.canAdd()) {\n return;\n }\n const item = seedFields(this.field().itemFields);\n const positionKey = this.field().positionField;\n if (positionKey !== undefined) {\n item[positionKey] = appendPosition(this.positioned());\n }\n const newIndex = this.items().length;\n this.updateItems((next) => {\n next.push(item);\n });\n // El ítem nuevo arranca visible.\n this.intent.update((map) => ({ ...map, [newIndex]: true }));\n }\n\n protected move(index: number, direction: -1 | 1): void {\n const target = index + direction;\n if (target < 0 || target >= this.items().length) {\n return;\n }\n this.applyMove(index, target);\n }\n\n protected onDrop(event: CdkDragDrop<Record<string, unknown>[]>): void {\n if (event.previousIndex === event.currentIndex) {\n return;\n }\n this.applyMove(event.previousIndex, event.currentIndex);\n }\n\n protected async remove(index: number): Promise<void> {\n if (!this.canRemove()) {\n return;\n }\n if (this.hasContent(index)) {\n const accepted = await this.notifications.confirm({\n title: `Eliminar elemento ${index + 1}`,\n message: 'Esta acción no se puede deshacer.',\n confirmLabel: 'Eliminar',\n cancelLabel: 'Cancelar',\n });\n if (!accepted) {\n return;\n }\n }\n this.updateItems((next) => {\n next.splice(index, 1);\n });\n this.intent.set({});\n }\n\n /**\n * Contexto fresco por ítem: los nodos se atan a la identidad (NG01904).\n *\n * `$implicit` lleva el objeto completo: en el `ng-template` del host,\n * `let-child` (sin alias) lee `$implicit` — `NgTemplateOutlet` reparte el\n * contexto como propiedades top-level, no existe un alias mágico.\n */\n protected childContext(index: number): ListChildContext & { $implicit: ListChildContext } {\n const context: ListChildContext = {\n fields: this.field().itemFields,\n tree: itemTree(this.itemsTree(), index),\n prefix: `${this.pathPrefix()}${this.field().key}[${index}].`,\n errors: this.serverErrors(),\n exists: this.exists(),\n row: this.isRow(),\n colSpan: colSpanClass,\n };\n return { ...context, $implicit: context };\n }\n\n /** Ítem con algo distinto de sus `default` → pide confirmación. */\n private hasContent(index: number): boolean {\n const item = this.items()[index];\n if (item === undefined) {\n return false;\n }\n return JSON.stringify(item) !== JSON.stringify(seedFields(this.field().itemFields));\n }\n\n /**\n * Mueve el ítem y recalcula su `position` (base.md §8): los cambios de\n * posición se aplican ANTES del splice porque las claves referencian los\n * índices originales.\n */\n private applyMove(from: number, to: number): void {\n const positionKey = this.field().positionField;\n this.updateItems((next) => {\n if (positionKey !== undefined) {\n const result = computeMove(this.positioned(), String(from), to);\n for (const change of result.changes) {\n const item = next[Number(change.key)];\n if (item !== undefined) {\n item[positionKey] = change.position;\n }\n }\n }\n moveItemInArray(next, from, to);\n });\n this.intent.set({});\n }\n\n /** Posiciones actuales; sin `positionField` no se calculan. */\n private positioned(): PositionedItem[] {\n const positionKey = this.field().positionField;\n return this.items().map((item, index) => {\n const raw = positionKey !== undefined ? item[positionKey] : undefined;\n // Fallback: un registro viejo sin `position` se numera como rebalanceo.\n return {\n key: String(index),\n position: typeof raw === 'number' ? raw : (index + 1) * GAP,\n };\n });\n }\n\n private updateItems(mutator: (next: Record<string, unknown>[]) => void): void {\n this.state().controlValue.update((current) => {\n const next = [...current];\n mutator(next);\n return next;\n });\n }\n}\n"],"mappings":";;;;;;;;;;AA6KA,IAAa,YAAb,MAAa,UAAS;CACX,QAAQ,MAAM,SAAqB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACnC,OAAO,MAAM,SAAoB,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACjC,eAAe,MAA8B,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAChD,aAAa,MAAM,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,aAAA,CAAA,+BAAA,CAAA,CAAA;CACtB,SAAS,MAAM,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAErB,gBAAgB,MAAM,SAAyC,GAAA,YAAA,CAAA,EAAA,WAAA,gBAAA,CAAA,+BAAA,CAAA,CAAA;CAEvD,gBAAgB,OAAO,mBAAmB;CAG1C,SAAS,OAAgC,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,SAAA,CAAA,+BAAA,CAAA,CAAA;CAE5C,YAAY,eAC3B,SAAkC,KAAK,KAAI,GAAI,KAAK,MAAK,CAAE,CAAC,GAAG,GAChE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CACgB,QAAQ,eAAe,KAAK,UAAS,CAAE,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACxC,QAAQ,eAAe,KAAK,MAAK,CAAE,CAAC,MAAK,KAAM,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAElD,WAAW,eAAe,KAAK,MAAK,CAAE,CAAC,YAAY,MAAK,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGjE,SAAM;EACd,MAAM,MAAM,KAAK,MAAK,CAAE,CAAC;EACzB,OAAO,QAAQ,UAAa,KAAK,MAAK,CAAE,CAAC,SAAS;CACpD;CAEU,YAAS;EACjB,OAAO,KAAK,MAAK,CAAE,CAAC,UAAU,KAAK,MAAK,CAAE,CAAC,OAAO;CACpD;CAEU,cAAW;EACnB,OAAO,KAAK,MAAK,CAAE,CAAC,gBAAgB;CACtC;CAEU,QAAK;EACb,OAAO,KAAK,MAAK,CAAE,CAAC,gBAAgB;CACtC;CAEU,UAAU,OAAa;EAC/B,QAAQ,KAAK,MAAK,CAAE,CAAC,gBAAgB,UAAU,UAAU;CAC3D;CAEU,QAAQ,OAAa;EAC7B,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC;EAC1B,MAAM,MAAM,KAAK,MAAK,CAAE,CAAC;EACzB,MAAM,QAAQ,QAAQ,UAAa,SAAS,SAAY,KAAK,OAAO;EACpE,OAAO,OAAO,UAAU,YAAY,UAAU,KAAK,QAAQ,IAAI,QAAQ;CACzE;CAIU,OAAO,OAAa;EAC5B,MAAM,SAAS,KAAK,OAAM,CAAE,CAAC;EAC7B,IAAI,WAAW,QACb,OAAO;EAET,OAAO,KAAK,WAAW,KAAK;CAC9B;CAEU,OAAO,OAAa;EAC5B,MAAM,WAAW,CAAC,KAAK,OAAO,KAAK;EACnC,KAAK,OAAO,QAAQ,SAAS;GAAE,GAAG;IAAM,QAAQ;EAAQ,EAAG;CAC7D;CAGA,WAAW,OAAa;EACtB,MAAM,QAAQ,SAAS,KAAK,UAAS,GAAI,KAAK,CAAC,CAAA;EAC/C,IAAI,MAAM,QAAO,KAAM,MAAM,QAAO,GAClC,OAAO;EAET,MAAM,SAAS,GAAG,KAAK,WAAU,IAAK,KAAK,MAAK,CAAE,CAAC,IAAG,GAAI,MAAK;EAC/D,OAAO,OAAO,KAAK,KAAK,aAAY,CAAE,CAAC,CAAC,MAAM,SAAS,KAAK,WAAW,MAAM,CAAC;CAChF;CAIU,MAAG;EACX,IAAI,CAAC,KAAK,OAAM,GACd;EAEF,MAAM,OAAO,WAAW,KAAK,MAAK,CAAE,CAAC,UAAU;EAC/C,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,IAAI,gBAAgB,QAClB,KAAK,eAAe,eAAe,KAAK,WAAU,CAAE;EAEtD,MAAM,WAAW,KAAK,MAAK,CAAE,CAAC;EAC9B,KAAK,aAAa,SAAQ;GACxB,KAAK,KAAK,IAAI;EAChB,CAAC;EAED,KAAK,OAAO,QAAQ,SAAS;GAAE,GAAG;IAAM,WAAW;EAAI,EAAG;CAC5D;CAEU,KAAK,OAAe,WAAiB;EAC7C,MAAM,SAAS,QAAQ;EACvB,IAAI,SAAS,KAAK,UAAU,KAAK,MAAK,CAAE,CAAC,QACvC;EAEF,KAAK,UAAU,OAAO,MAAM;CAC9B;CAEU,OAAO,OAA6C;EAC5D,IAAI,MAAM,kBAAkB,MAAM,cAChC;EAEF,KAAK,UAAU,MAAM,eAAe,MAAM,YAAY;CACxD;CAEU,MAAM,OAAO,OAAa;EAClC,IAAI,CAAC,KAAK,UAAS,GACjB;EAEF,IAAI,KAAK,WAAW,KAAK,GAOvB;OAAI,CAAC,MANkB,KAAK,cAAc,QAAQ;IAChD,OAAO,qBAAqB,QAAQ;IACpC,SAAS;IACT,cAAc;IACd,aAAa;GACd,CAAA,GAEC;EACF;EAEF,KAAK,aAAa,SAAQ;GACxB,KAAK,OAAO,OAAO,CAAC;EACtB,CAAC;EACD,KAAK,OAAO,IAAI,CAAA,CAAE;CACpB;CASU,aAAa,OAAa;EAClC,MAAM,UAA4B;GAChC,QAAQ,KAAK,MAAK,CAAE,CAAC;GACrB,MAAM,SAAS,KAAK,UAAS,GAAI,KAAK;GACtC,QAAQ,GAAG,KAAK,WAAU,IAAK,KAAK,MAAK,CAAE,CAAC,IAAG,GAAI,MAAK;GACxD,QAAQ,KAAK,aAAY;GACzB,QAAQ,KAAK,OAAM;GACnB,KAAK,KAAK,MAAK;GACf,SAAS;EACV;EACD,OAAO;GAAE,GAAG;GAAS,WAAW;EAAO;CACzC;CAGQ,WAAW,OAAa;EAC9B,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC;EAC1B,IAAI,SAAS,QACX,OAAO;EAET,OAAO,KAAK,UAAU,IAAI,MAAM,KAAK,UAAU,WAAW,KAAK,MAAK,CAAE,CAAC,UAAU,CAAC;CACpF;CAOQ,UAAU,MAAc,IAAU;EACxC,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,KAAK,aAAa,SAAQ;GACxB,IAAI,gBAAgB,QAAW;IAC7B,MAAM,SAAS,YAAY,KAAK,WAAU,GAAI,OAAO,IAAI,GAAG,EAAE;IAC9D,KAAK,MAAM,UAAU,OAAO,SAAS;KACnC,MAAM,OAAO,KAAK,OAAO,OAAO,GAAG;KACnC,IAAI,SAAS,QACX,KAAK,eAAe,OAAO;IAE/B;GACF;GACA,gBAAgB,MAAM,MAAM,EAAE;EAChC,CAAC;EACD,KAAK,OAAO,IAAI,CAAA,CAAE;CACpB;CAGQ,aAAU;EAChB,MAAM,cAAc,KAAK,MAAK,CAAE,CAAC;EACjC,OAAO,KAAK,MAAK,CAAE,CAAC,KAAK,MAAM,UAAS;GACtC,MAAM,MAAM,gBAAgB,SAAY,KAAK,eAAe;GAE5D,OAAO;IACL,KAAK,OAAO,KAAK;IACjB,UAAU,OAAO,QAAQ,WAAW,OAAO,QAAQ,KAAK;GACzD;EACH,CAAC;CACH;CAEQ,YAAY,SAAkD;EACpE,KAAK,MAAK,CAAE,CAAC,aAAa,QAAQ,YAAW;GAC3C,MAAM,OAAO,CAAC,GAAG,OAAO;GACxB,QAAQ,IAAI;GACZ,OAAO;EACT,CAAC;CACH;CAzMW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAS,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAT,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAS,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,cAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,YAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,QAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,eAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UA1HV;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwHT,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAlIC;IAAM,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACN;IAAO,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,SAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACP;IAAa,UAAA;IAAA,QAAA,CAAA,uBAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAW,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,SAAA;KAAA;KAAA;KAAA;KAAA;IAAA;IAAA,UAAA,CAAA,aAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACX;IAAe,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACf;IAAa,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACb;IAAkB,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAClB;IAAY,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MACZ;IAAgB,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;CAAA,CAAA;;AA4HP,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAS,YAAA,CAAA;EAvIrB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS;IACP;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACD;GACD,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyHX,CAAA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
-
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-
|
|
3
|
-
import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-
|
|
2
|
+
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs";
|
|
3
|
+
import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs";
|
|
4
4
|
import { t as optionsFromRows } from "./tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs";
|
|
5
5
|
import * as i0 from "@angular/core";
|
|
6
6
|
import { Component, computed, inject, input, model, resource } from "@angular/core";
|
|
@@ -248,4 +248,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
248
248
|
});
|
|
249
249
|
export { multiselect_exports as n, FieldMultiselect as t };
|
|
250
250
|
|
|
251
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-multiselect-
|
|
251
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-multiselect-
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/multiselect/multiselect.ts"],"sourcesContent":["import { Component, computed, inject, input, model, resource } from '@angular/core';\nimport { LucideX } from '@lucide/angular';\nimport type { FormValueControl } from '@angular/forms/signals';\n\nimport type { MultiselectField, SelectOption } from '../../core/models/schema.model';\nimport { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';\nimport { optionsFromRows } from '../../core/utils/source-options';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport type { RootTree } from '../field-node';\n\n/**\n * Selección múltiple con chips (base.md §5.1). `<select multiple>` no está\n * soportado por `[formField]`, así que es un control propio que cumple el\n * contrato `FormValueControl<string[]>`: el nodo llega desde el host con\n * `[formField]` y el valor se sincroniza por `model()`.\n *\n * Opciones: estáticas (`options`) o una fuente declarada (`source`), con la\n * misma regla XOR que el `select` y el mismo fetcher (F8 · R6.5). `tree`\n * existe para resolver `{campo}` en `source.params` con el registro del\n * formulario — sin él un multiselect con parámetros quedaría con el mismo\n * nombre de opción y otra regla que el select.\n */\n@Component({\n selector: 'app-field-multiselect',\n imports: [LucideX],\n template: `\n <div class=\"space-y-2\">\n @if (fallo()) {\n <p class=\"text-xs text-danger\" role=\"alert\">No se pudieron cargar las opciones.</p>\n } @else if (cargando()) {\n <p class=\"text-xs text-neutral\" role=\"status\">Cargando opciones…</p>\n }\n @if (value().length > 0) {\n <div class=\"flex flex-wrap gap-1.5\" data-testid=\"multiselect-chips\">\n @for (item of value(); track item) {\n <span\n class=\"inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs text-primary\"\n >\n {{ labelOf(item) }}\n <button\n type=\"button\"\n class=\"rounded-full px-0.5 hover:text-danger\"\n [attr.aria-label]=\"'Quitar ' + labelOf(item)\"\n [disabled]=\"disabled()\"\n (click)=\"remove(item)\"\n >\n <svg lucideX size=\"14\" />\n </button>\n </span>\n }\n </div>\n }\n <div class=\"flex flex-wrap gap-1.5\">\n @for (option of opciones(); track option.value) {\n @if (!isSelected(option.value)) {\n <button\n type=\"button\"\n class=\"rounded-full border border-neutral/30 px-2.5 py-1 text-xs hover:border-primary disabled:cursor-not-allowed disabled:opacity-50\"\n [disabled]=\"disabled() || (option.disabled ?? false)\"\n (click)=\"toggle(option)\"\n >\n {{ option.label }}\n </button>\n }\n }\n </div>\n </div>\n `,\n})\nexport class FieldMultiselect implements FormValueControl<string[]> {\n readonly field = input.required<MultiselectField>();\n\n /** Registro del formulario, sólo para resolver `{campo}` en `source.params`. */\n readonly tree = input<RootTree>();\n\n /** Sincronizado por el directive `FormField` del host. */\n readonly value = model.required<string[]>();\n readonly disabled = input(false);\n\n private readonly ds = inject(DataSourceService);\n\n private readonly registro = computed<Record<string, unknown> | undefined>(() => {\n const source = this.field().source;\n const tree = this.tree();\n if (source === undefined || source.params === undefined || tree === undefined) return undefined;\n return tree().value();\n });\n\n /** `undefined` sin fuente ⇒ `resource` queda idle y jamás llama al loader. */\n private readonly claveFuente = computed(() => {\n const source = this.field().source;\n return source === undefined ? undefined : dataSourceKey(source, this.registro());\n });\n\n private readonly fuenteResource = resource({\n params: () => this.claveFuente(),\n loader: async () => {\n const source = this.field().source;\n if (source === undefined) return [];\n return this.ds.rows(source, this.registro());\n },\n });\n\n /** Estáticas del schema o filas de la fuente declarada (F8 · R6.5). */\n protected readonly opciones = computed<SelectOption[]>(() => {\n const field = this.field();\n if (field.source === undefined) return field.options ?? [];\n // `value()` del resource TIRA en estado error (contract de Angular):\n // se mira el estado antes de leerlo, o el render rompe en vez de\n // pintar el aviso de fallo.\n if (this.fuenteResource.status() === 'error') return [];\n return optionsFromRows(this.fuenteResource.value() ?? [], {\n source: field.source,\n target: getSchema(field.source.resource),\n });\n });\n\n /** Sólo aplica con `source`: una lista estática no tiene vuelta que esperar. */\n protected readonly cargando = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'loading',\n );\n\n protected readonly fallo = computed(\n () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'error',\n );\n\n protected toggle(option: SelectOption): void {\n if (this.disabled()) {\n return;\n }\n this.value.update((current) =>\n current.includes(option.value)\n ? current.filter((item) => item !== option.value)\n : [...current, option.value],\n );\n }\n\n protected remove(item: string): void {\n if (this.disabled()) {\n return;\n }\n this.value.update((current) => current.filter((value) => value !== item));\n }\n\n protected isSelected(item: string): boolean {\n return this.value().includes(item);\n }\n\n protected labelOf(item: string): string {\n return this.opciones().find((option) => option.value === item)?.label ?? item;\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-
|
|
1
|
+
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CL4IZDce.mjs";
|
|
2
2
|
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
3
|
-
import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-
|
|
3
|
+
import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-vAi2DYL9.mjs";
|
|
4
4
|
import { n as FieldAria } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
|
|
5
5
|
import { t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
6
6
|
import { t as optionsFromRows } from "./tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs";
|
|
@@ -306,4 +306,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
306
306
|
});
|
|
307
307
|
export { FieldRelation };
|
|
308
308
|
|
|
309
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-relation-
|
|
309
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-relation-WCflawIU.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/relation/relation.ts"],"sourcesContent":["import { Component, computed, inject, input, resource, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport { LucidePlus } from '@lucide/angular';\n\nimport type { RelationField, SelectOption } from '../../core/models/schema.model';\nimport { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';\nimport { RelationDrawerService } from '../../core/services/relation-drawer.service';\nimport { optionsFromRows } from '../../core/utils/source-options';\nimport { Button } from '../../shared/components/button/button';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\n\n/**\n * Referencia a otro recurso (base.md §5.2): `<select>` con opciones del\n * listado del recurso destino, cargadas desde `field.source` (F8 · R6.5).\n *\n * La vuelta no la arma este componente: pasa por `DataSourceService`, que\n * centraliza la caché por `resource+view+params`. El `resource()` de Angular\n * de acá sólo aporta el estado (loading/error/recarga), y su `params` es la\n * clave de la fuente ya interpolada — así una relación con `params`\n * dependientes del formulario se recarga cuando cambia el dato, y una sin\n * `params` no se entera de las teclas.\n *\n * Más de 7 opciones → búsqueda encima del select (plan 6.1, guía §1). Se\n * filtra un `<select>` nativo y no se construye un combobox propio: conserva\n * la navegación con teclado del navegador, el valor seleccionado visible y\n * la lista de opciones accesible sin gestionar `aria-activedescendant`.\n */\n@Component({\n selector: 'app-field-relation',\n imports: [Button, FieldAria, FormField, LucidePlus, Skeleton],\n template: `\n @switch (optionsResource.status()) {\n @case ('loading') {\n <!-- El output es labelable: el for del label del host sigue teniendo\n ancla, y como ya es live region anuncia la carga. El skeleton\n reemplaza al texto con animate-pulse (plan 6.7). -->\n <output class=\"block\" [id]=\"state().name()\" aria-busy=\"true\">\n <app-skeleton [lines]=\"1\" [height]=\"40\" />\n <span class=\"sr-only\">Cargando opciones…</span>\n </output>\n }\n @case ('error') {\n <!-- Output en vez de un párrafo: es labelable (el label del host\n tiene ancla) y ya es live region, así que con role=alert el\n fallo de carga se anuncia sin repetir el texto en otro nodo. -->\n <output class=\"field-input block text-danger\" [id]=\"state().name()\" role=\"alert\">\n No se pudieron cargar las opciones.\n </output>\n }\n @default {\n @if (useSearch()) {\n <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. El filtro\n no reemplaza al select, lo acota; así el teclado y el valor\n elegido siguen funcionando igual. -->\n <input\n type=\"search\"\n class=\"mb-1 w-full rounded-lg border border-neutral/30 bg-surface px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n placeholder=\"Buscar…\"\n aria-label=\"Buscar entre las opciones\"\n [attr.aria-controls]=\"state().name()\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n />\n }\n <select class=\"field-input\" [id]=\"state().name()\" [formField]=\"node()\">\n @if (!field().required) {\n <option value=\"\">{{ field().placeholder ?? '— Seleccionar —' }}</option>\n }\n @for (option of visibleOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n @if (noMatches()) {\n <p class=\"mt-1 text-xs text-neutral\" role=\"status\">\n Sin resultados para «{{ query() }}».\n </p>\n } @else if (options().length === 0) {\n <p class=\"mt-1 text-xs text-neutral\">Sin opciones disponibles.</p>\n }\n @if (targetSchema() !== undefined && canCreate()) {\n <!-- Un <option> no puede contener un botón: la acción va justo\n debajo del control, pegada al dropdown. -->\n <button\n app-button\n variant=\"ghost\"\n type=\"button\"\n class=\"mt-1 -ml-2\"\n (click)=\"requestCreate()\"\n >\n <svg lucidePlus size=\"16\" />\n Crear nuevo\n </button>\n }\n }\n }\n `,\n})\nexport class FieldRelation {\n readonly field = input.required<RelationField>();\n readonly tree = input.required<RootTree>();\n\n private readonly ds = inject(DataSourceService);\n private readonly drawer = inject(RelationDrawerService);\n\n protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));\n protected readonly state = computed(() => this.node()());\n\n /** Texto escrito en el buscador (sólo se usa si supera el umbral). */\n protected readonly query = signal('');\n\n protected readonly targetSchema = computed(() => getSchema(this.field().source.resource));\n\n protected readonly canCreate = computed(() => this.targetSchema()?.permissions?.create !== false);\n\n /** Umbral de la guía §1: 5–7 opciones dropdown simple, más de 7 con búsqueda. */\n protected readonly useSearch = computed(() => this.options().length > 7);\n\n protected onQuery(event: Event): void {\n this.query.set((event.target as HTMLInputElement).value);\n }\n\n /**\n * Opciones que coinciden con el término de búsqueda, SIN conservar la\n * elegida. Es la base del mensaje «Sin resultados».\n */\n protected readonly matches = computed<SelectOption[]>(() => {\n const all = this.options();\n const term = this.query().trim().toLowerCase();\n if (term === '') {\n return all;\n }\n return all.filter((option) => option.label.toLowerCase().includes(term));\n });\n\n /** El buscador está activo y no encontró nada nuevo (el elegido no cuenta). */\n protected readonly noMatches = computed(\n () => this.useSearch() && this.query().trim() !== '' && this.matches().length === 0,\n );\n\n /**\n * Opciones que se dibujan. Con búsqueda activa se acotan por etiqueta,\n * PERO la opción elegida se conserva siempre: si desapareciera del DOM el\n * select dejaría de mostrar el valor actual (guía §1: «valor seleccionado\n * siempre visible»). Por eso no se dibuja a partir de `matches`.\n */\n protected readonly visibleOptions = computed<SelectOption[]>(() => {\n const filtered = this.matches();\n if (this.query().trim() === '') {\n return filtered;\n }\n const selected = String(this.state().value() ?? '');\n if (filtered.some((option) => option.value === selected)) {\n return filtered;\n }\n const kept = this.options().find((option) => option.value === selected);\n return kept === undefined ? filtered : [...filtered, kept];\n });\n\n /**\n * Pide el panel de «Crear nuevo». El servicio guarda el callback que, al\n * confirmarse la creación, recarga las opciones y preselecciona la clave.\n */\n protected requestCreate(): void {\n const resource = this.field().source.resource;\n this.drawer.open(resource, (key) => {\n // La fila nueva todavía no está en la caché compartida: sin este\n // `invalidate` la recarga devolvería la lista vieja (F8 · R6.5).\n this.ds.invalidate(resource);\n this.optionsResource.reload();\n // Se setea DESPUÉS de pedir la recarga: si la opción todavía no está\n // en el DOM el select mostraría vacío, y al llegar se reengancha sola\n // (visibleOptions conserva la elegida aunque el filtro la oculte).\n this.node()().value.set(key);\n });\n }\n\n /**\n * Registro del formulario, sólo para resolver `{campo}` en `source.params`.\n *\n * Si la fuente no declara parámetros el registro NO se lee: la relación\n * no depende del formulario y no se recalcula con cada tecla.\n */\n private readonly registro = computed<Record<string, unknown> | undefined>(() =>\n this.field().source.params === undefined ? undefined : this.tree()().value(),\n );\n\n /** Clave de la fuente ya interpolada: es lo que `resource` compara. */\n private readonly claveFuente = computed(() =>\n dataSourceKey(this.field().source, this.registro()),\n );\n\n /**\n * Carga de las opciones. `resource()` sólo aporta estado (loading/error/\n * recarga): la vuelta y la caché son del `DataSourceService`, que así\n * comparte filas con el resto de consumidores de la misma fuente.\n */\n private readonly optionsResource = resource({\n params: () => this.claveFuente(),\n loader: async () => this.ds.rows(this.field().source, this.registro()),\n });\n\n /**\n * Opciones (F8): filas de la fuente declarada → `SelectOption`.\n *\n * El `@switch` del template sólo entra en `default` cuando no hubo fallo,\n * pero el estado se mira acá también: `value()` del resource TIRA en\n * estado error (contract de Angular) y un computed que se evalúe por otra\n * vía tiraría el render entero.\n */\n protected readonly options = computed<SelectOption[]>(() =>\n this.optionsResource.status() === 'error'\n ? []\n : optionsFromRows(this.optionsResource.value() ?? [], {\n source: this.field().source,\n target: this.targetSchema(),\n onlyActive: this.field().onlyActive,\n secondaryField: this.field().secondaryField,\n }),\n );\n}\n"],"mappings":";;;;;;;;;;;;AAqGA,IAAa,gBAAb,MAAa,cAAa;CACf,QAAQ,MAAM,SAAyB,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CACvC,OAAO,MAAM,SAAoB,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CAEzB,KAAK,OAAO,iBAAiB;CAC7B,SAAS,OAAO,qBAAqB;CAEnC,OAAO,eAAe,UAAkB,KAAK,KAAI,GAAI,KAAK,MAAK,CAAE,CAAC,GAAG,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,OAAA,CAAA,+BAAA,CAAA,CAAA;CACvE,QAAQ,eAAe,KAAK,KAAI,CAAE,CAAA,GAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAGrC,QAAQ,OAAO,IAAG,GAAA,YAAA,CAAA,EAAA,WAAA,QAAA,CAAA,+BAAA,CAAA,CAAA;CAElB,eAAe,eAAe,UAAU,KAAK,MAAK,CAAE,CAAC,OAAO,QAAQ,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,eAAA,CAAA,+BAAA,CAAA,CAAA;CAEtE,YAAY,eAAe,KAAK,aAAY,CAAE,EAAE,aAAa,WAAW,OAAM,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAG9E,YAAY,eAAe,KAAK,QAAO,CAAE,CAAC,SAAS,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAE9D,QAAQ,OAAY;EAC5B,KAAK,MAAM,IAAK,MAAM,OAA4B,KAAK;CACzD;CAMmB,UAAU,eAA8B;EACzD,MAAM,MAAM,KAAK,QAAO;EACxB,MAAM,OAAO,KAAK,MAAK,CAAE,CAAC,KAAI,CAAE,CAAC,YAAW;EAC5C,IAAI,SAAS,IACX,OAAO;EAET,OAAO,IAAI,QAAQ,WAAW,OAAO,MAAM,YAAW,CAAE,CAAC,SAAS,IAAI,CAAC;CACzE,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAGiB,YAAY,eACvB,KAAK,UAAS,KAAM,KAAK,MAAK,CAAE,CAAC,KAAI,MAAO,MAAM,KAAK,QAAO,CAAE,CAAC,WAAW,GACnF,GAAA,YAAA,CAAA,EAAA,WAAA,YAAA,CAAA,+BAAA,CAAA,CAAA;CAQkB,iBAAiB,eAA8B;EAChE,MAAM,WAAW,KAAK,QAAO;EAC7B,IAAI,KAAK,MAAK,CAAE,CAAC,KAAI,MAAO,IAC1B,OAAO;EAET,MAAM,WAAW,OAAO,KAAK,MAAK,CAAE,CAAC,MAAK,KAAM,EAAE;EAClD,IAAI,SAAS,MAAM,WAAW,OAAO,UAAU,QAAQ,GACrD,OAAO;EAET,MAAM,OAAO,KAAK,QAAO,CAAE,CAAC,MAAM,WAAW,OAAO,UAAU,QAAQ;EACtE,OAAO,SAAS,SAAY,WAAW,CAAC,GAAG,UAAU,IAAI;CAC3D,GAAE,GAAA,YAAA,CAAA,EAAA,WAAA,iBAAA,CAAA,+BAAA,CAAA,CAAA;CAMQ,gBAAa;EACrB,MAAM,WAAW,KAAK,MAAK,CAAE,CAAC,OAAO;EACrC,KAAK,OAAO,KAAK,WAAW,QAAO;GAGjC,KAAK,GAAG,WAAW,QAAQ;GAC3B,KAAK,gBAAgB,OAAM;GAI3B,KAAK,KAAI,CAAE,CAAA,CAAE,CAAC,MAAM,IAAI,GAAG;EAC7B,CAAC;CACH;CAQiB,WAAW,eAC1B,KAAK,MAAK,CAAE,CAAC,OAAO,WAAW,SAAY,SAAY,KAAK,KAAI,CAAE,CAAA,CAAE,CAAC,MAAK,GAC3E,GAAA,YAAA,CAAA,EAAA,WAAA,WAAA,CAAA,+BAAA,CAAA,CAAA;CAGgB,cAAc,eAC7B,cAAc,KAAK,MAAK,CAAE,CAAC,QAAQ,KAAK,SAAQ,CAAE,GACnD,GAAA,YAAA,CAAA,EAAA,WAAA,cAAA,CAAA,+BAAA,CAAA,CAAA;CAOgB,kBAAkB,SAAQ;EAAA,GAAA,YAAA,EAAA,WAAA,kBAAA,+BAAA,CAAA;EACzC,cAAc,KAAK,YAAW;EAC9B,QAAQ,YAAY,KAAK,GAAG,KAAK,KAAK,MAAK,CAAE,CAAC,QAAQ,KAAK,SAAQ,CAAE;CAAC,CAAA;CAWrD,UAAU,eAC3B,KAAK,gBAAgB,OAAM,MAAO,UAC9B,CAAA,IACA,gBAAgB,KAAK,gBAAgB,MAAK,KAAM,CAAA,GAAI;EAClD,QAAQ,KAAK,MAAK,CAAE,CAAC;EACrB,QAAQ,KAAK,aAAY;EACzB,YAAY,KAAK,MAAK,CAAE,CAAC;EACzB,gBAAgB,KAAK,MAAK,CAAE,CAAC;CAC9B,CAAA,GACN,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CAzHU,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAa,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAb,OAAA,OAAA,GAAA,qBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,MAAA;EAAa,cAAA;EAAA,UAAA;EAAA,QAAA;GAAA,OAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;GAAA,MAAA;IAAA,mBAAA;IAAA,YAAA;IAAA,UAAA;IAAA,YAAA;IAAA,mBAAA;GAAA;EAAA;EAAA,UAAA;EAAA,UAnEd;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAiET,UAAA;EAAA,cAAA;GAAA;IAAA,MAAA;IAAA,MAlES;IAAM,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;KAAA;KAAA;IAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAS,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAS,UAAA;IAAA,QAAA,CAAA,WAAA;IAAA,UAAA,CAAA,WAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAU,UAAA;GAAA;GAAA;IAAA,MAAA;IAAA,MAAE;IAAQ,UAAA;IAAA,QAAA;KAAA;KAAA;KAAA;IAAA;GAAA;EAAA;CAAA,CAAA;;AAoEjD,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA;EAtEzB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS;IAAC;IAAQ;IAAW;IAAW;IAAY;GAAQ;GAC5D,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAkEX,CAAA"}
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-relation-WCflawIU.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/relation/relation.ts"],"sourcesContent":["import { Component, computed, inject, input, resource, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport { LucidePlus } from '@lucide/angular';\n\nimport type { RelationField, SelectOption } from '../../core/models/schema.model';\nimport { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';\nimport { RelationDrawerService } from '../../core/services/relation-drawer.service';\nimport { optionsFromRows } from '../../core/utils/source-options';\nimport { Button } from '../../shared/components/button/button';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\n\n/**\n * Referencia a otro recurso (base.md §5.2): `<select>` con opciones del\n * listado del recurso destino, cargadas desde `field.source` (F8 · R6.5).\n *\n * La vuelta no la arma este componente: pasa por `DataSourceService`, que\n * centraliza la caché por `resource+view+params`. El `resource()` de Angular\n * de acá sólo aporta el estado (loading/error/recarga), y su `params` es la\n * clave de la fuente ya interpolada — así una relación con `params`\n * dependientes del formulario se recarga cuando cambia el dato, y una sin\n * `params` no se entera de las teclas.\n *\n * Más de 7 opciones → búsqueda encima del select (plan 6.1, guía §1). Se\n * filtra un `<select>` nativo y no se construye un combobox propio: conserva\n * la navegación con teclado del navegador, el valor seleccionado visible y\n * la lista de opciones accesible sin gestionar `aria-activedescendant`.\n */\n@Component({\n selector: 'app-field-relation',\n imports: [Button, FieldAria, FormField, LucidePlus, Skeleton],\n template: `\n @switch (optionsResource.status()) {\n @case ('loading') {\n <!-- El output es labelable: el for del label del host sigue teniendo\n ancla, y como ya es live region anuncia la carga. El skeleton\n reemplaza al texto con animate-pulse (plan 6.7). -->\n <output class=\"block\" [id]=\"state().name()\" aria-busy=\"true\">\n <app-skeleton [lines]=\"1\" [height]=\"40\" />\n <span class=\"sr-only\">Cargando opciones…</span>\n </output>\n }\n @case ('error') {\n <!-- Output en vez de un párrafo: es labelable (el label del host\n tiene ancla) y ya es live region, así que con role=alert el\n fallo de carga se anuncia sin repetir el texto en otro nodo. -->\n <output class=\"field-input block text-danger\" [id]=\"state().name()\" role=\"alert\">\n No se pudieron cargar las opciones.\n </output>\n }\n @default {\n @if (useSearch()) {\n <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. El filtro\n no reemplaza al select, lo acota; así el teclado y el valor\n elegido siguen funcionando igual. -->\n <input\n type=\"search\"\n class=\"mb-1 w-full rounded-lg border border-neutral/30 bg-surface px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n placeholder=\"Buscar…\"\n aria-label=\"Buscar entre las opciones\"\n [attr.aria-controls]=\"state().name()\"\n [value]=\"query()\"\n (input)=\"onQuery($event)\"\n />\n }\n <select class=\"field-input\" [id]=\"state().name()\" [formField]=\"node()\">\n @if (!field().required) {\n <option value=\"\">{{ field().placeholder ?? '— Seleccionar —' }}</option>\n }\n @for (option of visibleOptions(); track option.value) {\n <option [value]=\"option.value\">{{ option.label }}</option>\n }\n </select>\n @if (noMatches()) {\n <p class=\"mt-1 text-xs text-neutral\" role=\"status\">\n Sin resultados para «{{ query() }}».\n </p>\n } @else if (options().length === 0) {\n <p class=\"mt-1 text-xs text-neutral\">Sin opciones disponibles.</p>\n }\n @if (targetSchema() !== undefined && canCreate()) {\n <!-- Un <option> no puede contener un botón: la acción va justo\n debajo del control, pegada al dropdown. -->\n <button\n app-button\n variant=\"ghost\"\n type=\"button\"\n class=\"mt-1 -ml-2\"\n (click)=\"requestCreate()\"\n >\n <svg lucidePlus size=\"16\" />\n Crear nuevo\n </button>\n }\n }\n }\n `,\n})\nexport class FieldRelation {\n readonly field = input.required<RelationField>();\n readonly tree = input.required<RootTree>();\n\n private readonly ds = inject(DataSourceService);\n private readonly drawer = inject(RelationDrawerService);\n\n protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));\n protected readonly state = computed(() => this.node()());\n\n /** Texto escrito en el buscador (sólo se usa si supera el umbral). */\n protected readonly query = signal('');\n\n protected readonly targetSchema = computed(() => getSchema(this.field().source.resource));\n\n protected readonly canCreate = computed(() => this.targetSchema()?.permissions?.create !== false);\n\n /** Umbral de la guía §1: 5–7 opciones dropdown simple, más de 7 con búsqueda. */\n protected readonly useSearch = computed(() => this.options().length > 7);\n\n protected onQuery(event: Event): void {\n this.query.set((event.target as HTMLInputElement).value);\n }\n\n /**\n * Opciones que coinciden con el término de búsqueda, SIN conservar la\n * elegida. Es la base del mensaje «Sin resultados».\n */\n protected readonly matches = computed<SelectOption[]>(() => {\n const all = this.options();\n const term = this.query().trim().toLowerCase();\n if (term === '') {\n return all;\n }\n return all.filter((option) => option.label.toLowerCase().includes(term));\n });\n\n /** El buscador está activo y no encontró nada nuevo (el elegido no cuenta). */\n protected readonly noMatches = computed(\n () => this.useSearch() && this.query().trim() !== '' && this.matches().length === 0,\n );\n\n /**\n * Opciones que se dibujan. Con búsqueda activa se acotan por etiqueta,\n * PERO la opción elegida se conserva siempre: si desapareciera del DOM el\n * select dejaría de mostrar el valor actual (guía §1: «valor seleccionado\n * siempre visible»). Por eso no se dibuja a partir de `matches`.\n */\n protected readonly visibleOptions = computed<SelectOption[]>(() => {\n const filtered = this.matches();\n if (this.query().trim() === '') {\n return filtered;\n }\n const selected = String(this.state().value() ?? '');\n if (filtered.some((option) => option.value === selected)) {\n return filtered;\n }\n const kept = this.options().find((option) => option.value === selected);\n return kept === undefined ? filtered : [...filtered, kept];\n });\n\n /**\n * Pide el panel de «Crear nuevo». El servicio guarda el callback que, al\n * confirmarse la creación, recarga las opciones y preselecciona la clave.\n */\n protected requestCreate(): void {\n const resource = this.field().source.resource;\n this.drawer.open(resource, (key) => {\n // La fila nueva todavía no está en la caché compartida: sin este\n // `invalidate` la recarga devolvería la lista vieja (F8 · R6.5).\n this.ds.invalidate(resource);\n this.optionsResource.reload();\n // Se setea DESPUÉS de pedir la recarga: si la opción todavía no está\n // en el DOM el select mostraría vacío, y al llegar se reengancha sola\n // (visibleOptions conserva la elegida aunque el filtro la oculte).\n this.node()().value.set(key);\n });\n }\n\n /**\n * Registro del formulario, sólo para resolver `{campo}` en `source.params`.\n *\n * Si la fuente no declara parámetros el registro NO se lee: la relación\n * no depende del formulario y no se recalcula con cada tecla.\n */\n private readonly registro = computed<Record<string, unknown> | undefined>(() =>\n this.field().source.params === undefined ? undefined : this.tree()().value(),\n );\n\n /** Clave de la fuente ya interpolada: es lo que `resource` compara. */\n private readonly claveFuente = computed(() =>\n dataSourceKey(this.field().source, this.registro()),\n );\n\n /**\n * Carga de las opciones. `resource()` sólo aporta estado (loading/error/\n * recarga): la vuelta y la caché son del `DataSourceService`, que así\n * comparte filas con el resto de consumidores de la misma fuente.\n */\n private readonly optionsResource = resource({\n params: () => this.claveFuente(),\n loader: async () => this.ds.rows(this.field().source, this.registro()),\n });\n\n /**\n * Opciones (F8): filas de la fuente declarada → `SelectOption`.\n *\n * El `@switch` del template sólo entra en `default` cuando no hubo fallo,\n * pero el estado se mira acá también: `value()` del resource TIRA en\n * estado error (contract de Angular) y un computed que se evalúe por otra\n * vía tiraría el render entero.\n */\n protected readonly options = computed<SelectOption[]>(() =>\n this.optionsResource.status() === 'error'\n ? []\n : optionsFromRows(this.optionsResource.value() ?? [], {\n source: this.field().source,\n target: this.targetSchema(),\n onlyActive: this.field().onlyActive,\n secondaryField: this.field().secondaryField,\n }),\n );\n}\n"],"mappings":"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"}
|
|
@@ -103,7 +103,7 @@ const descriptor$22 = {
|
|
|
103
103
|
kind: "field",
|
|
104
104
|
type: "file",
|
|
105
105
|
since: "9.5",
|
|
106
|
-
load: () => import("./tinya-code-gas-admin-ui-file-
|
|
106
|
+
load: () => import("./tinya-code-gas-admin-ui-file-CKh878UH.mjs").then((n) => n.n).then((module) => module.FieldFile),
|
|
107
107
|
seed: "string",
|
|
108
108
|
surfaces: ["form", "display"]
|
|
109
109
|
};
|
|
@@ -126,7 +126,7 @@ const descriptor$20 = {
|
|
|
126
126
|
kind: "field",
|
|
127
127
|
type: "image",
|
|
128
128
|
since: "1.0",
|
|
129
|
-
load: () => import("./tinya-code-gas-admin-ui-image-
|
|
129
|
+
load: () => import("./tinya-code-gas-admin-ui-image-BPfPNwru.mjs").then((n) => n.n).then((module) => module.FieldImage),
|
|
130
130
|
seed: "string",
|
|
131
131
|
surfaces: ["form", "display"]
|
|
132
132
|
};
|
|
@@ -142,7 +142,7 @@ const descriptor$18 = {
|
|
|
142
142
|
kind: "field",
|
|
143
143
|
type: "list",
|
|
144
144
|
since: "1.0",
|
|
145
|
-
load: () => import("./tinya-code-gas-admin-ui-list-
|
|
145
|
+
load: () => import("./tinya-code-gas-admin-ui-list-BKr33pde.mjs").then((module) => module.FieldList),
|
|
146
146
|
seed: "array",
|
|
147
147
|
surfaces: ["form", "display"],
|
|
148
148
|
inputs: (config, extras) => ({
|
|
@@ -158,7 +158,7 @@ const descriptor$17 = {
|
|
|
158
158
|
kind: "field",
|
|
159
159
|
type: "multiselect",
|
|
160
160
|
since: "1.0",
|
|
161
|
-
load: () => import("./tinya-code-gas-admin-ui-multiselect-
|
|
161
|
+
load: () => import("./tinya-code-gas-admin-ui-multiselect-SfZJHfbh.mjs").then((n) => n.n).then((module) => module.FieldMultiselect),
|
|
162
162
|
seed: "array",
|
|
163
163
|
surfaces: ["form", "display"]
|
|
164
164
|
};
|
|
@@ -190,7 +190,7 @@ const descriptor$13 = {
|
|
|
190
190
|
kind: "field",
|
|
191
191
|
type: "relation",
|
|
192
192
|
since: "1.0",
|
|
193
|
-
load: () => import("./tinya-code-gas-admin-ui-relation-
|
|
193
|
+
load: () => import("./tinya-code-gas-admin-ui-relation-WCflawIU.mjs").then((module) => module.FieldRelation),
|
|
194
194
|
seed: "string",
|
|
195
195
|
surfaces: ["form", "display"]
|
|
196
196
|
};
|
|
@@ -198,7 +198,7 @@ const descriptor$12 = {
|
|
|
198
198
|
kind: "field",
|
|
199
199
|
type: "select",
|
|
200
200
|
since: "1.0",
|
|
201
|
-
load: () => import("./tinya-code-gas-admin-ui-select-
|
|
201
|
+
load: () => import("./tinya-code-gas-admin-ui-select-BaF4kSXl.mjs").then((module) => module.FieldSelect),
|
|
202
202
|
seed: "string",
|
|
203
203
|
surfaces: ["form", "display"]
|
|
204
204
|
};
|
|
@@ -460,7 +460,7 @@ const viewDescriptors = [
|
|
|
460
460
|
kind: "view",
|
|
461
461
|
type: "form",
|
|
462
462
|
since: "9.1",
|
|
463
|
-
load: async () => (await import("./tinya-code-gas-admin-ui-form-view-
|
|
463
|
+
load: async () => (await import("./tinya-code-gas-admin-ui-form-view-_LpdZb15.mjs").then((n) => n.n)).FormView
|
|
464
464
|
},
|
|
465
465
|
{
|
|
466
466
|
kind: "view",
|
|
@@ -472,13 +472,13 @@ const viewDescriptors = [
|
|
|
472
472
|
kind: "view",
|
|
473
473
|
type: "calendar",
|
|
474
474
|
since: "9.8",
|
|
475
|
-
load: async () => (await import("./tinya-code-gas-admin-ui-calendar-view-
|
|
475
|
+
load: async () => (await import("./tinya-code-gas-admin-ui-calendar-view-CquWGzw7.mjs")).CalendarView
|
|
476
476
|
},
|
|
477
477
|
{
|
|
478
478
|
kind: "view",
|
|
479
479
|
type: "kanban",
|
|
480
480
|
since: "9.8",
|
|
481
|
-
load: async () => (await import("./tinya-code-gas-admin-ui-kanban-view-
|
|
481
|
+
load: async () => (await import("./tinya-code-gas-admin-ui-kanban-view-C-w9mhiB.mjs")).KanbanView
|
|
482
482
|
},
|
|
483
483
|
{
|
|
484
484
|
kind: "view",
|
|
@@ -543,4 +543,4 @@ function renderKinds() {
|
|
|
543
543
|
}
|
|
544
544
|
export { register as a, unregister as c, SUPPORTED_FIELD_TYPE_LIST as d, fieldDescriptors as f, provideRenderDescriptors as i, viewDescriptors as l, columnFormatDescriptors as m, get as n, renderKinds as o, COLUMN_FORMAT_TYPES as p, isSupported as r, resolve as s, RENDER_CONTEXT_VACIO as t, layoutDescriptors as u };
|
|
545
545
|
|
|
546
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-render-registry-
|
|
546
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-render-registry-ghv7Mb1x.mjs.map
|