@tinya-code/gas-admin-ui 0.2.0-alpha.1 → 0.2.0-alpha.11
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 +300 -2
- package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs → tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs} +12 -7
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs → tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs → tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map → tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map} +1 -1
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +122 -0
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs → tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs → tinya-code-gas-admin-ui-file-Dmxioog7.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map → tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs → tinya-code-gas-admin-ui-form-view-C709rc57.mjs} +13 -10
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs → tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-image-lUe8nrUf.mjs.map → tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs → tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs} +4 -4
- package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs.map → tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs → tinya-code-gas-admin-ui-list-C8ijJHAx.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-list-CH7b7dKh.mjs.map → tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs → tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs.map → tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs → tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs} +3 -3
- package/fesm2022/{tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs.map → tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs → tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs} +59 -29
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +161 -0
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs → tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs} +249 -58
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs → tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs.map → tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map} +1 -1
- package/fesm2022/{tinya-code-gas-admin-ui-select-CL2kj7pM.mjs → tinya-code-gas-admin-ui-select-CDwX9yNO.mjs} +57 -53
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +1 -0
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs → tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs} +2 -2
- package/fesm2022/{tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs.map → tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map} +1 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +98 -46
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
- package/package.json +1 -1
- package/types/tinya-code-gas-admin-ui.d.ts +385 -51
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-select-CL2kj7pM.mjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-condition-evaluator-
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/view-navigation/view-navigation.service.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/condition-evaluator.ts"],"sourcesContent":["import { Injectable, inject } from '@angular/core';\nimport { Router, type ActivatedRouteSnapshot } from '@angular/router';\n\nimport type { NavigationTarget, SelectionBinding } from '../models/schema.model';\nimport { getSchema } from '../registry/schema-catalog';\nimport type { SelectionEvent } from '../../views/composer/view-context';\n\n/**\n * R9.9 · D4 — Servicio de navegación declarativa.\n *\n * Un leaf (hoja) sólo NAVEGA cuando hay `select` declarado; el motor vive\n * aquí, no en cada hoja. El flujo completo es:\n *\n * `ctx.emit/leaf → dispatch → buildSegments(pattern, {key}) →\n * router.navigate(matriz de comandos) → roleGuard`\n *\n * Invariantes:\n *\n * - **key-only**: la ÚNICA identidad que viaja es `event.key`. Los comandos\n * son una matriz de segmentos (nunca concat de strings) ⇒ el Router\n * codifica la clave (una clave `A/B` queda `A%2FB` en la URL) y ningún\n * dato del registro llega a la URL.\n * - **Fail closed**: sin binding, con acción distinta a la declarada o con\n * `mode` distinto de `route`, NO hay navegación — simplemente no ocurre.\n * - **Fail loud**: un target que el registry no declara lanza ruido antes de\n * navegar (el validador pre-render ya lo garantiza; esto es defensa en\n * profundidad).\n * - **Guards reales**: `router.navigate` ⇒ `roleGuard` corre igual que en\n * cualquier navegación declarada. Si el guard bloquea, no se registra\n * origen (no hay «atrás» que una sesión sin permiso deba volver a ver).\n */\n\n/**\n * Patrón de segmentos de la ruta destino (`['orders', ':key']`). Usa la\n * MISMA fuente que el validador (`getSchema`) para que lo validado en build\n * sea exactamente lo navegado en runtime. Inexistente ⇒ `undefined` (la\n * defensa en profundidad: el validador pre-render garantiza que esto no\n * ocurre en schemas válidos).\n */\nexport function targetRoutePattern(target: NavigationTarget): string[] | undefined {\n const destino = getSchema(target.resource);\n const vista = destino?.uiViews?.[target.view];\n if (!destino || !vista) return undefined;\n const relativa = vista.path ?? '';\n return [destino.id, ...(relativa === '' ? [] : relativa.split('/'))];\n}\n\n/**\n * Segmentos de la ruta destino con la clave sustituida. La clave es SIEMPRE\n * un segmento completo (no se parte en `/`); un param distinto de `:key` no\n * tiene fuente declarada ⇒ falla ruido, no navega a medias.\n */\nexport function buildSegments(pattern: readonly string[], key: string): string[] {\n const commands: string[] = [];\n for (const segmento of pattern) {\n if (segmento.startsWith(':')) {\n const nombre = segmento.slice(1);\n if (nombre !== 'key') {\n throw new Error(`Destino ':${nombre}' sin fuente declarada en SelectionEvent`);\n }\n commands.push(key);\n } else {\n commands.push(segmento);\n }\n }\n return commands;\n}\n\n@Injectable({ providedIn: 'root' })\nexport class ViewNavigationService {\n private readonly router = inject(Router);\n\n /** URL desde la que se navegó la última selección válida (origen del «atrás»). */\n private origen: string | undefined;\n\n /**\n * Transición declarada de un leaf. Inerte si no hay binding, si la acción\n * no es la declarada o si el modo no es `route`; falla ruido si el target\n * no existe en el registry.\n *\n * @returns `true` si el Router completó la navegación (guards aceptados).\n */\n async dispatch(binding: SelectionBinding | undefined, event: SelectionEvent): Promise<boolean> {\n if (binding === undefined) return false; // sin declaración ⇒ inerte\n if (binding.on !== event.action) return false; // fuera del scope declarado\n if (binding.mode !== 'route') return false; // comportamiento local del leaf\n\n const pattern = targetRoutePattern(binding.target);\n if (pattern === undefined) {\n throw new Error(\n `Destino '${binding.target.resource}/${binding.target.view}' no existe en el registry`,\n );\n }\n\n const urlOrigen = this.router.url;\n const ok = await this.router.navigate(['/', ...buildSegments(pattern, event.key)]);\n if (ok) this.origen = urlOrigen;\n return ok;\n }\n\n /**\n * Forma de volver (D4). Orden: `returnPath` declarado por el leaf ⇒\n * origen registrado por `dispatch` ⇒ raíz del recurso declarado en la ruta\n * actual (entrada directa a la URL: no hay origen en memoria). Fallback\n * final: la raíz de la aplicación.\n */\n async back(returnPath?: string): Promise<boolean> {\n if (returnPath !== undefined) return this.router.navigate([returnPath]);\n\n if (this.origen !== undefined) {\n const destino = this.origen;\n this.origen = undefined;\n return this.router.navigateByUrl(destino);\n }\n\n const recurso = this.recursoActual();\n return recurso === undefined\n ? this.router.navigate(['/'])\n : this.router.navigate(['/', recurso]);\n }\n\n /** Primer `resourceId` declarado en el árbol de rutas activo. */\n private recursoActual(): string | undefined {\n let nodo: ActivatedRouteSnapshot | null = this.router.routerState.snapshot.root;\n while (nodo !== null) {\n const id = nodo.data?.['resourceId'];\n if (id !== undefined) return String(id);\n nodo = nodo.firstChild;\n }\n return undefined;\n }\n}\n","import type { FieldCondition } from '../../core/models/schema.model';\n\n/**\n * Evaluador de condiciones `visibleWhen` / `readonlyWhen` (base.md §4):\n * comparaciones simples (igual, distinto, está vacío, contiene) sobre otros\n * campos del mismo nivel o del registro raíz.\n *\n * Nota de arquitectura: la importación de `FieldCondition` es SOLO de tipo\n * (se elimina en compilación) — no hay dependencia de runtime con `core`.\n */\n\n/** Resuelve una condición. Se resuelve contra el nivel propio; si la clave\n * no existe ahí, contra la raíz del registro. */\nexport function evaluateCondition(\n condition: FieldCondition,\n siblingValues: Record<string, unknown>,\n rootValues: Record<string, unknown> = {},\n): boolean {\n const source = condition.field in siblingValues ? siblingValues : rootValues;\n const actual = source[condition.field];\n const expected = condition.value;\n\n switch (condition.operator) {\n case 'eq':\n return actual === expected;\n case 'ne':\n return actual !== expected;\n case 'empty':\n return isEmpty(actual);\n case 'contains':\n return contains(actual, expected);\n }\n}\n\nfunction isEmpty(value: unknown): boolean {\n if (value === undefined || value === null || value === '') {\n return true;\n }\n if (Array.isArray(value)) {\n return value.length === 0;\n }\n return false;\n}\n\nfunction contains(actual: unknown, expected: unknown): boolean {\n if (typeof actual === 'string') {\n return actual.includes(String(expected));\n }\n if (Array.isArray(actual)) {\n return actual.includes(expected);\n }\n return false;\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-
|
|
1
|
+
import { i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
|
|
2
2
|
import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
|
|
3
3
|
import * as i0 from "@angular/core";
|
|
4
4
|
import { Service, inject } from "@angular/core";
|
|
@@ -85,4 +85,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
85
85
|
});
|
|
86
86
|
export { dataSourceKey as n, DataSourceService as t };
|
|
87
87
|
|
|
88
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-
|
|
88
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-data-source.service-
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/data-source.service.ts"],"sourcesContent":["import { inject, Service } from '@angular/core';\nimport { firstValueFrom } from 'rxjs';\n\nimport type { DataSource } from '../models/schema.model';\nimport { interpolateTemplate } from '../utils/template-interpolator';\nimport { getSchema } from '../registry/schema-catalog';\nimport { ApiService } from './api.service';\n\n/** Fila cruda de un listado: el motor no impone forma a las columnas. */\nexport type SourceRow = Record<string, unknown>;\n\n/**\n * Cuánto se reutiliza una respuesta pedida igual (F8 · R6.5).\n *\n * Un panel de administración lee mucho y escribe poco: medio minuto de\n * frescura cubre un recorrido por varios formularios sin repetir la misma\n * vuelta. Es una decisión de UX, no de contrato — `invalidate()` la salta\n * cuando el usuario acaba de crear lo que la fuente lista.\n */\nconst TTL_MS = 30_000;\n\n/** Techo de claves cacheadas: un Map sin límite es una fuga en una sesión larga. */\nconst MAX_CLAVES = 100;\n\n/**\n * `source.params` con cada `{campo}` resuelto contra `ctx`.\n *\n * - Un valor que se resuelve **vacío se OMITE** en vez de mandar `''`: el\n * back distingue «parámetro ausente» (fail-closed ⇒ sin filas) de un\n * `eq ''` que podría cruzarse con filas realmente vacías.\n * - Un parámetro sin marcador `{}` es literal y viaja tal cual.\n */\nexport function interpolateParams(\n params: Record<string, string> | undefined,\n ctx?: Record<string, unknown>,\n): Record<string, string> {\n const out: Record<string, string> = {};\n if (params === undefined) return out;\n for (const name of Object.keys(params)) {\n const raw = params[name];\n const value = interpolateTemplate(raw, ctx ?? {});\n if (value !== '') out[name] = value;\n }\n return out;\n}\n\n/**\n * Clave de caché de una fuente: `resource|view|params` (F8 · R6.5).\n *\n * Los parámetros van **resueltos** y ordenados, así que dos fuentes que\n * terminan pidiendo lo mismo comparten entrada, y un `{campo}` que cambia\n * es otra entrada. Pura: es también el valor que `resource({ params })` de\n * Angular compara para decidir si vuelve a cargar.\n */\nexport function dataSourceKey(source: DataSource, ctx?: Record<string, unknown>): string {\n const params = interpolateParams(source.params, ctx);\n const names = Object.keys(params).sort();\n const query = names\n .map((name) => `${encodeURIComponent(name)}=${encodeURIComponent(params[name])}`)\n .join('&');\n return `${source.resource}|${source.view ?? ''}|${query}`;\n}\n\n/**\n * Fetcher único de `source` (F8 · R6.5): relations, selects y widgets del\n * panel piden sus filas ACÁ y no cada uno desde su componente.\n *\n * - **Caché por `resource+view+params`** con TTL e invalidación explícita —\n * es la pieza que evita que seis widgets del panel disparen seis vueltas\n * iguales a la misma vista.\n * - **Deduplicación de vuelta en vuelo**: dos consumidores que pidan la misma\n * clave en el mismo ciclo comparten la promesa (no la respuesta, la vuelta).\n * - Un error **no queda cacheado**: se borra la entrada para que la recarga\n * del consumidor vuelva a intentarlo en vez de devolver siempre la misma\n * rechazada.\n */\n@Service()\nexport class DataSourceService {\n private readonly api = inject(ApiService);\n\n private readonly cache = new Map<string, { promise: Promise<SourceRow[]>; at: number }>();\n\n /**\n * Filas de la fuente declarada.\n *\n * `fresh` salta la caché (recarga tras una escritura); `ctx` sólo se usa\n * para resolver `{campo}` en `params` y no cambia la forma de la respuesta.\n */\n async rows(\n source: DataSource,\n ctx?: Record<string, unknown>,\n fresh = false,\n ): Promise<SourceRow[]> {\n const key = dataSourceKey(source, ctx);\n if (!fresh) {\n const hit = this.cache.get(key);\n if (hit !== undefined && Date.now() - hit.at < TTL_MS) return hit.promise;\n }\n const promise = this.fetch(source, ctx).catch((error: unknown) => {\n this.cache.delete(key);\n throw error;\n });\n this.cache.set(key, { promise, at: Date.now() });\n if (this.cache.size > MAX_CLAVES) {\n // El Map conserva el orden de inserción: sale la más vieja.\n const oldest = this.cache.keys().next();\n if (!oldest.done) this.cache.delete(oldest.value);\n }\n return promise;\n }\n\n /**\n * Vacía las filas ya pedidas. Sin argumento, todas; con un `resourceId`,\n * sólo las de ese recurso (lo que acaba de crear el drawer de relación).\n */\n invalidate(resourceId?: string): void {\n if (resourceId === undefined) {\n this.cache.clear();\n return;\n }\n const prefix = `${resourceId}|`;\n for (const key of [...this.cache.keys()]) {\n if (key.startsWith(prefix)) this.cache.delete(key);\n }\n }\n\n private async fetch(source: DataSource, ctx?: Record<string, unknown>): Promise<SourceRow[]> {\n const target = getSchema(source.resource);\n if (!target) {\n // Un id que ni siquiera existe en el registry es error de\n // configuración, no de red: el consumidor lo pinta como «no se\n // pudieron cargar» con este texto.\n throw new Error(`Recurso desconocido: ${source.resource}`);\n }\n const payload: Record<string, unknown> = {};\n if (source.view !== undefined && source.view !== '') payload['view'] = source.view;\n const params = interpolateParams(source.params, ctx);\n if (Object.keys(params).length > 0) payload['params'] = params;\n const rows = await firstValueFrom(this.api.list<SourceRow>(target.endpoint, payload));\n return Array.isArray(rows) ? rows : [];\n }\n}\n"],"mappings":";;;;;AAmBA,MAAM,SAAS;AAGf,MAAM,aAAa;AAUnB,SAAgB,kBACd,QACA,KAA6B;CAE7B,MAAM,MAA8B,CAAA;CACpC,IAAI,WAAW,QAAW,OAAO;CACjC,KAAK,MAAM,QAAQ,OAAO,KAAK,MAAM,GAAG;EACtC,MAAM,MAAM,OAAO;EACnB,MAAM,QAAQ,oBAAoB,KAAK,OAAO,CAAA,CAAE;EAChD,IAAI,UAAU,IAAI,IAAI,QAAQ;CAChC;CACA,OAAO;AACT;AAUA,SAAgB,cAAc,QAAoB,KAA6B;CAC7E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;CAEnD,MAAM,QADQ,OAAO,KAAK,MAAM,CAAC,CAAC,KACpB,CAAA,CACX,KAAK,SAAS,GAAG,mBAAmB,IAAI,EAAC,GAAI,mBAAmB,OAAO,KAAK,GAAG,CAAC,CAChF,KAAK,GAAG;CACX,OAAO,GAAG,OAAO,SAAQ,GAAI,OAAO,QAAQ,GAAE,GAAI;AACpD;AAgBA,IAAa,oBAAb,MAAa,kBAAiB;CACX,MAAM,OAAO,UAAU;CAEvB,wBAAQ,IAAI,IAAG;CAQhC,MAAM,KACJ,QACA,KACA,QAAQ,OAAK;EAEb,MAAM,MAAM,cAAc,QAAQ,GAAG;EACrC,IAAI,CAAC,OAAO;GACV,MAAM,MAAM,KAAK,MAAM,IAAI,GAAG;GAC9B,IAAI,QAAQ,UAAa,KAAK,IAAG,IAAK,IAAI,KAAK,QAAQ,OAAO,IAAI;EACpE;EACA,MAAM,UAAU,KAAK,MAAM,QAAQ,GAAG,CAAC,CAAC,OAAO,UAAkB;GAC/D,KAAK,MAAM,OAAO,GAAG;GACrB,MAAM;EACR,CAAC;EACD,KAAK,MAAM,IAAI,KAAK;GAAE;GAAS,IAAI,KAAK,IAAG;EAAE,CAAE;EAC/C,IAAI,KAAK,MAAM,OAAO,YAAY;GAEhC,MAAM,SAAS,KAAK,MAAM,KAAI,CAAE,CAAC,KAAI;GACrC,IAAI,CAAC,OAAO,MAAM,KAAK,MAAM,OAAO,OAAO,KAAK;EAClD;EACA,OAAO;CACT;CAMA,WAAW,YAAmB;EAC5B,IAAI,eAAe,QAAW;GAC5B,KAAK,MAAM,MAAK;GAChB;EACF;EACA,MAAM,SAAS,GAAG,WAAU;EAC5B,KAAK,MAAM,OAAO,CAAC,GAAG,KAAK,MAAM,KAAI,CAAE,GACrC,IAAI,IAAI,WAAW,MAAM,GAAG,KAAK,MAAM,OAAO,GAAG;CAErD;CAEQ,MAAM,MAAM,QAAoB,KAA6B;EACnE,MAAM,SAAS,UAAU,OAAO,QAAQ;EACxC,IAAI,CAAC,QAIH,MAAM,IAAI,MAAM,wBAAwB,OAAO,UAAU;EAE3D,MAAM,UAAmC,CAAA;EACzC,IAAI,OAAO,SAAS,UAAa,OAAO,SAAS,IAAI,QAAQ,UAAU,OAAO;EAC9E,MAAM,SAAS,kBAAkB,OAAO,QAAQ,GAAG;EACnD,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAAG,QAAQ,YAAY;EACxD,MAAM,OAAO,MAAM,eAAe,KAAK,IAAI,KAAgB,OAAO,UAAU,OAAO,CAAC;EACpF,OAAO,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAA;CACtC;CA/DW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAiB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAjB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAiB,CAAA;;AAAjB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAiB,YAAA,CAAA,EAD7B,MAAA,QAAA,CAAA"}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { n as FieldAria } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
|
|
2
|
+
import { t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
import { Component, computed, input } from "@angular/core";
|
|
5
|
+
import { FormField } from "@angular/forms/signals";
|
|
6
|
+
var FieldDropdown = class FieldDropdown {
|
|
7
|
+
field = input.required(...ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []);
|
|
8
|
+
tree = input.required(...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
|
|
9
|
+
node = computed(() => childTree(this.tree(), this.field().key), ...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
|
|
10
|
+
state = computed(() => this.node()(), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
|
|
11
|
+
options = computed(() => this.field().options, ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
|
|
12
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
13
|
+
minVersion: "12.0.0",
|
|
14
|
+
version: "22.2.3",
|
|
15
|
+
ngImport: i0,
|
|
16
|
+
type: FieldDropdown,
|
|
17
|
+
deps: [],
|
|
18
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
19
|
+
});
|
|
20
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
21
|
+
minVersion: "17.0.0",
|
|
22
|
+
version: "22.2.3",
|
|
23
|
+
type: FieldDropdown,
|
|
24
|
+
isStandalone: true,
|
|
25
|
+
selector: "app-field-dropdown",
|
|
26
|
+
inputs: {
|
|
27
|
+
field: {
|
|
28
|
+
classPropertyName: "field",
|
|
29
|
+
publicName: "field",
|
|
30
|
+
isSignal: true,
|
|
31
|
+
isRequired: true,
|
|
32
|
+
transformFunction: null
|
|
33
|
+
},
|
|
34
|
+
tree: {
|
|
35
|
+
classPropertyName: "tree",
|
|
36
|
+
publicName: "tree",
|
|
37
|
+
isSignal: true,
|
|
38
|
+
isRequired: true,
|
|
39
|
+
transformFunction: null
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
ngImport: i0,
|
|
43
|
+
template: `
|
|
44
|
+
<select
|
|
45
|
+
class="field-input"
|
|
46
|
+
[id]="state().name()"
|
|
47
|
+
[formField]="node()"
|
|
48
|
+
>
|
|
49
|
+
@if (!field().required) {
|
|
50
|
+
<option value="">{{ field().placeholder ?? '— Seleccionar —' }}</option>
|
|
51
|
+
}
|
|
52
|
+
@for (option of options(); track option.value) {
|
|
53
|
+
<option [value]="option.value" [disabled]="option.disabled ?? false">
|
|
54
|
+
{{ option.label }}
|
|
55
|
+
</option>
|
|
56
|
+
}
|
|
57
|
+
</select>
|
|
58
|
+
`,
|
|
59
|
+
isInline: true,
|
|
60
|
+
dependencies: [{
|
|
61
|
+
kind: "directive",
|
|
62
|
+
type: FieldAria,
|
|
63
|
+
selector: "input[id], select[id], textarea[id], output[id]"
|
|
64
|
+
}, {
|
|
65
|
+
kind: "directive",
|
|
66
|
+
type: FormField,
|
|
67
|
+
selector: "[formField]",
|
|
68
|
+
inputs: ["formField"],
|
|
69
|
+
exportAs: ["formField"]
|
|
70
|
+
}]
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
74
|
+
minVersion: "12.0.0",
|
|
75
|
+
version: "22.2.3",
|
|
76
|
+
ngImport: i0,
|
|
77
|
+
type: FieldDropdown,
|
|
78
|
+
decorators: [{
|
|
79
|
+
type: Component,
|
|
80
|
+
args: [{
|
|
81
|
+
selector: "app-field-dropdown",
|
|
82
|
+
imports: [FieldAria, FormField],
|
|
83
|
+
template: `
|
|
84
|
+
<select
|
|
85
|
+
class="field-input"
|
|
86
|
+
[id]="state().name()"
|
|
87
|
+
[formField]="node()"
|
|
88
|
+
>
|
|
89
|
+
@if (!field().required) {
|
|
90
|
+
<option value="">{{ field().placeholder ?? '— Seleccionar —' }}</option>
|
|
91
|
+
}
|
|
92
|
+
@for (option of options(); track option.value) {
|
|
93
|
+
<option [value]="option.value" [disabled]="option.disabled ?? false">
|
|
94
|
+
{{ option.label }}
|
|
95
|
+
</option>
|
|
96
|
+
}
|
|
97
|
+
</select>
|
|
98
|
+
`
|
|
99
|
+
}]
|
|
100
|
+
}],
|
|
101
|
+
propDecorators: {
|
|
102
|
+
field: [{
|
|
103
|
+
type: i0.Input,
|
|
104
|
+
args: [{
|
|
105
|
+
isSignal: true,
|
|
106
|
+
alias: "field",
|
|
107
|
+
required: true
|
|
108
|
+
}]
|
|
109
|
+
}],
|
|
110
|
+
tree: [{
|
|
111
|
+
type: i0.Input,
|
|
112
|
+
args: [{
|
|
113
|
+
isSignal: true,
|
|
114
|
+
alias: "tree",
|
|
115
|
+
required: true
|
|
116
|
+
}]
|
|
117
|
+
}]
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
export { FieldDropdown };
|
|
121
|
+
|
|
122
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/dropdown/dropdown.ts"],"sourcesContent":["import { Component, computed, input } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport type { DropdownField } from '../../core/models/schema.model';\nimport { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\n\n/**\n * Desplegable nativo (`<select>`) que SIEMPRE se renderiza como lista\n * desplegable, independientemente de cuántas opciones tenga.\n *\n * A diferencia de `select` (que usa radio buttons para 2–4 opciones\n * estáticas por la guía de control §1), este tipo nunca muta a radios.\n * Úsalo cuando necesites un dropdown verdadero aunque tenga pocas opciones.\n */\n@Component({\n selector: 'app-field-dropdown',\n imports: [FieldAria, FormField],\n template: `\n <select\n class=\"field-input\"\n [id]=\"state().name()\"\n [formField]=\"node()\"\n >\n @if (!field().required) {\n <option value=\"\">{{ field().placeholder ?? '— Seleccionar —' }}</option>\n }\n @for (option of options(); track option.value) {\n <option [value]=\"option.value\" [disabled]=\"option.disabled ?? false\">\n {{ option.label }}\n </option>\n }\n </select>\n `,\n})\nexport class FieldDropdown {\n readonly field = input.required<DropdownField>();\n readonly tree = input.required<RootTree>();\n\n protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));\n protected readonly state = computed(() => this.node()());\n\n /** Opciones estáticas del schema (dropdown no soporta `source`). */\n protected readonly options = computed(() => this.field().options);\n}\n"],"mappings":";;;;;AAmCA,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;CAEvB,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,UAAU,eAAe,KAAK,MAAK,CAAE,CAAC,SAAQ,GAAA,YAAA,CAAA,EAAA,WAAA,UAAA,CAAA,+BAAA,CAAA,CAAA;CARtD,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,UAjBd;;;;;;;;;;;;;;;;EAeT,UAAA;EAAA,cAAA,CAAA;GAAA,MAAA;GAAA,MAhBS;GAAS,UAAA;EAAA,GAAA;GAAA,MAAA;GAAA,MAAE;GAAS,UAAA;GAAA,QAAA,CAAA,WAAA;GAAA,UAAA,CAAA,WAAA;EAAA,CAAA;CAAA,CAAA;;AAkBnB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAa,YAAA,CAAA;EApBzB,MAAA;EAAU,MAAA,CAAA;GACT,UAAU;GACV,SAAS,CAAC,WAAW,SAAS;GAC9B,UAAU;;;;;;;;;;;;;;;;EAgBX,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { b as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
|
|
2
2
|
import * as i0 from "@angular/core";
|
|
3
3
|
import { Service } from "@angular/core";
|
|
4
4
|
var ErrorMapperService = class ErrorMapperService {
|
|
@@ -42,4 +42,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
42
42
|
});
|
|
43
43
|
export { ErrorMapperService as t };
|
|
44
44
|
|
|
45
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-error-mapper.service-
|
|
45
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-error-mapper.service-
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/services/error-mapper.service.ts"],"sourcesContent":["import { Service } from '@angular/core';\n\nimport { ApiError } from '../models/api.model';\n\nexport interface MappedError {\n /** Errores por ruta de campo (ej. `faq[2].answer`); se pintan en el campo exacto. */\n fields: Record<string, string>;\n /** Mensaje general; presente cuando el error no tiene ruta de campo (base.md §10). */\n general?: string;\n}\n\n/**\n * Convierte un error recibido en el formato que consume la UI:\n * rutas de campo para pintar el error donde corresponde, o mensaje general.\n *\n * Un422 trae VARIOS issues con ruta (`error.errors[]`, auth.md §6); cada uno\n * se convierte en `fields[path] = mensaje`, que es exactamente la clave que\n * después resuelve el host como `serverErrors[prefix + key]`. Si al final no\n * hay ninguna ruta pintable (error sin `errors[]`, issue sin `path`), el\n * mensaje del sobre queda en `general` como hasta ahora (base.md §10).\n */\n@Service()\nexport class ErrorMapperService {\n map(error: unknown): MappedError {\n if (error instanceof ApiError) {\n const fields: Record<string, string> = {};\n for (const issue of error.issues) {\n if (issue.path) {\n fields[issue.path] = issue.message;\n }\n }\n // Sobre con `path` suelto (sin `errors[]`): misma historia, un campo.\n if (error.fieldPath && fields[error.fieldPath] === undefined) {\n fields[error.fieldPath] = error.message;\n }\n if (Object.keys(fields).length > 0) {\n return { fields };\n }\n }\n return { fields: {}, general: this.messageOf(error) };\n }\n\n messageOf(error: unknown): string {\n if (error instanceof Error) {\n return error.message;\n }\n return 'Error inesperado';\n }\n}\n"],"mappings":";;;AAsBA,IAAa,qBAAb,MAAa,mBAAkB;CAC7B,IAAI,OAAc;EAChB,IAAI,iBAAiB,UAAU;GAC7B,MAAM,SAAiC,CAAA;GACvC,KAAK,MAAM,SAAS,MAAM,QACxB,IAAI,MAAM,MACR,OAAO,MAAM,QAAQ,MAAM;GAI/B,IAAI,MAAM,aAAa,OAAO,MAAM,eAAe,QACjD,OAAO,MAAM,aAAa,MAAM;GAElC,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,SAAS,GAC/B,OAAO,EAAE,OAAM;EAEnB;EACA,OAAO;GAAE,QAAQ,CAAA;GAAI,SAAS,KAAK,UAAU,KAAK;EAAC;CACrD;CAEA,UAAU,OAAc;EACtB,IAAI,iBAAiB,OACnB,OAAO,MAAM;EAEf,OAAO;CACT;CAzBW,OAAA,OAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;EAAkB,MAAA,CAAA;EAAA,QAAA,GAAA,gBAAA;CAAA,CAAA;CAAlB,OAAA,QAAA,GAAA,mBAAA;EAAA,YAAA;EAAA,SAAA;EAAA,UAAA;EAAA,MAAA;CAAkB,CAAA;;AAAlB,GAAA,yBAAA;CAAA,YAAA;CAAA,SAAA;CAAA,UAAA;CAAA,MAAA;CAAkB,YAAA,CAAA,EAD9B,MAAA,QAAA,CAAA"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { b as ApiError } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.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-CZPEPCeN.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";
|
|
@@ -405,4 +405,4 @@ function acceptMatches(accept, file) {
|
|
|
405
405
|
}
|
|
406
406
|
export { file_exports as n, FieldFile as t };
|
|
407
407
|
|
|
408
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-file-
|
|
408
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tinya-code-gas-admin-ui-file-B6_vveO5.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/file/file.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 { FileField } 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 archivo (base.md §7 · R9.5.3): el valor es SOLO la URL pública,\n * igual que `image`, pero para cualquier tipo de archivo (pdf, xlsx, zip…).\n *\n * Espejo de `FieldImage` (D27: es un control propio que no pasa por\n * `NgComponentOutlet`, porque su `value` es un `model()` que el host\n * sincroniza por `[formField]`). La ÚNICA diferencia de comportamiento: no\n * fuerza el prefijo `image/` — el catálogo de formatos lo define el backend\n * por campo (`formats` declarado) o la lista global, y el cliente sólo\n * respeta `accept`/`maxSizeMB` como validación de comodidad.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con archivo (nombre + Quitar / Ver) y\n * error (conserva el archivo anterior).\n */\n@Component({\n selector: 'app-field-file',\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=\"file-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=\"file-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"truncate text-sm font-medium\" [title]=\"value()\">{{ fileName() }}</p>\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 Abrir\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=\"file-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá un archivo 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 archivo\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 FieldFile implements FormValueControl<string> {\n readonly field = input.required<FileField>();\n /** Recurso dueño del archivo (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 /**\n * Nombre derivado de la URL para el preview. Cloudinary guarda el\n * `public_id` al final del path (…/v123/produccion/adjuntos/archivo_xyz):\n * lo presentable es el sufijo tras el último `/`, sin extensión ruidosa.\n */\n protected readonly fileName = computed(() => {\n const url = this.value();\n const segment = url.split('/').pop() ?? url;\n return decodeURIComponent(segment);\n });\n\n /** `accept` de `<input type=file>`: el declarado o `''` (sin forzar `image/*`). */\n protected readonly acceptAttr = computed(() => this.field().accept ?? '');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (mismo motivo que en `FieldImage`).\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 el archivo.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Archivo subido.');\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 el archivo. 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 const accept = this.field().accept;\n if (accept !== undefined && accept !== '' && !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 `El archivo supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `tipo/*` 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-file-Dmxioog7.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/file/file.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 { FileField } 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 archivo (base.md §7 · R9.5.3): el valor es SOLO la URL pública,\n * igual que `image`, pero para cualquier tipo de archivo (pdf, xlsx, zip…).\n *\n * Espejo de `FieldImage` (D27: es un control propio que no pasa por\n * `NgComponentOutlet`, porque su `value` es un `model()` que el host\n * sincroniza por `[formField]`). La ÚNICA diferencia de comportamiento: no\n * fuerza el prefijo `image/` — el catálogo de formatos lo define el backend\n * por campo (`formats` declarado) o la lista global, y el cliente sólo\n * respeta `accept`/`maxSizeMB` como validación de comodidad.\n *\n * Estados: vacío (zona punteada + arrastrar-soltar), subiendo (progreso; el\n * valor no cambia hasta la respuesta), con archivo (nombre + Quitar / Ver) y\n * error (conserva el archivo anterior).\n */\n@Component({\n selector: 'app-field-file',\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=\"file-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=\"file-preview\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"truncate text-sm font-medium\" [title]=\"value()\">{{ fileName() }}</p>\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 Abrir\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=\"file-dropzone\"\n (dragover)=\"$event.preventDefault()\"\n (drop)=\"onDrop($event)\"\n >\n <p class=\"text-sm text-neutral\">Arrastrá un archivo 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 archivo\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 FieldFile implements FormValueControl<string> {\n readonly field = input.required<FileField>();\n /** Recurso dueño del archivo (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 /**\n * Nombre derivado de la URL para el preview. Cloudinary guarda el\n * `public_id` al final del path (…/v123/produccion/adjuntos/archivo_xyz):\n * lo presentable es el sufijo tras el último `/`, sin extensión ruidosa.\n */\n protected readonly fileName = computed(() => {\n const url = this.value();\n const segment = url.split('/').pop() ?? url;\n return decodeURIComponent(segment);\n });\n\n /** `accept` de `<input type=file>`: el declarado o `''` (sin forzar `image/*`). */\n protected readonly acceptAttr = computed(() => this.field().accept ?? '');\n\n /**\n * Template ref `#fileInput`: Angular NO auto-asigna las template refs a\n * propiedades de clase (mismo motivo que en `FieldImage`).\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 el archivo.');\n return;\n }\n this.value.set(url);\n this.notifications.success('Archivo subido.');\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 el archivo. 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 const accept = this.field().accept;\n if (accept !== undefined && accept !== '' && !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 `El archivo supera ${maxSizeMB} MB.`;\n }\n return null;\n }\n}\n\n/** `accept` de `<input type=file>`: lista, `tipo/*` 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,20 +1,20 @@
|
|
|
1
1
|
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
-
import { c as ConfigService, d as AuthService, i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-
|
|
3
|
-
import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } from "./tinya-code-gas-admin-ui-render-registry-
|
|
4
|
-
import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-
|
|
2
|
+
import { c as ConfigService, d as AuthService, i as getSchema, l as ApiService } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
|
|
3
|
+
import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } from "./tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs";
|
|
4
|
+
import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs";
|
|
5
5
|
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
6
6
|
import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
|
|
7
7
|
import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
|
|
8
|
-
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-
|
|
8
|
+
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs";
|
|
9
9
|
import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
|
|
10
10
|
import { t as FIELD_ARIA } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
|
|
11
11
|
import { i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
12
|
-
import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-
|
|
13
|
-
import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-
|
|
14
|
-
import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-
|
|
12
|
+
import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-Dmxioog7.mjs";
|
|
13
|
+
import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs";
|
|
14
|
+
import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs";
|
|
15
15
|
import { t as esGrupoRadio } from "./tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs";
|
|
16
16
|
import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
|
|
17
|
-
import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-
|
|
17
|
+
import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs";
|
|
18
18
|
import { t as RelationDrawerService } from "./tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs";
|
|
19
19
|
import * as i0 from "@angular/core";
|
|
20
20
|
import { ApplicationRef, Component, DestroyRef, InjectionToken, Injector, PendingTasks, afterRenderEffect, computed, effect, forwardRef, inject, input, isDevMode, output, resource, signal, viewChild } from "@angular/core";
|
|
@@ -52,7 +52,10 @@ const DISPLAY_POR_DEFECTO = {
|
|
|
52
52
|
select: BADGE,
|
|
53
53
|
image: IMAGEN,
|
|
54
54
|
file: ARCHIVO,
|
|
55
|
-
list: LISTA
|
|
55
|
+
list: LISTA,
|
|
56
|
+
color: TEXTO,
|
|
57
|
+
"rich-select": TEXTO,
|
|
58
|
+
dropdown: TEXTO
|
|
56
59
|
};
|
|
57
60
|
function displayLoader(type) {
|
|
58
61
|
return Object.prototype.hasOwnProperty.call(DISPLAY_POR_DEFECTO, type) ? DISPLAY_POR_DEFECTO[type] : void 0;
|
|
@@ -4510,4 +4513,4 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
4510
4513
|
});
|
|
4511
4514
|
export { resolveShare as _, DrawerForm as a, buildFormSchema as c, actionTypes as d, actionsFor as f, isTriggerAction as g, isShareAction as h, CopyShare as i, FieldHost as l, registerAction as m, form_view_exports as n, buildPayload as o, getAction as p, Modal as r, seedModel as s, FormView as t, actionType as u, RenderHost as v };
|
|
4512
4515
|
|
|
4513
|
-
//# sourceMappingURL=tinya-code-gas-admin-ui-form-view-
|
|
4516
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map
|