@tinya-code/gas-admin-ui 0.0.0-stage → 0.1.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 +125 -2
- package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +281 -0
- package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +84 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +140 -0
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +113 -0
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +163 -0
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs +1022 -0
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +278 -0
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs +101 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +141 -0
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs +89 -0
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +140 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +119 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +125 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +152 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +211 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +116 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +111 -0
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +109 -0
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs +45 -0
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +50 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +24 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +27 -0
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs +408 -0
- package/fesm2022/tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs +4514 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +82 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +105 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +65 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +59 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +94 -0
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +72 -0
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +238 -0
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs +416 -0
- package/fesm2022/tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs +439 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +513 -0
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs +637 -0
- package/fesm2022/tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +127 -0
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs +251 -0
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +66 -0
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +114 -0
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +116 -0
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +235 -0
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +8 -0
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +124 -0
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +249 -0
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs +309 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +39 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs +546 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +131 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs +80 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs +292 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +119 -0
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +158 -0
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +26 -0
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +205 -0
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +432 -0
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +36 -0
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +223 -0
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs +60 -0
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +141 -0
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui.mjs +4403 -0
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
- package/package.json +31 -3
- package/types/tinya-code-gas-admin-ui.d.ts +3099 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __exportAll = (all, no_symbols) => {
|
|
3
|
+
let target = {};
|
|
4
|
+
for (var name in all) __defProp(target, name, {
|
|
5
|
+
get: all[name],
|
|
6
|
+
enumerable: true
|
|
7
|
+
});
|
|
8
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
9
|
+
return target;
|
|
10
|
+
};
|
|
11
|
+
export { __exportAll as t };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { n as AuthService, s as ApiError, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
import { InjectionToken, Service, inject, provideAppInitializer, signal } from "@angular/core";
|
|
4
|
+
import { HttpErrorResponse, HttpResponse, provideHttpClient, withInterceptors } from "@angular/common/http";
|
|
5
|
+
import { catchError, firstValueFrom, forkJoin, map, of, throwError } from "rxjs";
|
|
6
|
+
function httpMessage(status) {
|
|
7
|
+
switch (status) {
|
|
8
|
+
case 0: return "No se pudo conectar con el servidor. Revisa tu conexión.";
|
|
9
|
+
case 401: return "Tu sesión ha expirado. Vuelve a iniciar sesión.";
|
|
10
|
+
case 403: return "No tienes permisos para realizar esta acción.";
|
|
11
|
+
case 404: return "No se encontró el recurso solicitado.";
|
|
12
|
+
case 409: return "El registro cambió o ya existe. Recarga la página y vuelve a intentarlo.";
|
|
13
|
+
case 413: return "El archivo es demasiado grande.";
|
|
14
|
+
case 422: return "Los datos enviados no son válidos.";
|
|
15
|
+
default:
|
|
16
|
+
if (status >= 500) return "El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.";
|
|
17
|
+
return status >= 400 ? "No se pudo completar la operación." : "Error inesperado.";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isErrorBody(body) {
|
|
21
|
+
if (typeof body !== "object" || body === null) return false;
|
|
22
|
+
const error = body.error;
|
|
23
|
+
return typeof error === "object" && error !== null && typeof error.message === "string";
|
|
24
|
+
}
|
|
25
|
+
const apiErrorInterceptor = (req, next) => {
|
|
26
|
+
const auth = inject(AuthService);
|
|
27
|
+
return next(req).pipe(map((event) => {
|
|
28
|
+
if (event instanceof HttpResponse && isErrorBody(event.body)) {
|
|
29
|
+
const { status, message, path, code, errors } = event.body.error;
|
|
30
|
+
auth.applyAuthCode(code);
|
|
31
|
+
throw new ApiError(status, message, path, code, errors ?? []);
|
|
32
|
+
}
|
|
33
|
+
return event;
|
|
34
|
+
}), catchError((err) => {
|
|
35
|
+
if (err instanceof ApiError) return throwError(() => err);
|
|
36
|
+
if (err instanceof HttpErrorResponse) return throwError(() => new ApiError(err.status, httpMessage(err.status)));
|
|
37
|
+
return throwError(() => err);
|
|
38
|
+
}));
|
|
39
|
+
};
|
|
40
|
+
var ConfigService = class ConfigService {
|
|
41
|
+
api = inject(ApiService, { optional: true });
|
|
42
|
+
globalConfig = signal({ public_base_url: typeof window !== "undefined" ? window.location.origin : "" }, ...ngDevMode ? [{ debugName: "globalConfig" }] : /* istanbul ignore next */ []);
|
|
43
|
+
endpoints = [];
|
|
44
|
+
loaded = false;
|
|
45
|
+
inFlight = null;
|
|
46
|
+
load(endpoints) {
|
|
47
|
+
this.endpoints = endpoints;
|
|
48
|
+
this.ensure();
|
|
49
|
+
}
|
|
50
|
+
ensureLoaded() {
|
|
51
|
+
return this.ensure();
|
|
52
|
+
}
|
|
53
|
+
ensure() {
|
|
54
|
+
if (this.loaded) return Promise.resolve();
|
|
55
|
+
if (this.inFlight) return this.inFlight;
|
|
56
|
+
if (!this.api || typeof this.api.get !== "function" || this.endpoints.length === 0) return Promise.resolve();
|
|
57
|
+
this.inFlight = firstValueFrom(forkJoin(this.endpoints.map((endpoint) => this.api.get(endpoint).pipe(catchError(() => of(null)))))).then((blocks) => {
|
|
58
|
+
this.inFlight = null;
|
|
59
|
+
blocks.forEach((data) => {
|
|
60
|
+
if (data && typeof data === "object" && Object.keys(data).length > 0) this.globalConfig.update((current) => ({
|
|
61
|
+
...current,
|
|
62
|
+
...data
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
this.loaded = blocks.every((data) => data !== null);
|
|
66
|
+
});
|
|
67
|
+
return this.inFlight;
|
|
68
|
+
}
|
|
69
|
+
buildInterpolationContext(record) {
|
|
70
|
+
const config = this.globalConfig();
|
|
71
|
+
const baseUrl = String(config["public_base_url"] || window.location.origin).replace(/\/+$/, "");
|
|
72
|
+
return {
|
|
73
|
+
...config,
|
|
74
|
+
...record,
|
|
75
|
+
public_base_url: baseUrl
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
79
|
+
minVersion: "12.0.0",
|
|
80
|
+
version: "22.2.3",
|
|
81
|
+
ngImport: i0,
|
|
82
|
+
type: ConfigService,
|
|
83
|
+
deps: [],
|
|
84
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
85
|
+
});
|
|
86
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
87
|
+
minVersion: "22.0.0",
|
|
88
|
+
version: "22.2.3",
|
|
89
|
+
ngImport: i0,
|
|
90
|
+
type: ConfigService
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
94
|
+
minVersion: "12.0.0",
|
|
95
|
+
version: "22.2.3",
|
|
96
|
+
ngImport: i0,
|
|
97
|
+
type: ConfigService,
|
|
98
|
+
decorators: [{ type: Service }]
|
|
99
|
+
});
|
|
100
|
+
let catalog = [];
|
|
101
|
+
let authCatalog;
|
|
102
|
+
function setSchemaCatalog(schemas, auth) {
|
|
103
|
+
catalog = schemas;
|
|
104
|
+
authCatalog = auth;
|
|
105
|
+
}
|
|
106
|
+
function getSchema(id) {
|
|
107
|
+
return catalog.find((schema) => schema.id === id);
|
|
108
|
+
}
|
|
109
|
+
function allSchemas() {
|
|
110
|
+
return catalog;
|
|
111
|
+
}
|
|
112
|
+
function getAuth() {
|
|
113
|
+
return authCatalog;
|
|
114
|
+
}
|
|
115
|
+
const GAS_SCHEMAS = new InjectionToken("gas.admin.schemas");
|
|
116
|
+
function provideGasAdmin(config) {
|
|
117
|
+
setSchemaCatalog(config.schemas, config.auth);
|
|
118
|
+
const configEndpoints = () => config.schemas.flatMap((schema) => schema.kind === "singleton" && schema.endpoint?.get ? [{ get: schema.endpoint.get }] : []);
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
provide: GAS_SCHEMAS,
|
|
122
|
+
useValue: config.schemas
|
|
123
|
+
},
|
|
124
|
+
provideHttpClient(withInterceptors([apiErrorInterceptor])),
|
|
125
|
+
provideAppInitializer(() => inject(ConfigService).load(configEndpoints())),
|
|
126
|
+
provideAppInitializer(() => inject(AuthService).restore())
|
|
127
|
+
];
|
|
128
|
+
}
|
|
129
|
+
export { provideGasAdmin as a, apiErrorInterceptor as c, getSchema as i, allSchemas as n, setSchemaCatalog as o, getAuth as r, ConfigService as s, GAS_SCHEMAS as t };
|
|
130
|
+
|
|
131
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/core/interceptors/api-error.interceptor.ts","../../../projects/gas-admin-ui/src/lib/core/services/config.service.ts","../../../projects/gas-admin-ui/src/lib/core/registry/schema-catalog.ts"],"sourcesContent":["import { HttpErrorResponse, HttpInterceptorFn, HttpResponse } from '@angular/common/http';\nimport { inject } from '@angular/core';\nimport { catchError, map, throwError } from 'rxjs';\n\nimport { ApiError } from '../models/api.model';\nimport type { ApiErrorBody } from '../models/api.model';\nimport { AuthService } from '../services/auth.service';\n\n/**\n * Mensaje humano para un error HTTP real (§2).\n *\n * El backend siempre responde 200 con el error dentro del body —eso ya\n * viene en español—, así que esto sólo cubre lo que el transporte falla:\n * red caída, proxy y 5xx. Sin esto el usuario ve el texto crudo de Angular\n * («Http failure response for http://…»), que es inglés técnico.\n */\nfunction httpMessage(status: number): string {\n switch (status) {\n case 0:\n return 'No se pudo conectar con el servidor. Revisa tu conexión.';\n case 401:\n return 'Tu sesión ha expirado. Vuelve a iniciar sesión.';\n case 403:\n return 'No tienes permisos para realizar esta acción.';\n case 404:\n return 'No se encontró el recurso solicitado.';\n case 409:\n return 'El registro cambió o ya existe. Recarga la página y vuelve a intentarlo.';\n case 413:\n return 'El archivo es demasiado grande.';\n case 422:\n return 'Los datos enviados no son válidos.';\n default:\n if (status >= 500) {\n return 'El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.';\n }\n return status >= 400 ? 'No se pudo completar la operación.' : 'Error inesperado.';\n }\n}\n\nfunction isErrorBody(body: unknown): body is ApiErrorBody {\n if (typeof body !== 'object' || body === null) {\n return false;\n }\n const error = (body as ApiErrorBody).error;\n return typeof error === 'object' && error !== null && typeof error.message === 'string';\n}\n\n/**\n * El backend responde SIEMPRE HTTP 200 y pone los errores en el body\n * (api.md §7), así que se detectan en el canal de **éxito** y se relanzan\n * como `ApiError` (con `fieldPath` cuando el backend envía `path`).\n * Los errores HTTP reales (red caída, proxy) también se normalizan.\n *\n * **R2.9 — paso dos de dos.** Éste archivo es el ÚNICO punto donde el\n * sobre de error se abre; lo que decide qué ocurre con un `AUTH_*` es\n * `AuthService.applyAuthCode` (la tabla de §4.5). Aquí sólo se le entrega\n * el código y se propaga el `ApiError` con él — un segundo sitio que\n * «interprete» el código sería una regla duplicada y, a la larga, una que\n * se contradiga con la primera.\n */\nexport const apiErrorInterceptor: HttpInterceptorFn = (req, next) => {\n const auth = inject(AuthService);\n\n return next(req).pipe(\n map((event) => {\n if (event instanceof HttpResponse && isErrorBody(event.body)) {\n const { status, message, path, code, errors } = event.body.error;\n // R2.9: ésta es la ÚNICA llamada a la tabla de §4.5.\n auth.applyAuthCode(code);\n // `errors[]` es el422 con rutas (D42): va entero al ApiError para que\n // `ErrorMapperService` lo convierta en `serverErrors[prefix + key]`.\n throw new ApiError(status, message, path, code, errors ?? []);\n }\n return event;\n }),\n catchError((err: unknown) => {\n if (err instanceof ApiError) {\n return throwError(() => err);\n }\n if (err instanceof HttpErrorResponse) {\n return throwError(() => new ApiError(err.status, httpMessage(err.status)));\n }\n return throwError(() => err);\n }),\n );\n};\n","import { inject, Service, signal } from '@angular/core';\nimport { catchError, firstValueFrom, forkJoin, of } from 'rxjs';\n\nimport { ApiService } from './api.service';\nimport type { ResourceEndpoints } from '../models/schema.model';\n\n/**\n * Servicio global de configuración: reúne los singletons declarativos y los\n * deja disponibles para interpolación de plantillas (enlaces públicos,\n * mensajes para compartir).\n *\n * Es genérico a propósito: NO conoce ningún singleton concreto. Los\n * endpoints los inyecta `app.config.ts` con `provideAppInitializer`,\n * derivándolos del registry — agregar un singleton de configuración nuevo\n * = declarar su schema, sin tocar este fichero (refactormotor.md §1 — el\n * schema es el único plano de control).\n *\n * No hay efectos en el constructor: la carga es explícita (`load`), así un\n * TestBed que sólo provee un ApiService parcial no recibe una llamada\n * síncrona a `get` durante la construcción.\n */\n@Service()\nexport class ConfigService {\n private readonly api = inject(ApiService, { optional: true });\n\n /** Mapa reactivo de configuración global (p. ej. `public_base_url`). */\n readonly globalConfig = signal<Record<string, unknown>>({\n // Fallback genérico: el origen actual. Lo que declare el singleton\n // (p. ej. `share_message_template`) lo pone el backend, no el front.\n public_base_url: typeof window !== 'undefined' ? window.location.origin : '',\n });\n\n /** Endpoints conocidos; los guarda `load` para que `ensureLoaded` pueda reintentar. */\n private endpoints: readonly ResourceEndpoints[] = [];\n\n /**\n * `true` cuando la última carga terminó SIN fallos. Si algún singleton\n * falló (o ninguno respondió), queda `false` y el próximo punto crítico\n * vuelve a intentar: no hay estado «cargado a medias para siempre».\n */\n private loaded = false;\n\n /** Carga en curso: `ensureLoaded` la reutiliza en vez de duplicar viajes. */\n private inFlight: Promise<void> | null = null;\n\n /**\n * Carga todos los endpoints de configuración recibidos. Silenciosa: un\n * singleton que falle o que devuelva vacío no rompe nada, simplemente no\n * aporta claves (y deja `loaded = false` para poder reintentar).\n */\n load(endpoints: readonly ResourceEndpoints[]): void {\n this.endpoints = endpoints;\n void this.ensure();\n }\n\n /**\n * Garantiza la config global antes de un punto que la necesita.\n *\n * El load del bootstrap NO es bloqueante y sólo intenta una vez: si la app\n * arrancó sin sesión (el 401 de los singletons se traga en el `catchError`)\n * o la red tardó, la sesión seguía con la config vacía y, por ejemplo, el\n * mensaje de `copy-share` degradaba a «sólo la URL» (R8.7). Acá el reintento\n * corre con la sesión ya iniciada y responde bien; si ya se cargó, es un\n * no-op síncrono.\n */\n ensureLoaded(): Promise<void> {\n return this.ensure();\n }\n\n private ensure(): Promise<void> {\n if (this.loaded) return Promise.resolve();\n if (this.inFlight) return this.inFlight;\n if (!this.api || typeof this.api.get !== 'function' || this.endpoints.length === 0) {\n return Promise.resolve();\n }\n\n this.inFlight = firstValueFrom(\n forkJoin(\n this.endpoints.map((endpoint) =>\n this.api!.get<Record<string, unknown>>(endpoint).pipe(\n // `null` marca el fallo: con `of({})` un singleton caído era\n // indistinguible de uno realmente vacío y la carga quedaría\n // «hecha» sin datos.\n catchError(() => of(null)),\n ),\n ),\n ),\n ).then((blocks) => {\n this.inFlight = null;\n blocks.forEach((data) => {\n if (data && typeof data === 'object' && Object.keys(data).length > 0) {\n this.globalConfig.update((current) => ({ ...current, ...data }));\n }\n });\n this.loaded = blocks.every((data) => data !== null);\n });\n\n return this.inFlight;\n }\n\n /**\n * Contexto de datos para interpolar plantillas del schema\n * (`urlTemplate`, `shareTextTemplate`, `postCreate.description`, …).\n *\n * Sólo aporta datos y la base pública — la FORMA de la URL pública\n * (`/p/{ref}`) la declara el propio schema en su `urlTemplate`; este\n * servicio no decide cómo se arma el enlace.\n */\n buildInterpolationContext(record: Record<string, unknown>): Record<string, unknown> {\n const config = this.globalConfig();\n const baseUrl = String(config['public_base_url'] || window.location.origin).replace(/\\/+$/, '');\n\n return {\n ...config,\n ...record,\n public_base_url: baseUrl,\n };\n }\n}\n","import {\n EnvironmentProviders,\n InjectionToken,\n Provider,\n inject,\n provideAppInitializer,\n} from '@angular/core';\nimport { provideHttpClient, withInterceptors } from '@angular/common/http';\n\nimport { apiErrorInterceptor } from '../interceptors/api-error.interceptor';\nimport { AuthService } from '../services/auth.service';\nimport { ConfigService } from '../services/config.service';\nimport type { AuthSchema, ResourceSchema } from '../models/schema.model';\n\n/**\n * Catálogo de schemas del consumidor (F1 · G-F2).\n *\n * Antes de la extracción el motor importaba `getSchema` del\n * `schemas/registry.ts` de la app. Ese acoplamiento se rompe con un\n * catálogo a nivel módulo: el consumidor llama\n * `provideGasAdmin({ schemas, auth })` en su `app.config.ts` y el motor\n * resuelve en runtime con la MISMA semántica que antes (un solo catálogo\n * por aplicación, seteado una vez en el bootstrap).\n *\n * `provideGasAdmin` se ejecuta al evaluarse el `app.config` (módulo), y\n * todo uso del catálogo (guards, componentes, rutas dinámicas) ocurre en\n * runtime posterior al bootstrap — el orden es determinístico.\n */\n\nlet catalog: readonly ResourceSchema[] = [];\nlet authCatalog: AuthSchema | undefined;\n\n/** Fija el catálogo (uso interno de `provideGasAdmin`; testable directo). */\nexport function setSchemaCatalog(\n schemas: readonly ResourceSchema[],\n auth?: AuthSchema,\n): void {\n catalog = schemas;\n authCatalog = auth;\n}\n\n/** Busca un schema por `id`; `undefined` si no existe. */\nexport function getSchema(id: string): ResourceSchema | undefined {\n return catalog.find((schema) => schema.id === id);\n}\n\n/** Catálogo completo en orden (menú lateral). */\nexport function allSchemas(): readonly ResourceSchema[] {\n return catalog;\n}\n\n/** Declaración de la pantalla de login; `undefined` sin `auth` en config. */\nexport function getAuth(): AuthSchema | undefined {\n return authCatalog;\n}\n\n/** Token DI del catálogo (para inyectarlo donde un `inject()` es posible). */\nexport const GAS_SCHEMAS = new InjectionToken<readonly ResourceSchema[]>(\n 'gas.admin.schemas',\n);\n\n/** Configuración del motor. */\nexport interface GasAdminConfig {\n /** Catálogo ordenado de recursos (menú + rutas + resolución `getSchema`). */\n schemas: readonly ResourceSchema[];\n /** Declaración de la pantalla de login (opcional: sin ella, sin `/login`). */\n auth?: AuthSchema;\n}\n\n/**\n * Wiring completo del motor: catálogo + HTTP con el interceptor de\n * errores del envelope + loaders de config y sesión en el bootstrap\n * (antes en el `app.config` del consumidor).\n *\n * El consumidor conserva `provideRouter` y `{ provide: API_URL, ... }`\n * (su environment decide la URL del Web App).\n */\nexport function provideGasAdmin(\n config: GasAdminConfig,\n): (Provider | EnvironmentProviders)[] {\n setSchemaCatalog(config.schemas, config.auth);\n // Singletons de configuración: los declara el catálogo, no el servicio.\n // Un singleton nuevo con `endpoint.get` se carga solo.\n const configEndpoints = () =>\n config.schemas.flatMap((schema) =>\n schema.kind === 'singleton' && schema.endpoint?.get\n ? [{ get: schema.endpoint.get }]\n : [],\n );\n return [\n { provide: GAS_SCHEMAS, useValue: config.schemas },\n // Sin `authInterceptor`: el token viaja en el cuerpo del envelope\n // (baseapi §2.3, criterio 11: ninguna cabecera custom).\n provideHttpClient(withInterceptors([apiErrorInterceptor])),\n // Carga de config en bootstrap (fuera del constructor del servicio):\n // un singleton que falle no rompe el arranque.\n provideAppInitializer(() => inject(ConfigService).load(configEndpoints())),\n // Sesión (R2.12): revalida contra `/auth/me` el token que restauró el\n // AuthService de `sessionStorage`. Corre ANTES del primer guard.\n provideAppInitializer(() => inject(AuthService).restore()),\n ];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { n as get } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
|
|
2
|
+
const COL_SPANS = {
|
|
3
|
+
1: "sm:col-span-1",
|
|
4
|
+
2: "sm:col-span-2",
|
|
5
|
+
3: "sm:col-span-3",
|
|
6
|
+
4: "sm:col-span-4",
|
|
7
|
+
5: "sm:col-span-5",
|
|
8
|
+
6: "sm:col-span-6",
|
|
9
|
+
7: "sm:col-span-7",
|
|
10
|
+
8: "sm:col-span-8",
|
|
11
|
+
9: "sm:col-span-9",
|
|
12
|
+
10: "sm:col-span-10",
|
|
13
|
+
11: "sm:col-span-11",
|
|
14
|
+
12: "sm:col-span-12"
|
|
15
|
+
};
|
|
16
|
+
function colSpanClass(width) {
|
|
17
|
+
return COL_SPANS[width ?? 12];
|
|
18
|
+
}
|
|
19
|
+
function seedValue(field) {
|
|
20
|
+
const descriptor = get("field", field.type);
|
|
21
|
+
if (!descriptor) return;
|
|
22
|
+
if (!descriptor.seed) throw new Error(`[seed] el descriptor de "${field.type}" no declara \`seed\`.`);
|
|
23
|
+
switch (descriptor.seed) {
|
|
24
|
+
case "string": return "";
|
|
25
|
+
case "number": return null;
|
|
26
|
+
case "boolean": return false;
|
|
27
|
+
case "array": return [];
|
|
28
|
+
case "recursive": return seedHijos(field, descriptor.seed);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function seedHijos(field, seed) {
|
|
32
|
+
if (seed !== "recursive" || !esGrupo(field)) throw new Error(`[seed] "${field.type}" declara \`seed: 'recursive'\` pero no trae subcampos.`);
|
|
33
|
+
return seedFields(field.fields);
|
|
34
|
+
}
|
|
35
|
+
function esGrupo(field) {
|
|
36
|
+
return field.type === "group";
|
|
37
|
+
}
|
|
38
|
+
function seedFieldValue(field) {
|
|
39
|
+
if (field.default === void 0) return seedValue(field);
|
|
40
|
+
if (field.type === "group" && isRecord(field.default)) return {
|
|
41
|
+
...seedFields(field.fields),
|
|
42
|
+
...field.default
|
|
43
|
+
};
|
|
44
|
+
if (field.type === "list" && Array.isArray(field.default)) return field.default.map((item) => isRecord(item) ? {
|
|
45
|
+
...seedFields(field.itemFields),
|
|
46
|
+
...item
|
|
47
|
+
} : item);
|
|
48
|
+
return field.default;
|
|
49
|
+
}
|
|
50
|
+
function seedFields(fields) {
|
|
51
|
+
const model = {};
|
|
52
|
+
for (const field of fields) model[field.key] = seedFieldValue(field);
|
|
53
|
+
return model;
|
|
54
|
+
}
|
|
55
|
+
function mergeRecord(fields, record) {
|
|
56
|
+
const merged = {};
|
|
57
|
+
for (const field of fields) {
|
|
58
|
+
const incoming = record[field.key];
|
|
59
|
+
if (field.type === "group") {
|
|
60
|
+
merged[field.key] = isRecord(incoming) ? mergeRecord(field.fields, incoming) : seedFieldValue(field);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (field.type === "list") {
|
|
64
|
+
const itemSeed = seedFields(field.itemFields);
|
|
65
|
+
merged[field.key] = Array.isArray(incoming) ? incoming.map((item) => isRecord(item) ? {
|
|
66
|
+
...itemSeed,
|
|
67
|
+
...item
|
|
68
|
+
} : item) : seedFieldValue(field);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
merged[field.key] = incoming !== void 0 ? incoming : seedFieldValue(field);
|
|
72
|
+
}
|
|
73
|
+
return merged;
|
|
74
|
+
}
|
|
75
|
+
function isRecord(value) {
|
|
76
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
77
|
+
}
|
|
78
|
+
export { colSpanClass as a, seedFields as i, mergeRecord as n, seedFieldValue as r, isRecord as t };
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-seed-CNbQtREA.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/shared/utils/col-span.ts","../../../projects/gas-admin-ui/src/lib/shared/utils/seed.ts"],"sourcesContent":["/**\n * Anchos de columna del layout (base.md §4: `width` 1–12).\n *\n * `sm:col-span-*` como literales: Tailwind v4 solo genera las clases que ve\n * en el código, así que no pueden construirse por concatenación.\n */\nconst COL_SPANS: Record<number, string> = {\n 1: 'sm:col-span-1',\n 2: 'sm:col-span-2',\n 3: 'sm:col-span-3',\n 4: 'sm:col-span-4',\n 5: 'sm:col-span-5',\n 6: 'sm:col-span-6',\n 7: 'sm:col-span-7',\n 8: 'sm:col-span-8',\n 9: 'sm:col-span-9',\n 10: 'sm:col-span-10',\n 11: 'sm:col-span-11',\n 12: 'sm:col-span-12',\n};\n\n/** Clase `sm:col-span-*` para el ancho del campo (por defecto: 12). */\nexport function colSpanClass(width: number | undefined): string {\n return COL_SPANS[width ?? 12];\n}\n","import type { FieldSchema, GroupField } from '../../core/models/schema.model';\nimport { get } from '../../core/registry/render-registry';\n\n/**\n * Forma del valor inicial que siembra un campo (D32 · R3.8).\n *\n * Son **todos** los valores posibles del dominio: un campo de formulario\n * nuevo nace como cadena, número, booleano, lista o como contenedor que\n * siembra sus hijos. Si un tipo llegara a necesitar otra forma, ampliar este\n * `type` obliga a compilar el `switch` de `seedValue` — que es exactamente\n * el fail-loud que la regla 4 pide.\n */\nexport type SeedKind = 'string' | 'number' | 'boolean' | 'array' | 'recursive';\n\n/**\n * Siembra del modelo del formulario: un valor por defecto para CADA campo\n * declarado en el schema (base.md §10).\n *\n * Compartido entre `form-model` (crear/recargar) y los componentes de lista\n * (crear ítems nuevos con la forma de sus `itemFields`).\n */\n\n/**\n * Valor inicial cuando el schema no declara `default`.\n *\n * **D32 · ya no despacha por tipo.** El `switch` de 20 ramas que obligaba a\n * editar este fichero al añadir un campo — y que §0 contaba como tocar el\n * motor — pasa a resolver el nombre que declara el propio descriptor\n * (`RenderDescriptor.seed`). «Añadir un tipo» queda en carpeta + manifiesto\n * + unión: este archivo no se entera.\n *\n * Dos salidas distintas, ninguna silenciosa:\n *\n * · **tipo fuera del catálogo** ⇒ `undefined`, la misma degradación\n * controlada que el render (R1.10): el campo no se pinta, así que su\n * valor no existe;\n * · **descriptor sin `seed`** ⇒ lanza. Es un error de programación que\n * `core/registry/contract.spec.ts` detecta en CI antes de llegar acá.\n */\nexport function seedValue(field: FieldSchema): unknown {\n const descriptor = get('field', field.type);\n\n if (!descriptor) {\n return undefined;\n }\n if (!descriptor.seed) {\n throw new Error(`[seed] el descriptor de \"${field.type}\" no declara \\`seed\\`.`);\n }\n\n switch (descriptor.seed) {\n case 'string':\n return '';\n case 'number':\n return null;\n case 'boolean':\n return false;\n case 'array':\n return [];\n case 'recursive':\n return seedHijos(field, descriptor.seed);\n }\n}\n\n/**\n * Siembra recursiva de un contenedor.\n *\n * El único `seed` recursivo del catálogo es `group`, pero el registro no lo\n * sabe — sólo lleva un nombre. El estrechado se comprueba aquí y **falla\n * ruidoso** si algún día un tipo declara `'recursive'` sin traer subcampos.\n */\nfunction seedHijos(field: FieldSchema, seed: 'recursive'): unknown {\n if (seed !== 'recursive' || !esGrupo(field)) {\n throw new Error(`[seed] \"${field.type}\" declara \\`seed: 'recursive'\\` pero no trae subcampos.`);\n }\n return seedFields(field.fields);\n}\n\n/** Estrechado de forma, no de despacho: no decide qué hacer, sólo accede. */\nfunction esGrupo(field: FieldSchema): field is GroupField {\n return field.type === 'group';\n}\n\n/** Valor sembrado de un campo, fusionando `default` con la forma del schema. */\nexport function seedFieldValue(field: FieldSchema): unknown {\n if (field.default === undefined) {\n return seedValue(field);\n }\n if (field.type === 'group' && isRecord(field.default)) {\n // Un `default` de grupo podría traer claves incompletas: los hijos se\n // acceden por árbol, así que la forma completa es obligatoria.\n return { ...seedFields(field.fields), ...field.default };\n }\n if (field.type === 'list' && Array.isArray(field.default)) {\n return field.default.map((item) =>\n isRecord(item) ? { ...seedFields(field.itemFields), ...item } : item,\n );\n }\n return field.default;\n}\n\n/**\n * Siembra recursiva: los `group` anidan sus propios campos. Exportada porque\n * la lista crea sus ítems nuevos con la misma forma (`base.md §6`).\n */\nexport function seedFields(fields: readonly FieldSchema[]): Record<string, unknown> {\n const model: Record<string, unknown> = {};\n for (const field of fields) {\n model[field.key] = seedFieldValue(field);\n }\n return model;\n}\n\n/**\n * Fusiona el registro leído con la forma sembrada del schema.\n *\n * El backend puede omitir claves (p. ej. un ítem guardado antes de que el\n * schema agregara un campo): sin esta fusión el árbol no tendría nodo para\n * ese campo y `childTree` lanzaría al renderizar. Lo ausente toma su\n * `default` y los extras del registro (p. ej. `position` de los ítems) se\n * conservan.\n */\nexport function mergeRecord(\n fields: readonly FieldSchema[],\n record: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = {};\n for (const field of fields) {\n const incoming = record[field.key];\n if (field.type === 'group') {\n merged[field.key] = isRecord(incoming)\n ? mergeRecord(field.fields, incoming)\n : seedFieldValue(field);\n continue;\n }\n if (field.type === 'list') {\n const itemSeed = seedFields(field.itemFields);\n merged[field.key] = Array.isArray(incoming)\n ? incoming.map((item) => (isRecord(item) ? { ...itemSeed, ...item } : item))\n : seedFieldValue(field);\n continue;\n }\n merged[field.key] = incoming !== undefined ? incoming : seedFieldValue(field);\n }\n return merged;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n"],"mappings":";AAMA,MAAM,YAAoC;CACxC,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,IAAI;CACJ,IAAI;CACJ,IAAI;AACL;AAGD,SAAgB,aAAa,OAAyB;CACpD,OAAO,UAAU,SAAS;AAC5B;ACeA,SAAgB,UAAU,OAAkB;CAC1C,MAAM,aAAa,IAAI,SAAS,MAAM,IAAI;CAE1C,IAAI,CAAC,YACH;CAEF,IAAI,CAAC,WAAW,MACd,MAAM,IAAI,MAAM,4BAA4B,MAAM,KAAI,uBAAwB;CAGhF,QAAQ,WAAW,MAAnB;EACE,KAAK,UACH,OAAO;EACT,KAAK,UACH,OAAO;EACT,KAAK,WACH,OAAO;EACT,KAAK,SACH,OAAO,CAAA;EACT,KAAK,aACH,OAAO,UAAU,OAAO,WAAW,IAAI;CAC3C;AACF;AASA,SAAS,UAAU,OAAoB,MAAiB;CACtD,IAAI,SAAS,eAAe,CAAC,QAAQ,KAAK,GACxC,MAAM,IAAI,MAAM,WAAW,MAAM,KAAI,wDAAyD;CAEhG,OAAO,WAAW,MAAM,MAAM;AAChC;AAGA,SAAS,QAAQ,OAAkB;CACjC,OAAO,MAAM,SAAS;AACxB;AAGA,SAAgB,eAAe,OAAkB;CAC/C,IAAI,MAAM,YAAY,QACpB,OAAO,UAAU,KAAK;CAExB,IAAI,MAAM,SAAS,WAAW,SAAS,MAAM,OAAO,GAGlD,OAAO;EAAE,GAAG,WAAW,MAAM,MAAM;EAAG,GAAG,MAAM;CAAO;CAExD,IAAI,MAAM,SAAS,UAAU,MAAM,QAAQ,MAAM,OAAO,GACtD,OAAO,MAAM,QAAQ,KAAK,SACxB,SAAS,IAAI,IAAI;EAAE,GAAG,WAAW,MAAM,UAAU;EAAG,GAAG;CAAI,IAAK,IAAI;CAGxE,OAAO,MAAM;AACf;AAMA,SAAgB,WAAW,QAA8B;CACvD,MAAM,QAAiC,CAAA;CACvC,KAAK,MAAM,SAAS,QAClB,MAAM,MAAM,OAAO,eAAe,KAAK;CAEzC,OAAO;AACT;AAWA,SAAgB,YACd,QACA,QAA+B;CAE/B,MAAM,SAAkC,CAAA;CACxC,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,WAAW,OAAO,MAAM;EAC9B,IAAI,MAAM,SAAS,SAAS;GAC1B,OAAO,MAAM,OAAO,SAAS,QAAQ,IACjC,YAAY,MAAM,QAAQ,QAAQ,IAClC,eAAe,KAAK;GACxB;EACF;EACA,IAAI,MAAM,SAAS,QAAQ;GACzB,MAAM,WAAW,WAAW,MAAM,UAAU;GAC5C,OAAO,MAAM,OAAO,MAAM,QAAQ,QAAQ,IACtC,SAAS,KAAK,SAAU,SAAS,IAAI,IAAI;IAAE,GAAG;IAAU,GAAG;GAAI,IAAK,IAAK,IACzE,eAAe,KAAK;GACxB;EACF;EACA,OAAO,MAAM,OAAO,aAAa,SAAY,WAAW,eAAe,KAAK;CAC9E;CACA,OAAO;AACT;AAEA,SAAgB,SAAS,OAAc;CACrC,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E"}
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
|
|
2
|
+
import { n as dataSourceKey, t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs";
|
|
3
|
+
import { n as FieldAria } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
|
|
4
|
+
import { t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
5
|
+
import { t as optionsFromRows } from "./tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs";
|
|
6
|
+
import { t as esGrupoRadio } from "./tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs";
|
|
7
|
+
import * as i0 from "@angular/core";
|
|
8
|
+
import { Component, computed, inject, input, resource, signal } from "@angular/core";
|
|
9
|
+
import { FormField } from "@angular/forms/signals";
|
|
10
|
+
var FieldSelect = class FieldSelect {
|
|
11
|
+
field = input.required(...ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []);
|
|
12
|
+
tree = input.required(...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
|
|
13
|
+
ds = inject(DataSourceService);
|
|
14
|
+
node = computed(() => childTree(this.tree(), this.field().key), ...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
|
|
15
|
+
state = computed(() => this.node()(), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
|
|
16
|
+
query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
|
|
17
|
+
registro = computed(() => {
|
|
18
|
+
const source = this.field().source;
|
|
19
|
+
if (source === void 0 || source.params === void 0) return void 0;
|
|
20
|
+
return this.tree()().value();
|
|
21
|
+
}, ...ngDevMode ? [{ debugName: "registro" }] : /* istanbul ignore next */ []);
|
|
22
|
+
claveFuente = computed(() => {
|
|
23
|
+
const source = this.field().source;
|
|
24
|
+
return source === void 0 ? void 0 : dataSourceKey(source, this.registro());
|
|
25
|
+
}, ...ngDevMode ? [{ debugName: "claveFuente" }] : /* istanbul ignore next */ []);
|
|
26
|
+
fuenteResource = resource({
|
|
27
|
+
...ngDevMode ? { debugName: "fuenteResource" } : /* istanbul ignore next */ {},
|
|
28
|
+
params: () => this.claveFuente(),
|
|
29
|
+
loader: async () => {
|
|
30
|
+
const source = this.field().source;
|
|
31
|
+
if (source === void 0) return [];
|
|
32
|
+
return this.ds.rows(source, this.registro());
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
options = computed(() => {
|
|
36
|
+
const field = this.field();
|
|
37
|
+
if (field.source === void 0) return field.options ?? [];
|
|
38
|
+
if (this.fuenteResource.status() === "error") return [];
|
|
39
|
+
return optionsFromRows(this.fuenteResource.value() ?? [], {
|
|
40
|
+
source: field.source,
|
|
41
|
+
target: getSchema(field.source.resource)
|
|
42
|
+
});
|
|
43
|
+
}, ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
|
|
44
|
+
cargando = computed(() => this.claveFuente() !== void 0 && this.fuenteResource.status() === "loading", ...ngDevMode ? [{ debugName: "cargando" }] : /* istanbul ignore next */ []);
|
|
45
|
+
fallo = computed(() => this.claveFuente() !== void 0 && this.fuenteResource.status() === "error", ...ngDevMode ? [{ debugName: "fallo" }] : /* istanbul ignore next */ []);
|
|
46
|
+
useSearch = computed(() => this.options().length > 7, ...ngDevMode ? [{ debugName: "useSearch" }] : /* istanbul ignore next */ []);
|
|
47
|
+
useRadio = computed(() => esGrupoRadio(this.field()), ...ngDevMode ? [{ debugName: "useRadio" }] : /* istanbul ignore next */ []);
|
|
48
|
+
onQuery(event) {
|
|
49
|
+
this.query.set(event.target.value);
|
|
50
|
+
}
|
|
51
|
+
matches = computed(() => {
|
|
52
|
+
const all = this.options();
|
|
53
|
+
const term = this.query().trim().toLowerCase();
|
|
54
|
+
if (term === "") return all;
|
|
55
|
+
return all.filter((option) => option.label.toLowerCase().includes(term));
|
|
56
|
+
}, ...ngDevMode ? [{ debugName: "matches" }] : /* istanbul ignore next */ []);
|
|
57
|
+
noMatches = computed(() => this.useSearch() && this.query().trim() !== "" && this.matches().length === 0, ...ngDevMode ? [{ debugName: "noMatches" }] : /* istanbul ignore next */ []);
|
|
58
|
+
visibleOptions = computed(() => {
|
|
59
|
+
const filtered = this.matches();
|
|
60
|
+
if (this.query().trim() === "") return filtered;
|
|
61
|
+
const selected = String(this.state().value() ?? "");
|
|
62
|
+
if (filtered.some((option) => option.value === selected)) return filtered;
|
|
63
|
+
const kept = this.options().find((option) => option.value === selected);
|
|
64
|
+
return kept === void 0 ? filtered : [...filtered, kept];
|
|
65
|
+
}, ...ngDevMode ? [{ debugName: "visibleOptions" }] : /* istanbul ignore next */ []);
|
|
66
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
67
|
+
minVersion: "12.0.0",
|
|
68
|
+
version: "22.2.3",
|
|
69
|
+
ngImport: i0,
|
|
70
|
+
type: FieldSelect,
|
|
71
|
+
deps: [],
|
|
72
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
73
|
+
});
|
|
74
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
75
|
+
minVersion: "17.0.0",
|
|
76
|
+
version: "22.2.3",
|
|
77
|
+
type: FieldSelect,
|
|
78
|
+
isStandalone: true,
|
|
79
|
+
selector: "app-field-select",
|
|
80
|
+
inputs: {
|
|
81
|
+
field: {
|
|
82
|
+
classPropertyName: "field",
|
|
83
|
+
publicName: "field",
|
|
84
|
+
isSignal: true,
|
|
85
|
+
isRequired: true,
|
|
86
|
+
transformFunction: null
|
|
87
|
+
},
|
|
88
|
+
tree: {
|
|
89
|
+
classPropertyName: "tree",
|
|
90
|
+
publicName: "tree",
|
|
91
|
+
isSignal: true,
|
|
92
|
+
isRequired: true,
|
|
93
|
+
transformFunction: null
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
ngImport: i0,
|
|
97
|
+
template: `
|
|
98
|
+
@if (fallo()) {
|
|
99
|
+
<p class="mb-1 text-xs text-danger" role="alert">No se pudieron cargar las opciones.</p>
|
|
100
|
+
} @else if (cargando()) {
|
|
101
|
+
<p class="mb-1 text-xs text-neutral" role="status">Cargando opciones…</p>
|
|
102
|
+
}
|
|
103
|
+
@if (useRadio()) {
|
|
104
|
+
<!-- Guía de control: 2–4 opciones estáticas → radio buttons, no un
|
|
105
|
+
desplegable. Cada opción es un label implícito (texto y control
|
|
106
|
+
juntos) y el grupo se nombra con aria-label: el rótulo del host
|
|
107
|
+
se dibuja como texto porque un grupo no es un único control con
|
|
108
|
+
el que emparejar un for. formField va en CADA input: Signal
|
|
109
|
+
Forms soporta radio (checked = value === element.value) y setea
|
|
110
|
+
name/disabled del grupo por estado de campo. -->
|
|
111
|
+
<div
|
|
112
|
+
class="mt-1 flex flex-col gap-1"
|
|
113
|
+
role="radiogroup"
|
|
114
|
+
[attr.aria-label]="field().label"
|
|
115
|
+
data-testid="select-radios"
|
|
116
|
+
>
|
|
117
|
+
@if (!field().required) {
|
|
118
|
+
<label class="flex w-fit items-center gap-2 text-sm">
|
|
119
|
+
<input
|
|
120
|
+
type="radio"
|
|
121
|
+
class="accent-primary"
|
|
122
|
+
[id]="state().name() + '-__vacio'"
|
|
123
|
+
value=""
|
|
124
|
+
[formField]="node()"
|
|
125
|
+
/>
|
|
126
|
+
<span class="text-neutral">{{ field().placeholder ?? '— Seleccionar —' }}</span>
|
|
127
|
+
</label>
|
|
128
|
+
}
|
|
129
|
+
@for (option of options(); track option.value) {
|
|
130
|
+
<label class="flex w-fit items-center gap-2 text-sm">
|
|
131
|
+
<input
|
|
132
|
+
type="radio"
|
|
133
|
+
class="accent-primary"
|
|
134
|
+
[id]="state().name() + '-' + option.value"
|
|
135
|
+
[value]="option.value"
|
|
136
|
+
[formField]="node()"
|
|
137
|
+
/>
|
|
138
|
+
<span>{{ option.label }}</span>
|
|
139
|
+
</label>
|
|
140
|
+
}
|
|
141
|
+
</div>
|
|
142
|
+
} @else if (useSearch()) {
|
|
143
|
+
<!-- Guía §1: más de 7 opciones → dropdown con búsqueda. Se filtra el
|
|
144
|
+
select nativo en vez de construir un combobox: el teclado, el
|
|
145
|
+
anuncio de la opción activa y el valor elegido siguen siendo los
|
|
146
|
+
del navegador. -->
|
|
147
|
+
<input
|
|
148
|
+
type="search"
|
|
149
|
+
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"
|
|
150
|
+
placeholder="Buscar…"
|
|
151
|
+
aria-label="Buscar entre las opciones"
|
|
152
|
+
[attr.aria-controls]="state().name()"
|
|
153
|
+
[value]="query()"
|
|
154
|
+
(input)="onQuery($event)"
|
|
155
|
+
/>
|
|
156
|
+
}
|
|
157
|
+
<select class="field-input" [id]="state().name()" [formField]="node()">
|
|
158
|
+
@if (!field().required) {
|
|
159
|
+
<option value="">{{ field().placeholder ?? '— Seleccionar —' }}</option>
|
|
160
|
+
}
|
|
161
|
+
@for (option of visibleOptions(); track option.value) {
|
|
162
|
+
<option [value]="option.value" [disabled]="option.disabled ?? false">
|
|
163
|
+
{{ option.label }}
|
|
164
|
+
</option>
|
|
165
|
+
}
|
|
166
|
+
</select>
|
|
167
|
+
@if (noMatches()) {
|
|
168
|
+
<p class="mt-1 text-xs text-neutral" role="status">Sin resultados para «{{ query() }}».</p>
|
|
169
|
+
}
|
|
170
|
+
`,
|
|
171
|
+
isInline: true,
|
|
172
|
+
dependencies: [{
|
|
173
|
+
kind: "directive",
|
|
174
|
+
type: FieldAria,
|
|
175
|
+
selector: "input[id], select[id], textarea[id], output[id]"
|
|
176
|
+
}, {
|
|
177
|
+
kind: "directive",
|
|
178
|
+
type: FormField,
|
|
179
|
+
selector: "[formField]",
|
|
180
|
+
inputs: ["formField"],
|
|
181
|
+
exportAs: ["formField"]
|
|
182
|
+
}]
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
186
|
+
minVersion: "12.0.0",
|
|
187
|
+
version: "22.2.3",
|
|
188
|
+
ngImport: i0,
|
|
189
|
+
type: FieldSelect,
|
|
190
|
+
decorators: [{
|
|
191
|
+
type: Component,
|
|
192
|
+
args: [{
|
|
193
|
+
selector: "app-field-select",
|
|
194
|
+
imports: [FieldAria, FormField],
|
|
195
|
+
template: `
|
|
196
|
+
@if (fallo()) {
|
|
197
|
+
<p class="mb-1 text-xs text-danger" role="alert">No se pudieron cargar las opciones.</p>
|
|
198
|
+
} @else if (cargando()) {
|
|
199
|
+
<p class="mb-1 text-xs text-neutral" role="status">Cargando opciones…</p>
|
|
200
|
+
}
|
|
201
|
+
@if (useRadio()) {
|
|
202
|
+
<!-- Guía de control: 2–4 opciones estáticas → radio buttons, no un
|
|
203
|
+
desplegable. Cada opción es un label implícito (texto y control
|
|
204
|
+
juntos) y el grupo se nombra con aria-label: el rótulo del host
|
|
205
|
+
se dibuja como texto porque un grupo no es un único control con
|
|
206
|
+
el que emparejar un for. formField va en CADA input: Signal
|
|
207
|
+
Forms soporta radio (checked = value === element.value) y setea
|
|
208
|
+
name/disabled del grupo por estado de campo. -->
|
|
209
|
+
<div
|
|
210
|
+
class="mt-1 flex flex-col gap-1"
|
|
211
|
+
role="radiogroup"
|
|
212
|
+
[attr.aria-label]="field().label"
|
|
213
|
+
data-testid="select-radios"
|
|
214
|
+
>
|
|
215
|
+
@if (!field().required) {
|
|
216
|
+
<label class="flex w-fit items-center gap-2 text-sm">
|
|
217
|
+
<input
|
|
218
|
+
type="radio"
|
|
219
|
+
class="accent-primary"
|
|
220
|
+
[id]="state().name() + '-__vacio'"
|
|
221
|
+
value=""
|
|
222
|
+
[formField]="node()"
|
|
223
|
+
/>
|
|
224
|
+
<span class="text-neutral">{{ field().placeholder ?? '— Seleccionar —' }}</span>
|
|
225
|
+
</label>
|
|
226
|
+
}
|
|
227
|
+
@for (option of options(); track option.value) {
|
|
228
|
+
<label class="flex w-fit items-center gap-2 text-sm">
|
|
229
|
+
<input
|
|
230
|
+
type="radio"
|
|
231
|
+
class="accent-primary"
|
|
232
|
+
[id]="state().name() + '-' + option.value"
|
|
233
|
+
[value]="option.value"
|
|
234
|
+
[formField]="node()"
|
|
235
|
+
/>
|
|
236
|
+
<span>{{ option.label }}</span>
|
|
237
|
+
</label>
|
|
238
|
+
}
|
|
239
|
+
</div>
|
|
240
|
+
} @else if (useSearch()) {
|
|
241
|
+
<!-- Guía §1: más de 7 opciones → dropdown con búsqueda. Se filtra el
|
|
242
|
+
select nativo en vez de construir un combobox: el teclado, el
|
|
243
|
+
anuncio de la opción activa y el valor elegido siguen siendo los
|
|
244
|
+
del navegador. -->
|
|
245
|
+
<input
|
|
246
|
+
type="search"
|
|
247
|
+
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"
|
|
248
|
+
placeholder="Buscar…"
|
|
249
|
+
aria-label="Buscar entre las opciones"
|
|
250
|
+
[attr.aria-controls]="state().name()"
|
|
251
|
+
[value]="query()"
|
|
252
|
+
(input)="onQuery($event)"
|
|
253
|
+
/>
|
|
254
|
+
}
|
|
255
|
+
<select class="field-input" [id]="state().name()" [formField]="node()">
|
|
256
|
+
@if (!field().required) {
|
|
257
|
+
<option value="">{{ field().placeholder ?? '— Seleccionar —' }}</option>
|
|
258
|
+
}
|
|
259
|
+
@for (option of visibleOptions(); track option.value) {
|
|
260
|
+
<option [value]="option.value" [disabled]="option.disabled ?? false">
|
|
261
|
+
{{ option.label }}
|
|
262
|
+
</option>
|
|
263
|
+
}
|
|
264
|
+
</select>
|
|
265
|
+
@if (noMatches()) {
|
|
266
|
+
<p class="mt-1 text-xs text-neutral" role="status">Sin resultados para «{{ query() }}».</p>
|
|
267
|
+
}
|
|
268
|
+
`
|
|
269
|
+
}]
|
|
270
|
+
}],
|
|
271
|
+
propDecorators: {
|
|
272
|
+
field: [{
|
|
273
|
+
type: i0.Input,
|
|
274
|
+
args: [{
|
|
275
|
+
isSignal: true,
|
|
276
|
+
alias: "field",
|
|
277
|
+
required: true
|
|
278
|
+
}]
|
|
279
|
+
}],
|
|
280
|
+
tree: [{
|
|
281
|
+
type: i0.Input,
|
|
282
|
+
args: [{
|
|
283
|
+
isSignal: true,
|
|
284
|
+
alias: "tree",
|
|
285
|
+
required: true
|
|
286
|
+
}]
|
|
287
|
+
}]
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
export { FieldSelect };
|
|
291
|
+
|
|
292
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tinya-code-gas-admin-ui-select-DzaRfHXO.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/fields/select/select.ts"],"sourcesContent":["import { Component, computed, inject, input, resource, signal } from '@angular/core';\nimport { FormField } from '@angular/forms/signals';\n\nimport type { SelectField, 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 { FieldAria } from '../field-aria';\nimport { childTree, type RootTree } from '../field-node';\nimport { esGrupoRadio } from './radio-mode';\n\n/**\n * Desplegable de un solo valor (base.md §5.1).\n *\n * Sus opciones se declaran de dos formas y **exactamente una** (XOR, D62):\n * `options` estáticas en el schema o `source` — una fuente declarada que\n * pide `DataSourceService` con la misma caché que usan las relaciones y los\n * widgets (F8 · R6.5). Mientras la vuelta no llega se anuncia; sin anuncio\n * un desplegable vacío se confundiría con «esto no tiene opciones».\n */\n@Component({\n selector: 'app-field-select',\n imports: [FieldAria, FormField],\n template: `\n @if (fallo()) {\n <p class=\"mb-1 text-xs text-danger\" role=\"alert\">No se pudieron cargar las opciones.</p>\n } @else if (cargando()) {\n <p class=\"mb-1 text-xs text-neutral\" role=\"status\">Cargando opciones…</p>\n }\n @if (useRadio()) {\n <!-- Guía de control: 2–4 opciones estáticas → radio buttons, no un\n desplegable. Cada opción es un label implícito (texto y control\n juntos) y el grupo se nombra con aria-label: el rótulo del host\n se dibuja como texto porque un grupo no es un único control con\n el que emparejar un for. formField va en CADA input: Signal\n Forms soporta radio (checked = value === element.value) y setea\n name/disabled del grupo por estado de campo. -->\n <div\n class=\"mt-1 flex flex-col gap-1\"\n role=\"radiogroup\"\n [attr.aria-label]=\"field().label\"\n data-testid=\"select-radios\"\n >\n @if (!field().required) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-__vacio'\"\n value=\"\"\n [formField]=\"node()\"\n />\n <span class=\"text-neutral\">{{ field().placeholder ?? '— Seleccionar —' }}</span>\n </label>\n }\n @for (option of options(); track option.value) {\n <label class=\"flex w-fit items-center gap-2 text-sm\">\n <input\n type=\"radio\"\n class=\"accent-primary\"\n [id]=\"state().name() + '-' + option.value\"\n [value]=\"option.value\"\n [formField]=\"node()\"\n />\n <span>{{ option.label }}</span>\n </label>\n }\n </div>\n } @else if (useSearch()) {\n <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. Se filtra el\n select nativo en vez de construir un combobox: el teclado, el\n anuncio de la opción activa y el valor elegido siguen siendo los\n del navegador. -->\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\" [disabled]=\"option.disabled ?? false\">\n {{ option.label }}\n </option>\n }\n </select>\n @if (noMatches()) {\n <p class=\"mt-1 text-xs text-neutral\" role=\"status\">Sin resultados para «{{ query() }}».</p>\n }\n `,\n})\nexport class FieldSelect {\n readonly field = input.required<SelectField>();\n readonly tree = input.required<RootTree>();\n\n private readonly ds = inject(DataSourceService);\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 /**\n * Registro del formulario, sólo para resolver `{campo}` en `source.params`.\n * Sin parámetros declarados no se lee: el campo no depende del formulario.\n */\n private readonly registro = computed<Record<string, unknown> | undefined>(() => {\n const source = this.field().source;\n if (source === undefined || source.params === undefined) return undefined;\n return this.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 /**\n * Opciones efectivas: las estáticas del schema o las filas de la fuente\n * declarada, mapeadas con la misma regla que una relación (F8 · R6.5).\n */\n protected readonly options = 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 propio render rompe en vez\n // de 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 /** 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 /** Guía §1: 2–4 opciones estáticas → radio buttons (ver `radio-mode.ts`). */\n protected readonly useRadio = computed(() => esGrupoRadio(this.field()));\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, SIN conservar la elegida: es la\n * 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, pero\n * la opción elegida se conserva siempre aunque no coincida con el término:\n * si desapareciera del DOM, el select dejaría de mostrar el valor actual\n * (guía §1: «valor seleccionado siempre visible»).\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"],"mappings":"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"}
|