@tinya-code/gas-admin-ui 0.0.0-stage → 0.1.0-alpha.1
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 +63 -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 +4400 -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 +3091 -0
|
@@ -0,0 +1,4514 @@
|
|
|
1
|
+
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
+
import { n as AuthService, t as ApiService } from "./tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs";
|
|
3
|
+
import { i as getSchema, s as ConfigService } from "./tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs";
|
|
4
|
+
import { n as get, r as isSupported, s as resolve, t as RENDER_CONTEXT_VACIO } from "./tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs";
|
|
5
|
+
import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs";
|
|
6
|
+
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
7
|
+
import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
|
|
8
|
+
import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
|
|
9
|
+
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs";
|
|
10
|
+
import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
|
|
11
|
+
import { t as FIELD_ARIA } from "./tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs";
|
|
12
|
+
import { i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
13
|
+
import { t as FieldFile } from "./tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs";
|
|
14
|
+
import { t as FieldImage } from "./tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs";
|
|
15
|
+
import { t as FieldMultiselect } from "./tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs";
|
|
16
|
+
import { t as esGrupoRadio } from "./tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs";
|
|
17
|
+
import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
|
|
18
|
+
import { a as colSpanClass, i as seedFields, n as mergeRecord, r as seedFieldValue, t as isRecord } from "./tinya-code-gas-admin-ui-seed-CNbQtREA.mjs";
|
|
19
|
+
import { t as RelationDrawerService } from "./tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs";
|
|
20
|
+
import * as i0 from "@angular/core";
|
|
21
|
+
import { ApplicationRef, Component, DestroyRef, InjectionToken, Injector, PendingTasks, afterRenderEffect, computed, effect, forwardRef, inject, input, isDevMode, output, resource, signal, viewChild } from "@angular/core";
|
|
22
|
+
import { firstValueFrom, of } from "rxjs";
|
|
23
|
+
import { ActivatedRoute, Router } from "@angular/router";
|
|
24
|
+
import { LucideCheckCircle2, LucideCircleAlert, LucideCopy, LucideInfo, LucideShare, LucideShare2, LucideX } from "@lucide/angular";
|
|
25
|
+
import { takeUntilDestroyed, toSignal } from "@angular/core/rxjs-interop";
|
|
26
|
+
import { NgComponentOutlet } from "@angular/common";
|
|
27
|
+
import { FormField, applyEach, disabled, email, form, hidden, max, maxLength, min, minLength, pattern, readonly, required, submit, validate, validateAsync } from "@angular/forms/signals";
|
|
28
|
+
const TEXTO = () => import("./tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs").then((module) => module.FieldDisplayText);
|
|
29
|
+
const FECHA = () => import("./tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs").then((module) => module.FieldDisplayDate);
|
|
30
|
+
const NUMERO = () => import("./tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs").then((module) => module.FieldDisplayNumber);
|
|
31
|
+
const BADGE = () => import("./tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs").then((n) => n.t).then((module) => module.FieldDisplayBadge);
|
|
32
|
+
const IMAGEN = () => import("./tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs").then((module) => module.FieldDisplayImage);
|
|
33
|
+
const ARCHIVO = () => import("./tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs").then((module) => module.FieldDisplayFile);
|
|
34
|
+
const LISTA = () => import("./tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs").then((module) => module.FieldDisplayList);
|
|
35
|
+
const DISPLAY_POR_DEFECTO = {
|
|
36
|
+
text: TEXTO,
|
|
37
|
+
textarea: TEXTO,
|
|
38
|
+
slug: TEXTO,
|
|
39
|
+
url: TEXTO,
|
|
40
|
+
email: TEXTO,
|
|
41
|
+
phone: TEXTO,
|
|
42
|
+
"readonly-text": TEXTO,
|
|
43
|
+
relation: TEXTO,
|
|
44
|
+
group: TEXTO,
|
|
45
|
+
"key-value": TEXTO,
|
|
46
|
+
"string-list": TEXTO,
|
|
47
|
+
multiselect: TEXTO,
|
|
48
|
+
time: TEXTO,
|
|
49
|
+
date: FECHA,
|
|
50
|
+
number: NUMERO,
|
|
51
|
+
currency: NUMERO,
|
|
52
|
+
boolean: BADGE,
|
|
53
|
+
select: BADGE,
|
|
54
|
+
image: IMAGEN,
|
|
55
|
+
file: ARCHIVO,
|
|
56
|
+
list: LISTA
|
|
57
|
+
};
|
|
58
|
+
function displayLoader(type) {
|
|
59
|
+
return Object.prototype.hasOwnProperty.call(DISPLAY_POR_DEFECTO, type) ? DISPLAY_POR_DEFECTO[type] : void 0;
|
|
60
|
+
}
|
|
61
|
+
var UnknownRender = class UnknownRender {
|
|
62
|
+
type = input.required(...ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []);
|
|
63
|
+
label = input("", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
64
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
65
|
+
minVersion: "12.0.0",
|
|
66
|
+
version: "22.2.3",
|
|
67
|
+
ngImport: i0,
|
|
68
|
+
type: UnknownRender,
|
|
69
|
+
deps: [],
|
|
70
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
71
|
+
});
|
|
72
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
73
|
+
minVersion: "17.0.0",
|
|
74
|
+
version: "22.2.3",
|
|
75
|
+
type: UnknownRender,
|
|
76
|
+
isStandalone: true,
|
|
77
|
+
selector: "app-unknown-render",
|
|
78
|
+
inputs: {
|
|
79
|
+
type: {
|
|
80
|
+
classPropertyName: "type",
|
|
81
|
+
publicName: "type",
|
|
82
|
+
isSignal: true,
|
|
83
|
+
isRequired: true,
|
|
84
|
+
transformFunction: null
|
|
85
|
+
},
|
|
86
|
+
label: {
|
|
87
|
+
classPropertyName: "label",
|
|
88
|
+
publicName: "label",
|
|
89
|
+
isSignal: true,
|
|
90
|
+
isRequired: false,
|
|
91
|
+
transformFunction: null
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
ngImport: i0,
|
|
95
|
+
template: `
|
|
96
|
+
<div
|
|
97
|
+
class="mb-4 rounded-lg border border-amber-400/50 bg-amber-500/10 px-3 py-2"
|
|
98
|
+
data-field-unknown
|
|
99
|
+
>
|
|
100
|
+
<p class="flex items-center gap-1.5 text-xs font-medium text-amber-600">
|
|
101
|
+
<svg lucideInfo size="14" class="shrink-0" aria-hidden="true" />
|
|
102
|
+
Campo sin renderizador en esta versión
|
|
103
|
+
</p>
|
|
104
|
+
<p class="mt-1 text-help text-neutral">
|
|
105
|
+
Tipo <code class="rounded bg-amber-500/20 px-1 font-mono">{{ type() }}</code>
|
|
106
|
+
@if (label()) {
|
|
107
|
+
del campo «{{ label() }}»
|
|
108
|
+
}
|
|
109
|
+
— el valor existe en el schema y se conserva; sólo no se dibuja.
|
|
110
|
+
</p>
|
|
111
|
+
</div>
|
|
112
|
+
`,
|
|
113
|
+
isInline: true,
|
|
114
|
+
dependencies: [{
|
|
115
|
+
kind: "component",
|
|
116
|
+
type: LucideInfo,
|
|
117
|
+
selector: "svg[lucideInfo]"
|
|
118
|
+
}]
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
122
|
+
minVersion: "12.0.0",
|
|
123
|
+
version: "22.2.3",
|
|
124
|
+
ngImport: i0,
|
|
125
|
+
type: UnknownRender,
|
|
126
|
+
decorators: [{
|
|
127
|
+
type: Component,
|
|
128
|
+
args: [{
|
|
129
|
+
selector: "app-unknown-render",
|
|
130
|
+
imports: [LucideInfo],
|
|
131
|
+
template: `
|
|
132
|
+
<div
|
|
133
|
+
class="mb-4 rounded-lg border border-amber-400/50 bg-amber-500/10 px-3 py-2"
|
|
134
|
+
data-field-unknown
|
|
135
|
+
>
|
|
136
|
+
<p class="flex items-center gap-1.5 text-xs font-medium text-amber-600">
|
|
137
|
+
<svg lucideInfo size="14" class="shrink-0" aria-hidden="true" />
|
|
138
|
+
Campo sin renderizador en esta versión
|
|
139
|
+
</p>
|
|
140
|
+
<p class="mt-1 text-help text-neutral">
|
|
141
|
+
Tipo <code class="rounded bg-amber-500/20 px-1 font-mono">{{ type() }}</code>
|
|
142
|
+
@if (label()) {
|
|
143
|
+
del campo «{{ label() }}»
|
|
144
|
+
}
|
|
145
|
+
— el valor existe en el schema y se conserva; sólo no se dibuja.
|
|
146
|
+
</p>
|
|
147
|
+
</div>
|
|
148
|
+
`
|
|
149
|
+
}]
|
|
150
|
+
}],
|
|
151
|
+
propDecorators: {
|
|
152
|
+
type: [{
|
|
153
|
+
type: i0.Input,
|
|
154
|
+
args: [{
|
|
155
|
+
isSignal: true,
|
|
156
|
+
alias: "type",
|
|
157
|
+
required: true
|
|
158
|
+
}]
|
|
159
|
+
}],
|
|
160
|
+
label: [{
|
|
161
|
+
type: i0.Input,
|
|
162
|
+
args: [{
|
|
163
|
+
isSignal: true,
|
|
164
|
+
alias: "label",
|
|
165
|
+
required: false
|
|
166
|
+
}]
|
|
167
|
+
}]
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
const RENDER_CONTEXT = new InjectionToken("RENDER_CONTEXT");
|
|
171
|
+
var RenderHost = class RenderHost {
|
|
172
|
+
kind = input("field", ...ngDevMode ? [{ debugName: "kind" }] : /* istanbul ignore next */ []);
|
|
173
|
+
type = input.required(...ngDevMode ? [{ debugName: "type" }] : /* istanbul ignore next */ []);
|
|
174
|
+
config = input.required(...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
|
|
175
|
+
ctx = input(RENDER_CONTEXT_VACIO, ...ngDevMode ? [{ debugName: "ctx" }] : /* istanbul ignore next */ []);
|
|
176
|
+
data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
|
|
177
|
+
mode = input("form", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
|
|
178
|
+
value = input(null, ...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
|
|
179
|
+
chip = input(false, ...ngDevMode ? [{ debugName: "chip" }] : /* istanbul ignore next */ []);
|
|
180
|
+
dev = isDevMode();
|
|
181
|
+
pendientes = inject(PendingTasks);
|
|
182
|
+
esCampo = computed(() => this.kind() === "field", ...ngDevMode ? [{ debugName: "esCampo" }] : /* istanbul ignore next */ []);
|
|
183
|
+
esDisplay = computed(() => this.mode() === "display", ...ngDevMode ? [{ debugName: "esDisplay" }] : /* istanbul ignore next */ []);
|
|
184
|
+
declaraDisplay = computed(() => get(this.kind(), this.type())?.surfaces?.includes("display") ?? false, ...ngDevMode ? [{ debugName: "declaraDisplay" }] : /* istanbul ignore next */ []);
|
|
185
|
+
loaderDisplay = computed(() => this.declaraDisplay() ? displayLoader(this.type()) : void 0, ...ngDevMode ? [{ debugName: "loaderDisplay" }] : /* istanbul ignore next */ []);
|
|
186
|
+
soportaDisplay = computed(() => this.loaderDisplay() !== void 0, ...ngDevMode ? [{ debugName: "soportaDisplay" }] : /* istanbul ignore next */ []);
|
|
187
|
+
vacio = computed(() => {
|
|
188
|
+
const valor = this.value();
|
|
189
|
+
return valor === null || valor === void 0 || valor === "";
|
|
190
|
+
}, ...ngDevMode ? [{ debugName: "vacio" }] : /* istanbul ignore next */ []);
|
|
191
|
+
cfgCampo = computed(() => this.config(), ...ngDevMode ? [{ debugName: "cfgCampo" }] : /* istanbul ignore next */ []);
|
|
192
|
+
node = computed(() => childTree(this.ctx().tree, this.cfgCampo().key), ...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
|
|
193
|
+
state = computed(() => this.node()(), ...ngDevMode ? [{ debugName: "state" }] : /* istanbul ignore next */ []);
|
|
194
|
+
name = computed(() => this.state().name(), ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
|
|
195
|
+
supported = computed(() => {
|
|
196
|
+
if (!isSupported(this.kind(), this.type())) return false;
|
|
197
|
+
return this.esCampo() ? !this.state().hidden() : true;
|
|
198
|
+
}, ...ngDevMode ? [{ debugName: "supported" }] : /* istanbul ignore next */ []);
|
|
199
|
+
unsupported = computed(() => this.esDisplay() ? !this.soportaDisplay() : !isSupported(this.kind(), this.type()), ...ngDevMode ? [{ debugName: "unsupported" }] : /* istanbul ignore next */ []);
|
|
200
|
+
showUnknown = computed(() => this.unsupported() && this.dev, ...ngDevMode ? [{ debugName: "showUnknown" }] : /* istanbul ignore next */ []);
|
|
201
|
+
_trazaTipoDesconocido = effect(() => {
|
|
202
|
+
if (!this.unsupported() || this.dev) return;
|
|
203
|
+
const superficie = this.esDisplay() ? "display" : "form";
|
|
204
|
+
console.warn(`[render-host] tipo sin renderizador (${superficie}): "${this.type()}" — se omite el control.`);
|
|
205
|
+
}, ...ngDevMode ? [{ debugName: "_trazaTipoDesconocido" }] : /* istanbul ignore next */ []);
|
|
206
|
+
control = signal(null, ...ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []);
|
|
207
|
+
carga = 0;
|
|
208
|
+
_cargar = effect(() => {
|
|
209
|
+
const type = this.type();
|
|
210
|
+
const carga = ++this.carga;
|
|
211
|
+
this.control.set(null);
|
|
212
|
+
if (this.esDisplay()) {
|
|
213
|
+
const loader = this.loaderDisplay();
|
|
214
|
+
if (!loader) return;
|
|
215
|
+
this.pendientes.run(async () => {
|
|
216
|
+
const componente = await loader();
|
|
217
|
+
if (this.carga === carga) this.control.set(componente);
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (!isSupported(this.kind(), type)) return;
|
|
222
|
+
this.pendientes.run(async () => {
|
|
223
|
+
const componente = await resolve(this.kind(), type, UnknownRender);
|
|
224
|
+
if (this.carga === carga) this.control.set(componente);
|
|
225
|
+
});
|
|
226
|
+
}, ...ngDevMode ? [{ debugName: "_cargar" }] : /* istanbul ignore next */ []);
|
|
227
|
+
displayInputs = computed(() => ({
|
|
228
|
+
field: this.config(),
|
|
229
|
+
value: this.value(),
|
|
230
|
+
chip: this.chip()
|
|
231
|
+
}), ...ngDevMode ? [{ debugName: "displayInputs" }] : /* istanbul ignore next */ []);
|
|
232
|
+
controlInputs = computed(() => {
|
|
233
|
+
const cfg = this.config();
|
|
234
|
+
const extras = {
|
|
235
|
+
ctx: this.ctx(),
|
|
236
|
+
data: this.data(),
|
|
237
|
+
groupContent: this.groupTpl(),
|
|
238
|
+
childTemplate: this.childTpl()
|
|
239
|
+
};
|
|
240
|
+
const descriptor = get(this.kind(), this.type());
|
|
241
|
+
if (descriptor?.inputs) return descriptor.inputs(cfg, extras);
|
|
242
|
+
return this.esCampo() ? {
|
|
243
|
+
field: cfg,
|
|
244
|
+
tree: this.ctx().tree
|
|
245
|
+
} : {
|
|
246
|
+
widget: cfg,
|
|
247
|
+
data: this.data()
|
|
248
|
+
};
|
|
249
|
+
}, ...ngDevMode ? [{ debugName: "controlInputs" }] : /* istanbul ignore next */ []);
|
|
250
|
+
group = computed(() => this.config(), ...ngDevMode ? [{ debugName: "group" }] : /* istanbul ignore next */ []);
|
|
251
|
+
list = computed(() => this.config(), ...ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []);
|
|
252
|
+
groupTpl = viewChild("groupContent", ...ngDevMode ? [{ debugName: "groupTpl" }] : /* istanbul ignore next */ []);
|
|
253
|
+
childTpl = viewChild("childTpl", ...ngDevMode ? [{ debugName: "childTpl" }] : /* istanbul ignore next */ []);
|
|
254
|
+
groupChildCtx = computed(() => ({
|
|
255
|
+
...this.ctx(),
|
|
256
|
+
tree: groupTree(this.ctx().tree, this.config().key),
|
|
257
|
+
pathPrefix: `${this.ctx().pathPrefix}${this.config().key}.`
|
|
258
|
+
}), ...ngDevMode ? [{ debugName: "groupChildCtx" }] : /* istanbul ignore next */ []);
|
|
259
|
+
listChildCtx(child) {
|
|
260
|
+
return {
|
|
261
|
+
tree: child.tree,
|
|
262
|
+
serverErrors: child.errors,
|
|
263
|
+
pathPrefix: child.prefix,
|
|
264
|
+
exists: child.exists,
|
|
265
|
+
resource: this.ctx().resource
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
colSpan(width) {
|
|
269
|
+
return colSpanClass(width);
|
|
270
|
+
}
|
|
271
|
+
multiselectCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "multiselectCfg" }] : /* istanbul ignore next */ []);
|
|
272
|
+
imageCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "imageCfg" }] : /* istanbul ignore next */ []);
|
|
273
|
+
fileCfg = computed(() => this.config(), ...ngDevMode ? [{ debugName: "fileCfg" }] : /* istanbul ignore next */ []);
|
|
274
|
+
multiselectNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "multiselectNode" }] : /* istanbul ignore next */ []);
|
|
275
|
+
imageNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "imageNode" }] : /* istanbul ignore next */ []);
|
|
276
|
+
fileNode = computed(() => childTree(this.ctx().tree, this.config().key), ...ngDevMode ? [{ debugName: "fileNode" }] : /* istanbul ignore next */ []);
|
|
277
|
+
errorMessage = computed(() => {
|
|
278
|
+
const serverError = this.ctx().serverErrors[`${this.ctx().pathPrefix}${this.config().key}`];
|
|
279
|
+
if (serverError) return serverError;
|
|
280
|
+
const state = this.state();
|
|
281
|
+
if (!state.invalid() || !state.touched()) return null;
|
|
282
|
+
return state.errors()[0]?.message ?? "Revisa este campo.";
|
|
283
|
+
}, ...ngDevMode ? [{ debugName: "errorMessage" }] : /* istanbul ignore next */ []);
|
|
284
|
+
composite = computed(() => {
|
|
285
|
+
const cfg = this.cfgCampo();
|
|
286
|
+
if ((/* @__PURE__ */ new Set([
|
|
287
|
+
"list",
|
|
288
|
+
"key-value",
|
|
289
|
+
"string-list",
|
|
290
|
+
"multiselect"
|
|
291
|
+
])).has(cfg.type)) return true;
|
|
292
|
+
return cfg.type === "select" && esGrupoRadio(cfg);
|
|
293
|
+
}, ...ngDevMode ? [{ debugName: "composite" }] : /* istanbul ignore next */ []);
|
|
294
|
+
helpId = computed(() => this.cfgCampo().help && !this.errorMessage() ? `${this.name()}-help` : null, ...ngDevMode ? [{ debugName: "helpId" }] : /* istanbul ignore next */ []);
|
|
295
|
+
errorId = computed(() => this.errorMessage() ? `${this.name()}-error` : null, ...ngDevMode ? [{ debugName: "errorId" }] : /* istanbul ignore next */ []);
|
|
296
|
+
describedBy = computed(() => [this.helpId(), this.errorId()].filter(Boolean).join(" ") || null, ...ngDevMode ? [{ debugName: "describedBy" }] : /* istanbul ignore next */ []);
|
|
297
|
+
invalid = computed(() => this.errorMessage() !== null, ...ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []);
|
|
298
|
+
required = computed(() => this.cfgCampo().required ?? false, ...ngDevMode ? [{ debugName: "required" }] : /* istanbul ignore next */ []);
|
|
299
|
+
schema = computed(() => getSchema(this.ctx().resource), ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
300
|
+
markOptionals = computed(() => {
|
|
301
|
+
const fields = this.schema()?.fields ?? [];
|
|
302
|
+
if (fields.length === 0) return false;
|
|
303
|
+
return fields.filter((field) => field.required).length / fields.length >= .75;
|
|
304
|
+
}, ...ngDevMode ? [{ debugName: "markOptionals" }] : /* istanbul ignore next */ []);
|
|
305
|
+
showRequiredMark = computed(() => !this.markOptionals() && this.cfgCampo().required, ...ngDevMode ? [{ debugName: "showRequiredMark" }] : /* istanbul ignore next */ []);
|
|
306
|
+
showOptionalMark = computed(() => this.markOptionals() && !this.cfgCampo().required, ...ngDevMode ? [{ debugName: "showOptionalMark" }] : /* istanbul ignore next */ []);
|
|
307
|
+
get tree() {
|
|
308
|
+
return this.ctx().tree;
|
|
309
|
+
}
|
|
310
|
+
get serverErrors() {
|
|
311
|
+
return this.ctx().serverErrors;
|
|
312
|
+
}
|
|
313
|
+
get pathPrefix() {
|
|
314
|
+
return this.ctx().pathPrefix;
|
|
315
|
+
}
|
|
316
|
+
get exists() {
|
|
317
|
+
return this.ctx().exists;
|
|
318
|
+
}
|
|
319
|
+
get resource() {
|
|
320
|
+
return this.ctx().resource;
|
|
321
|
+
}
|
|
322
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
323
|
+
minVersion: "12.0.0",
|
|
324
|
+
version: "22.2.3",
|
|
325
|
+
ngImport: i0,
|
|
326
|
+
type: RenderHost,
|
|
327
|
+
deps: [],
|
|
328
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
329
|
+
});
|
|
330
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
331
|
+
minVersion: "17.0.0",
|
|
332
|
+
version: "22.2.3",
|
|
333
|
+
type: RenderHost,
|
|
334
|
+
isStandalone: true,
|
|
335
|
+
selector: "app-render-host",
|
|
336
|
+
inputs: {
|
|
337
|
+
kind: {
|
|
338
|
+
classPropertyName: "kind",
|
|
339
|
+
publicName: "kind",
|
|
340
|
+
isSignal: true,
|
|
341
|
+
isRequired: false,
|
|
342
|
+
transformFunction: null
|
|
343
|
+
},
|
|
344
|
+
type: {
|
|
345
|
+
classPropertyName: "type",
|
|
346
|
+
publicName: "type",
|
|
347
|
+
isSignal: true,
|
|
348
|
+
isRequired: true,
|
|
349
|
+
transformFunction: null
|
|
350
|
+
},
|
|
351
|
+
config: {
|
|
352
|
+
classPropertyName: "config",
|
|
353
|
+
publicName: "config",
|
|
354
|
+
isSignal: true,
|
|
355
|
+
isRequired: true,
|
|
356
|
+
transformFunction: null
|
|
357
|
+
},
|
|
358
|
+
ctx: {
|
|
359
|
+
classPropertyName: "ctx",
|
|
360
|
+
publicName: "ctx",
|
|
361
|
+
isSignal: true,
|
|
362
|
+
isRequired: false,
|
|
363
|
+
transformFunction: null
|
|
364
|
+
},
|
|
365
|
+
data: {
|
|
366
|
+
classPropertyName: "data",
|
|
367
|
+
publicName: "data",
|
|
368
|
+
isSignal: true,
|
|
369
|
+
isRequired: false,
|
|
370
|
+
transformFunction: null
|
|
371
|
+
},
|
|
372
|
+
mode: {
|
|
373
|
+
classPropertyName: "mode",
|
|
374
|
+
publicName: "mode",
|
|
375
|
+
isSignal: true,
|
|
376
|
+
isRequired: false,
|
|
377
|
+
transformFunction: null
|
|
378
|
+
},
|
|
379
|
+
value: {
|
|
380
|
+
classPropertyName: "value",
|
|
381
|
+
publicName: "value",
|
|
382
|
+
isSignal: true,
|
|
383
|
+
isRequired: false,
|
|
384
|
+
transformFunction: null
|
|
385
|
+
},
|
|
386
|
+
chip: {
|
|
387
|
+
classPropertyName: "chip",
|
|
388
|
+
publicName: "chip",
|
|
389
|
+
isSignal: true,
|
|
390
|
+
isRequired: false,
|
|
391
|
+
transformFunction: null
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
providers: [{
|
|
395
|
+
provide: FIELD_ARIA,
|
|
396
|
+
useExisting: forwardRef(() => RenderHost)
|
|
397
|
+
}, {
|
|
398
|
+
provide: RENDER_CONTEXT,
|
|
399
|
+
useExisting: forwardRef(() => RenderHost)
|
|
400
|
+
}],
|
|
401
|
+
viewQueries: [{
|
|
402
|
+
propertyName: "groupTpl",
|
|
403
|
+
first: true,
|
|
404
|
+
predicate: ["groupContent"],
|
|
405
|
+
descendants: true,
|
|
406
|
+
isSignal: true
|
|
407
|
+
}, {
|
|
408
|
+
propertyName: "childTpl",
|
|
409
|
+
first: true,
|
|
410
|
+
predicate: ["childTpl"],
|
|
411
|
+
descendants: true,
|
|
412
|
+
isSignal: true
|
|
413
|
+
}],
|
|
414
|
+
ngImport: i0,
|
|
415
|
+
template: `
|
|
416
|
+
<ng-template #groupContent>
|
|
417
|
+
@for (child of group().fields; track child.key) {
|
|
418
|
+
<div [class]="colSpan(child.width)">
|
|
419
|
+
<app-render-host
|
|
420
|
+
[kind]="kind()"
|
|
421
|
+
[type]="child.type"
|
|
422
|
+
[config]="child"
|
|
423
|
+
[ctx]="groupChildCtx()"
|
|
424
|
+
/>
|
|
425
|
+
</div>
|
|
426
|
+
}
|
|
427
|
+
</ng-template>
|
|
428
|
+
|
|
429
|
+
<ng-template #childTpl let-child>
|
|
430
|
+
@for (sub of child.fields; track sub.key) {
|
|
431
|
+
<div [class]="child.row ? 'min-w-0 grow' : colSpan(sub.width)">
|
|
432
|
+
<app-render-host
|
|
433
|
+
[kind]="kind()"
|
|
434
|
+
[type]="sub.type"
|
|
435
|
+
[config]="sub"
|
|
436
|
+
[ctx]="listChildCtx(child)"
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
}
|
|
440
|
+
</ng-template>
|
|
441
|
+
|
|
442
|
+
@if (esDisplay()) {
|
|
443
|
+
<!-- Rama de display (R4.1 · R4.2): sólo el valor crudo. El host no
|
|
444
|
+
lee el árbol, no dibuja etiqueta ni error y no conoce la
|
|
445
|
+
sección: detail-view agrupa y este bloque pinta. -->
|
|
446
|
+
@if (vacio()) {
|
|
447
|
+
<span aria-hidden="true">—</span>
|
|
448
|
+
} @else if (soportaDisplay()) {
|
|
449
|
+
@if (control()) {
|
|
450
|
+
<ng-container
|
|
451
|
+
[ngComponentOutlet]="control()"
|
|
452
|
+
[ngComponentOutletInputs]="displayInputs()"
|
|
453
|
+
/>
|
|
454
|
+
} @else {
|
|
455
|
+
<app-skeleton [lines]="1" [height]="20" />
|
|
456
|
+
}
|
|
457
|
+
} @else if (showUnknown()) {
|
|
458
|
+
<app-unknown-render [type]="type()" [label]="config().label" />
|
|
459
|
+
}
|
|
460
|
+
} @else if (supported()) {
|
|
461
|
+
@if (esCampo()) {
|
|
462
|
+
<div class="mb-4" [class.field-enter]="cfgCampo().visibleWhen" [attr.data-field]="name()">
|
|
463
|
+
@if (type() !== 'group') {
|
|
464
|
+
@if (composite()) {
|
|
465
|
+
<!-- Sin control único no existe ancla para el atributo for del
|
|
466
|
+
label: apuntaría a un id inexistente. Se dibuja como texto
|
|
467
|
+
(en listas/key-value los hijos reciben su etiqueta por
|
|
468
|
+
campo anidado; en un select en modo radio cada opción
|
|
469
|
+
trae su label implícito y el grupo va con aria-label). -->
|
|
470
|
+
<div class="mb-1 text-label font-medium">
|
|
471
|
+
{{ config().label }}
|
|
472
|
+
@if (showRequiredMark()) {
|
|
473
|
+
<span aria-hidden="true" class="text-danger">*</span>
|
|
474
|
+
}
|
|
475
|
+
@if (showOptionalMark()) {
|
|
476
|
+
<span class="font-normal text-neutral">(opcional)</span>
|
|
477
|
+
}
|
|
478
|
+
</div>
|
|
479
|
+
} @else {
|
|
480
|
+
<label [for]="name()" class="mb-1 block text-label font-medium">
|
|
481
|
+
{{ config().label }}
|
|
482
|
+
@if (showRequiredMark()) {
|
|
483
|
+
<span aria-hidden="true" class="text-danger">*</span>
|
|
484
|
+
}
|
|
485
|
+
@if (showOptionalMark()) {
|
|
486
|
+
<span class="font-normal text-neutral">(opcional)</span>
|
|
487
|
+
}
|
|
488
|
+
</label>
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
@if (type() === 'multiselect') {
|
|
493
|
+
<!-- Tres de los veintiuno no pueden pasar por "NgComponentOutlet": su
|
|
494
|
+
sincronización con Signal Forms vive en ɵɵcontrol, una
|
|
495
|
+
instrucción que el compilador emite únicamente junto al binding
|
|
496
|
+
formField de la propia plantilla. Sin ese binding escrito en el
|
|
497
|
+
HTML el model() de value nunca se inicializa y el control lanza
|
|
498
|
+
NG0952. Se declaran a mano (D27) y el resto de tipos siguen
|
|
499
|
+
saliendo del registro. -->
|
|
500
|
+
<app-field-multiselect
|
|
501
|
+
[id]="name()"
|
|
502
|
+
[field]="multiselectCfg()"
|
|
503
|
+
[formField]="multiselectNode()"
|
|
504
|
+
[tree]="ctx().tree"
|
|
505
|
+
/>
|
|
506
|
+
} @else if (type() === 'image') {
|
|
507
|
+
<app-field-image
|
|
508
|
+
[id]="name()"
|
|
509
|
+
[field]="imageCfg()"
|
|
510
|
+
[formField]="imageNode()"
|
|
511
|
+
[resource]="ctx().resource"
|
|
512
|
+
/>
|
|
513
|
+
} @else if (type() === 'file') {
|
|
514
|
+
<app-field-file
|
|
515
|
+
[id]="name()"
|
|
516
|
+
[field]="fileCfg()"
|
|
517
|
+
[formField]="fileNode()"
|
|
518
|
+
[resource]="ctx().resource"
|
|
519
|
+
/>
|
|
520
|
+
} @else {
|
|
521
|
+
@if (control()) {
|
|
522
|
+
<ng-container
|
|
523
|
+
[ngComponentOutlet]="control()"
|
|
524
|
+
[ngComponentOutletInputs]="controlInputs()"
|
|
525
|
+
/>
|
|
526
|
+
} @else {
|
|
527
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
@if (errorMessage(); as message) {
|
|
532
|
+
<p
|
|
533
|
+
[id]="errorId()"
|
|
534
|
+
class="mt-1 flex items-center gap-1 text-help text-danger"
|
|
535
|
+
role="alert"
|
|
536
|
+
>
|
|
537
|
+
<svg lucideCircleAlert size="14" class="shrink-0" />
|
|
538
|
+
{{ message }}
|
|
539
|
+
</p>
|
|
540
|
+
} @else if (cfgCampo().help; as help) {
|
|
541
|
+
<p [id]="helpId()" class="mt-1 text-help text-neutral">{{ help }}</p>
|
|
542
|
+
}
|
|
543
|
+
</div>
|
|
544
|
+
} @else {
|
|
545
|
+
<!-- Rama de widget (R3.6): el mismo outlet y el mismo cableado que
|
|
546
|
+
un campo, pero sin el envoltorio — un widget no tiene etiqueta
|
|
547
|
+
asociada, ni marca required, ni error, ni ayuda. El ancho en la
|
|
548
|
+
grilla lo pone su host (app-widget-host), no el motor. -->
|
|
549
|
+
@if (control()) {
|
|
550
|
+
<ng-container
|
|
551
|
+
[ngComponentOutlet]="control()"
|
|
552
|
+
[ngComponentOutletInputs]="controlInputs()"
|
|
553
|
+
/>
|
|
554
|
+
} @else {
|
|
555
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} @else if (showUnknown()) {
|
|
559
|
+
<!-- R1.10: degradación controlada. Un tipo fuera del catálogo no
|
|
560
|
+
revienta; en dev se avisa y en producción sólo se omite. -->
|
|
561
|
+
<app-unknown-render [type]="type()" [label]="config().label" />
|
|
562
|
+
}
|
|
563
|
+
`,
|
|
564
|
+
isInline: true,
|
|
565
|
+
dependencies: [
|
|
566
|
+
{
|
|
567
|
+
kind: "component",
|
|
568
|
+
type: RenderHost,
|
|
569
|
+
selector: "app-render-host",
|
|
570
|
+
inputs: [
|
|
571
|
+
"kind",
|
|
572
|
+
"type",
|
|
573
|
+
"config",
|
|
574
|
+
"ctx",
|
|
575
|
+
"data",
|
|
576
|
+
"mode",
|
|
577
|
+
"value",
|
|
578
|
+
"chip"
|
|
579
|
+
]
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
kind: "component",
|
|
583
|
+
type: FieldFile,
|
|
584
|
+
selector: "app-field-file",
|
|
585
|
+
inputs: [
|
|
586
|
+
"field",
|
|
587
|
+
"resource",
|
|
588
|
+
"value",
|
|
589
|
+
"disabled"
|
|
590
|
+
],
|
|
591
|
+
outputs: ["valueChange", "touch"]
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
kind: "component",
|
|
595
|
+
type: FieldImage,
|
|
596
|
+
selector: "app-field-image",
|
|
597
|
+
inputs: [
|
|
598
|
+
"field",
|
|
599
|
+
"resource",
|
|
600
|
+
"value",
|
|
601
|
+
"disabled"
|
|
602
|
+
],
|
|
603
|
+
outputs: ["valueChange", "touch"]
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
kind: "component",
|
|
607
|
+
type: FieldMultiselect,
|
|
608
|
+
selector: "app-field-multiselect",
|
|
609
|
+
inputs: [
|
|
610
|
+
"field",
|
|
611
|
+
"tree",
|
|
612
|
+
"value",
|
|
613
|
+
"disabled"
|
|
614
|
+
],
|
|
615
|
+
outputs: ["valueChange"]
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
kind: "directive",
|
|
619
|
+
type: FormField,
|
|
620
|
+
selector: "[formField]",
|
|
621
|
+
inputs: ["formField"],
|
|
622
|
+
exportAs: ["formField"]
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
kind: "component",
|
|
626
|
+
type: LucideCircleAlert,
|
|
627
|
+
selector: "svg[lucideCircleAlert], svg[lucideAlertCircle]"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
kind: "directive",
|
|
631
|
+
type: NgComponentOutlet,
|
|
632
|
+
selector: "[ngComponentOutlet]",
|
|
633
|
+
inputs: [
|
|
634
|
+
"ngComponentOutlet",
|
|
635
|
+
"ngComponentOutletInputs",
|
|
636
|
+
"ngComponentOutletInjector",
|
|
637
|
+
"ngComponentOutletEnvironmentInjector",
|
|
638
|
+
"ngComponentOutletContent",
|
|
639
|
+
"ngComponentOutletNgModule"
|
|
640
|
+
],
|
|
641
|
+
exportAs: ["ngComponentOutlet"]
|
|
642
|
+
},
|
|
643
|
+
{
|
|
644
|
+
kind: "component",
|
|
645
|
+
type: Skeleton,
|
|
646
|
+
selector: "app-skeleton",
|
|
647
|
+
inputs: [
|
|
648
|
+
"lines",
|
|
649
|
+
"height",
|
|
650
|
+
"width"
|
|
651
|
+
]
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
kind: "component",
|
|
655
|
+
type: UnknownRender,
|
|
656
|
+
selector: "app-unknown-render",
|
|
657
|
+
inputs: ["type", "label"]
|
|
658
|
+
}
|
|
659
|
+
]
|
|
660
|
+
});
|
|
661
|
+
};
|
|
662
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
663
|
+
minVersion: "12.0.0",
|
|
664
|
+
version: "22.2.3",
|
|
665
|
+
ngImport: i0,
|
|
666
|
+
type: RenderHost,
|
|
667
|
+
decorators: [{
|
|
668
|
+
type: Component,
|
|
669
|
+
args: [{
|
|
670
|
+
selector: "app-render-host",
|
|
671
|
+
imports: [
|
|
672
|
+
FieldFile,
|
|
673
|
+
FieldImage,
|
|
674
|
+
FieldMultiselect,
|
|
675
|
+
FormField,
|
|
676
|
+
LucideCircleAlert,
|
|
677
|
+
NgComponentOutlet,
|
|
678
|
+
RenderHost,
|
|
679
|
+
Skeleton,
|
|
680
|
+
UnknownRender
|
|
681
|
+
],
|
|
682
|
+
providers: [{
|
|
683
|
+
provide: FIELD_ARIA,
|
|
684
|
+
useExisting: forwardRef(() => RenderHost)
|
|
685
|
+
}, {
|
|
686
|
+
provide: RENDER_CONTEXT,
|
|
687
|
+
useExisting: forwardRef(() => RenderHost)
|
|
688
|
+
}],
|
|
689
|
+
template: `
|
|
690
|
+
<ng-template #groupContent>
|
|
691
|
+
@for (child of group().fields; track child.key) {
|
|
692
|
+
<div [class]="colSpan(child.width)">
|
|
693
|
+
<app-render-host
|
|
694
|
+
[kind]="kind()"
|
|
695
|
+
[type]="child.type"
|
|
696
|
+
[config]="child"
|
|
697
|
+
[ctx]="groupChildCtx()"
|
|
698
|
+
/>
|
|
699
|
+
</div>
|
|
700
|
+
}
|
|
701
|
+
</ng-template>
|
|
702
|
+
|
|
703
|
+
<ng-template #childTpl let-child>
|
|
704
|
+
@for (sub of child.fields; track sub.key) {
|
|
705
|
+
<div [class]="child.row ? 'min-w-0 grow' : colSpan(sub.width)">
|
|
706
|
+
<app-render-host
|
|
707
|
+
[kind]="kind()"
|
|
708
|
+
[type]="sub.type"
|
|
709
|
+
[config]="sub"
|
|
710
|
+
[ctx]="listChildCtx(child)"
|
|
711
|
+
/>
|
|
712
|
+
</div>
|
|
713
|
+
}
|
|
714
|
+
</ng-template>
|
|
715
|
+
|
|
716
|
+
@if (esDisplay()) {
|
|
717
|
+
<!-- Rama de display (R4.1 · R4.2): sólo el valor crudo. El host no
|
|
718
|
+
lee el árbol, no dibuja etiqueta ni error y no conoce la
|
|
719
|
+
sección: detail-view agrupa y este bloque pinta. -->
|
|
720
|
+
@if (vacio()) {
|
|
721
|
+
<span aria-hidden="true">—</span>
|
|
722
|
+
} @else if (soportaDisplay()) {
|
|
723
|
+
@if (control()) {
|
|
724
|
+
<ng-container
|
|
725
|
+
[ngComponentOutlet]="control()"
|
|
726
|
+
[ngComponentOutletInputs]="displayInputs()"
|
|
727
|
+
/>
|
|
728
|
+
} @else {
|
|
729
|
+
<app-skeleton [lines]="1" [height]="20" />
|
|
730
|
+
}
|
|
731
|
+
} @else if (showUnknown()) {
|
|
732
|
+
<app-unknown-render [type]="type()" [label]="config().label" />
|
|
733
|
+
}
|
|
734
|
+
} @else if (supported()) {
|
|
735
|
+
@if (esCampo()) {
|
|
736
|
+
<div class="mb-4" [class.field-enter]="cfgCampo().visibleWhen" [attr.data-field]="name()">
|
|
737
|
+
@if (type() !== 'group') {
|
|
738
|
+
@if (composite()) {
|
|
739
|
+
<!-- Sin control único no existe ancla para el atributo for del
|
|
740
|
+
label: apuntaría a un id inexistente. Se dibuja como texto
|
|
741
|
+
(en listas/key-value los hijos reciben su etiqueta por
|
|
742
|
+
campo anidado; en un select en modo radio cada opción
|
|
743
|
+
trae su label implícito y el grupo va con aria-label). -->
|
|
744
|
+
<div class="mb-1 text-label font-medium">
|
|
745
|
+
{{ config().label }}
|
|
746
|
+
@if (showRequiredMark()) {
|
|
747
|
+
<span aria-hidden="true" class="text-danger">*</span>
|
|
748
|
+
}
|
|
749
|
+
@if (showOptionalMark()) {
|
|
750
|
+
<span class="font-normal text-neutral">(opcional)</span>
|
|
751
|
+
}
|
|
752
|
+
</div>
|
|
753
|
+
} @else {
|
|
754
|
+
<label [for]="name()" class="mb-1 block text-label font-medium">
|
|
755
|
+
{{ config().label }}
|
|
756
|
+
@if (showRequiredMark()) {
|
|
757
|
+
<span aria-hidden="true" class="text-danger">*</span>
|
|
758
|
+
}
|
|
759
|
+
@if (showOptionalMark()) {
|
|
760
|
+
<span class="font-normal text-neutral">(opcional)</span>
|
|
761
|
+
}
|
|
762
|
+
</label>
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@if (type() === 'multiselect') {
|
|
767
|
+
<!-- Tres de los veintiuno no pueden pasar por "NgComponentOutlet": su
|
|
768
|
+
sincronización con Signal Forms vive en ɵɵcontrol, una
|
|
769
|
+
instrucción que el compilador emite únicamente junto al binding
|
|
770
|
+
formField de la propia plantilla. Sin ese binding escrito en el
|
|
771
|
+
HTML el model() de value nunca se inicializa y el control lanza
|
|
772
|
+
NG0952. Se declaran a mano (D27) y el resto de tipos siguen
|
|
773
|
+
saliendo del registro. -->
|
|
774
|
+
<app-field-multiselect
|
|
775
|
+
[id]="name()"
|
|
776
|
+
[field]="multiselectCfg()"
|
|
777
|
+
[formField]="multiselectNode()"
|
|
778
|
+
[tree]="ctx().tree"
|
|
779
|
+
/>
|
|
780
|
+
} @else if (type() === 'image') {
|
|
781
|
+
<app-field-image
|
|
782
|
+
[id]="name()"
|
|
783
|
+
[field]="imageCfg()"
|
|
784
|
+
[formField]="imageNode()"
|
|
785
|
+
[resource]="ctx().resource"
|
|
786
|
+
/>
|
|
787
|
+
} @else if (type() === 'file') {
|
|
788
|
+
<app-field-file
|
|
789
|
+
[id]="name()"
|
|
790
|
+
[field]="fileCfg()"
|
|
791
|
+
[formField]="fileNode()"
|
|
792
|
+
[resource]="ctx().resource"
|
|
793
|
+
/>
|
|
794
|
+
} @else {
|
|
795
|
+
@if (control()) {
|
|
796
|
+
<ng-container
|
|
797
|
+
[ngComponentOutlet]="control()"
|
|
798
|
+
[ngComponentOutletInputs]="controlInputs()"
|
|
799
|
+
/>
|
|
800
|
+
} @else {
|
|
801
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
@if (errorMessage(); as message) {
|
|
806
|
+
<p
|
|
807
|
+
[id]="errorId()"
|
|
808
|
+
class="mt-1 flex items-center gap-1 text-help text-danger"
|
|
809
|
+
role="alert"
|
|
810
|
+
>
|
|
811
|
+
<svg lucideCircleAlert size="14" class="shrink-0" />
|
|
812
|
+
{{ message }}
|
|
813
|
+
</p>
|
|
814
|
+
} @else if (cfgCampo().help; as help) {
|
|
815
|
+
<p [id]="helpId()" class="mt-1 text-help text-neutral">{{ help }}</p>
|
|
816
|
+
}
|
|
817
|
+
</div>
|
|
818
|
+
} @else {
|
|
819
|
+
<!-- Rama de widget (R3.6): el mismo outlet y el mismo cableado que
|
|
820
|
+
un campo, pero sin el envoltorio — un widget no tiene etiqueta
|
|
821
|
+
asociada, ni marca required, ni error, ni ayuda. El ancho en la
|
|
822
|
+
grilla lo pone su host (app-widget-host), no el motor. -->
|
|
823
|
+
@if (control()) {
|
|
824
|
+
<ng-container
|
|
825
|
+
[ngComponentOutlet]="control()"
|
|
826
|
+
[ngComponentOutletInputs]="controlInputs()"
|
|
827
|
+
/>
|
|
828
|
+
} @else {
|
|
829
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
} @else if (showUnknown()) {
|
|
833
|
+
<!-- R1.10: degradación controlada. Un tipo fuera del catálogo no
|
|
834
|
+
revienta; en dev se avisa y en producción sólo se omite. -->
|
|
835
|
+
<app-unknown-render [type]="type()" [label]="config().label" />
|
|
836
|
+
}
|
|
837
|
+
`
|
|
838
|
+
}]
|
|
839
|
+
}],
|
|
840
|
+
propDecorators: {
|
|
841
|
+
kind: [{
|
|
842
|
+
type: i0.Input,
|
|
843
|
+
args: [{
|
|
844
|
+
isSignal: true,
|
|
845
|
+
alias: "kind",
|
|
846
|
+
required: false
|
|
847
|
+
}]
|
|
848
|
+
}],
|
|
849
|
+
type: [{
|
|
850
|
+
type: i0.Input,
|
|
851
|
+
args: [{
|
|
852
|
+
isSignal: true,
|
|
853
|
+
alias: "type",
|
|
854
|
+
required: true
|
|
855
|
+
}]
|
|
856
|
+
}],
|
|
857
|
+
config: [{
|
|
858
|
+
type: i0.Input,
|
|
859
|
+
args: [{
|
|
860
|
+
isSignal: true,
|
|
861
|
+
alias: "config",
|
|
862
|
+
required: true
|
|
863
|
+
}]
|
|
864
|
+
}],
|
|
865
|
+
ctx: [{
|
|
866
|
+
type: i0.Input,
|
|
867
|
+
args: [{
|
|
868
|
+
isSignal: true,
|
|
869
|
+
alias: "ctx",
|
|
870
|
+
required: false
|
|
871
|
+
}]
|
|
872
|
+
}],
|
|
873
|
+
data: [{
|
|
874
|
+
type: i0.Input,
|
|
875
|
+
args: [{
|
|
876
|
+
isSignal: true,
|
|
877
|
+
alias: "data",
|
|
878
|
+
required: false
|
|
879
|
+
}]
|
|
880
|
+
}],
|
|
881
|
+
mode: [{
|
|
882
|
+
type: i0.Input,
|
|
883
|
+
args: [{
|
|
884
|
+
isSignal: true,
|
|
885
|
+
alias: "mode",
|
|
886
|
+
required: false
|
|
887
|
+
}]
|
|
888
|
+
}],
|
|
889
|
+
value: [{
|
|
890
|
+
type: i0.Input,
|
|
891
|
+
args: [{
|
|
892
|
+
isSignal: true,
|
|
893
|
+
alias: "value",
|
|
894
|
+
required: false
|
|
895
|
+
}]
|
|
896
|
+
}],
|
|
897
|
+
chip: [{
|
|
898
|
+
type: i0.Input,
|
|
899
|
+
args: [{
|
|
900
|
+
isSignal: true,
|
|
901
|
+
alias: "chip",
|
|
902
|
+
required: false
|
|
903
|
+
}]
|
|
904
|
+
}],
|
|
905
|
+
groupTpl: [{
|
|
906
|
+
type: i0.ViewChild,
|
|
907
|
+
args: ["groupContent", { isSignal: true }]
|
|
908
|
+
}],
|
|
909
|
+
childTpl: [{
|
|
910
|
+
type: i0.ViewChild,
|
|
911
|
+
args: ["childTpl", { isSignal: true }]
|
|
912
|
+
}]
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
function buildShareMessage(url, shareText) {
|
|
916
|
+
const text = (shareText ?? "").trim();
|
|
917
|
+
if (!text) return url;
|
|
918
|
+
if (url && mentionsSite(text, url)) return text;
|
|
919
|
+
return url ? `${text} ${url}` : text;
|
|
920
|
+
}
|
|
921
|
+
function mentionsSite(text, url) {
|
|
922
|
+
if (text.includes(url)) return true;
|
|
923
|
+
try {
|
|
924
|
+
const { origin } = new URL(url);
|
|
925
|
+
return origin !== "null" && text.includes(origin);
|
|
926
|
+
} catch {
|
|
927
|
+
return false;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async function writeClipboard(text) {
|
|
931
|
+
const clipboard = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
932
|
+
if (clipboard && typeof clipboard.writeText === "function") try {
|
|
933
|
+
await clipboard.writeText(text);
|
|
934
|
+
return true;
|
|
935
|
+
} catch {}
|
|
936
|
+
return legacyCopy(text);
|
|
937
|
+
}
|
|
938
|
+
function legacyCopy(text) {
|
|
939
|
+
if (typeof document === "undefined" || typeof document.execCommand !== "function") return false;
|
|
940
|
+
const area = document.createElement("textarea");
|
|
941
|
+
area.value = text;
|
|
942
|
+
area.setAttribute("readonly", "");
|
|
943
|
+
area.style.position = "fixed";
|
|
944
|
+
area.style.top = "0";
|
|
945
|
+
area.style.opacity = "0";
|
|
946
|
+
document.body.appendChild(area);
|
|
947
|
+
area.select();
|
|
948
|
+
let copied = false;
|
|
949
|
+
try {
|
|
950
|
+
copied = document.execCommand("copy");
|
|
951
|
+
} catch {
|
|
952
|
+
copied = false;
|
|
953
|
+
}
|
|
954
|
+
document.body.removeChild(area);
|
|
955
|
+
return copied;
|
|
956
|
+
}
|
|
957
|
+
function isShareAction(action) {
|
|
958
|
+
return action.type === "copy-share";
|
|
959
|
+
}
|
|
960
|
+
function isTriggerAction(action) {
|
|
961
|
+
return action.type === "trigger";
|
|
962
|
+
}
|
|
963
|
+
function resolveShare(action, vars) {
|
|
964
|
+
const template = action.shareTextTemplate || vars["share_message_template"];
|
|
965
|
+
return {
|
|
966
|
+
url: interpolateTemplate(action.urlTemplate, vars),
|
|
967
|
+
shareText: template ? interpolateTemplate(template, vars) : ""
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
const actionDescriptors = [
|
|
971
|
+
{
|
|
972
|
+
type: "copy-share",
|
|
973
|
+
surfaces: [
|
|
974
|
+
"row",
|
|
975
|
+
"detail",
|
|
976
|
+
"post-create"
|
|
977
|
+
],
|
|
978
|
+
icon: LucideShare2,
|
|
979
|
+
run: async (ctx, action) => {
|
|
980
|
+
const { url, shareText } = resolveShare(action, ctx.vars);
|
|
981
|
+
const message = buildShareMessage(url, shareText);
|
|
982
|
+
if (await writeClipboard(message)) ctx.toast.success("Mensaje copiado al portapapeles.");
|
|
983
|
+
else ctx.toast.info(`Copiar manualmente: ${message}`);
|
|
984
|
+
return { ok: true };
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
type: "navigate",
|
|
989
|
+
surfaces: ["row", "detail"],
|
|
990
|
+
run: async (ctx, action) => {
|
|
991
|
+
const url = interpolateTemplate(action.urlTemplate, ctx.vars);
|
|
992
|
+
if (/^https?:\/\//.test(url)) window.open(url, "_blank", "noopener");
|
|
993
|
+
else await ctx.router.navigateByUrl(url);
|
|
994
|
+
return { ok: true };
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
{
|
|
998
|
+
type: "trigger",
|
|
999
|
+
surfaces: ["row", "detail"],
|
|
1000
|
+
confirm: true,
|
|
1001
|
+
run: async (ctx, action) => {
|
|
1002
|
+
const declared = action;
|
|
1003
|
+
const rawKey = ctx.record?.[ctx.keyField];
|
|
1004
|
+
if (rawKey === null || rawKey === void 0 || rawKey === "") throw new Error(`La acción «${declared.label}» necesita la clave ${ctx.keyField} del registro.`);
|
|
1005
|
+
const name = declared.name ?? declared.id;
|
|
1006
|
+
const route = `/admin/${ctx.resource}/${String(rawKey)}/actions/${name}`;
|
|
1007
|
+
await firstValueFrom(ctx.api.request("POST", route));
|
|
1008
|
+
return {
|
|
1009
|
+
ok: true,
|
|
1010
|
+
reload: true
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
];
|
|
1015
|
+
const ACCIONES = /* @__PURE__ */ new Map();
|
|
1016
|
+
function registerAction(descriptor) {
|
|
1017
|
+
ACCIONES.set(descriptor.type, descriptor);
|
|
1018
|
+
}
|
|
1019
|
+
function getAction(type) {
|
|
1020
|
+
return ACCIONES.get(type);
|
|
1021
|
+
}
|
|
1022
|
+
function actionsFor(surface) {
|
|
1023
|
+
return [...ACCIONES.values()].filter((descriptor) => descriptor.surfaces.includes(surface));
|
|
1024
|
+
}
|
|
1025
|
+
function actionType(action) {
|
|
1026
|
+
return action.type ?? "navigate";
|
|
1027
|
+
}
|
|
1028
|
+
function actionTypes() {
|
|
1029
|
+
return [...ACCIONES.keys()];
|
|
1030
|
+
}
|
|
1031
|
+
for (const descriptor of actionDescriptors) registerAction(descriptor);
|
|
1032
|
+
var FieldHost = class FieldHost {
|
|
1033
|
+
field = input.required(...ngDevMode ? [{ debugName: "field" }] : /* istanbul ignore next */ []);
|
|
1034
|
+
tree = input.required(...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
|
|
1035
|
+
exists = input(false, ...ngDevMode ? [{ debugName: "exists" }] : /* istanbul ignore next */ []);
|
|
1036
|
+
serverErrors = input({}, ...ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []);
|
|
1037
|
+
pathPrefix = input("", ...ngDevMode ? [{ debugName: "pathPrefix" }] : /* istanbul ignore next */ []);
|
|
1038
|
+
resource = input("", ...ngDevMode ? [{ debugName: "resource" }] : /* istanbul ignore next */ []);
|
|
1039
|
+
ctx = computed(() => ({
|
|
1040
|
+
tree: this.tree(),
|
|
1041
|
+
serverErrors: this.serverErrors(),
|
|
1042
|
+
pathPrefix: this.pathPrefix(),
|
|
1043
|
+
exists: this.exists(),
|
|
1044
|
+
resource: this.resource()
|
|
1045
|
+
}), ...ngDevMode ? [{ debugName: "ctx" }] : /* istanbul ignore next */ []);
|
|
1046
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1047
|
+
minVersion: "12.0.0",
|
|
1048
|
+
version: "22.2.3",
|
|
1049
|
+
ngImport: i0,
|
|
1050
|
+
type: FieldHost,
|
|
1051
|
+
deps: [],
|
|
1052
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1053
|
+
});
|
|
1054
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1055
|
+
minVersion: "17.1.0",
|
|
1056
|
+
version: "22.2.3",
|
|
1057
|
+
type: FieldHost,
|
|
1058
|
+
isStandalone: true,
|
|
1059
|
+
selector: "app-field-host",
|
|
1060
|
+
inputs: {
|
|
1061
|
+
field: {
|
|
1062
|
+
classPropertyName: "field",
|
|
1063
|
+
publicName: "field",
|
|
1064
|
+
isSignal: true,
|
|
1065
|
+
isRequired: true,
|
|
1066
|
+
transformFunction: null
|
|
1067
|
+
},
|
|
1068
|
+
tree: {
|
|
1069
|
+
classPropertyName: "tree",
|
|
1070
|
+
publicName: "tree",
|
|
1071
|
+
isSignal: true,
|
|
1072
|
+
isRequired: true,
|
|
1073
|
+
transformFunction: null
|
|
1074
|
+
},
|
|
1075
|
+
exists: {
|
|
1076
|
+
classPropertyName: "exists",
|
|
1077
|
+
publicName: "exists",
|
|
1078
|
+
isSignal: true,
|
|
1079
|
+
isRequired: false,
|
|
1080
|
+
transformFunction: null
|
|
1081
|
+
},
|
|
1082
|
+
serverErrors: {
|
|
1083
|
+
classPropertyName: "serverErrors",
|
|
1084
|
+
publicName: "serverErrors",
|
|
1085
|
+
isSignal: true,
|
|
1086
|
+
isRequired: false,
|
|
1087
|
+
transformFunction: null
|
|
1088
|
+
},
|
|
1089
|
+
pathPrefix: {
|
|
1090
|
+
classPropertyName: "pathPrefix",
|
|
1091
|
+
publicName: "pathPrefix",
|
|
1092
|
+
isSignal: true,
|
|
1093
|
+
isRequired: false,
|
|
1094
|
+
transformFunction: null
|
|
1095
|
+
},
|
|
1096
|
+
resource: {
|
|
1097
|
+
classPropertyName: "resource",
|
|
1098
|
+
publicName: "resource",
|
|
1099
|
+
isSignal: true,
|
|
1100
|
+
isRequired: false,
|
|
1101
|
+
transformFunction: null
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
ngImport: i0,
|
|
1105
|
+
template: `
|
|
1106
|
+
<app-render-host kind="field" [type]="field().type" [config]="field()" [ctx]="ctx()" />
|
|
1107
|
+
`,
|
|
1108
|
+
isInline: true,
|
|
1109
|
+
dependencies: [{
|
|
1110
|
+
kind: "component",
|
|
1111
|
+
type: RenderHost,
|
|
1112
|
+
selector: "app-render-host",
|
|
1113
|
+
inputs: [
|
|
1114
|
+
"kind",
|
|
1115
|
+
"type",
|
|
1116
|
+
"config",
|
|
1117
|
+
"ctx",
|
|
1118
|
+
"data",
|
|
1119
|
+
"mode",
|
|
1120
|
+
"value",
|
|
1121
|
+
"chip"
|
|
1122
|
+
]
|
|
1123
|
+
}]
|
|
1124
|
+
});
|
|
1125
|
+
};
|
|
1126
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1127
|
+
minVersion: "12.0.0",
|
|
1128
|
+
version: "22.2.3",
|
|
1129
|
+
ngImport: i0,
|
|
1130
|
+
type: FieldHost,
|
|
1131
|
+
decorators: [{
|
|
1132
|
+
type: Component,
|
|
1133
|
+
args: [{
|
|
1134
|
+
selector: "app-field-host",
|
|
1135
|
+
imports: [RenderHost],
|
|
1136
|
+
template: `
|
|
1137
|
+
<app-render-host kind="field" [type]="field().type" [config]="field()" [ctx]="ctx()" />
|
|
1138
|
+
`
|
|
1139
|
+
}]
|
|
1140
|
+
}],
|
|
1141
|
+
propDecorators: {
|
|
1142
|
+
field: [{
|
|
1143
|
+
type: i0.Input,
|
|
1144
|
+
args: [{
|
|
1145
|
+
isSignal: true,
|
|
1146
|
+
alias: "field",
|
|
1147
|
+
required: true
|
|
1148
|
+
}]
|
|
1149
|
+
}],
|
|
1150
|
+
tree: [{
|
|
1151
|
+
type: i0.Input,
|
|
1152
|
+
args: [{
|
|
1153
|
+
isSignal: true,
|
|
1154
|
+
alias: "tree",
|
|
1155
|
+
required: true
|
|
1156
|
+
}]
|
|
1157
|
+
}],
|
|
1158
|
+
exists: [{
|
|
1159
|
+
type: i0.Input,
|
|
1160
|
+
args: [{
|
|
1161
|
+
isSignal: true,
|
|
1162
|
+
alias: "exists",
|
|
1163
|
+
required: false
|
|
1164
|
+
}]
|
|
1165
|
+
}],
|
|
1166
|
+
serverErrors: [{
|
|
1167
|
+
type: i0.Input,
|
|
1168
|
+
args: [{
|
|
1169
|
+
isSignal: true,
|
|
1170
|
+
alias: "serverErrors",
|
|
1171
|
+
required: false
|
|
1172
|
+
}]
|
|
1173
|
+
}],
|
|
1174
|
+
pathPrefix: [{
|
|
1175
|
+
type: i0.Input,
|
|
1176
|
+
args: [{
|
|
1177
|
+
isSignal: true,
|
|
1178
|
+
alias: "pathPrefix",
|
|
1179
|
+
required: false
|
|
1180
|
+
}]
|
|
1181
|
+
}],
|
|
1182
|
+
resource: [{
|
|
1183
|
+
type: i0.Input,
|
|
1184
|
+
args: [{
|
|
1185
|
+
isSignal: true,
|
|
1186
|
+
alias: "resource",
|
|
1187
|
+
required: false
|
|
1188
|
+
}]
|
|
1189
|
+
}]
|
|
1190
|
+
}
|
|
1191
|
+
});
|
|
1192
|
+
const FOCUSABLE = "a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])";
|
|
1193
|
+
function isVisibleNow(element) {
|
|
1194
|
+
if (element.hasAttribute("hidden") || element.closest("[hidden]") !== null) return false;
|
|
1195
|
+
const check = element.checkVisibility;
|
|
1196
|
+
return typeof check !== "function" || check.call(element);
|
|
1197
|
+
}
|
|
1198
|
+
const WIDTHS = {
|
|
1199
|
+
sm: "max-w-sm",
|
|
1200
|
+
md: "max-w-lg",
|
|
1201
|
+
lg: "max-w-2xl"
|
|
1202
|
+
};
|
|
1203
|
+
let titleCounter$1 = 0;
|
|
1204
|
+
var Drawer = class Drawer {
|
|
1205
|
+
open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
1206
|
+
title = input("Editar", ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
1207
|
+
subtitle = input(null, ...ngDevMode ? [{ debugName: "subtitle" }] : /* istanbul ignore next */ []);
|
|
1208
|
+
size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
|
|
1209
|
+
closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
|
|
1210
|
+
close = output();
|
|
1211
|
+
titleId = `drawer-title-${++titleCounter$1}`;
|
|
1212
|
+
widthClass = computed(() => WIDTHS[this.size()], ...ngDevMode ? [{ debugName: "widthClass" }] : /* istanbul ignore next */ []);
|
|
1213
|
+
dialogRef = viewChild("dialog", ...ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []);
|
|
1214
|
+
lastFocused = null;
|
|
1215
|
+
focusEffect = afterRenderEffect(() => {
|
|
1216
|
+
const open = this.open();
|
|
1217
|
+
const dialog = this.dialogRef();
|
|
1218
|
+
if (open && dialog) {
|
|
1219
|
+
const active = document.activeElement;
|
|
1220
|
+
if (active !== dialog.nativeElement && this.lastFocused === null) this.lastFocused = active instanceof HTMLElement ? active : null;
|
|
1221
|
+
dialog.nativeElement.focus();
|
|
1222
|
+
} else if (!open && this.lastFocused !== null) {
|
|
1223
|
+
this.lastFocused.focus();
|
|
1224
|
+
this.lastFocused = null;
|
|
1225
|
+
}
|
|
1226
|
+
}, ...ngDevMode ? [{ debugName: "focusEffect" }] : /* istanbul ignore next */ []);
|
|
1227
|
+
ngOnDestroy() {
|
|
1228
|
+
if (this.lastFocused?.isConnected) this.lastFocused.focus();
|
|
1229
|
+
this.lastFocused = null;
|
|
1230
|
+
}
|
|
1231
|
+
onEscape() {
|
|
1232
|
+
if (this.open()) this.close.emit();
|
|
1233
|
+
}
|
|
1234
|
+
onTab(rawEvent) {
|
|
1235
|
+
const event = rawEvent;
|
|
1236
|
+
const dialog = this.dialogRef()?.nativeElement;
|
|
1237
|
+
if (!this.open() || !dialog) return;
|
|
1238
|
+
const focusable = Array.from(dialog.querySelectorAll(FOCUSABLE)).filter((element) => isVisibleNow(element));
|
|
1239
|
+
if (focusable.length === 0) {
|
|
1240
|
+
event.preventDefault();
|
|
1241
|
+
dialog.focus();
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
const first = focusable[0];
|
|
1245
|
+
const last = focusable[focusable.length - 1];
|
|
1246
|
+
const active = document.activeElement;
|
|
1247
|
+
if (event.shiftKey && (active === first || active === dialog)) {
|
|
1248
|
+
event.preventDefault();
|
|
1249
|
+
last.focus();
|
|
1250
|
+
} else if (!event.shiftKey && active === last) {
|
|
1251
|
+
event.preventDefault();
|
|
1252
|
+
first.focus();
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1256
|
+
minVersion: "12.0.0",
|
|
1257
|
+
version: "22.2.3",
|
|
1258
|
+
ngImport: i0,
|
|
1259
|
+
type: Drawer,
|
|
1260
|
+
deps: [],
|
|
1261
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1262
|
+
});
|
|
1263
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1264
|
+
minVersion: "17.0.0",
|
|
1265
|
+
version: "22.2.3",
|
|
1266
|
+
type: Drawer,
|
|
1267
|
+
isStandalone: true,
|
|
1268
|
+
selector: "app-drawer",
|
|
1269
|
+
inputs: {
|
|
1270
|
+
open: {
|
|
1271
|
+
classPropertyName: "open",
|
|
1272
|
+
publicName: "open",
|
|
1273
|
+
isSignal: true,
|
|
1274
|
+
isRequired: false,
|
|
1275
|
+
transformFunction: null
|
|
1276
|
+
},
|
|
1277
|
+
title: {
|
|
1278
|
+
classPropertyName: "title",
|
|
1279
|
+
publicName: "title",
|
|
1280
|
+
isSignal: true,
|
|
1281
|
+
isRequired: false,
|
|
1282
|
+
transformFunction: null
|
|
1283
|
+
},
|
|
1284
|
+
subtitle: {
|
|
1285
|
+
classPropertyName: "subtitle",
|
|
1286
|
+
publicName: "subtitle",
|
|
1287
|
+
isSignal: true,
|
|
1288
|
+
isRequired: false,
|
|
1289
|
+
transformFunction: null
|
|
1290
|
+
},
|
|
1291
|
+
size: {
|
|
1292
|
+
classPropertyName: "size",
|
|
1293
|
+
publicName: "size",
|
|
1294
|
+
isSignal: true,
|
|
1295
|
+
isRequired: false,
|
|
1296
|
+
transformFunction: null
|
|
1297
|
+
},
|
|
1298
|
+
closeLabel: {
|
|
1299
|
+
classPropertyName: "closeLabel",
|
|
1300
|
+
publicName: "closeLabel",
|
|
1301
|
+
isSignal: true,
|
|
1302
|
+
isRequired: false,
|
|
1303
|
+
transformFunction: null
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
outputs: { close: "close" },
|
|
1307
|
+
host: { listeners: {
|
|
1308
|
+
"document:keydown.escape": "onEscape()",
|
|
1309
|
+
"document:keydown.tab": "onTab($event)",
|
|
1310
|
+
"document:keydown.shift.tab": "onTab($event)"
|
|
1311
|
+
} },
|
|
1312
|
+
viewQueries: [{
|
|
1313
|
+
propertyName: "dialogRef",
|
|
1314
|
+
first: true,
|
|
1315
|
+
predicate: ["dialog"],
|
|
1316
|
+
descendants: true,
|
|
1317
|
+
isSignal: true
|
|
1318
|
+
}],
|
|
1319
|
+
ngImport: i0,
|
|
1320
|
+
template: `
|
|
1321
|
+
@if (open()) {
|
|
1322
|
+
<div class="fixed inset-0 z-50">
|
|
1323
|
+
<div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
|
|
1324
|
+
<section
|
|
1325
|
+
#dialog
|
|
1326
|
+
role="dialog"
|
|
1327
|
+
aria-modal="true"
|
|
1328
|
+
[attr.aria-labelledby]="titleId"
|
|
1329
|
+
tabindex="-1"
|
|
1330
|
+
class="absolute inset-y-0 right-0 flex w-full flex-col bg-surface-raised shadow-2xl focus:outline-none"
|
|
1331
|
+
[class]="widthClass()"
|
|
1332
|
+
>
|
|
1333
|
+
<header
|
|
1334
|
+
class="flex items-start justify-between gap-4 border-b border-neutral/20 px-5 py-4"
|
|
1335
|
+
>
|
|
1336
|
+
<div>
|
|
1337
|
+
<h2 [id]="titleId" class="font-display text-section font-semibold">{{ title() }}</h2>
|
|
1338
|
+
@if (subtitle(); as subtitleText) {
|
|
1339
|
+
<p class="mt-1 text-sm text-neutral/70">{{ subtitleText }}</p>
|
|
1340
|
+
}
|
|
1341
|
+
</div>
|
|
1342
|
+
<button
|
|
1343
|
+
type="button"
|
|
1344
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
|
|
1345
|
+
[attr.aria-label]="closeLabel()"
|
|
1346
|
+
(click)="close.emit()"
|
|
1347
|
+
>
|
|
1348
|
+
<svg lucideX size="18" />
|
|
1349
|
+
</button>
|
|
1350
|
+
</header>
|
|
1351
|
+
|
|
1352
|
+
<div class="flex-1 overflow-y-auto px-5 py-4">
|
|
1353
|
+
<ng-content />
|
|
1354
|
+
</div>
|
|
1355
|
+
|
|
1356
|
+
<div class="border-t border-neutral/20 px-5 py-4">
|
|
1357
|
+
<ng-content select="[drawer-actions]" />
|
|
1358
|
+
</div>
|
|
1359
|
+
</section>
|
|
1360
|
+
</div>
|
|
1361
|
+
}
|
|
1362
|
+
`,
|
|
1363
|
+
isInline: true,
|
|
1364
|
+
dependencies: [{
|
|
1365
|
+
kind: "component",
|
|
1366
|
+
type: LucideX,
|
|
1367
|
+
selector: "svg[lucideX]"
|
|
1368
|
+
}]
|
|
1369
|
+
});
|
|
1370
|
+
};
|
|
1371
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1372
|
+
minVersion: "12.0.0",
|
|
1373
|
+
version: "22.2.3",
|
|
1374
|
+
ngImport: i0,
|
|
1375
|
+
type: Drawer,
|
|
1376
|
+
decorators: [{
|
|
1377
|
+
type: Component,
|
|
1378
|
+
args: [{
|
|
1379
|
+
selector: "app-drawer",
|
|
1380
|
+
imports: [LucideX],
|
|
1381
|
+
host: {
|
|
1382
|
+
"(document:keydown.escape)": "onEscape()",
|
|
1383
|
+
"(document:keydown.tab)": "onTab($event)",
|
|
1384
|
+
"(document:keydown.shift.tab)": "onTab($event)"
|
|
1385
|
+
},
|
|
1386
|
+
template: `
|
|
1387
|
+
@if (open()) {
|
|
1388
|
+
<div class="fixed inset-0 z-50">
|
|
1389
|
+
<div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
|
|
1390
|
+
<section
|
|
1391
|
+
#dialog
|
|
1392
|
+
role="dialog"
|
|
1393
|
+
aria-modal="true"
|
|
1394
|
+
[attr.aria-labelledby]="titleId"
|
|
1395
|
+
tabindex="-1"
|
|
1396
|
+
class="absolute inset-y-0 right-0 flex w-full flex-col bg-surface-raised shadow-2xl focus:outline-none"
|
|
1397
|
+
[class]="widthClass()"
|
|
1398
|
+
>
|
|
1399
|
+
<header
|
|
1400
|
+
class="flex items-start justify-between gap-4 border-b border-neutral/20 px-5 py-4"
|
|
1401
|
+
>
|
|
1402
|
+
<div>
|
|
1403
|
+
<h2 [id]="titleId" class="font-display text-section font-semibold">{{ title() }}</h2>
|
|
1404
|
+
@if (subtitle(); as subtitleText) {
|
|
1405
|
+
<p class="mt-1 text-sm text-neutral/70">{{ subtitleText }}</p>
|
|
1406
|
+
}
|
|
1407
|
+
</div>
|
|
1408
|
+
<button
|
|
1409
|
+
type="button"
|
|
1410
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
|
|
1411
|
+
[attr.aria-label]="closeLabel()"
|
|
1412
|
+
(click)="close.emit()"
|
|
1413
|
+
>
|
|
1414
|
+
<svg lucideX size="18" />
|
|
1415
|
+
</button>
|
|
1416
|
+
</header>
|
|
1417
|
+
|
|
1418
|
+
<div class="flex-1 overflow-y-auto px-5 py-4">
|
|
1419
|
+
<ng-content />
|
|
1420
|
+
</div>
|
|
1421
|
+
|
|
1422
|
+
<div class="border-t border-neutral/20 px-5 py-4">
|
|
1423
|
+
<ng-content select="[drawer-actions]" />
|
|
1424
|
+
</div>
|
|
1425
|
+
</section>
|
|
1426
|
+
</div>
|
|
1427
|
+
}
|
|
1428
|
+
`
|
|
1429
|
+
}]
|
|
1430
|
+
}],
|
|
1431
|
+
propDecorators: {
|
|
1432
|
+
open: [{
|
|
1433
|
+
type: i0.Input,
|
|
1434
|
+
args: [{
|
|
1435
|
+
isSignal: true,
|
|
1436
|
+
alias: "open",
|
|
1437
|
+
required: false
|
|
1438
|
+
}]
|
|
1439
|
+
}],
|
|
1440
|
+
title: [{
|
|
1441
|
+
type: i0.Input,
|
|
1442
|
+
args: [{
|
|
1443
|
+
isSignal: true,
|
|
1444
|
+
alias: "title",
|
|
1445
|
+
required: false
|
|
1446
|
+
}]
|
|
1447
|
+
}],
|
|
1448
|
+
subtitle: [{
|
|
1449
|
+
type: i0.Input,
|
|
1450
|
+
args: [{
|
|
1451
|
+
isSignal: true,
|
|
1452
|
+
alias: "subtitle",
|
|
1453
|
+
required: false
|
|
1454
|
+
}]
|
|
1455
|
+
}],
|
|
1456
|
+
size: [{
|
|
1457
|
+
type: i0.Input,
|
|
1458
|
+
args: [{
|
|
1459
|
+
isSignal: true,
|
|
1460
|
+
alias: "size",
|
|
1461
|
+
required: false
|
|
1462
|
+
}]
|
|
1463
|
+
}],
|
|
1464
|
+
closeLabel: [{
|
|
1465
|
+
type: i0.Input,
|
|
1466
|
+
args: [{
|
|
1467
|
+
isSignal: true,
|
|
1468
|
+
alias: "closeLabel",
|
|
1469
|
+
required: false
|
|
1470
|
+
}]
|
|
1471
|
+
}],
|
|
1472
|
+
close: [{
|
|
1473
|
+
type: i0.Output,
|
|
1474
|
+
args: ["close"]
|
|
1475
|
+
}],
|
|
1476
|
+
dialogRef: [{
|
|
1477
|
+
type: i0.ViewChild,
|
|
1478
|
+
args: ["dialog", { isSignal: true }]
|
|
1479
|
+
}]
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
function validateMinWords(value, min) {
|
|
1483
|
+
const words = wordCount(value);
|
|
1484
|
+
if (words === null) return null;
|
|
1485
|
+
return words >= min ? null : `Debe tener al menos ${min} palabras.`;
|
|
1486
|
+
}
|
|
1487
|
+
function validateMaxWords(value, max) {
|
|
1488
|
+
const words = wordCount(value);
|
|
1489
|
+
if (words === null) return null;
|
|
1490
|
+
return words <= max ? null : `Como máximo ${max} palabras.`;
|
|
1491
|
+
}
|
|
1492
|
+
function isEmpty(value) {
|
|
1493
|
+
return value === void 0 || value === null || value === "";
|
|
1494
|
+
}
|
|
1495
|
+
function wordCount(value) {
|
|
1496
|
+
if (isEmpty(value) || typeof value !== "string") return null;
|
|
1497
|
+
return value.trim().split(/\s+/).filter(Boolean).length;
|
|
1498
|
+
}
|
|
1499
|
+
const TEXT_TYPES = /* @__PURE__ */ new Set([
|
|
1500
|
+
"text",
|
|
1501
|
+
"textarea",
|
|
1502
|
+
"email",
|
|
1503
|
+
"url",
|
|
1504
|
+
"slug",
|
|
1505
|
+
"phone",
|
|
1506
|
+
"time",
|
|
1507
|
+
"readonly-text",
|
|
1508
|
+
"select",
|
|
1509
|
+
"relation"
|
|
1510
|
+
]);
|
|
1511
|
+
const LIST_TYPES = /* @__PURE__ */ new Set([
|
|
1512
|
+
"list",
|
|
1513
|
+
"string-list",
|
|
1514
|
+
"key-value"
|
|
1515
|
+
]);
|
|
1516
|
+
function minLengthOf(field) {
|
|
1517
|
+
return TEXT_TYPES.has(field.type) ? field.validators?.minLength : void 0;
|
|
1518
|
+
}
|
|
1519
|
+
function maxLengthOf(field) {
|
|
1520
|
+
if (!TEXT_TYPES.has(field.type)) return void 0;
|
|
1521
|
+
if (field.type === "text") return field.validators?.maxLength ?? field.maxLength;
|
|
1522
|
+
return field.validators?.maxLength;
|
|
1523
|
+
}
|
|
1524
|
+
function patternOf(field) {
|
|
1525
|
+
if (!TEXT_TYPES.has(field.type)) return void 0;
|
|
1526
|
+
if (field.type === "text") return field.validators?.pattern ?? field.pattern;
|
|
1527
|
+
return field.validators?.pattern;
|
|
1528
|
+
}
|
|
1529
|
+
function minWordsOf(field) {
|
|
1530
|
+
if (!TEXT_TYPES.has(field.type)) return void 0;
|
|
1531
|
+
if (field.type === "textarea") return field.validators?.minWords ?? field.minWords;
|
|
1532
|
+
return field.validators?.minWords;
|
|
1533
|
+
}
|
|
1534
|
+
function maxWordsOf(field) {
|
|
1535
|
+
if (!TEXT_TYPES.has(field.type)) return void 0;
|
|
1536
|
+
if (field.type === "textarea") return field.validators?.maxWords ?? field.maxWords;
|
|
1537
|
+
return field.validators?.maxWords;
|
|
1538
|
+
}
|
|
1539
|
+
function numberBoundOf(field, bound) {
|
|
1540
|
+
if (field.type === "number") return field.validators?.[bound] ?? (bound === "min" ? field.min : field.max);
|
|
1541
|
+
if (field.type === "currency") return field.validators?.[bound];
|
|
1542
|
+
}
|
|
1543
|
+
function minItemsOf(field) {
|
|
1544
|
+
if (!LIST_TYPES.has(field.type)) return void 0;
|
|
1545
|
+
const list = field;
|
|
1546
|
+
return list.required ? Math.max(list.min ?? 1, 1) : list.min;
|
|
1547
|
+
}
|
|
1548
|
+
function maxItemsOf(field) {
|
|
1549
|
+
if (!LIST_TYPES.has(field.type)) return void 0;
|
|
1550
|
+
return field.max;
|
|
1551
|
+
}
|
|
1552
|
+
function toError(kind, message) {
|
|
1553
|
+
return message === null ? null : {
|
|
1554
|
+
kind,
|
|
1555
|
+
message
|
|
1556
|
+
};
|
|
1557
|
+
}
|
|
1558
|
+
function patternMessageFor(field) {
|
|
1559
|
+
const hint = field.help?.trim().replace(/[.。]+$/, "");
|
|
1560
|
+
return hint ? `Formato esperado: ${hint}.` : "Revisa el formato de este campo.";
|
|
1561
|
+
}
|
|
1562
|
+
function validateUrl(value) {
|
|
1563
|
+
if (value === void 0 || value === null || value === "") return null;
|
|
1564
|
+
if (typeof value !== "string") return URL_MESSAGE;
|
|
1565
|
+
return /^https?:\/\/\S+$/i.test(value) ? null : URL_MESSAGE;
|
|
1566
|
+
}
|
|
1567
|
+
const URL_MESSAGE = "Debe ser una URL válida (http o https).";
|
|
1568
|
+
function dateError(value, bound, iso) {
|
|
1569
|
+
if (typeof value !== "string" || value === "") return null;
|
|
1570
|
+
if (!(bound === "min" ? value < iso : value > iso)) return null;
|
|
1571
|
+
return {
|
|
1572
|
+
kind: `${bound}-date`,
|
|
1573
|
+
message: bound === "min" ? `La fecha no puede ser anterior a ${iso}.` : `La fecha no puede ser posterior a ${iso}.`
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
function registerUrlRule(path) {
|
|
1577
|
+
validate(path, (ctx) => toError("url", validateUrl(ctx.value())));
|
|
1578
|
+
}
|
|
1579
|
+
function registerEmailRule(path) {
|
|
1580
|
+
email(path, { message: "Debe ser un correo electrónico válido." });
|
|
1581
|
+
}
|
|
1582
|
+
function registerUniqueRule(ctx) {
|
|
1583
|
+
const { path, field, schema, api, excludeKey } = ctx;
|
|
1584
|
+
const endpoint = schema.endpoint;
|
|
1585
|
+
const keyField = schema.keyField;
|
|
1586
|
+
const fieldName = field.key;
|
|
1587
|
+
validateAsync(path, {
|
|
1588
|
+
debounce: 300,
|
|
1589
|
+
params: (c) => {
|
|
1590
|
+
const value = c.value().trim();
|
|
1591
|
+
return value === "" ? void 0 : {
|
|
1592
|
+
value,
|
|
1593
|
+
exclude: excludeKey
|
|
1594
|
+
};
|
|
1595
|
+
},
|
|
1596
|
+
factory: (params) => resource({
|
|
1597
|
+
params,
|
|
1598
|
+
loader: async ({ params: request }) => {
|
|
1599
|
+
const rows = await firstValueFrom(api.list(endpoint));
|
|
1600
|
+
if (!Array.isArray(rows)) return [];
|
|
1601
|
+
return rows.filter((row) => row[fieldName] === request.value && (request.exclude === void 0 || row[keyField] !== request.exclude));
|
|
1602
|
+
}
|
|
1603
|
+
}),
|
|
1604
|
+
onError: () => null,
|
|
1605
|
+
onSuccess: (rows, c) => {
|
|
1606
|
+
if (c.value().trim() === "" || !Array.isArray(rows) || rows.length === 0) return null;
|
|
1607
|
+
return {
|
|
1608
|
+
kind: "unique",
|
|
1609
|
+
message: "Este valor ya existe en el registro."
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
const FIELD_VALIDATORS = [
|
|
1615
|
+
{
|
|
1616
|
+
name: "required",
|
|
1617
|
+
declares: ({ field }) => field.required === true && field.type !== "boolean" && field.type !== "group" && !LIST_TYPES.has(field.type),
|
|
1618
|
+
register: ({ path }) => required(path, { message: "Campo obligatorio." })
|
|
1619
|
+
},
|
|
1620
|
+
{
|
|
1621
|
+
name: "email",
|
|
1622
|
+
declares: ({ field }) => field.type === "email",
|
|
1623
|
+
register: ({ path }) => registerEmailRule(path)
|
|
1624
|
+
},
|
|
1625
|
+
{
|
|
1626
|
+
name: "url",
|
|
1627
|
+
declares: ({ field }) => field.type === "url",
|
|
1628
|
+
register: ({ path }) => registerUrlRule(path)
|
|
1629
|
+
},
|
|
1630
|
+
{
|
|
1631
|
+
name: "min-length",
|
|
1632
|
+
declares: ({ field }) => minLengthOf(field) !== void 0,
|
|
1633
|
+
register: ({ path, field }) => {
|
|
1634
|
+
const min = minLengthOf(field);
|
|
1635
|
+
if (min !== void 0) minLength(path, min, { message: `Debe tener al menos ${min} caracteres.` });
|
|
1636
|
+
}
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
name: "max-length",
|
|
1640
|
+
declares: ({ field }) => maxLengthOf(field) !== void 0,
|
|
1641
|
+
register: ({ path, field }) => {
|
|
1642
|
+
const max = maxLengthOf(field);
|
|
1643
|
+
if (max !== void 0) maxLength(path, max, { message: `Como máximo ${max} caracteres.` });
|
|
1644
|
+
}
|
|
1645
|
+
},
|
|
1646
|
+
{
|
|
1647
|
+
name: "pattern",
|
|
1648
|
+
declares: ({ field }) => patternOf(field) !== void 0,
|
|
1649
|
+
register: ({ path, field }) => {
|
|
1650
|
+
const pattern$1 = patternOf(field);
|
|
1651
|
+
if (pattern$1 !== void 0) pattern(path, new RegExp(pattern$1), { message: patternMessageFor(field) });
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
name: "min-words",
|
|
1656
|
+
declares: ({ field }) => minWordsOf(field) !== void 0,
|
|
1657
|
+
register: ({ path, field }) => {
|
|
1658
|
+
const min = minWordsOf(field);
|
|
1659
|
+
if (min !== void 0) validate(path, (ctx) => toError("min-words", validateMinWords(ctx.value(), min)));
|
|
1660
|
+
}
|
|
1661
|
+
},
|
|
1662
|
+
{
|
|
1663
|
+
name: "max-words",
|
|
1664
|
+
declares: ({ field }) => maxWordsOf(field) !== void 0,
|
|
1665
|
+
register: ({ path, field }) => {
|
|
1666
|
+
const max = maxWordsOf(field);
|
|
1667
|
+
if (max !== void 0) validate(path, (ctx) => toError("max-words", validateMaxWords(ctx.value(), max)));
|
|
1668
|
+
}
|
|
1669
|
+
},
|
|
1670
|
+
{
|
|
1671
|
+
name: "min",
|
|
1672
|
+
declares: ({ field }) => numberBoundOf(field, "min") !== void 0,
|
|
1673
|
+
register: ({ path, field }) => {
|
|
1674
|
+
const min$1 = numberBoundOf(field, "min");
|
|
1675
|
+
if (min$1 !== void 0) min(path, min$1, { message: `Debe ser como mínimo ${min$1}.` });
|
|
1676
|
+
}
|
|
1677
|
+
},
|
|
1678
|
+
{
|
|
1679
|
+
name: "max",
|
|
1680
|
+
declares: ({ field }) => numberBoundOf(field, "max") !== void 0,
|
|
1681
|
+
register: ({ path, field }) => {
|
|
1682
|
+
const max$1 = numberBoundOf(field, "max");
|
|
1683
|
+
if (max$1 !== void 0) max(path, max$1, { message: `Debe ser como máximo ${max$1}.` });
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
name: "min-items",
|
|
1688
|
+
declares: ({ field }) => minItemsOf(field) !== void 0,
|
|
1689
|
+
register: ({ path, field }) => {
|
|
1690
|
+
const min = minItemsOf(field);
|
|
1691
|
+
if (min !== void 0) minLength(path, min, { message: min === 1 ? "Debe tener al menos 1 elemento." : `Debe tener al menos ${min} elementos.` });
|
|
1692
|
+
}
|
|
1693
|
+
},
|
|
1694
|
+
{
|
|
1695
|
+
name: "max-items",
|
|
1696
|
+
declares: ({ field }) => maxItemsOf(field) !== void 0,
|
|
1697
|
+
register: ({ path, field }) => {
|
|
1698
|
+
const max = maxItemsOf(field);
|
|
1699
|
+
if (max !== void 0) maxLength(path, max, { message: max === 1 ? "Como máximo 1 elemento." : `Como máximo ${max} elementos.` });
|
|
1700
|
+
}
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: "date-min",
|
|
1704
|
+
declares: ({ field }) => field.type === "date" && field.min !== void 0,
|
|
1705
|
+
register: ({ path, field }) => {
|
|
1706
|
+
const min = field.type === "date" ? field.min : void 0;
|
|
1707
|
+
if (min !== void 0) validate(path, (ctx) => dateError(ctx.value(), "min", min));
|
|
1708
|
+
}
|
|
1709
|
+
},
|
|
1710
|
+
{
|
|
1711
|
+
name: "date-max",
|
|
1712
|
+
declares: ({ field }) => field.type === "date" && field.max !== void 0,
|
|
1713
|
+
register: ({ path, field }) => {
|
|
1714
|
+
const max = field.type === "date" ? field.max : void 0;
|
|
1715
|
+
if (max !== void 0) validate(path, (ctx) => dateError(ctx.value(), "max", max));
|
|
1716
|
+
}
|
|
1717
|
+
},
|
|
1718
|
+
{
|
|
1719
|
+
name: "unique",
|
|
1720
|
+
declares: ({ field, schema }) => field.validators?.unique === true && TEXT_TYPES.has(field.type) && !!schema.endpoint.list,
|
|
1721
|
+
register: (ctx) => registerUniqueRule(ctx)
|
|
1722
|
+
}
|
|
1723
|
+
];
|
|
1724
|
+
FIELD_VALIDATORS.map((entry) => entry.name);
|
|
1725
|
+
function buildFormSchema(schema, api, excludeKey) {
|
|
1726
|
+
return (root) => {
|
|
1727
|
+
applyRulesToFields({
|
|
1728
|
+
schema,
|
|
1729
|
+
api,
|
|
1730
|
+
excludeKey,
|
|
1731
|
+
root,
|
|
1732
|
+
container: root,
|
|
1733
|
+
siblings: schema.fields
|
|
1734
|
+
}, schema.fields);
|
|
1735
|
+
};
|
|
1736
|
+
}
|
|
1737
|
+
function applyRulesToFields(scope, fields) {
|
|
1738
|
+
for (const field of fields) {
|
|
1739
|
+
if (!isSupported("field", field.type)) continue;
|
|
1740
|
+
applyRulesToField(scope, field, pathAt(scope.container, field.key));
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
function applyRulesToField(scope, field, path) {
|
|
1744
|
+
applyLifecycleRules(scope, field, path);
|
|
1745
|
+
const ctx = {
|
|
1746
|
+
path,
|
|
1747
|
+
field,
|
|
1748
|
+
schema: scope.schema,
|
|
1749
|
+
api: scope.api,
|
|
1750
|
+
excludeKey: scope.excludeKey
|
|
1751
|
+
};
|
|
1752
|
+
for (const entry of FIELD_VALIDATORS) if (entry.declares(ctx)) entry.register(ctx);
|
|
1753
|
+
switch (field.type) {
|
|
1754
|
+
case "group":
|
|
1755
|
+
applyRulesToFields({
|
|
1756
|
+
...scope,
|
|
1757
|
+
container: path,
|
|
1758
|
+
siblings: field.fields
|
|
1759
|
+
}, field.fields);
|
|
1760
|
+
break;
|
|
1761
|
+
case "list":
|
|
1762
|
+
applyEach(path, (item) => applyRulesToFields({
|
|
1763
|
+
...scope,
|
|
1764
|
+
container: item,
|
|
1765
|
+
siblings: field.itemFields
|
|
1766
|
+
}, field.itemFields));
|
|
1767
|
+
break;
|
|
1768
|
+
case "string-list": applyEach(path, (item) => applyStringItemRules(item, field.itemType));
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
function modeAppliesTo(mode, isEdit) {
|
|
1772
|
+
return mode === "always" || mode === "update" && isEdit || mode === "create" && !isEdit;
|
|
1773
|
+
}
|
|
1774
|
+
function applyLifecycleRules(scope, field, path) {
|
|
1775
|
+
if (field.visibleWhen) {
|
|
1776
|
+
const condition = field.visibleWhen;
|
|
1777
|
+
hidden(path, { when: (ctx) => !evaluateCondition(condition, ctx.valueOf(conditionPath(scope, condition.field))) });
|
|
1778
|
+
}
|
|
1779
|
+
if (field.dependsOn) {
|
|
1780
|
+
const parentKey = field.dependsOn;
|
|
1781
|
+
const parentPath = pathAt(scope.siblings.some((candidate) => candidate.key === parentKey) ? scope.container : scope.root, parentKey);
|
|
1782
|
+
disabled(path, ({ valueOf }) => isEmptyValue(valueOf(parentPath)));
|
|
1783
|
+
}
|
|
1784
|
+
if (field.readonly) readonly(path, { when: () => true });
|
|
1785
|
+
if (field.readonlyOn && modeAppliesTo(field.readonlyOn, scope.excludeKey !== void 0)) readonly(path, { when: () => true });
|
|
1786
|
+
if (field.hiddenOn && modeAppliesTo(field.hiddenOn, scope.excludeKey !== void 0)) hidden(path, { when: () => true });
|
|
1787
|
+
if (field.readonlyWhen) {
|
|
1788
|
+
const condition = field.readonlyWhen;
|
|
1789
|
+
readonly(path, { when: (ctx) => evaluateCondition(condition, ctx.valueOf(conditionPath(scope, condition.field))) });
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
function isEmptyValue(value) {
|
|
1793
|
+
if (value === void 0 || value === null || value === "") return true;
|
|
1794
|
+
return Array.isArray(value) && value.length === 0;
|
|
1795
|
+
}
|
|
1796
|
+
function conditionPath(scope, key) {
|
|
1797
|
+
return scope.siblings.some((field) => field.key === key) ? scope.container : scope.root;
|
|
1798
|
+
}
|
|
1799
|
+
function applyStringItemRules(path, itemType) {
|
|
1800
|
+
if (itemType === "email") registerEmailRule(path);
|
|
1801
|
+
if (itemType === "url") registerUrlRule(path);
|
|
1802
|
+
}
|
|
1803
|
+
function pathAt(parent, key) {
|
|
1804
|
+
const path = parent[key];
|
|
1805
|
+
if (path === void 0) throw new Error(`El formulario no contiene la ruta "${key}".`);
|
|
1806
|
+
return path;
|
|
1807
|
+
}
|
|
1808
|
+
function seedModel(schema) {
|
|
1809
|
+
return seedFields(schema.fields);
|
|
1810
|
+
}
|
|
1811
|
+
function buildPayload(schema, root) {
|
|
1812
|
+
const payload = {};
|
|
1813
|
+
collectPayload(root, schema.fields, payload, schema.positionField);
|
|
1814
|
+
return payload;
|
|
1815
|
+
}
|
|
1816
|
+
function collectPayload(tree, fields, payload, excludedKey) {
|
|
1817
|
+
for (const field of fields) {
|
|
1818
|
+
if (excludedKey !== void 0 && field.key === excludedKey) continue;
|
|
1819
|
+
const state = childTree(tree, field.key)();
|
|
1820
|
+
if (state.hidden()) continue;
|
|
1821
|
+
if (field.type === "group") {
|
|
1822
|
+
const nested = {};
|
|
1823
|
+
collectPayload(childTree(tree, field.key), field.fields, nested, void 0);
|
|
1824
|
+
payload[field.key] = nested;
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
if (field.type === "list") {
|
|
1828
|
+
const items = listTree(tree, field.key);
|
|
1829
|
+
const raw = state.value() ?? [];
|
|
1830
|
+
const list = [];
|
|
1831
|
+
for (let index = 0; index < raw.length; index++) {
|
|
1832
|
+
const nested = {};
|
|
1833
|
+
collectPayload(itemTree(items, index), field.itemFields, nested, void 0);
|
|
1834
|
+
const positionKey = field.positionField;
|
|
1835
|
+
const position = positionKey !== void 0 ? raw[index]?.[positionKey] : void 0;
|
|
1836
|
+
if (positionKey !== void 0 && position !== void 0) nested[positionKey] = position;
|
|
1837
|
+
list.push(nested);
|
|
1838
|
+
}
|
|
1839
|
+
payload[field.key] = list;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
payload[field.key] = state.value();
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
var DrawerForm = class DrawerForm {
|
|
1846
|
+
open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
1847
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
1848
|
+
fields = input(void 0, ...ngDevMode ? [{ debugName: "fields" }] : /* istanbul ignore next */ []);
|
|
1849
|
+
mode = input("create", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
|
|
1850
|
+
recordKey = input(null, ...ngDevMode ? [{ debugName: "recordKey" }] : /* istanbul ignore next */ []);
|
|
1851
|
+
title = input(null, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
1852
|
+
size = input("md", ...ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []);
|
|
1853
|
+
saved = output();
|
|
1854
|
+
close = output();
|
|
1855
|
+
api = inject(ApiService);
|
|
1856
|
+
notifications = inject(NotificationService);
|
|
1857
|
+
errorMapper = inject(ErrorMapperService);
|
|
1858
|
+
injector = inject(Injector);
|
|
1859
|
+
destroyRef = inject(DestroyRef);
|
|
1860
|
+
tree = signal(null, ...ngDevMode ? [{ debugName: "tree" }] : /* istanbul ignore next */ []);
|
|
1861
|
+
status = signal("idle", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
1862
|
+
saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
|
|
1863
|
+
generalError = signal("", ...ngDevMode ? [{ debugName: "generalError" }] : /* istanbul ignore next */ []);
|
|
1864
|
+
fieldErrors = signal({}, ...ngDevMode ? [{ debugName: "fieldErrors" }] : /* istanbul ignore next */ []);
|
|
1865
|
+
loadError = signal("", ...ngDevMode ? [{ debugName: "loadError" }] : /* istanbul ignore next */ []);
|
|
1866
|
+
visibleFields = computed(() => this.fields() ?? this.schema().fields, ...ngDevMode ? [{ debugName: "visibleFields" }] : /* istanbul ignore next */ []);
|
|
1867
|
+
drawerTitle = computed(() => {
|
|
1868
|
+
if (this.title() !== null) return this.title();
|
|
1869
|
+
const label = this.schema().label;
|
|
1870
|
+
return this.mode() === "create" ? `Crear ${label}` : `Editar ${label}`;
|
|
1871
|
+
}, ...ngDevMode ? [{ debugName: "drawerTitle" }] : /* istanbul ignore next */ []);
|
|
1872
|
+
ngOnInit() {
|
|
1873
|
+
this.build();
|
|
1874
|
+
}
|
|
1875
|
+
build() {
|
|
1876
|
+
const schema = this.schema();
|
|
1877
|
+
const recordKey = this.recordKey();
|
|
1878
|
+
const schemaFn = buildFormSchema(schema, this.api, recordKey ?? void 0);
|
|
1879
|
+
const tree = form(signal(seedModel(schema)), schemaFn, { injector: this.injector });
|
|
1880
|
+
if (this.mode() === "create" || recordKey === null) {
|
|
1881
|
+
this.tree.set(tree);
|
|
1882
|
+
this.status.set("ready");
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
this.status.set("loading");
|
|
1886
|
+
this.api.get(schema.endpoint, recordKey).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
|
|
1887
|
+
next: (record) => {
|
|
1888
|
+
tree().value.set({
|
|
1889
|
+
...seedModel(schema),
|
|
1890
|
+
...record
|
|
1891
|
+
});
|
|
1892
|
+
this.tree.set(tree);
|
|
1893
|
+
this.status.set("ready");
|
|
1894
|
+
},
|
|
1895
|
+
error: (error) => {
|
|
1896
|
+
this.loadError.set(this.errorMapper.messageOf(error));
|
|
1897
|
+
this.status.set("error");
|
|
1898
|
+
}
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
async save() {
|
|
1902
|
+
const tree = this.tree();
|
|
1903
|
+
const schema = this.schema();
|
|
1904
|
+
if (tree === null || this.saving()) return;
|
|
1905
|
+
this.saving.set(true);
|
|
1906
|
+
this.generalError.set("");
|
|
1907
|
+
this.fieldErrors.set({});
|
|
1908
|
+
try {
|
|
1909
|
+
if (!await submit(tree, {
|
|
1910
|
+
action: async () => {
|
|
1911
|
+
await this.persist(tree, schema);
|
|
1912
|
+
return null;
|
|
1913
|
+
},
|
|
1914
|
+
onInvalid: () => this.markTouched(tree)
|
|
1915
|
+
}) || Object.keys(this.fieldErrors()).length > 0 || this.generalError() !== "") this.markTouched(tree);
|
|
1916
|
+
} finally {
|
|
1917
|
+
this.saving.set(false);
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
markTouched(tree) {
|
|
1921
|
+
for (const field of this.visibleFields()) childTree(tree, field.key)().markAsTouched();
|
|
1922
|
+
}
|
|
1923
|
+
async persist(tree, schema) {
|
|
1924
|
+
const payload = buildPayload(schema, tree);
|
|
1925
|
+
try {
|
|
1926
|
+
if (this.mode() === "create") {
|
|
1927
|
+
const created = await firstValueFrom(this.api.create(schema.endpoint, payload));
|
|
1928
|
+
this.notifications.success("Registro creado.");
|
|
1929
|
+
this.saved.emit(created ?? {});
|
|
1930
|
+
this.close.emit();
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
const updated = await firstValueFrom(this.api.update(schema.endpoint, this.recordKey() ?? void 0, payload));
|
|
1934
|
+
this.notifications.success("Registro guardado.");
|
|
1935
|
+
this.saved.emit(updated ?? {});
|
|
1936
|
+
this.close.emit();
|
|
1937
|
+
} catch (error) {
|
|
1938
|
+
const mapped = this.errorMapper.map(error);
|
|
1939
|
+
this.fieldErrors.set(mapped.fields);
|
|
1940
|
+
const withoutFields = Object.keys(mapped.fields).length === 0;
|
|
1941
|
+
this.generalError.set(mapped.general ?? (withoutFields ? this.errorMapper.messageOf(error) : ""));
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1945
|
+
minVersion: "12.0.0",
|
|
1946
|
+
version: "22.2.3",
|
|
1947
|
+
ngImport: i0,
|
|
1948
|
+
type: DrawerForm,
|
|
1949
|
+
deps: [],
|
|
1950
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1951
|
+
});
|
|
1952
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1953
|
+
minVersion: "17.0.0",
|
|
1954
|
+
version: "22.2.3",
|
|
1955
|
+
type: DrawerForm,
|
|
1956
|
+
isStandalone: true,
|
|
1957
|
+
selector: "app-drawer-form",
|
|
1958
|
+
inputs: {
|
|
1959
|
+
open: {
|
|
1960
|
+
classPropertyName: "open",
|
|
1961
|
+
publicName: "open",
|
|
1962
|
+
isSignal: true,
|
|
1963
|
+
isRequired: false,
|
|
1964
|
+
transformFunction: null
|
|
1965
|
+
},
|
|
1966
|
+
schema: {
|
|
1967
|
+
classPropertyName: "schema",
|
|
1968
|
+
publicName: "schema",
|
|
1969
|
+
isSignal: true,
|
|
1970
|
+
isRequired: true,
|
|
1971
|
+
transformFunction: null
|
|
1972
|
+
},
|
|
1973
|
+
fields: {
|
|
1974
|
+
classPropertyName: "fields",
|
|
1975
|
+
publicName: "fields",
|
|
1976
|
+
isSignal: true,
|
|
1977
|
+
isRequired: false,
|
|
1978
|
+
transformFunction: null
|
|
1979
|
+
},
|
|
1980
|
+
mode: {
|
|
1981
|
+
classPropertyName: "mode",
|
|
1982
|
+
publicName: "mode",
|
|
1983
|
+
isSignal: true,
|
|
1984
|
+
isRequired: false,
|
|
1985
|
+
transformFunction: null
|
|
1986
|
+
},
|
|
1987
|
+
recordKey: {
|
|
1988
|
+
classPropertyName: "recordKey",
|
|
1989
|
+
publicName: "recordKey",
|
|
1990
|
+
isSignal: true,
|
|
1991
|
+
isRequired: false,
|
|
1992
|
+
transformFunction: null
|
|
1993
|
+
},
|
|
1994
|
+
title: {
|
|
1995
|
+
classPropertyName: "title",
|
|
1996
|
+
publicName: "title",
|
|
1997
|
+
isSignal: true,
|
|
1998
|
+
isRequired: false,
|
|
1999
|
+
transformFunction: null
|
|
2000
|
+
},
|
|
2001
|
+
size: {
|
|
2002
|
+
classPropertyName: "size",
|
|
2003
|
+
publicName: "size",
|
|
2004
|
+
isSignal: true,
|
|
2005
|
+
isRequired: false,
|
|
2006
|
+
transformFunction: null
|
|
2007
|
+
}
|
|
2008
|
+
},
|
|
2009
|
+
outputs: {
|
|
2010
|
+
saved: "saved",
|
|
2011
|
+
close: "close"
|
|
2012
|
+
},
|
|
2013
|
+
ngImport: i0,
|
|
2014
|
+
template: `
|
|
2015
|
+
<app-drawer [open]="open()" [title]="drawerTitle()" [size]="size()" (close)="close.emit()">
|
|
2016
|
+
@if (status() === 'loading') {
|
|
2017
|
+
<app-skeleton [lines]="4" />
|
|
2018
|
+
} @else if (status() === 'error') {
|
|
2019
|
+
<p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
|
|
2020
|
+
{{ loadError() }}
|
|
2021
|
+
</p>
|
|
2022
|
+
} @else if (tree(); as formTree) {
|
|
2023
|
+
@if (generalError()) {
|
|
2024
|
+
<p
|
|
2025
|
+
role="alert"
|
|
2026
|
+
class="mb-4 rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger"
|
|
2027
|
+
data-drawer-error
|
|
2028
|
+
>
|
|
2029
|
+
{{ generalError() }}
|
|
2030
|
+
</p>
|
|
2031
|
+
}
|
|
2032
|
+
@for (field of visibleFields(); track field.key) {
|
|
2033
|
+
<div class="mb-4">
|
|
2034
|
+
<app-field-host
|
|
2035
|
+
[field]="field"
|
|
2036
|
+
[tree]="formTree"
|
|
2037
|
+
[exists]="mode() === 'edit'"
|
|
2038
|
+
[serverErrors]="fieldErrors()"
|
|
2039
|
+
[resource]="schema().id"
|
|
2040
|
+
/>
|
|
2041
|
+
</div>
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
<div drawer-actions class="flex justify-end gap-2">
|
|
2046
|
+
<button app-button variant="outline" type="button" (click)="close.emit()">Cancelar</button>
|
|
2047
|
+
<button app-button type="button" [loading]="saving()" (click)="save()">Guardar</button>
|
|
2048
|
+
</div>
|
|
2049
|
+
</app-drawer>
|
|
2050
|
+
`,
|
|
2051
|
+
isInline: true,
|
|
2052
|
+
dependencies: [
|
|
2053
|
+
{
|
|
2054
|
+
kind: "component",
|
|
2055
|
+
type: Button,
|
|
2056
|
+
selector: "button[app-button]",
|
|
2057
|
+
inputs: [
|
|
2058
|
+
"variant",
|
|
2059
|
+
"size",
|
|
2060
|
+
"type",
|
|
2061
|
+
"disabled",
|
|
2062
|
+
"loading"
|
|
2063
|
+
]
|
|
2064
|
+
},
|
|
2065
|
+
{
|
|
2066
|
+
kind: "component",
|
|
2067
|
+
type: Drawer,
|
|
2068
|
+
selector: "app-drawer",
|
|
2069
|
+
inputs: [
|
|
2070
|
+
"open",
|
|
2071
|
+
"title",
|
|
2072
|
+
"subtitle",
|
|
2073
|
+
"size",
|
|
2074
|
+
"closeLabel"
|
|
2075
|
+
],
|
|
2076
|
+
outputs: ["close"]
|
|
2077
|
+
},
|
|
2078
|
+
{
|
|
2079
|
+
kind: "component",
|
|
2080
|
+
type: FieldHost,
|
|
2081
|
+
selector: "app-field-host",
|
|
2082
|
+
inputs: [
|
|
2083
|
+
"field",
|
|
2084
|
+
"tree",
|
|
2085
|
+
"exists",
|
|
2086
|
+
"serverErrors",
|
|
2087
|
+
"pathPrefix",
|
|
2088
|
+
"resource"
|
|
2089
|
+
]
|
|
2090
|
+
},
|
|
2091
|
+
{
|
|
2092
|
+
kind: "component",
|
|
2093
|
+
type: Skeleton,
|
|
2094
|
+
selector: "app-skeleton",
|
|
2095
|
+
inputs: [
|
|
2096
|
+
"lines",
|
|
2097
|
+
"height",
|
|
2098
|
+
"width"
|
|
2099
|
+
]
|
|
2100
|
+
}
|
|
2101
|
+
]
|
|
2102
|
+
});
|
|
2103
|
+
};
|
|
2104
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2105
|
+
minVersion: "12.0.0",
|
|
2106
|
+
version: "22.2.3",
|
|
2107
|
+
ngImport: i0,
|
|
2108
|
+
type: DrawerForm,
|
|
2109
|
+
decorators: [{
|
|
2110
|
+
type: Component,
|
|
2111
|
+
args: [{
|
|
2112
|
+
selector: "app-drawer-form",
|
|
2113
|
+
imports: [
|
|
2114
|
+
Button,
|
|
2115
|
+
Drawer,
|
|
2116
|
+
FieldHost,
|
|
2117
|
+
Skeleton
|
|
2118
|
+
],
|
|
2119
|
+
template: `
|
|
2120
|
+
<app-drawer [open]="open()" [title]="drawerTitle()" [size]="size()" (close)="close.emit()">
|
|
2121
|
+
@if (status() === 'loading') {
|
|
2122
|
+
<app-skeleton [lines]="4" />
|
|
2123
|
+
} @else if (status() === 'error') {
|
|
2124
|
+
<p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
|
|
2125
|
+
{{ loadError() }}
|
|
2126
|
+
</p>
|
|
2127
|
+
} @else if (tree(); as formTree) {
|
|
2128
|
+
@if (generalError()) {
|
|
2129
|
+
<p
|
|
2130
|
+
role="alert"
|
|
2131
|
+
class="mb-4 rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger"
|
|
2132
|
+
data-drawer-error
|
|
2133
|
+
>
|
|
2134
|
+
{{ generalError() }}
|
|
2135
|
+
</p>
|
|
2136
|
+
}
|
|
2137
|
+
@for (field of visibleFields(); track field.key) {
|
|
2138
|
+
<div class="mb-4">
|
|
2139
|
+
<app-field-host
|
|
2140
|
+
[field]="field"
|
|
2141
|
+
[tree]="formTree"
|
|
2142
|
+
[exists]="mode() === 'edit'"
|
|
2143
|
+
[serverErrors]="fieldErrors()"
|
|
2144
|
+
[resource]="schema().id"
|
|
2145
|
+
/>
|
|
2146
|
+
</div>
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
<div drawer-actions class="flex justify-end gap-2">
|
|
2151
|
+
<button app-button variant="outline" type="button" (click)="close.emit()">Cancelar</button>
|
|
2152
|
+
<button app-button type="button" [loading]="saving()" (click)="save()">Guardar</button>
|
|
2153
|
+
</div>
|
|
2154
|
+
</app-drawer>
|
|
2155
|
+
`
|
|
2156
|
+
}]
|
|
2157
|
+
}],
|
|
2158
|
+
propDecorators: {
|
|
2159
|
+
open: [{
|
|
2160
|
+
type: i0.Input,
|
|
2161
|
+
args: [{
|
|
2162
|
+
isSignal: true,
|
|
2163
|
+
alias: "open",
|
|
2164
|
+
required: false
|
|
2165
|
+
}]
|
|
2166
|
+
}],
|
|
2167
|
+
schema: [{
|
|
2168
|
+
type: i0.Input,
|
|
2169
|
+
args: [{
|
|
2170
|
+
isSignal: true,
|
|
2171
|
+
alias: "schema",
|
|
2172
|
+
required: true
|
|
2173
|
+
}]
|
|
2174
|
+
}],
|
|
2175
|
+
fields: [{
|
|
2176
|
+
type: i0.Input,
|
|
2177
|
+
args: [{
|
|
2178
|
+
isSignal: true,
|
|
2179
|
+
alias: "fields",
|
|
2180
|
+
required: false
|
|
2181
|
+
}]
|
|
2182
|
+
}],
|
|
2183
|
+
mode: [{
|
|
2184
|
+
type: i0.Input,
|
|
2185
|
+
args: [{
|
|
2186
|
+
isSignal: true,
|
|
2187
|
+
alias: "mode",
|
|
2188
|
+
required: false
|
|
2189
|
+
}]
|
|
2190
|
+
}],
|
|
2191
|
+
recordKey: [{
|
|
2192
|
+
type: i0.Input,
|
|
2193
|
+
args: [{
|
|
2194
|
+
isSignal: true,
|
|
2195
|
+
alias: "recordKey",
|
|
2196
|
+
required: false
|
|
2197
|
+
}]
|
|
2198
|
+
}],
|
|
2199
|
+
title: [{
|
|
2200
|
+
type: i0.Input,
|
|
2201
|
+
args: [{
|
|
2202
|
+
isSignal: true,
|
|
2203
|
+
alias: "title",
|
|
2204
|
+
required: false
|
|
2205
|
+
}]
|
|
2206
|
+
}],
|
|
2207
|
+
size: [{
|
|
2208
|
+
type: i0.Input,
|
|
2209
|
+
args: [{
|
|
2210
|
+
isSignal: true,
|
|
2211
|
+
alias: "size",
|
|
2212
|
+
required: false
|
|
2213
|
+
}]
|
|
2214
|
+
}],
|
|
2215
|
+
saved: [{
|
|
2216
|
+
type: i0.Output,
|
|
2217
|
+
args: ["saved"]
|
|
2218
|
+
}],
|
|
2219
|
+
close: [{
|
|
2220
|
+
type: i0.Output,
|
|
2221
|
+
args: ["close"]
|
|
2222
|
+
}]
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
function collectDependsOn(fields, container = []) {
|
|
2226
|
+
const links = [];
|
|
2227
|
+
for (const field of fields) {
|
|
2228
|
+
if (field.type === "group") {
|
|
2229
|
+
links.push(...collectDependsOn(field.fields, [...container, field.key]));
|
|
2230
|
+
continue;
|
|
2231
|
+
}
|
|
2232
|
+
if (field.dependsOn === void 0) continue;
|
|
2233
|
+
links.push({
|
|
2234
|
+
container,
|
|
2235
|
+
childKey: field.key,
|
|
2236
|
+
parentKey: field.dependsOn,
|
|
2237
|
+
resetValue: seedFieldValue(field),
|
|
2238
|
+
id: `${container.join(".")}.${field.key}<-${field.dependsOn}`
|
|
2239
|
+
});
|
|
2240
|
+
}
|
|
2241
|
+
return links;
|
|
2242
|
+
}
|
|
2243
|
+
var CopyShare = class CopyShare {
|
|
2244
|
+
url = input.required(...ngDevMode ? [{ debugName: "url" }] : /* istanbul ignore next */ []);
|
|
2245
|
+
label = input("Copiar enlace", ...ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []);
|
|
2246
|
+
shareText = input("", ...ngDevMode ? [{ debugName: "shareText" }] : /* istanbul ignore next */ []);
|
|
2247
|
+
notify = inject(NotificationService);
|
|
2248
|
+
canShare = computed(() => typeof navigator !== "undefined" && typeof navigator.share === "function", ...ngDevMode ? [{ debugName: "canShare" }] : /* istanbul ignore next */ []);
|
|
2249
|
+
waUrl = computed(() => {
|
|
2250
|
+
const text = buildShareMessage(this.url(), this.shareText());
|
|
2251
|
+
return `https://wa.me/?text=${encodeURIComponent(text)}`;
|
|
2252
|
+
}, ...ngDevMode ? [{ debugName: "waUrl" }] : /* istanbul ignore next */ []);
|
|
2253
|
+
async copy() {
|
|
2254
|
+
if (await writeClipboard(buildShareMessage(this.url(), this.shareText()))) this.notify.success("Mensaje copiado.");
|
|
2255
|
+
else this.notify.error("No se pudo copiar el mensaje.");
|
|
2256
|
+
}
|
|
2257
|
+
async share() {
|
|
2258
|
+
if (!this.canShare()) {
|
|
2259
|
+
this.shareWhatsApp();
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
try {
|
|
2263
|
+
await navigator.share({
|
|
2264
|
+
url: this.url(),
|
|
2265
|
+
text: this.shareText()
|
|
2266
|
+
});
|
|
2267
|
+
} catch {}
|
|
2268
|
+
}
|
|
2269
|
+
shareWhatsApp() {
|
|
2270
|
+
window.open(this.waUrl(), "_blank", "noopener,noreferrer");
|
|
2271
|
+
}
|
|
2272
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
2273
|
+
minVersion: "12.0.0",
|
|
2274
|
+
version: "22.2.3",
|
|
2275
|
+
ngImport: i0,
|
|
2276
|
+
type: CopyShare,
|
|
2277
|
+
deps: [],
|
|
2278
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2279
|
+
});
|
|
2280
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2281
|
+
minVersion: "17.0.0",
|
|
2282
|
+
version: "22.2.3",
|
|
2283
|
+
type: CopyShare,
|
|
2284
|
+
isStandalone: true,
|
|
2285
|
+
selector: "app-copy-share",
|
|
2286
|
+
inputs: {
|
|
2287
|
+
url: {
|
|
2288
|
+
classPropertyName: "url",
|
|
2289
|
+
publicName: "url",
|
|
2290
|
+
isSignal: true,
|
|
2291
|
+
isRequired: true,
|
|
2292
|
+
transformFunction: null
|
|
2293
|
+
},
|
|
2294
|
+
label: {
|
|
2295
|
+
classPropertyName: "label",
|
|
2296
|
+
publicName: "label",
|
|
2297
|
+
isSignal: true,
|
|
2298
|
+
isRequired: false,
|
|
2299
|
+
transformFunction: null
|
|
2300
|
+
},
|
|
2301
|
+
shareText: {
|
|
2302
|
+
classPropertyName: "shareText",
|
|
2303
|
+
publicName: "shareText",
|
|
2304
|
+
isSignal: true,
|
|
2305
|
+
isRequired: false,
|
|
2306
|
+
transformFunction: null
|
|
2307
|
+
}
|
|
2308
|
+
},
|
|
2309
|
+
ngImport: i0,
|
|
2310
|
+
template: `
|
|
2311
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
2312
|
+
<button app-button type="button" variant="outline" (click)="copy()">
|
|
2313
|
+
<svg lucideCopy size="16" />
|
|
2314
|
+
{{ label() }}
|
|
2315
|
+
</button>
|
|
2316
|
+
|
|
2317
|
+
@if (canShare()) {
|
|
2318
|
+
<button app-button type="button" variant="ghost" (click)="share()">
|
|
2319
|
+
<svg lucideShare size="16" />
|
|
2320
|
+
Compartir
|
|
2321
|
+
</button>
|
|
2322
|
+
} @else {
|
|
2323
|
+
<button app-button type="button" variant="ghost" (click)="shareWhatsApp()">
|
|
2324
|
+
<svg lucideShare size="16" />
|
|
2325
|
+
WhatsApp
|
|
2326
|
+
</button>
|
|
2327
|
+
}
|
|
2328
|
+
</div>
|
|
2329
|
+
`,
|
|
2330
|
+
isInline: true,
|
|
2331
|
+
dependencies: [
|
|
2332
|
+
{
|
|
2333
|
+
kind: "component",
|
|
2334
|
+
type: Button,
|
|
2335
|
+
selector: "button[app-button]",
|
|
2336
|
+
inputs: [
|
|
2337
|
+
"variant",
|
|
2338
|
+
"size",
|
|
2339
|
+
"type",
|
|
2340
|
+
"disabled",
|
|
2341
|
+
"loading"
|
|
2342
|
+
]
|
|
2343
|
+
},
|
|
2344
|
+
{
|
|
2345
|
+
kind: "component",
|
|
2346
|
+
type: LucideCopy,
|
|
2347
|
+
selector: "svg[lucideCopy]"
|
|
2348
|
+
},
|
|
2349
|
+
{
|
|
2350
|
+
kind: "component",
|
|
2351
|
+
type: LucideShare,
|
|
2352
|
+
selector: "svg[lucideShare]"
|
|
2353
|
+
}
|
|
2354
|
+
]
|
|
2355
|
+
});
|
|
2356
|
+
};
|
|
2357
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2358
|
+
minVersion: "12.0.0",
|
|
2359
|
+
version: "22.2.3",
|
|
2360
|
+
ngImport: i0,
|
|
2361
|
+
type: CopyShare,
|
|
2362
|
+
decorators: [{
|
|
2363
|
+
type: Component,
|
|
2364
|
+
args: [{
|
|
2365
|
+
selector: "app-copy-share",
|
|
2366
|
+
imports: [
|
|
2367
|
+
Button,
|
|
2368
|
+
LucideCopy,
|
|
2369
|
+
LucideShare
|
|
2370
|
+
],
|
|
2371
|
+
template: `
|
|
2372
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
2373
|
+
<button app-button type="button" variant="outline" (click)="copy()">
|
|
2374
|
+
<svg lucideCopy size="16" />
|
|
2375
|
+
{{ label() }}
|
|
2376
|
+
</button>
|
|
2377
|
+
|
|
2378
|
+
@if (canShare()) {
|
|
2379
|
+
<button app-button type="button" variant="ghost" (click)="share()">
|
|
2380
|
+
<svg lucideShare size="16" />
|
|
2381
|
+
Compartir
|
|
2382
|
+
</button>
|
|
2383
|
+
} @else {
|
|
2384
|
+
<button app-button type="button" variant="ghost" (click)="shareWhatsApp()">
|
|
2385
|
+
<svg lucideShare size="16" />
|
|
2386
|
+
WhatsApp
|
|
2387
|
+
</button>
|
|
2388
|
+
}
|
|
2389
|
+
</div>
|
|
2390
|
+
`
|
|
2391
|
+
}]
|
|
2392
|
+
}],
|
|
2393
|
+
propDecorators: {
|
|
2394
|
+
url: [{
|
|
2395
|
+
type: i0.Input,
|
|
2396
|
+
args: [{
|
|
2397
|
+
isSignal: true,
|
|
2398
|
+
alias: "url",
|
|
2399
|
+
required: true
|
|
2400
|
+
}]
|
|
2401
|
+
}],
|
|
2402
|
+
label: [{
|
|
2403
|
+
type: i0.Input,
|
|
2404
|
+
args: [{
|
|
2405
|
+
isSignal: true,
|
|
2406
|
+
alias: "label",
|
|
2407
|
+
required: false
|
|
2408
|
+
}]
|
|
2409
|
+
}],
|
|
2410
|
+
shareText: [{
|
|
2411
|
+
type: i0.Input,
|
|
2412
|
+
args: [{
|
|
2413
|
+
isSignal: true,
|
|
2414
|
+
alias: "shareText",
|
|
2415
|
+
required: false
|
|
2416
|
+
}]
|
|
2417
|
+
}]
|
|
2418
|
+
}
|
|
2419
|
+
});
|
|
2420
|
+
let titleCounter = 0;
|
|
2421
|
+
var Modal = class Modal {
|
|
2422
|
+
open = input(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
2423
|
+
title = input.required(...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
2424
|
+
closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
|
|
2425
|
+
close = output();
|
|
2426
|
+
titleId = `modal-title-${++titleCounter}`;
|
|
2427
|
+
dialogRef = viewChild("dialog", ...ngDevMode ? [{ debugName: "dialogRef" }] : /* istanbul ignore next */ []);
|
|
2428
|
+
lastFocused = null;
|
|
2429
|
+
focusEffect = afterRenderEffect(() => {
|
|
2430
|
+
const open = this.open();
|
|
2431
|
+
const dialog = this.dialogRef();
|
|
2432
|
+
if (open && dialog) {
|
|
2433
|
+
const active = document.activeElement;
|
|
2434
|
+
if (active !== dialog.nativeElement && this.lastFocused === null) this.lastFocused = active instanceof HTMLElement ? active : null;
|
|
2435
|
+
dialog.nativeElement.focus();
|
|
2436
|
+
} else if (!open && this.lastFocused !== null) {
|
|
2437
|
+
this.lastFocused.focus();
|
|
2438
|
+
this.lastFocused = null;
|
|
2439
|
+
}
|
|
2440
|
+
}, ...ngDevMode ? [{ debugName: "focusEffect" }] : /* istanbul ignore next */ []);
|
|
2441
|
+
ngOnDestroy() {
|
|
2442
|
+
if (this.lastFocused?.isConnected) this.lastFocused.focus();
|
|
2443
|
+
this.lastFocused = null;
|
|
2444
|
+
}
|
|
2445
|
+
onEscape() {
|
|
2446
|
+
if (this.open()) this.close.emit();
|
|
2447
|
+
}
|
|
2448
|
+
onTab(rawEvent) {
|
|
2449
|
+
const event = rawEvent;
|
|
2450
|
+
const dialog = this.dialogRef()?.nativeElement;
|
|
2451
|
+
if (!this.open() || !dialog) return;
|
|
2452
|
+
const focusable = Array.from(dialog.querySelectorAll(FOCUSABLE)).filter((element) => isVisibleNow(element));
|
|
2453
|
+
if (focusable.length === 0) {
|
|
2454
|
+
event.preventDefault();
|
|
2455
|
+
dialog.focus();
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
const first = focusable[0];
|
|
2459
|
+
const last = focusable[focusable.length - 1];
|
|
2460
|
+
const active = document.activeElement;
|
|
2461
|
+
if (event.shiftKey && (active === first || active === dialog)) {
|
|
2462
|
+
event.preventDefault();
|
|
2463
|
+
last.focus();
|
|
2464
|
+
} else if (!event.shiftKey && active === last) {
|
|
2465
|
+
event.preventDefault();
|
|
2466
|
+
first.focus();
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
2470
|
+
minVersion: "12.0.0",
|
|
2471
|
+
version: "22.2.3",
|
|
2472
|
+
ngImport: i0,
|
|
2473
|
+
type: Modal,
|
|
2474
|
+
deps: [],
|
|
2475
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2476
|
+
});
|
|
2477
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2478
|
+
minVersion: "17.0.0",
|
|
2479
|
+
version: "22.2.3",
|
|
2480
|
+
type: Modal,
|
|
2481
|
+
isStandalone: true,
|
|
2482
|
+
selector: "app-modal",
|
|
2483
|
+
inputs: {
|
|
2484
|
+
open: {
|
|
2485
|
+
classPropertyName: "open",
|
|
2486
|
+
publicName: "open",
|
|
2487
|
+
isSignal: true,
|
|
2488
|
+
isRequired: false,
|
|
2489
|
+
transformFunction: null
|
|
2490
|
+
},
|
|
2491
|
+
title: {
|
|
2492
|
+
classPropertyName: "title",
|
|
2493
|
+
publicName: "title",
|
|
2494
|
+
isSignal: true,
|
|
2495
|
+
isRequired: true,
|
|
2496
|
+
transformFunction: null
|
|
2497
|
+
},
|
|
2498
|
+
closeLabel: {
|
|
2499
|
+
classPropertyName: "closeLabel",
|
|
2500
|
+
publicName: "closeLabel",
|
|
2501
|
+
isSignal: true,
|
|
2502
|
+
isRequired: false,
|
|
2503
|
+
transformFunction: null
|
|
2504
|
+
}
|
|
2505
|
+
},
|
|
2506
|
+
outputs: { close: "close" },
|
|
2507
|
+
host: { listeners: {
|
|
2508
|
+
"document:keydown.escape": "onEscape()",
|
|
2509
|
+
"document:keydown.tab": "onTab($event)",
|
|
2510
|
+
"document:keydown.shift.tab": "onTab($event)"
|
|
2511
|
+
} },
|
|
2512
|
+
viewQueries: [{
|
|
2513
|
+
propertyName: "dialogRef",
|
|
2514
|
+
first: true,
|
|
2515
|
+
predicate: ["dialog"],
|
|
2516
|
+
descendants: true,
|
|
2517
|
+
isSignal: true
|
|
2518
|
+
}],
|
|
2519
|
+
ngImport: i0,
|
|
2520
|
+
template: `
|
|
2521
|
+
@if (open()) {
|
|
2522
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
2523
|
+
<div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
|
|
2524
|
+
<div
|
|
2525
|
+
#dialog
|
|
2526
|
+
role="dialog"
|
|
2527
|
+
aria-modal="true"
|
|
2528
|
+
[attr.aria-labelledby]="titleId"
|
|
2529
|
+
tabindex="-1"
|
|
2530
|
+
class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
|
|
2531
|
+
>
|
|
2532
|
+
<div class="mb-4 flex items-start justify-between gap-4">
|
|
2533
|
+
<h2 [id]="titleId" class="font-display text-section font-semibold">
|
|
2534
|
+
{{ title() }}
|
|
2535
|
+
</h2>
|
|
2536
|
+
<button
|
|
2537
|
+
type="button"
|
|
2538
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
|
|
2539
|
+
[attr.aria-label]="closeLabel()"
|
|
2540
|
+
(click)="close.emit()"
|
|
2541
|
+
>
|
|
2542
|
+
<svg lucideX size="18" />
|
|
2543
|
+
</button>
|
|
2544
|
+
</div>
|
|
2545
|
+
<ng-content />
|
|
2546
|
+
</div>
|
|
2547
|
+
</div>
|
|
2548
|
+
}
|
|
2549
|
+
`,
|
|
2550
|
+
isInline: true,
|
|
2551
|
+
dependencies: [{
|
|
2552
|
+
kind: "component",
|
|
2553
|
+
type: LucideX,
|
|
2554
|
+
selector: "svg[lucideX]"
|
|
2555
|
+
}]
|
|
2556
|
+
});
|
|
2557
|
+
};
|
|
2558
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2559
|
+
minVersion: "12.0.0",
|
|
2560
|
+
version: "22.2.3",
|
|
2561
|
+
ngImport: i0,
|
|
2562
|
+
type: Modal,
|
|
2563
|
+
decorators: [{
|
|
2564
|
+
type: Component,
|
|
2565
|
+
args: [{
|
|
2566
|
+
selector: "app-modal",
|
|
2567
|
+
imports: [LucideX],
|
|
2568
|
+
host: {
|
|
2569
|
+
"(document:keydown.escape)": "onEscape()",
|
|
2570
|
+
"(document:keydown.tab)": "onTab($event)",
|
|
2571
|
+
"(document:keydown.shift.tab)": "onTab($event)"
|
|
2572
|
+
},
|
|
2573
|
+
template: `
|
|
2574
|
+
@if (open()) {
|
|
2575
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
2576
|
+
<div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
|
|
2577
|
+
<div
|
|
2578
|
+
#dialog
|
|
2579
|
+
role="dialog"
|
|
2580
|
+
aria-modal="true"
|
|
2581
|
+
[attr.aria-labelledby]="titleId"
|
|
2582
|
+
tabindex="-1"
|
|
2583
|
+
class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
|
|
2584
|
+
>
|
|
2585
|
+
<div class="mb-4 flex items-start justify-between gap-4">
|
|
2586
|
+
<h2 [id]="titleId" class="font-display text-section font-semibold">
|
|
2587
|
+
{{ title() }}
|
|
2588
|
+
</h2>
|
|
2589
|
+
<button
|
|
2590
|
+
type="button"
|
|
2591
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
|
|
2592
|
+
[attr.aria-label]="closeLabel()"
|
|
2593
|
+
(click)="close.emit()"
|
|
2594
|
+
>
|
|
2595
|
+
<svg lucideX size="18" />
|
|
2596
|
+
</button>
|
|
2597
|
+
</div>
|
|
2598
|
+
<ng-content />
|
|
2599
|
+
</div>
|
|
2600
|
+
</div>
|
|
2601
|
+
}
|
|
2602
|
+
`
|
|
2603
|
+
}]
|
|
2604
|
+
}],
|
|
2605
|
+
propDecorators: {
|
|
2606
|
+
open: [{
|
|
2607
|
+
type: i0.Input,
|
|
2608
|
+
args: [{
|
|
2609
|
+
isSignal: true,
|
|
2610
|
+
alias: "open",
|
|
2611
|
+
required: false
|
|
2612
|
+
}]
|
|
2613
|
+
}],
|
|
2614
|
+
title: [{
|
|
2615
|
+
type: i0.Input,
|
|
2616
|
+
args: [{
|
|
2617
|
+
isSignal: true,
|
|
2618
|
+
alias: "title",
|
|
2619
|
+
required: true
|
|
2620
|
+
}]
|
|
2621
|
+
}],
|
|
2622
|
+
closeLabel: [{
|
|
2623
|
+
type: i0.Input,
|
|
2624
|
+
args: [{
|
|
2625
|
+
isSignal: true,
|
|
2626
|
+
alias: "closeLabel",
|
|
2627
|
+
required: false
|
|
2628
|
+
}]
|
|
2629
|
+
}],
|
|
2630
|
+
close: [{
|
|
2631
|
+
type: i0.Output,
|
|
2632
|
+
args: ["close"]
|
|
2633
|
+
}],
|
|
2634
|
+
dialogRef: [{
|
|
2635
|
+
type: i0.ViewChild,
|
|
2636
|
+
args: ["dialog", { isSignal: true }]
|
|
2637
|
+
}]
|
|
2638
|
+
}
|
|
2639
|
+
});
|
|
2640
|
+
var PostCreateModal = class PostCreateModal {
|
|
2641
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
2642
|
+
config = input.required(...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
|
|
2643
|
+
record = input.required(...ngDevMode ? [{ debugName: "record" }] : /* istanbul ignore next */ []);
|
|
2644
|
+
configValues = input({}, ...ngDevMode ? [{ debugName: "configValues" }] : /* istanbul ignore next */ []);
|
|
2645
|
+
closed = output();
|
|
2646
|
+
configService = inject(ConfigService);
|
|
2647
|
+
constructor() {
|
|
2648
|
+
this.configService.ensureLoaded();
|
|
2649
|
+
}
|
|
2650
|
+
context = computed(() => ({
|
|
2651
|
+
...this.configService.buildInterpolationContext(this.record()),
|
|
2652
|
+
...this.configValues()
|
|
2653
|
+
}), ...ngDevMode ? [{ debugName: "context" }] : /* istanbul ignore next */ []);
|
|
2654
|
+
title = computed(() => {
|
|
2655
|
+
const raw = this.config().title || "¡Registro Creado con Éxito!";
|
|
2656
|
+
return interpolateTemplate(raw, this.context());
|
|
2657
|
+
}, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
2658
|
+
description = computed(() => {
|
|
2659
|
+
const raw = this.config().description || "El registro fue creado correctamente y ya se encuentra guardado en el sistema.";
|
|
2660
|
+
return interpolateTemplate(raw, this.context());
|
|
2661
|
+
}, ...ngDevMode ? [{ debugName: "description" }] : /* istanbul ignore next */ []);
|
|
2662
|
+
summaryItems = computed(() => {
|
|
2663
|
+
const fields = this.config().summaryFields || [];
|
|
2664
|
+
const rec = this.record();
|
|
2665
|
+
const schemaFields = this.schema().fields || [];
|
|
2666
|
+
return fields.map((key) => {
|
|
2667
|
+
const label = schemaFields.find((f) => f.key === key)?.label || key.replace(/_/g, " ");
|
|
2668
|
+
const rawVal = rec[key];
|
|
2669
|
+
return {
|
|
2670
|
+
key,
|
|
2671
|
+
label,
|
|
2672
|
+
value: rawVal === null || rawVal === void 0 || rawVal === "" ? "—" : String(rawVal)
|
|
2673
|
+
};
|
|
2674
|
+
});
|
|
2675
|
+
}, ...ngDevMode ? [{ debugName: "summaryItems" }] : /* istanbul ignore next */ []);
|
|
2676
|
+
copyShareAction = computed(() => (this.config().actions || []).find(isShareAction) ?? null, ...ngDevMode ? [{ debugName: "copyShareAction" }] : /* istanbul ignore next */ []);
|
|
2677
|
+
shareOf(action) {
|
|
2678
|
+
return resolveShare(action, this.context());
|
|
2679
|
+
}
|
|
2680
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
2681
|
+
minVersion: "12.0.0",
|
|
2682
|
+
version: "22.2.3",
|
|
2683
|
+
ngImport: i0,
|
|
2684
|
+
type: PostCreateModal,
|
|
2685
|
+
deps: [],
|
|
2686
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2687
|
+
});
|
|
2688
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2689
|
+
minVersion: "17.0.0",
|
|
2690
|
+
version: "22.2.3",
|
|
2691
|
+
type: PostCreateModal,
|
|
2692
|
+
isStandalone: true,
|
|
2693
|
+
selector: "app-post-create-modal",
|
|
2694
|
+
inputs: {
|
|
2695
|
+
schema: {
|
|
2696
|
+
classPropertyName: "schema",
|
|
2697
|
+
publicName: "schema",
|
|
2698
|
+
isSignal: true,
|
|
2699
|
+
isRequired: true,
|
|
2700
|
+
transformFunction: null
|
|
2701
|
+
},
|
|
2702
|
+
config: {
|
|
2703
|
+
classPropertyName: "config",
|
|
2704
|
+
publicName: "config",
|
|
2705
|
+
isSignal: true,
|
|
2706
|
+
isRequired: true,
|
|
2707
|
+
transformFunction: null
|
|
2708
|
+
},
|
|
2709
|
+
record: {
|
|
2710
|
+
classPropertyName: "record",
|
|
2711
|
+
publicName: "record",
|
|
2712
|
+
isSignal: true,
|
|
2713
|
+
isRequired: true,
|
|
2714
|
+
transformFunction: null
|
|
2715
|
+
},
|
|
2716
|
+
configValues: {
|
|
2717
|
+
classPropertyName: "configValues",
|
|
2718
|
+
publicName: "configValues",
|
|
2719
|
+
isSignal: true,
|
|
2720
|
+
isRequired: false,
|
|
2721
|
+
transformFunction: null
|
|
2722
|
+
}
|
|
2723
|
+
},
|
|
2724
|
+
outputs: { closed: "closed" },
|
|
2725
|
+
ngImport: i0,
|
|
2726
|
+
template: `
|
|
2727
|
+
<app-modal [open]="true" [title]="title()" (close)="closed.emit()">
|
|
2728
|
+
<div class="flex flex-col gap-4">
|
|
2729
|
+
<!-- Encabezado con icono de éxito -->
|
|
2730
|
+
<div class="flex items-center gap-3">
|
|
2731
|
+
<div
|
|
2732
|
+
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
|
|
2733
|
+
>
|
|
2734
|
+
<svg lucideCheckCircle2 size="22" />
|
|
2735
|
+
</div>
|
|
2736
|
+
<div>
|
|
2737
|
+
<p class="text-sm text-neutral-light">{{ description() }}</p>
|
|
2738
|
+
</div>
|
|
2739
|
+
</div>
|
|
2740
|
+
|
|
2741
|
+
<!-- Tarjeta de campos de resumen -->
|
|
2742
|
+
@if (summaryItems().length > 0) {
|
|
2743
|
+
<div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
|
|
2744
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
|
|
2745
|
+
@for (item of summaryItems(); track item.key) {
|
|
2746
|
+
<div class="flex flex-col">
|
|
2747
|
+
<dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
|
|
2748
|
+
<dd class="text-sm font-semibold text-neutral-light truncate">
|
|
2749
|
+
{{ item.value }}
|
|
2750
|
+
</dd>
|
|
2751
|
+
</div>
|
|
2752
|
+
}
|
|
2753
|
+
</dl>
|
|
2754
|
+
</div>
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
<!-- Acciones declaradas (ej. CopyShare) -->
|
|
2758
|
+
@if (copyShareAction(); as action) {
|
|
2759
|
+
<div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
|
|
2760
|
+
<p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
|
|
2761
|
+
@let share = shareOf(action);
|
|
2762
|
+
<app-copy-share
|
|
2763
|
+
[url]="share.url"
|
|
2764
|
+
[shareText]="share.shareText"
|
|
2765
|
+
[label]="action.label"
|
|
2766
|
+
/>
|
|
2767
|
+
</div>
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
<!-- Botón de cierre primario -->
|
|
2771
|
+
<div class="mt-2 flex justify-end">
|
|
2772
|
+
<button app-button type="button" variant="primary" (click)="closed.emit()">
|
|
2773
|
+
Continuar
|
|
2774
|
+
</button>
|
|
2775
|
+
</div>
|
|
2776
|
+
</div>
|
|
2777
|
+
</app-modal>
|
|
2778
|
+
`,
|
|
2779
|
+
isInline: true,
|
|
2780
|
+
dependencies: [
|
|
2781
|
+
{
|
|
2782
|
+
kind: "component",
|
|
2783
|
+
type: Modal,
|
|
2784
|
+
selector: "app-modal",
|
|
2785
|
+
inputs: [
|
|
2786
|
+
"open",
|
|
2787
|
+
"title",
|
|
2788
|
+
"closeLabel"
|
|
2789
|
+
],
|
|
2790
|
+
outputs: ["close"]
|
|
2791
|
+
},
|
|
2792
|
+
{
|
|
2793
|
+
kind: "component",
|
|
2794
|
+
type: Button,
|
|
2795
|
+
selector: "button[app-button]",
|
|
2796
|
+
inputs: [
|
|
2797
|
+
"variant",
|
|
2798
|
+
"size",
|
|
2799
|
+
"type",
|
|
2800
|
+
"disabled",
|
|
2801
|
+
"loading"
|
|
2802
|
+
]
|
|
2803
|
+
},
|
|
2804
|
+
{
|
|
2805
|
+
kind: "component",
|
|
2806
|
+
type: CopyShare,
|
|
2807
|
+
selector: "app-copy-share",
|
|
2808
|
+
inputs: [
|
|
2809
|
+
"url",
|
|
2810
|
+
"label",
|
|
2811
|
+
"shareText"
|
|
2812
|
+
]
|
|
2813
|
+
},
|
|
2814
|
+
{
|
|
2815
|
+
kind: "component",
|
|
2816
|
+
type: LucideCheckCircle2,
|
|
2817
|
+
selector: "svg[lucideCircleCheck], svg[lucideCheckCircle2]"
|
|
2818
|
+
}
|
|
2819
|
+
]
|
|
2820
|
+
});
|
|
2821
|
+
};
|
|
2822
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2823
|
+
minVersion: "12.0.0",
|
|
2824
|
+
version: "22.2.3",
|
|
2825
|
+
ngImport: i0,
|
|
2826
|
+
type: PostCreateModal,
|
|
2827
|
+
decorators: [{
|
|
2828
|
+
type: Component,
|
|
2829
|
+
args: [{
|
|
2830
|
+
selector: "app-post-create-modal",
|
|
2831
|
+
imports: [
|
|
2832
|
+
Modal,
|
|
2833
|
+
Button,
|
|
2834
|
+
CopyShare,
|
|
2835
|
+
LucideCheckCircle2
|
|
2836
|
+
],
|
|
2837
|
+
template: `
|
|
2838
|
+
<app-modal [open]="true" [title]="title()" (close)="closed.emit()">
|
|
2839
|
+
<div class="flex flex-col gap-4">
|
|
2840
|
+
<!-- Encabezado con icono de éxito -->
|
|
2841
|
+
<div class="flex items-center gap-3">
|
|
2842
|
+
<div
|
|
2843
|
+
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
|
|
2844
|
+
>
|
|
2845
|
+
<svg lucideCheckCircle2 size="22" />
|
|
2846
|
+
</div>
|
|
2847
|
+
<div>
|
|
2848
|
+
<p class="text-sm text-neutral-light">{{ description() }}</p>
|
|
2849
|
+
</div>
|
|
2850
|
+
</div>
|
|
2851
|
+
|
|
2852
|
+
<!-- Tarjeta de campos de resumen -->
|
|
2853
|
+
@if (summaryItems().length > 0) {
|
|
2854
|
+
<div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
|
|
2855
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
|
|
2856
|
+
@for (item of summaryItems(); track item.key) {
|
|
2857
|
+
<div class="flex flex-col">
|
|
2858
|
+
<dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
|
|
2859
|
+
<dd class="text-sm font-semibold text-neutral-light truncate">
|
|
2860
|
+
{{ item.value }}
|
|
2861
|
+
</dd>
|
|
2862
|
+
</div>
|
|
2863
|
+
}
|
|
2864
|
+
</dl>
|
|
2865
|
+
</div>
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
<!-- Acciones declaradas (ej. CopyShare) -->
|
|
2869
|
+
@if (copyShareAction(); as action) {
|
|
2870
|
+
<div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
|
|
2871
|
+
<p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
|
|
2872
|
+
@let share = shareOf(action);
|
|
2873
|
+
<app-copy-share
|
|
2874
|
+
[url]="share.url"
|
|
2875
|
+
[shareText]="share.shareText"
|
|
2876
|
+
[label]="action.label"
|
|
2877
|
+
/>
|
|
2878
|
+
</div>
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
<!-- Botón de cierre primario -->
|
|
2882
|
+
<div class="mt-2 flex justify-end">
|
|
2883
|
+
<button app-button type="button" variant="primary" (click)="closed.emit()">
|
|
2884
|
+
Continuar
|
|
2885
|
+
</button>
|
|
2886
|
+
</div>
|
|
2887
|
+
</div>
|
|
2888
|
+
</app-modal>
|
|
2889
|
+
`
|
|
2890
|
+
}]
|
|
2891
|
+
}],
|
|
2892
|
+
ctorParameters: () => [],
|
|
2893
|
+
propDecorators: {
|
|
2894
|
+
schema: [{
|
|
2895
|
+
type: i0.Input,
|
|
2896
|
+
args: [{
|
|
2897
|
+
isSignal: true,
|
|
2898
|
+
alias: "schema",
|
|
2899
|
+
required: true
|
|
2900
|
+
}]
|
|
2901
|
+
}],
|
|
2902
|
+
config: [{
|
|
2903
|
+
type: i0.Input,
|
|
2904
|
+
args: [{
|
|
2905
|
+
isSignal: true,
|
|
2906
|
+
alias: "config",
|
|
2907
|
+
required: true
|
|
2908
|
+
}]
|
|
2909
|
+
}],
|
|
2910
|
+
record: [{
|
|
2911
|
+
type: i0.Input,
|
|
2912
|
+
args: [{
|
|
2913
|
+
isSignal: true,
|
|
2914
|
+
alias: "record",
|
|
2915
|
+
required: true
|
|
2916
|
+
}]
|
|
2917
|
+
}],
|
|
2918
|
+
configValues: [{
|
|
2919
|
+
type: i0.Input,
|
|
2920
|
+
args: [{
|
|
2921
|
+
isSignal: true,
|
|
2922
|
+
alias: "configValues",
|
|
2923
|
+
required: false
|
|
2924
|
+
}]
|
|
2925
|
+
}],
|
|
2926
|
+
closed: [{
|
|
2927
|
+
type: i0.Output,
|
|
2928
|
+
args: ["closed"]
|
|
2929
|
+
}]
|
|
2930
|
+
}
|
|
2931
|
+
});
|
|
2932
|
+
var form_view_exports = /* @__PURE__ */ __exportAll({ FormView: () => FormView });
|
|
2933
|
+
const TAB_ACTIVE = "border-b-2 border-primary px-3 py-2 text-sm font-medium text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
|
2934
|
+
const TAB_INACTIVE = "border-b-2 border-transparent px-3 py-2 text-sm text-neutral hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
|
2935
|
+
const DRAFT_DEBOUNCE_MS = 800;
|
|
2936
|
+
function readTimestamp(record) {
|
|
2937
|
+
if (!record) return;
|
|
2938
|
+
for (const key of [
|
|
2939
|
+
"updated_at",
|
|
2940
|
+
"updatedAt",
|
|
2941
|
+
"updated"
|
|
2942
|
+
]) {
|
|
2943
|
+
const raw = record[key];
|
|
2944
|
+
if (typeof raw === "number") return raw < 0xe8d4a51000 ? raw * 1e3 : raw;
|
|
2945
|
+
if (typeof raw === "string") {
|
|
2946
|
+
const parsed = Date.parse(raw);
|
|
2947
|
+
if (!Number.isNaN(parsed)) return parsed;
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
}
|
|
2951
|
+
var FormView = class FormView {
|
|
2952
|
+
relationDrawer = inject(RelationDrawerService);
|
|
2953
|
+
route = inject(ActivatedRoute);
|
|
2954
|
+
router = inject(Router);
|
|
2955
|
+
nav = inject(ViewNavigationService);
|
|
2956
|
+
api = inject(ApiService);
|
|
2957
|
+
auth = inject(AuthService);
|
|
2958
|
+
notifications = inject(NotificationService);
|
|
2959
|
+
errorMapper = inject(ErrorMapperService);
|
|
2960
|
+
injector = inject(Injector);
|
|
2961
|
+
appRef = inject(ApplicationRef);
|
|
2962
|
+
params = toSignal(this.route.paramMap, { requireSync: true });
|
|
2963
|
+
routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
|
|
2964
|
+
schema = computed(() => {
|
|
2965
|
+
const resourceId = this.routeData()["resourceId"] ?? this.params().get("id");
|
|
2966
|
+
return getSchema(typeof resourceId === "string" ? resourceId : "");
|
|
2967
|
+
}, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
2968
|
+
resourceId = computed(() => this.schema()?.id ?? "", ...ngDevMode ? [{ debugName: "resourceId" }] : /* istanbul ignore next */ []);
|
|
2969
|
+
createdWithSummary = signal(null, ...ngDevMode ? [{ debugName: "createdWithSummary" }] : /* istanbul ignore next */ []);
|
|
2970
|
+
mode = computed(() => {
|
|
2971
|
+
const schema = this.schema();
|
|
2972
|
+
if (!schema) return "single";
|
|
2973
|
+
if (this.params().get("key") !== null) return "edit";
|
|
2974
|
+
return schema.kind === "singleton" ? "single" : "create";
|
|
2975
|
+
}, ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
|
|
2976
|
+
heading = computed(() => {
|
|
2977
|
+
const schema = this.schema();
|
|
2978
|
+
if (!schema) return "";
|
|
2979
|
+
switch (this.mode()) {
|
|
2980
|
+
case "create": return `Nueva ${schema.label}`;
|
|
2981
|
+
case "edit": return `Editar ${schema.label}`;
|
|
2982
|
+
case "single": return schema.labelPlural;
|
|
2983
|
+
}
|
|
2984
|
+
}, ...ngDevMode ? [{ debugName: "heading" }] : /* istanbul ignore next */ []);
|
|
2985
|
+
status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
2986
|
+
errorCarga = void 0;
|
|
2987
|
+
errorDeCarga() {
|
|
2988
|
+
return this.status() === "error" ? this.errorCarga : void 0;
|
|
2989
|
+
}
|
|
2990
|
+
saving = signal(false, ...ngDevMode ? [{ debugName: "saving" }] : /* istanbul ignore next */ []);
|
|
2991
|
+
serverErrors = signal({ fields: {} }, ...ngDevMode ? [{ debugName: "serverErrors" }] : /* istanbul ignore next */ []);
|
|
2992
|
+
activeSection = signal(null, ...ngDevMode ? [{ debugName: "activeSection" }] : /* istanbul ignore next */ []);
|
|
2993
|
+
attemptedSteps = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "attemptedSteps" }] : /* istanbul ignore next */ []);
|
|
2994
|
+
openSections = signal({}, ...ngDevMode ? [{ debugName: "openSections" }] : /* istanbul ignore next */ []);
|
|
2995
|
+
formTree = signal(null, ...ngDevMode ? [{ debugName: "formTree" }] : /* istanbul ignore next */ []);
|
|
2996
|
+
loadEpoch = 0;
|
|
2997
|
+
draftOffer = signal(null, ...ngDevMode ? [{ debugName: "draftOffer" }] : /* istanbul ignore next */ []);
|
|
2998
|
+
loadedSnapshot = "";
|
|
2999
|
+
loadedRecord = null;
|
|
3000
|
+
dependsOnSnapshot = /* @__PURE__ */ new Map();
|
|
3001
|
+
dependsOnTree = null;
|
|
3002
|
+
submitted = signal(false, ...ngDevMode ? [{ debugName: "submitted" }] : /* istanbul ignore next */ []);
|
|
3003
|
+
constructor() {
|
|
3004
|
+
this.route.paramMap.subscribe(() => this.rebuild());
|
|
3005
|
+
effect((onCleanup) => {
|
|
3006
|
+
const tree = this.formTree();
|
|
3007
|
+
if (!tree) return;
|
|
3008
|
+
const value = tree().value();
|
|
3009
|
+
if (this.status() !== "ready" || JSON.stringify(value) === this.loadedSnapshot) return;
|
|
3010
|
+
const timer = setTimeout(() => this.saveDraft(value), DRAFT_DEBOUNCE_MS);
|
|
3011
|
+
onCleanup(() => clearTimeout(timer));
|
|
3012
|
+
});
|
|
3013
|
+
effect(() => {
|
|
3014
|
+
const tree = this.formTree();
|
|
3015
|
+
const schema = this.schema();
|
|
3016
|
+
if (tree === null || !schema) {
|
|
3017
|
+
this.dependsOnSnapshot.clear();
|
|
3018
|
+
this.dependsOnTree = null;
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
if (this.dependsOnTree !== tree) {
|
|
3022
|
+
this.dependsOnTree = tree;
|
|
3023
|
+
this.dependsOnSnapshot.clear();
|
|
3024
|
+
}
|
|
3025
|
+
for (const link of collectDependsOn(schema.fields)) {
|
|
3026
|
+
let container = tree;
|
|
3027
|
+
for (const key of link.container) container = groupTree(container, key);
|
|
3028
|
+
const parentValue = JSON.stringify(childTree(container, link.parentKey)().value() ?? null);
|
|
3029
|
+
const previous = this.dependsOnSnapshot.get(link.id);
|
|
3030
|
+
if (previous !== void 0 && previous !== parentValue) childTree(container, link.childKey)().value.set(link.resetValue);
|
|
3031
|
+
this.dependsOnSnapshot.set(link.id, parentValue);
|
|
3032
|
+
}
|
|
3033
|
+
});
|
|
3034
|
+
}
|
|
3035
|
+
layoutMode = computed(() => {
|
|
3036
|
+
const mode = this.schema()?.layout?.mode ?? "sections";
|
|
3037
|
+
if (!isSupported("layout", mode)) throw new Error(`No existe un descriptor para el layout "${mode}".`);
|
|
3038
|
+
return mode;
|
|
3039
|
+
}, ...ngDevMode ? [{ debugName: "layoutMode" }] : /* istanbul ignore next */ []);
|
|
3040
|
+
layoutColumns = computed(() => this.schema()?.layout?.columns ?? 2, ...ngDevMode ? [{ debugName: "layoutColumns" }] : /* istanbul ignore next */ []);
|
|
3041
|
+
renderedFields = computed(() => (this.schema()?.fields ?? []).filter((field) => isSupported("field", field.type)), ...ngDevMode ? [{ debugName: "renderedFields" }] : /* istanbul ignore next */ []);
|
|
3042
|
+
summaryErrors = computed(() => {
|
|
3043
|
+
if (!this.submitted()) return [];
|
|
3044
|
+
const errors = this.collectErrors();
|
|
3045
|
+
if (this.layoutMode() !== "steps") return errors;
|
|
3046
|
+
const attempted = this.attemptedSteps();
|
|
3047
|
+
return errors.filter((entry) => !entry.section || attempted.has(entry.section));
|
|
3048
|
+
}, ...ngDevMode ? [{ debugName: "summaryErrors" }] : /* istanbul ignore next */ []);
|
|
3049
|
+
sectionViews = computed(() => {
|
|
3050
|
+
const layout = this.schema()?.layout;
|
|
3051
|
+
if (!layout) return [];
|
|
3052
|
+
const rendered = this.renderedFields();
|
|
3053
|
+
return (layout.sections ?? []).map((section) => ({
|
|
3054
|
+
id: section.id,
|
|
3055
|
+
label: section.label,
|
|
3056
|
+
fields: rendered.filter((field) => field.section === section.id)
|
|
3057
|
+
})).filter((view) => view.fields.length > 0);
|
|
3058
|
+
}, ...ngDevMode ? [{ debugName: "sectionViews" }] : /* istanbul ignore next */ []);
|
|
3059
|
+
unsectionedFields = computed(() => {
|
|
3060
|
+
const ids = new Set(this.sectionViews().map((view) => view.id));
|
|
3061
|
+
return this.renderedFields().filter((field) => field.section === void 0 || !ids.has(field.section));
|
|
3062
|
+
}, ...ngDevMode ? [{ debugName: "unsectionedFields" }] : /* istanbul ignore next */ []);
|
|
3063
|
+
visibleSections = computed(() => {
|
|
3064
|
+
const sections = this.sectionViews();
|
|
3065
|
+
if (this.layoutMode() !== "tabs" && this.layoutMode() !== "steps") return sections;
|
|
3066
|
+
const active = sections.find((section) => section.id === this.activeSection()) ?? sections[0];
|
|
3067
|
+
return active ? [active] : [];
|
|
3068
|
+
}, ...ngDevMode ? [{ debugName: "visibleSections" }] : /* istanbul ignore next */ []);
|
|
3069
|
+
dirty = computed(() => this.formTree()?.().dirty() ?? false, ...ngDevMode ? [{ debugName: "dirty" }] : /* istanbul ignore next */ []);
|
|
3070
|
+
exists = computed(() => this.mode() !== "create", ...ngDevMode ? [{ debugName: "exists" }] : /* istanbul ignore next */ []);
|
|
3071
|
+
canSave = computed(() => {
|
|
3072
|
+
const schema = this.schema();
|
|
3073
|
+
if (!schema) return false;
|
|
3074
|
+
if (this.layoutMode() === "steps" && !this.isLastStep()) return false;
|
|
3075
|
+
const operation = this.mode() === "create" ? "create" : "update";
|
|
3076
|
+
return this.auth.canDoOperation(schema, operation);
|
|
3077
|
+
}, ...ngDevMode ? [{ debugName: "canSave" }] : /* istanbul ignore next */ []);
|
|
3078
|
+
canDelete = computed(() => {
|
|
3079
|
+
const schema = this.schema();
|
|
3080
|
+
return this.mode() === "edit" && schema && this.auth.canDoOperation(schema, "remove");
|
|
3081
|
+
}, ...ngDevMode ? [{ debugName: "canDelete" }] : /* istanbul ignore next */ []);
|
|
3082
|
+
rebuild() {
|
|
3083
|
+
const schema = this.schema();
|
|
3084
|
+
this.serverErrors.set({ fields: {} });
|
|
3085
|
+
this.openSections.set({});
|
|
3086
|
+
this.activeSection.set(null);
|
|
3087
|
+
this.attemptedSteps.set(/* @__PURE__ */ new Set());
|
|
3088
|
+
this.submitted.set(false);
|
|
3089
|
+
if (!schema) {
|
|
3090
|
+
this.formTree.set(null);
|
|
3091
|
+
this.status.set("error");
|
|
3092
|
+
return;
|
|
3093
|
+
}
|
|
3094
|
+
const model = signal(seedModel(schema), ...ngDevMode ? [{ debugName: "model" }] : /* istanbul ignore next */ []);
|
|
3095
|
+
const schemaFn = buildFormSchema(schema, this.api, this.recordKey() ?? void 0);
|
|
3096
|
+
this.formTree.set(form(model, schemaFn, { injector: this.injector }));
|
|
3097
|
+
this.activeSection.set(this.sectionViews()[0]?.id ?? null);
|
|
3098
|
+
const tree = this.formTree();
|
|
3099
|
+
this.loadedSnapshot = JSON.stringify(tree ? tree().value() : {});
|
|
3100
|
+
this.loadedRecord = null;
|
|
3101
|
+
this.draftOffer.set(null);
|
|
3102
|
+
if (this.mode() === "create") {
|
|
3103
|
+
this.status.set("ready");
|
|
3104
|
+
this.offerDraft(null);
|
|
3105
|
+
return;
|
|
3106
|
+
}
|
|
3107
|
+
this.status.set("loading");
|
|
3108
|
+
this.load();
|
|
3109
|
+
}
|
|
3110
|
+
async load() {
|
|
3111
|
+
const schema = this.schema();
|
|
3112
|
+
const tree = this.formTree();
|
|
3113
|
+
if (!schema || !tree) return;
|
|
3114
|
+
const epoch = ++this.loadEpoch;
|
|
3115
|
+
try {
|
|
3116
|
+
const record = await firstValueFrom(this.api.get(schema.endpoint, this.recordKey() ?? void 0));
|
|
3117
|
+
if (epoch !== this.loadEpoch) return;
|
|
3118
|
+
this.applyRecord(tree, schema, record ?? {});
|
|
3119
|
+
this.loadedRecord = record ?? {};
|
|
3120
|
+
this.errorCarga = void 0;
|
|
3121
|
+
this.status.set("ready");
|
|
3122
|
+
this.offerDraft(record ?? null);
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
if (epoch !== this.loadEpoch) return;
|
|
3125
|
+
this.errorCarga = error;
|
|
3126
|
+
this.status.set("error");
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
applyRecord(tree, schema, record) {
|
|
3130
|
+
tree().value.set(mergeRecord(schema.fields, record));
|
|
3131
|
+
this.loadedSnapshot = JSON.stringify(tree().value());
|
|
3132
|
+
}
|
|
3133
|
+
draftStorageKey() {
|
|
3134
|
+
return `draft:${this.resourceId()}:${this.recordKey() ?? "nuevo"}`;
|
|
3135
|
+
}
|
|
3136
|
+
saveDraft(value) {
|
|
3137
|
+
try {
|
|
3138
|
+
localStorage.setItem(this.draftStorageKey(), JSON.stringify({
|
|
3139
|
+
savedAt: Date.now(),
|
|
3140
|
+
value
|
|
3141
|
+
}));
|
|
3142
|
+
} catch {}
|
|
3143
|
+
}
|
|
3144
|
+
offerDraft(serverRecord) {
|
|
3145
|
+
let raw = null;
|
|
3146
|
+
try {
|
|
3147
|
+
raw = localStorage.getItem(this.draftStorageKey());
|
|
3148
|
+
} catch {
|
|
3149
|
+
return;
|
|
3150
|
+
}
|
|
3151
|
+
if (!raw) return;
|
|
3152
|
+
let draft = null;
|
|
3153
|
+
try {
|
|
3154
|
+
draft = JSON.parse(raw);
|
|
3155
|
+
} catch {
|
|
3156
|
+
return;
|
|
3157
|
+
}
|
|
3158
|
+
if (!draft || typeof draft.savedAt !== "number" || !isRecord(draft.value)) {
|
|
3159
|
+
this.clearDraft();
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
const serverTs = readTimestamp(serverRecord);
|
|
3163
|
+
if (serverTs !== void 0 && serverTs >= draft.savedAt) {
|
|
3164
|
+
this.clearDraft();
|
|
3165
|
+
return;
|
|
3166
|
+
}
|
|
3167
|
+
this.draftOffer.set({
|
|
3168
|
+
savedAt: draft.savedAt,
|
|
3169
|
+
value: draft.value
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
restoreDraft() {
|
|
3173
|
+
const offer = this.draftOffer();
|
|
3174
|
+
const tree = this.formTree();
|
|
3175
|
+
const schema = this.schema();
|
|
3176
|
+
if (!offer || !tree || !schema) return;
|
|
3177
|
+
this.applyRecord(tree, schema, offer.value);
|
|
3178
|
+
this.draftOffer.set(null);
|
|
3179
|
+
this.notifications.info("Borrador restaurado.");
|
|
3180
|
+
}
|
|
3181
|
+
discardDraft() {
|
|
3182
|
+
this.clearDraft();
|
|
3183
|
+
this.draftOffer.set(null);
|
|
3184
|
+
}
|
|
3185
|
+
clearDraft() {
|
|
3186
|
+
try {
|
|
3187
|
+
localStorage.removeItem(this.draftStorageKey());
|
|
3188
|
+
} catch {}
|
|
3189
|
+
}
|
|
3190
|
+
draftAge(savedAt) {
|
|
3191
|
+
const minutes = Math.max(1, Math.round((Date.now() - savedAt) / 6e4));
|
|
3192
|
+
if (minutes < 60) return `hace ${minutes} min`;
|
|
3193
|
+
const hours = Math.round(minutes / 60);
|
|
3194
|
+
return `hace ${hours} ${hours === 1 ? "hora" : "horas"}`;
|
|
3195
|
+
}
|
|
3196
|
+
restablecer() {
|
|
3197
|
+
const tree = this.formTree();
|
|
3198
|
+
const schema = this.schema();
|
|
3199
|
+
if (!tree || !schema) return;
|
|
3200
|
+
this.applyRecord(tree, schema, this.loadedRecord ?? seedModel(schema));
|
|
3201
|
+
this.serverErrors.set({ fields: {} });
|
|
3202
|
+
this.submitted.set(false);
|
|
3203
|
+
this.attemptedSteps.set(/* @__PURE__ */ new Set());
|
|
3204
|
+
this.draftOffer.set(null);
|
|
3205
|
+
this.clearDraft();
|
|
3206
|
+
this.notifications.info("Se restauraron los valores originales.");
|
|
3207
|
+
}
|
|
3208
|
+
retry() {
|
|
3209
|
+
this.load();
|
|
3210
|
+
}
|
|
3211
|
+
volverDeError() {
|
|
3212
|
+
this.nav.back();
|
|
3213
|
+
}
|
|
3214
|
+
async onSubmit(event) {
|
|
3215
|
+
event.preventDefault();
|
|
3216
|
+
const tree = this.formTree();
|
|
3217
|
+
if (!tree || this.saving()) return;
|
|
3218
|
+
this.serverErrors.set({ fields: {} });
|
|
3219
|
+
this.saving.set(true);
|
|
3220
|
+
try {
|
|
3221
|
+
if (!await submit(tree, {
|
|
3222
|
+
action: async () => {
|
|
3223
|
+
await this.persist(tree);
|
|
3224
|
+
return null;
|
|
3225
|
+
},
|
|
3226
|
+
onInvalid: () => this.revealErrors()
|
|
3227
|
+
}) || this.hasServerErrors()) this.revealErrors();
|
|
3228
|
+
} finally {
|
|
3229
|
+
this.saving.set(false);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
async persist(tree) {
|
|
3233
|
+
const schema = this.schema();
|
|
3234
|
+
if (!schema) return;
|
|
3235
|
+
const payload = buildPayload(schema, tree);
|
|
3236
|
+
try {
|
|
3237
|
+
if (this.mode() === "create") {
|
|
3238
|
+
const created = await firstValueFrom(this.api.create(schema.endpoint, payload));
|
|
3239
|
+
tree().reset();
|
|
3240
|
+
this.clearDraft();
|
|
3241
|
+
this.draftOffer.set(null);
|
|
3242
|
+
if (schema.postCreate?.mode === "modal") {
|
|
3243
|
+
this.createdWithSummary.set(created ?? payload);
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
this.notifications.success("Registro creado.");
|
|
3247
|
+
this.router.navigate(["/", schema.id]);
|
|
3248
|
+
return;
|
|
3249
|
+
}
|
|
3250
|
+
await firstValueFrom(this.api.update(schema.endpoint, this.mode() === "edit" ? this.recordKey() ?? void 0 : void 0, payload));
|
|
3251
|
+
this.notifications.success("Registro guardado.");
|
|
3252
|
+
tree().reset();
|
|
3253
|
+
this.clearDraft();
|
|
3254
|
+
this.draftOffer.set(null);
|
|
3255
|
+
this.loadedSnapshot = JSON.stringify(tree().value());
|
|
3256
|
+
} catch (err) {
|
|
3257
|
+
this.applyServerErrors(this.errorMapper.map(err));
|
|
3258
|
+
return;
|
|
3259
|
+
}
|
|
3260
|
+
try {
|
|
3261
|
+
const record = await firstValueFrom(this.api.get(schema.endpoint, this.recordKey() ?? void 0));
|
|
3262
|
+
this.applyRecord(tree, schema, record ?? {});
|
|
3263
|
+
tree().reset();
|
|
3264
|
+
} catch {
|
|
3265
|
+
this.notifications.error("No se pudo recargar el registro.");
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
applyServerErrors(mapped) {
|
|
3269
|
+
const rendered = new Set(this.renderedFields().map((field) => field.key));
|
|
3270
|
+
const fields = {};
|
|
3271
|
+
const loose = [];
|
|
3272
|
+
if (mapped.general) loose.push(mapped.general);
|
|
3273
|
+
for (const [path, message] of Object.entries(mapped.fields)) {
|
|
3274
|
+
const root = path.split(/[.[]/)[0];
|
|
3275
|
+
if (rendered.has(root)) fields[path] = message;
|
|
3276
|
+
else loose.push(message);
|
|
3277
|
+
}
|
|
3278
|
+
this.serverErrors.set({
|
|
3279
|
+
fields,
|
|
3280
|
+
general: loose.length > 0 ? loose.join(" ") : void 0
|
|
3281
|
+
});
|
|
3282
|
+
}
|
|
3283
|
+
hasServerErrors() {
|
|
3284
|
+
const current = this.serverErrors();
|
|
3285
|
+
return current.general !== void 0 || Object.keys(current.fields).length > 0;
|
|
3286
|
+
}
|
|
3287
|
+
revealErrors() {
|
|
3288
|
+
this.submitted.set(true);
|
|
3289
|
+
const sections = this.sectionViews();
|
|
3290
|
+
if (this.layoutMode() === "sections") this.openSections.update((open) => {
|
|
3291
|
+
const next = { ...open };
|
|
3292
|
+
for (const section of sections) next[section.id] = true;
|
|
3293
|
+
return next;
|
|
3294
|
+
});
|
|
3295
|
+
const withProblem = sections.find((section) => this.sectionHasProblem(section));
|
|
3296
|
+
if (withProblem && this.layoutMode() === "tabs") this.activeSection.set(withProblem.id);
|
|
3297
|
+
if (this.layoutMode() === "steps") {
|
|
3298
|
+
const invalidSteps = sections.filter((section) => this.sectionHasProblem(section)).map((section) => section.id);
|
|
3299
|
+
this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, ...invalidSteps]));
|
|
3300
|
+
if (withProblem) this.activeSection.set(withProblem.id);
|
|
3301
|
+
}
|
|
3302
|
+
setTimeout(() => {
|
|
3303
|
+
if (this.summaryErrors().length >= 2) this.focusSummary();
|
|
3304
|
+
else this.scrollToFirstError();
|
|
3305
|
+
});
|
|
3306
|
+
}
|
|
3307
|
+
scrollToFirstError() {
|
|
3308
|
+
for (const wrap of document.querySelectorAll("[data-field]")) {
|
|
3309
|
+
if (!wrap.querySelector("p[role=\"alert\"]")) continue;
|
|
3310
|
+
wrap.scrollIntoView({
|
|
3311
|
+
behavior: "smooth",
|
|
3312
|
+
block: "center"
|
|
3313
|
+
});
|
|
3314
|
+
wrap.querySelector("input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]")?.focus({ preventScroll: true });
|
|
3315
|
+
return;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
async remove() {
|
|
3319
|
+
const schema = this.schema();
|
|
3320
|
+
const key = this.recordKey();
|
|
3321
|
+
if (!schema || this.mode() !== "edit" || !key) return;
|
|
3322
|
+
const title = this.title();
|
|
3323
|
+
if (!await this.notifications.confirm({
|
|
3324
|
+
title: title ? `Eliminar «${title}»` : `Eliminar ${schema.label.toLowerCase()}`,
|
|
3325
|
+
message: "Esta acción no se puede deshacer.",
|
|
3326
|
+
confirmLabel: "Eliminar",
|
|
3327
|
+
cancelLabel: "Cancelar"
|
|
3328
|
+
})) return;
|
|
3329
|
+
try {
|
|
3330
|
+
await firstValueFrom(this.api.remove(schema.endpoint, key));
|
|
3331
|
+
this.notifications.success("Registro eliminado.");
|
|
3332
|
+
this.back();
|
|
3333
|
+
} catch (err) {
|
|
3334
|
+
this.notifications.error(this.errorMapper.messageOf(err));
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
async canDeactivate() {
|
|
3338
|
+
if (!this.dirty()) return true;
|
|
3339
|
+
return this.notifications.confirm({
|
|
3340
|
+
title: "Cambios sin guardar",
|
|
3341
|
+
message: "Si sales ahora, se perderán los cambios realizados.",
|
|
3342
|
+
confirmLabel: "Salir sin guardar",
|
|
3343
|
+
cancelLabel: "Seguir editando"
|
|
3344
|
+
});
|
|
3345
|
+
}
|
|
3346
|
+
back() {
|
|
3347
|
+
this.router.navigate(["/", this.schema()?.id]);
|
|
3348
|
+
}
|
|
3349
|
+
onPostCreateClosed() {
|
|
3350
|
+
const schema = this.schema();
|
|
3351
|
+
this.createdWithSummary.set(null);
|
|
3352
|
+
if (schema) this.router.navigate(["/", schema.id]);
|
|
3353
|
+
}
|
|
3354
|
+
colSpan(field) {
|
|
3355
|
+
return colSpanClass(field.width);
|
|
3356
|
+
}
|
|
3357
|
+
tabId(sectionId) {
|
|
3358
|
+
return `section-tab-${sectionId}`;
|
|
3359
|
+
}
|
|
3360
|
+
onTablistKeydown(event) {
|
|
3361
|
+
const tabs = this.sectionViews();
|
|
3362
|
+
if (tabs.length < 2) return;
|
|
3363
|
+
const current = Math.max(tabs.findIndex((section) => section.id === this.activeSection()), 0);
|
|
3364
|
+
let next;
|
|
3365
|
+
switch (event.key) {
|
|
3366
|
+
case "ArrowRight":
|
|
3367
|
+
next = (current + 1) % tabs.length;
|
|
3368
|
+
break;
|
|
3369
|
+
case "ArrowLeft":
|
|
3370
|
+
next = (current - 1 + tabs.length) % tabs.length;
|
|
3371
|
+
break;
|
|
3372
|
+
case "Home":
|
|
3373
|
+
next = 0;
|
|
3374
|
+
break;
|
|
3375
|
+
case "End":
|
|
3376
|
+
next = tabs.length - 1;
|
|
3377
|
+
break;
|
|
3378
|
+
default: return;
|
|
3379
|
+
}
|
|
3380
|
+
event.preventDefault();
|
|
3381
|
+
this.selectSection(tabs[next].id);
|
|
3382
|
+
const active = this.activeSection() ?? tabs[current].id;
|
|
3383
|
+
document.getElementById(this.tabId(active))?.focus();
|
|
3384
|
+
}
|
|
3385
|
+
tabClass(section) {
|
|
3386
|
+
return section.id === this.activeSection() ? TAB_ACTIVE : TAB_INACTIVE;
|
|
3387
|
+
}
|
|
3388
|
+
isFirstStep() {
|
|
3389
|
+
return this.sectionViews().findIndex((section) => section.id === this.activeSection()) <= 0;
|
|
3390
|
+
}
|
|
3391
|
+
isLastStep() {
|
|
3392
|
+
const sections = this.sectionViews();
|
|
3393
|
+
const index = sections.findIndex((section) => section.id === this.activeSection());
|
|
3394
|
+
return sections.length === 0 || index >= sections.length - 1;
|
|
3395
|
+
}
|
|
3396
|
+
previousStep() {
|
|
3397
|
+
const sections = this.sectionViews();
|
|
3398
|
+
const current = sections.findIndex((section) => section.id === this.activeSection());
|
|
3399
|
+
if (current > 0) {
|
|
3400
|
+
this.activeSection.set(sections[current - 1].id);
|
|
3401
|
+
this.scrollToStep();
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
nextStep() {
|
|
3405
|
+
const sections = this.sectionViews();
|
|
3406
|
+
const current = sections.findIndex((section) => section.id === this.activeSection());
|
|
3407
|
+
if (current < 0 || current >= sections.length - 1) return;
|
|
3408
|
+
if (current === 0 && this.unsectionedHasLocalErrors()) {
|
|
3409
|
+
this.revealUnsectionedErrors();
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
const section = sections[current];
|
|
3413
|
+
if (this.sectionHasLocalErrors(section.id)) {
|
|
3414
|
+
this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, section.id]));
|
|
3415
|
+
this.revealSectionErrors(section.id);
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
this.activeSection.set(sections[current + 1].id);
|
|
3419
|
+
this.scrollToStep();
|
|
3420
|
+
}
|
|
3421
|
+
unsectionedHasLocalErrors() {
|
|
3422
|
+
const tree = this.formTree();
|
|
3423
|
+
return tree !== null && this.unsectionedFields().some((field) => childTree(tree, field.key)().invalid());
|
|
3424
|
+
}
|
|
3425
|
+
revealUnsectionedErrors() {
|
|
3426
|
+
const tree = this.formTree();
|
|
3427
|
+
if (!tree) return;
|
|
3428
|
+
this.submitted.set(true);
|
|
3429
|
+
for (const field of this.unsectionedFields()) childTree(tree, field.key)().markAsTouched();
|
|
3430
|
+
setTimeout(() => this.scrollToFirstError());
|
|
3431
|
+
}
|
|
3432
|
+
scrollToStep() {
|
|
3433
|
+
setTimeout(() => document.getElementById("step-panel")?.scrollIntoView({
|
|
3434
|
+
behavior: "smooth",
|
|
3435
|
+
block: "start"
|
|
3436
|
+
}));
|
|
3437
|
+
}
|
|
3438
|
+
selectSection(id) {
|
|
3439
|
+
const current = this.activeSection();
|
|
3440
|
+
if (this.submitted() && current !== null && current !== id && this.sectionHasLocalErrors(current)) {
|
|
3441
|
+
this.revealSectionErrors(current);
|
|
3442
|
+
return;
|
|
3443
|
+
}
|
|
3444
|
+
this.activeSection.set(id);
|
|
3445
|
+
}
|
|
3446
|
+
sectionHasLocalErrors(sectionId) {
|
|
3447
|
+
const tree = this.formTree();
|
|
3448
|
+
const section = this.sectionViews().find((view) => view.id === sectionId);
|
|
3449
|
+
if (!tree || !section) return false;
|
|
3450
|
+
return section.fields.some((field) => childTree(tree, field.key)().invalid());
|
|
3451
|
+
}
|
|
3452
|
+
revealSectionErrors(sectionId) {
|
|
3453
|
+
const tree = this.formTree();
|
|
3454
|
+
const section = this.sectionViews().find((view) => view.id === sectionId);
|
|
3455
|
+
if (!tree || !section) return;
|
|
3456
|
+
this.submitted.set(true);
|
|
3457
|
+
this.attemptedSteps.update((attempted) => /* @__PURE__ */ new Set([...attempted, sectionId]));
|
|
3458
|
+
for (const field of section.fields) childTree(tree, field.key)().markAsTouched();
|
|
3459
|
+
setTimeout(() => this.scrollToFirstError());
|
|
3460
|
+
}
|
|
3461
|
+
isSectionOpen(id) {
|
|
3462
|
+
return this.openSections()[id] ?? true;
|
|
3463
|
+
}
|
|
3464
|
+
setSectionOpen(id, event) {
|
|
3465
|
+
const open = event.target.open;
|
|
3466
|
+
this.openSections.update((sections) => ({
|
|
3467
|
+
...sections,
|
|
3468
|
+
[id]: open
|
|
3469
|
+
}));
|
|
3470
|
+
}
|
|
3471
|
+
goToSection(id) {
|
|
3472
|
+
this.openSections.update((open) => ({
|
|
3473
|
+
...open,
|
|
3474
|
+
[id]: true
|
|
3475
|
+
}));
|
|
3476
|
+
setTimeout(() => {
|
|
3477
|
+
const section = document.getElementById(`section-${id}`);
|
|
3478
|
+
if (!section) return;
|
|
3479
|
+
section.scrollIntoView({
|
|
3480
|
+
behavior: "smooth",
|
|
3481
|
+
block: "start"
|
|
3482
|
+
});
|
|
3483
|
+
section.querySelector("summary")?.focus({ preventScroll: true });
|
|
3484
|
+
});
|
|
3485
|
+
}
|
|
3486
|
+
fieldHasProblem(field) {
|
|
3487
|
+
const root = field.key;
|
|
3488
|
+
if (Object.keys(this.serverErrors().fields).some((path) => path === root || path.startsWith(`${root}.`) || path.startsWith(`${root}[`))) return true;
|
|
3489
|
+
const tree = this.formTree();
|
|
3490
|
+
return tree !== null && childTree(tree, root)().invalid();
|
|
3491
|
+
}
|
|
3492
|
+
sectionHasProblem(section) {
|
|
3493
|
+
return section.fields.some((field) => this.fieldHasProblem(field));
|
|
3494
|
+
}
|
|
3495
|
+
sectionProgress(section) {
|
|
3496
|
+
if (!this.sectionHasProblem(section)) return "complete";
|
|
3497
|
+
if (this.layoutMode() === "steps" && !this.attemptedSteps().has(section.id)) return "pending";
|
|
3498
|
+
return this.submitted() ? "problem" : "pending";
|
|
3499
|
+
}
|
|
3500
|
+
collectErrors() {
|
|
3501
|
+
const tree = this.formTree();
|
|
3502
|
+
if (!tree) return [];
|
|
3503
|
+
const server = this.serverErrors().fields;
|
|
3504
|
+
const entries = [];
|
|
3505
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3506
|
+
const walk = (fields, subtree, inheritedSection) => {
|
|
3507
|
+
for (const field of fields) {
|
|
3508
|
+
const section = field.section ?? inheritedSection;
|
|
3509
|
+
const state = childTree(subtree, field.key)();
|
|
3510
|
+
const serverMessage = this.serverMessageFor(field.key, server);
|
|
3511
|
+
if (field.type === "group") {
|
|
3512
|
+
const before = entries.length;
|
|
3513
|
+
walk(field.fields, groupTree(subtree, field.key), section);
|
|
3514
|
+
if (entries.length === before && (serverMessage || state.invalid())) {
|
|
3515
|
+
seen.add(state.name());
|
|
3516
|
+
entries.push({
|
|
3517
|
+
id: state.name(),
|
|
3518
|
+
label: field.label,
|
|
3519
|
+
message: serverMessage ?? "Revisa este campo",
|
|
3520
|
+
section
|
|
3521
|
+
});
|
|
3522
|
+
}
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
if (!state.invalid() && !serverMessage) continue;
|
|
3526
|
+
const composite = field.type === "list" || field.type === "key-value" || field.type === "string-list";
|
|
3527
|
+
const id = state.name();
|
|
3528
|
+
if (seen.has(id)) continue;
|
|
3529
|
+
seen.add(id);
|
|
3530
|
+
entries.push({
|
|
3531
|
+
id,
|
|
3532
|
+
label: field.label,
|
|
3533
|
+
message: serverMessage ?? state.errors()[0]?.message ?? (composite ? "Hay elementos con errores" : "Revisa este campo"),
|
|
3534
|
+
section
|
|
3535
|
+
});
|
|
3536
|
+
}
|
|
3537
|
+
};
|
|
3538
|
+
walk(this.renderedFields(), tree);
|
|
3539
|
+
return entries;
|
|
3540
|
+
}
|
|
3541
|
+
serverMessageFor(root, server) {
|
|
3542
|
+
for (const [path, message] of Object.entries(server)) if (path === root || path.startsWith(`${root}.`) || path.startsWith(`${root}[`)) return message;
|
|
3543
|
+
}
|
|
3544
|
+
async goToError(entry, event) {
|
|
3545
|
+
event.preventDefault();
|
|
3546
|
+
if (entry.section) {
|
|
3547
|
+
if (this.layoutMode() === "tabs" || this.layoutMode() === "steps") this.activeSection.set(entry.section);
|
|
3548
|
+
else this.openSections.update((open) => ({
|
|
3549
|
+
...open,
|
|
3550
|
+
[entry.section]: true
|
|
3551
|
+
}));
|
|
3552
|
+
}
|
|
3553
|
+
await this.appRef.whenStable();
|
|
3554
|
+
this.focusField(entry.id);
|
|
3555
|
+
}
|
|
3556
|
+
focusField(id) {
|
|
3557
|
+
const wrap = document.querySelector(`[data-field="${id}"]`);
|
|
3558
|
+
if (!wrap) return;
|
|
3559
|
+
wrap.scrollIntoView({
|
|
3560
|
+
behavior: "smooth",
|
|
3561
|
+
block: "center"
|
|
3562
|
+
});
|
|
3563
|
+
wrap.querySelector("input:not([type=\"hidden\"]), textarea, select, [contenteditable=\"true\"]")?.focus({ preventScroll: true });
|
|
3564
|
+
}
|
|
3565
|
+
focusSummary() {
|
|
3566
|
+
const summary = document.querySelector("[data-error-summary]");
|
|
3567
|
+
summary?.scrollIntoView({
|
|
3568
|
+
behavior: "smooth",
|
|
3569
|
+
block: "start"
|
|
3570
|
+
});
|
|
3571
|
+
summary?.focus({ preventScroll: true });
|
|
3572
|
+
}
|
|
3573
|
+
title() {
|
|
3574
|
+
const tree = this.formTree();
|
|
3575
|
+
const schema = this.schema();
|
|
3576
|
+
if (!tree || !schema) return "";
|
|
3577
|
+
const value = tree().value()[schema.titleField];
|
|
3578
|
+
return typeof value === "string" ? value : "";
|
|
3579
|
+
}
|
|
3580
|
+
recordKey() {
|
|
3581
|
+
return this.params().get("key");
|
|
3582
|
+
}
|
|
3583
|
+
drawerSchema(resource) {
|
|
3584
|
+
return getSchema(resource);
|
|
3585
|
+
}
|
|
3586
|
+
relationCreated(record, request) {
|
|
3587
|
+
const target = getSchema(request.resource);
|
|
3588
|
+
const key = target === void 0 ? void 0 : record[target.keyField];
|
|
3589
|
+
this.relationDrawer.clear();
|
|
3590
|
+
if (key === void 0 || key === null || key === "") return;
|
|
3591
|
+
request.onCreated(String(key));
|
|
3592
|
+
}
|
|
3593
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
3594
|
+
minVersion: "12.0.0",
|
|
3595
|
+
version: "22.2.3",
|
|
3596
|
+
ngImport: i0,
|
|
3597
|
+
type: FormView,
|
|
3598
|
+
deps: [],
|
|
3599
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
3600
|
+
});
|
|
3601
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
3602
|
+
minVersion: "17.0.0",
|
|
3603
|
+
version: "22.2.3",
|
|
3604
|
+
type: FormView,
|
|
3605
|
+
isStandalone: true,
|
|
3606
|
+
selector: "app-form-view",
|
|
3607
|
+
ngImport: i0,
|
|
3608
|
+
template: `
|
|
3609
|
+
@if (schema()) {
|
|
3610
|
+
<header class="mb-6 flex flex-wrap items-start justify-between gap-4">
|
|
3611
|
+
<div>
|
|
3612
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
3613
|
+
<h1 class="font-display text-2xl font-semibold">{{ heading() }}</h1>
|
|
3614
|
+
@if (dirty()) {
|
|
3615
|
+
<span
|
|
3616
|
+
class="rounded-full bg-accent/15 px-2.5 py-0.5 text-xs font-medium text-accent-text"
|
|
3617
|
+
>
|
|
3618
|
+
Sin guardar
|
|
3619
|
+
</span>
|
|
3620
|
+
}
|
|
3621
|
+
</div>
|
|
3622
|
+
@if (mode() === 'single') {
|
|
3623
|
+
<p class="text-sm text-neutral">Configuración única</p>
|
|
3624
|
+
}
|
|
3625
|
+
</div>
|
|
3626
|
+
@if (mode() !== 'single') {
|
|
3627
|
+
<button app-button variant="outline" type="button" (click)="back()">
|
|
3628
|
+
Volver al listado
|
|
3629
|
+
</button>
|
|
3630
|
+
}
|
|
3631
|
+
</header>
|
|
3632
|
+
|
|
3633
|
+
@switch (status()) {
|
|
3634
|
+
@case ('loading') {
|
|
3635
|
+
<app-skeleton [lines]="8" [height]="48" />
|
|
3636
|
+
}
|
|
3637
|
+
@case ('error') {
|
|
3638
|
+
<app-empty-state
|
|
3639
|
+
title="No se pudo cargar el registro"
|
|
3640
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
3641
|
+
>
|
|
3642
|
+
<button app-button variant="outline" type="button" (click)="retry()">Reintentar</button>
|
|
3643
|
+
<button app-button variant="outline" type="button" (click)="volverDeError()">
|
|
3644
|
+
Volver
|
|
3645
|
+
</button>
|
|
3646
|
+
</app-empty-state>
|
|
3647
|
+
}
|
|
3648
|
+
@case ('ready') {
|
|
3649
|
+
@if (formTree(); as tree) {
|
|
3650
|
+
@if (serverErrors().general; as general) {
|
|
3651
|
+
<p
|
|
3652
|
+
class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger"
|
|
3653
|
+
role="alert"
|
|
3654
|
+
>
|
|
3655
|
+
{{ general }}
|
|
3656
|
+
</p>
|
|
3657
|
+
}
|
|
3658
|
+
|
|
3659
|
+
<form novalidate [attr.aria-busy]="saving()" (submit)="onSubmit($event)">
|
|
3660
|
+
@if (draftOffer(); as offer) {
|
|
3661
|
+
<!-- §4.6: se OFRECE, nunca se restaura solo. role=status para
|
|
3662
|
+
anunciarlo sin arrebatarle el foco a quien está escribiendo. -->
|
|
3663
|
+
<div
|
|
3664
|
+
class="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-neutral/20 bg-surface px-4 py-3 text-sm"
|
|
3665
|
+
role="status"
|
|
3666
|
+
>
|
|
3667
|
+
<span class="text-neutral">
|
|
3668
|
+
Hay un borrador guardado {{ draftAge(offer.savedAt) }}.
|
|
3669
|
+
</span>
|
|
3670
|
+
<button app-button variant="outline" type="button" (click)="restoreDraft()">
|
|
3671
|
+
Restaurar
|
|
3672
|
+
</button>
|
|
3673
|
+
<button app-button variant="ghost" type="button" (click)="discardDraft()">
|
|
3674
|
+
Descartar
|
|
3675
|
+
</button>
|
|
3676
|
+
</div>
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
@if (summaryErrors().length >= 2) {
|
|
3680
|
+
<!-- Con uno solo no se pinta: el foco ya está en el campo y
|
|
3681
|
+
el resumen sólo restaría. Con 2+ es la única forma de
|
|
3682
|
+
ver que hay varios (§4.11). tabindex -1 para poder
|
|
3683
|
+
recibir el foco y que role=alert lo anuncie. -->
|
|
3684
|
+
<div
|
|
3685
|
+
data-error-summary
|
|
3686
|
+
tabindex="-1"
|
|
3687
|
+
role="alert"
|
|
3688
|
+
aria-labelledby="error-summary-title"
|
|
3689
|
+
class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 focus:outline-2 focus:outline-danger"
|
|
3690
|
+
>
|
|
3691
|
+
<p id="error-summary-title" class="text-sm font-semibold text-danger">
|
|
3692
|
+
Hay {{ summaryErrors().length }} errores que corregir
|
|
3693
|
+
</p>
|
|
3694
|
+
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
|
|
3695
|
+
@for (entry of summaryErrors(); track entry.id) {
|
|
3696
|
+
<li>
|
|
3697
|
+
<a
|
|
3698
|
+
href="#"
|
|
3699
|
+
class="text-danger underline underline-offset-2 hover:text-danger/80"
|
|
3700
|
+
(click)="goToError(entry, $event)"
|
|
3701
|
+
>
|
|
3702
|
+
{{ entry.label }} — {{ entry.message }}
|
|
3703
|
+
</a>
|
|
3704
|
+
</li>
|
|
3705
|
+
}
|
|
3706
|
+
</ul>
|
|
3707
|
+
</div>
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
@if (layoutMode() === 'grid') {
|
|
3711
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
3712
|
+
@for (field of renderedFields(); track field.key) {
|
|
3713
|
+
<div [class]="colSpan(field)">
|
|
3714
|
+
<app-field-host
|
|
3715
|
+
[field]="field"
|
|
3716
|
+
[tree]="tree"
|
|
3717
|
+
[exists]="exists()"
|
|
3718
|
+
[serverErrors]="serverErrors().fields"
|
|
3719
|
+
[resource]="resourceId()"
|
|
3720
|
+
/>
|
|
3721
|
+
</div>
|
|
3722
|
+
}
|
|
3723
|
+
</div>
|
|
3724
|
+
} @else if (unsectionedFields().length > 0) {
|
|
3725
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
3726
|
+
@for (field of unsectionedFields(); track field.key) {
|
|
3727
|
+
<div [class]="colSpan(field)">
|
|
3728
|
+
<app-field-host
|
|
3729
|
+
[field]="field"
|
|
3730
|
+
[tree]="tree"
|
|
3731
|
+
[exists]="exists()"
|
|
3732
|
+
[serverErrors]="serverErrors().fields"
|
|
3733
|
+
[resource]="resourceId()"
|
|
3734
|
+
/>
|
|
3735
|
+
</div>
|
|
3736
|
+
}
|
|
3737
|
+
</div>
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
@if (layoutMode() === 'tabs') {
|
|
3741
|
+
<div
|
|
3742
|
+
role="tablist"
|
|
3743
|
+
class="mb-4 flex flex-wrap gap-1 border-b border-neutral/20"
|
|
3744
|
+
(keydown)="onTablistKeydown($event)"
|
|
3745
|
+
>
|
|
3746
|
+
@for (section of sectionViews(); track section.id) {
|
|
3747
|
+
<button
|
|
3748
|
+
type="button"
|
|
3749
|
+
role="tab"
|
|
3750
|
+
[id]="tabId(section.id)"
|
|
3751
|
+
[class]="tabClass(section)"
|
|
3752
|
+
[attr.aria-selected]="activeSection() === section.id"
|
|
3753
|
+
[attr.tabindex]="activeSection() === section.id ? 0 : -1"
|
|
3754
|
+
(click)="selectSection(section.id)"
|
|
3755
|
+
>
|
|
3756
|
+
{{ section.label }}
|
|
3757
|
+
<!-- Progreso (§4.3, decisión A): el ✓ siempre que esté
|
|
3758
|
+
completa; el ⚠ recién después del primer Guardar.
|
|
3759
|
+
El glifo va oculto para lectores y el estado se
|
|
3760
|
+
cuenta aparte en texto. -->
|
|
3761
|
+
@switch (sectionProgress(section)) {
|
|
3762
|
+
@case ('problem') {
|
|
3763
|
+
<span class="ml-1 text-danger" aria-hidden="true">⚠</span>
|
|
3764
|
+
<span class="sr-only"> — con errores</span>
|
|
3765
|
+
}
|
|
3766
|
+
@case ('complete') {
|
|
3767
|
+
<span class="ml-1 text-success" aria-hidden="true">✓</span>
|
|
3768
|
+
<span class="sr-only"> — completa</span>
|
|
3769
|
+
}
|
|
3770
|
+
}
|
|
3771
|
+
</button>
|
|
3772
|
+
}
|
|
3773
|
+
</div>
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
@if (layoutMode() === 'steps') {
|
|
3777
|
+
<ol aria-label="Progreso del formulario" class="mb-4 flex flex-wrap gap-2">
|
|
3778
|
+
@for (section of sectionViews(); track section.id; let index = $index) {
|
|
3779
|
+
<li
|
|
3780
|
+
class="rounded-full border px-3 py-1 text-sm"
|
|
3781
|
+
[class.border-primary]="activeSection() === section.id"
|
|
3782
|
+
[class.font-semibold]="activeSection() === section.id"
|
|
3783
|
+
[attr.aria-current]="activeSection() === section.id ? 'step' : null"
|
|
3784
|
+
>
|
|
3785
|
+
{{ index + 1 }}. {{ section.label }}
|
|
3786
|
+
</li>
|
|
3787
|
+
}
|
|
3788
|
+
</ol>
|
|
3789
|
+
}
|
|
3790
|
+
|
|
3791
|
+
@if (layoutMode() === 'tabs' || layoutMode() === 'steps') {
|
|
3792
|
+
@for (section of visibleSections(); track section.id) {
|
|
3793
|
+
<div
|
|
3794
|
+
role="tabpanel"
|
|
3795
|
+
[attr.id]="layoutMode() === 'steps' ? 'step-panel' : null"
|
|
3796
|
+
class="grid grid-cols-1 gap-x-4 sm:grid-cols-12"
|
|
3797
|
+
>
|
|
3798
|
+
@for (field of section.fields; track field.key) {
|
|
3799
|
+
<div [class]="colSpan(field)">
|
|
3800
|
+
<app-field-host
|
|
3801
|
+
[field]="field"
|
|
3802
|
+
[tree]="tree"
|
|
3803
|
+
[exists]="exists()"
|
|
3804
|
+
[serverErrors]="serverErrors().fields"
|
|
3805
|
+
[resource]="resourceId()"
|
|
3806
|
+
/>
|
|
3807
|
+
</div>
|
|
3808
|
+
}
|
|
3809
|
+
</div>
|
|
3810
|
+
}
|
|
3811
|
+
@if (layoutMode() === 'steps' && sectionViews().length > 1) {
|
|
3812
|
+
<div class="mt-4 flex justify-between gap-3">
|
|
3813
|
+
<button
|
|
3814
|
+
app-button
|
|
3815
|
+
variant="outline"
|
|
3816
|
+
type="button"
|
|
3817
|
+
[disabled]="isFirstStep()"
|
|
3818
|
+
(click)="previousStep()"
|
|
3819
|
+
>
|
|
3820
|
+
Anterior
|
|
3821
|
+
</button>
|
|
3822
|
+
@if (!isLastStep()) {
|
|
3823
|
+
<button app-button type="button" (click)="nextStep()">Continuar</button>
|
|
3824
|
+
}
|
|
3825
|
+
</div>
|
|
3826
|
+
}
|
|
3827
|
+
} @else if (layoutMode() === 'columns') {
|
|
3828
|
+
<div
|
|
3829
|
+
[class]="
|
|
3830
|
+
layoutColumns() === 3
|
|
3831
|
+
? 'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'
|
|
3832
|
+
: 'grid grid-cols-1 gap-4 md:grid-cols-2'
|
|
3833
|
+
"
|
|
3834
|
+
>
|
|
3835
|
+
@for (section of sectionViews(); track section.id) {
|
|
3836
|
+
<section class="rounded-xl border border-neutral/20 bg-surface p-4">
|
|
3837
|
+
<h2 class="mb-3 text-section font-semibold">{{ section.label }}</h2>
|
|
3838
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
3839
|
+
@for (field of section.fields; track field.key) {
|
|
3840
|
+
<div [class]="colSpan(field)">
|
|
3841
|
+
<app-field-host
|
|
3842
|
+
[field]="field"
|
|
3843
|
+
[tree]="tree"
|
|
3844
|
+
[exists]="exists()"
|
|
3845
|
+
[serverErrors]="serverErrors().fields"
|
|
3846
|
+
[resource]="resourceId()"
|
|
3847
|
+
/>
|
|
3848
|
+
</div>
|
|
3849
|
+
}
|
|
3850
|
+
</div>
|
|
3851
|
+
</section>
|
|
3852
|
+
}
|
|
3853
|
+
</div>
|
|
3854
|
+
} @else if (layoutMode() === 'sections') {
|
|
3855
|
+
<!-- Índice interno (§5.3): en escritorio, índice sticky a la
|
|
3856
|
+
izquierda; en móvil, chips horizontales. En tabs las
|
|
3857
|
+
pestañas ya hacen de índice, así que esto no se dibuja. -->
|
|
3858
|
+
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
|
|
3859
|
+
<nav
|
|
3860
|
+
aria-label="Secciones"
|
|
3861
|
+
class="sticky top-4 hidden shrink-0 self-start lg:block lg:w-44"
|
|
3862
|
+
>
|
|
3863
|
+
<p
|
|
3864
|
+
class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral"
|
|
3865
|
+
aria-hidden="true"
|
|
3866
|
+
>
|
|
3867
|
+
Secciones
|
|
3868
|
+
</p>
|
|
3869
|
+
<ul class="space-y-1 border-l border-neutral/20 pl-3">
|
|
3870
|
+
@for (section of visibleSections(); track section.id) {
|
|
3871
|
+
<li>
|
|
3872
|
+
<button
|
|
3873
|
+
type="button"
|
|
3874
|
+
class="w-full rounded px-2 py-1 text-left text-sm hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
3875
|
+
[class.font-medium]="sectionProgress(section) === 'problem'"
|
|
3876
|
+
(click)="goToSection(section.id)"
|
|
3877
|
+
>
|
|
3878
|
+
{{ section.label }}
|
|
3879
|
+
@switch (sectionProgress(section)) {
|
|
3880
|
+
@case ('problem') {
|
|
3881
|
+
<span class="ml-1 text-danger" aria-hidden="true">⚠</span>
|
|
3882
|
+
<span class="sr-only"> — con errores</span>
|
|
3883
|
+
}
|
|
3884
|
+
@case ('complete') {
|
|
3885
|
+
<span class="ml-1 text-success" aria-hidden="true">✓</span>
|
|
3886
|
+
<span class="sr-only"> — completa</span>
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
</button>
|
|
3890
|
+
</li>
|
|
3891
|
+
}
|
|
3892
|
+
</ul>
|
|
3893
|
+
</nav>
|
|
3894
|
+
|
|
3895
|
+
<div class="min-w-0 flex-1" [class.lg:grid]="layoutMode() === 'columns'">
|
|
3896
|
+
<nav
|
|
3897
|
+
aria-label="Secciones"
|
|
3898
|
+
class="-mx-1 mb-4 flex gap-2 overflow-x-auto px-1 pb-1 lg:hidden"
|
|
3899
|
+
>
|
|
3900
|
+
@for (section of visibleSections(); track section.id) {
|
|
3901
|
+
<button
|
|
3902
|
+
type="button"
|
|
3903
|
+
class="shrink-0 rounded-full border border-neutral/30 px-3 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
3904
|
+
[class.border-danger]="sectionProgress(section) === 'problem'"
|
|
3905
|
+
[class.text-danger]="sectionProgress(section) === 'problem'"
|
|
3906
|
+
[class.font-medium]="sectionProgress(section) === 'problem'"
|
|
3907
|
+
(click)="goToSection(section.id)"
|
|
3908
|
+
>
|
|
3909
|
+
{{ section.label }}
|
|
3910
|
+
</button>
|
|
3911
|
+
}
|
|
3912
|
+
</nav>
|
|
3913
|
+
|
|
3914
|
+
@for (section of visibleSections(); track section.id) {
|
|
3915
|
+
<details
|
|
3916
|
+
id="section-{{ section.id }}"
|
|
3917
|
+
class="mb-4 rounded-xl border border-neutral/20 bg-surface scroll-mt-4"
|
|
3918
|
+
[open]="isSectionOpen(section.id)"
|
|
3919
|
+
(toggle)="setSectionOpen(section.id, $event)"
|
|
3920
|
+
>
|
|
3921
|
+
<summary
|
|
3922
|
+
tabindex="-1"
|
|
3923
|
+
class="flex cursor-pointer items-center justify-between gap-2 rounded-xl px-4 py-3 text-section font-semibold hover:bg-neutral/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
3924
|
+
>
|
|
3925
|
+
<span>{{ section.label }}</span>
|
|
3926
|
+
@if (sectionProgress(section) === 'problem') {
|
|
3927
|
+
<span class="text-xs font-medium text-danger">Revisar</span>
|
|
3928
|
+
}
|
|
3929
|
+
</summary>
|
|
3930
|
+
<div class="grid grid-cols-1 gap-x-4 px-4 pb-4 sm:grid-cols-12">
|
|
3931
|
+
@for (field of section.fields; track field.key) {
|
|
3932
|
+
<div [class]="colSpan(field)">
|
|
3933
|
+
<app-field-host
|
|
3934
|
+
[field]="field"
|
|
3935
|
+
[tree]="tree"
|
|
3936
|
+
[exists]="exists()"
|
|
3937
|
+
[serverErrors]="serverErrors().fields"
|
|
3938
|
+
[resource]="resourceId()"
|
|
3939
|
+
/>
|
|
3940
|
+
</div>
|
|
3941
|
+
}
|
|
3942
|
+
</div>
|
|
3943
|
+
</details>
|
|
3944
|
+
}
|
|
3945
|
+
</div>
|
|
3946
|
+
</div>
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3949
|
+
<!-- Sticky (§5.1): en products son 19 campos y Cancelar/Guardar
|
|
3950
|
+
desaparecían al final del scroll. bg-surface es opaco: sin
|
|
3951
|
+
él el contenido vería pasar por detrás. -->
|
|
3952
|
+
<div
|
|
3953
|
+
class="sticky bottom-0 z-10 mt-6 flex flex-wrap items-center justify-end gap-3 border-t border-neutral/20 bg-surface pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
|
|
3954
|
+
>
|
|
3955
|
+
@if (canDelete()) {
|
|
3956
|
+
<div class="mr-auto">
|
|
3957
|
+
<button app-button variant="danger" type="button" (click)="remove()">
|
|
3958
|
+
Eliminar
|
|
3959
|
+
</button>
|
|
3960
|
+
</div>
|
|
3961
|
+
}
|
|
3962
|
+
@if (mode() !== 'create') {
|
|
3963
|
+
<!-- §5: descartar lo escrito y volver a lo del servidor. -->
|
|
3964
|
+
<button app-button variant="ghost" type="button" (click)="restablecer()">
|
|
3965
|
+
Restablecer
|
|
3966
|
+
</button>
|
|
3967
|
+
}
|
|
3968
|
+
<button app-button variant="outline" type="button" (click)="back()">
|
|
3969
|
+
Cancelar
|
|
3970
|
+
</button>
|
|
3971
|
+
<button app-button type="submit" [loading]="saving()" [disabled]="!canSave()">
|
|
3972
|
+
{{ saving() ? 'Guardando…' : 'Guardar' }}
|
|
3973
|
+
</button>
|
|
3974
|
+
</div>
|
|
3975
|
+
</form>
|
|
3976
|
+
}
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
} @else {
|
|
3980
|
+
<app-empty-state
|
|
3981
|
+
title="Recurso no encontrado"
|
|
3982
|
+
message="No existe un schema con ese id en el registry."
|
|
3983
|
+
/>
|
|
3984
|
+
}
|
|
3985
|
+
|
|
3986
|
+
<!-- «Crear nuevo» desde el dropdown de una relación (plan 6.4). Se monta
|
|
3987
|
+
y desmonta con el pedido: el Drawer devuelve el foco al botón que lo
|
|
3988
|
+
abrió al destruirse. -->
|
|
3989
|
+
@if (relationDrawer.request(); as createRequest) {
|
|
3990
|
+
@if (drawerSchema(createRequest.resource); as target) {
|
|
3991
|
+
<app-drawer-form
|
|
3992
|
+
[open]="true"
|
|
3993
|
+
[schema]="target"
|
|
3994
|
+
mode="create"
|
|
3995
|
+
[title]="'Crear ' + target.label"
|
|
3996
|
+
(close)="relationDrawer.clear()"
|
|
3997
|
+
(saved)="relationCreated($event, createRequest)"
|
|
3998
|
+
/>
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
|
|
4002
|
+
@if (createdWithSummary(); as createdRec) {
|
|
4003
|
+
@if (schema()?.postCreate; as postCreateConfig) {
|
|
4004
|
+
<app-post-create-modal
|
|
4005
|
+
[schema]="schema()!"
|
|
4006
|
+
[config]="postCreateConfig"
|
|
4007
|
+
[record]="createdRec"
|
|
4008
|
+
(closed)="onPostCreateClosed()"
|
|
4009
|
+
/>
|
|
4010
|
+
}
|
|
4011
|
+
}
|
|
4012
|
+
`,
|
|
4013
|
+
isInline: true,
|
|
4014
|
+
dependencies: [
|
|
4015
|
+
{
|
|
4016
|
+
kind: "component",
|
|
4017
|
+
type: Button,
|
|
4018
|
+
selector: "button[app-button]",
|
|
4019
|
+
inputs: [
|
|
4020
|
+
"variant",
|
|
4021
|
+
"size",
|
|
4022
|
+
"type",
|
|
4023
|
+
"disabled",
|
|
4024
|
+
"loading"
|
|
4025
|
+
]
|
|
4026
|
+
},
|
|
4027
|
+
{
|
|
4028
|
+
kind: "component",
|
|
4029
|
+
type: DrawerForm,
|
|
4030
|
+
selector: "app-drawer-form",
|
|
4031
|
+
inputs: [
|
|
4032
|
+
"open",
|
|
4033
|
+
"schema",
|
|
4034
|
+
"fields",
|
|
4035
|
+
"mode",
|
|
4036
|
+
"recordKey",
|
|
4037
|
+
"title",
|
|
4038
|
+
"size"
|
|
4039
|
+
],
|
|
4040
|
+
outputs: ["saved", "close"]
|
|
4041
|
+
},
|
|
4042
|
+
{
|
|
4043
|
+
kind: "component",
|
|
4044
|
+
type: EmptyState,
|
|
4045
|
+
selector: "app-empty-state",
|
|
4046
|
+
inputs: ["title", "message"]
|
|
4047
|
+
},
|
|
4048
|
+
{
|
|
4049
|
+
kind: "component",
|
|
4050
|
+
type: FieldHost,
|
|
4051
|
+
selector: "app-field-host",
|
|
4052
|
+
inputs: [
|
|
4053
|
+
"field",
|
|
4054
|
+
"tree",
|
|
4055
|
+
"exists",
|
|
4056
|
+
"serverErrors",
|
|
4057
|
+
"pathPrefix",
|
|
4058
|
+
"resource"
|
|
4059
|
+
]
|
|
4060
|
+
},
|
|
4061
|
+
{
|
|
4062
|
+
kind: "component",
|
|
4063
|
+
type: PostCreateModal,
|
|
4064
|
+
selector: "app-post-create-modal",
|
|
4065
|
+
inputs: [
|
|
4066
|
+
"schema",
|
|
4067
|
+
"config",
|
|
4068
|
+
"record",
|
|
4069
|
+
"configValues"
|
|
4070
|
+
],
|
|
4071
|
+
outputs: ["closed"]
|
|
4072
|
+
},
|
|
4073
|
+
{
|
|
4074
|
+
kind: "component",
|
|
4075
|
+
type: Skeleton,
|
|
4076
|
+
selector: "app-skeleton",
|
|
4077
|
+
inputs: [
|
|
4078
|
+
"lines",
|
|
4079
|
+
"height",
|
|
4080
|
+
"width"
|
|
4081
|
+
]
|
|
4082
|
+
}
|
|
4083
|
+
]
|
|
4084
|
+
});
|
|
4085
|
+
};
|
|
4086
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
4087
|
+
minVersion: "12.0.0",
|
|
4088
|
+
version: "22.2.3",
|
|
4089
|
+
ngImport: i0,
|
|
4090
|
+
type: FormView,
|
|
4091
|
+
decorators: [{
|
|
4092
|
+
type: Component,
|
|
4093
|
+
args: [{
|
|
4094
|
+
selector: "app-form-view",
|
|
4095
|
+
imports: [
|
|
4096
|
+
Button,
|
|
4097
|
+
DrawerForm,
|
|
4098
|
+
EmptyState,
|
|
4099
|
+
FieldHost,
|
|
4100
|
+
PostCreateModal,
|
|
4101
|
+
Skeleton
|
|
4102
|
+
],
|
|
4103
|
+
template: `
|
|
4104
|
+
@if (schema()) {
|
|
4105
|
+
<header class="mb-6 flex flex-wrap items-start justify-between gap-4">
|
|
4106
|
+
<div>
|
|
4107
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
4108
|
+
<h1 class="font-display text-2xl font-semibold">{{ heading() }}</h1>
|
|
4109
|
+
@if (dirty()) {
|
|
4110
|
+
<span
|
|
4111
|
+
class="rounded-full bg-accent/15 px-2.5 py-0.5 text-xs font-medium text-accent-text"
|
|
4112
|
+
>
|
|
4113
|
+
Sin guardar
|
|
4114
|
+
</span>
|
|
4115
|
+
}
|
|
4116
|
+
</div>
|
|
4117
|
+
@if (mode() === 'single') {
|
|
4118
|
+
<p class="text-sm text-neutral">Configuración única</p>
|
|
4119
|
+
}
|
|
4120
|
+
</div>
|
|
4121
|
+
@if (mode() !== 'single') {
|
|
4122
|
+
<button app-button variant="outline" type="button" (click)="back()">
|
|
4123
|
+
Volver al listado
|
|
4124
|
+
</button>
|
|
4125
|
+
}
|
|
4126
|
+
</header>
|
|
4127
|
+
|
|
4128
|
+
@switch (status()) {
|
|
4129
|
+
@case ('loading') {
|
|
4130
|
+
<app-skeleton [lines]="8" [height]="48" />
|
|
4131
|
+
}
|
|
4132
|
+
@case ('error') {
|
|
4133
|
+
<app-empty-state
|
|
4134
|
+
title="No se pudo cargar el registro"
|
|
4135
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
4136
|
+
>
|
|
4137
|
+
<button app-button variant="outline" type="button" (click)="retry()">Reintentar</button>
|
|
4138
|
+
<button app-button variant="outline" type="button" (click)="volverDeError()">
|
|
4139
|
+
Volver
|
|
4140
|
+
</button>
|
|
4141
|
+
</app-empty-state>
|
|
4142
|
+
}
|
|
4143
|
+
@case ('ready') {
|
|
4144
|
+
@if (formTree(); as tree) {
|
|
4145
|
+
@if (serverErrors().general; as general) {
|
|
4146
|
+
<p
|
|
4147
|
+
class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 text-sm text-danger"
|
|
4148
|
+
role="alert"
|
|
4149
|
+
>
|
|
4150
|
+
{{ general }}
|
|
4151
|
+
</p>
|
|
4152
|
+
}
|
|
4153
|
+
|
|
4154
|
+
<form novalidate [attr.aria-busy]="saving()" (submit)="onSubmit($event)">
|
|
4155
|
+
@if (draftOffer(); as offer) {
|
|
4156
|
+
<!-- §4.6: se OFRECE, nunca se restaura solo. role=status para
|
|
4157
|
+
anunciarlo sin arrebatarle el foco a quien está escribiendo. -->
|
|
4158
|
+
<div
|
|
4159
|
+
class="mb-4 flex flex-wrap items-center gap-3 rounded-lg border border-neutral/20 bg-surface px-4 py-3 text-sm"
|
|
4160
|
+
role="status"
|
|
4161
|
+
>
|
|
4162
|
+
<span class="text-neutral">
|
|
4163
|
+
Hay un borrador guardado {{ draftAge(offer.savedAt) }}.
|
|
4164
|
+
</span>
|
|
4165
|
+
<button app-button variant="outline" type="button" (click)="restoreDraft()">
|
|
4166
|
+
Restaurar
|
|
4167
|
+
</button>
|
|
4168
|
+
<button app-button variant="ghost" type="button" (click)="discardDraft()">
|
|
4169
|
+
Descartar
|
|
4170
|
+
</button>
|
|
4171
|
+
</div>
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
@if (summaryErrors().length >= 2) {
|
|
4175
|
+
<!-- Con uno solo no se pinta: el foco ya está en el campo y
|
|
4176
|
+
el resumen sólo restaría. Con 2+ es la única forma de
|
|
4177
|
+
ver que hay varios (§4.11). tabindex -1 para poder
|
|
4178
|
+
recibir el foco y que role=alert lo anuncie. -->
|
|
4179
|
+
<div
|
|
4180
|
+
data-error-summary
|
|
4181
|
+
tabindex="-1"
|
|
4182
|
+
role="alert"
|
|
4183
|
+
aria-labelledby="error-summary-title"
|
|
4184
|
+
class="mb-4 rounded-lg border border-danger/30 bg-danger/10 px-4 py-3 focus:outline-2 focus:outline-danger"
|
|
4185
|
+
>
|
|
4186
|
+
<p id="error-summary-title" class="text-sm font-semibold text-danger">
|
|
4187
|
+
Hay {{ summaryErrors().length }} errores que corregir
|
|
4188
|
+
</p>
|
|
4189
|
+
<ul class="mt-2 list-disc space-y-1 pl-5 text-sm">
|
|
4190
|
+
@for (entry of summaryErrors(); track entry.id) {
|
|
4191
|
+
<li>
|
|
4192
|
+
<a
|
|
4193
|
+
href="#"
|
|
4194
|
+
class="text-danger underline underline-offset-2 hover:text-danger/80"
|
|
4195
|
+
(click)="goToError(entry, $event)"
|
|
4196
|
+
>
|
|
4197
|
+
{{ entry.label }} — {{ entry.message }}
|
|
4198
|
+
</a>
|
|
4199
|
+
</li>
|
|
4200
|
+
}
|
|
4201
|
+
</ul>
|
|
4202
|
+
</div>
|
|
4203
|
+
}
|
|
4204
|
+
|
|
4205
|
+
@if (layoutMode() === 'grid') {
|
|
4206
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
4207
|
+
@for (field of renderedFields(); track field.key) {
|
|
4208
|
+
<div [class]="colSpan(field)">
|
|
4209
|
+
<app-field-host
|
|
4210
|
+
[field]="field"
|
|
4211
|
+
[tree]="tree"
|
|
4212
|
+
[exists]="exists()"
|
|
4213
|
+
[serverErrors]="serverErrors().fields"
|
|
4214
|
+
[resource]="resourceId()"
|
|
4215
|
+
/>
|
|
4216
|
+
</div>
|
|
4217
|
+
}
|
|
4218
|
+
</div>
|
|
4219
|
+
} @else if (unsectionedFields().length > 0) {
|
|
4220
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
4221
|
+
@for (field of unsectionedFields(); track field.key) {
|
|
4222
|
+
<div [class]="colSpan(field)">
|
|
4223
|
+
<app-field-host
|
|
4224
|
+
[field]="field"
|
|
4225
|
+
[tree]="tree"
|
|
4226
|
+
[exists]="exists()"
|
|
4227
|
+
[serverErrors]="serverErrors().fields"
|
|
4228
|
+
[resource]="resourceId()"
|
|
4229
|
+
/>
|
|
4230
|
+
</div>
|
|
4231
|
+
}
|
|
4232
|
+
</div>
|
|
4233
|
+
}
|
|
4234
|
+
|
|
4235
|
+
@if (layoutMode() === 'tabs') {
|
|
4236
|
+
<div
|
|
4237
|
+
role="tablist"
|
|
4238
|
+
class="mb-4 flex flex-wrap gap-1 border-b border-neutral/20"
|
|
4239
|
+
(keydown)="onTablistKeydown($event)"
|
|
4240
|
+
>
|
|
4241
|
+
@for (section of sectionViews(); track section.id) {
|
|
4242
|
+
<button
|
|
4243
|
+
type="button"
|
|
4244
|
+
role="tab"
|
|
4245
|
+
[id]="tabId(section.id)"
|
|
4246
|
+
[class]="tabClass(section)"
|
|
4247
|
+
[attr.aria-selected]="activeSection() === section.id"
|
|
4248
|
+
[attr.tabindex]="activeSection() === section.id ? 0 : -1"
|
|
4249
|
+
(click)="selectSection(section.id)"
|
|
4250
|
+
>
|
|
4251
|
+
{{ section.label }}
|
|
4252
|
+
<!-- Progreso (§4.3, decisión A): el ✓ siempre que esté
|
|
4253
|
+
completa; el ⚠ recién después del primer Guardar.
|
|
4254
|
+
El glifo va oculto para lectores y el estado se
|
|
4255
|
+
cuenta aparte en texto. -->
|
|
4256
|
+
@switch (sectionProgress(section)) {
|
|
4257
|
+
@case ('problem') {
|
|
4258
|
+
<span class="ml-1 text-danger" aria-hidden="true">⚠</span>
|
|
4259
|
+
<span class="sr-only"> — con errores</span>
|
|
4260
|
+
}
|
|
4261
|
+
@case ('complete') {
|
|
4262
|
+
<span class="ml-1 text-success" aria-hidden="true">✓</span>
|
|
4263
|
+
<span class="sr-only"> — completa</span>
|
|
4264
|
+
}
|
|
4265
|
+
}
|
|
4266
|
+
</button>
|
|
4267
|
+
}
|
|
4268
|
+
</div>
|
|
4269
|
+
}
|
|
4270
|
+
|
|
4271
|
+
@if (layoutMode() === 'steps') {
|
|
4272
|
+
<ol aria-label="Progreso del formulario" class="mb-4 flex flex-wrap gap-2">
|
|
4273
|
+
@for (section of sectionViews(); track section.id; let index = $index) {
|
|
4274
|
+
<li
|
|
4275
|
+
class="rounded-full border px-3 py-1 text-sm"
|
|
4276
|
+
[class.border-primary]="activeSection() === section.id"
|
|
4277
|
+
[class.font-semibold]="activeSection() === section.id"
|
|
4278
|
+
[attr.aria-current]="activeSection() === section.id ? 'step' : null"
|
|
4279
|
+
>
|
|
4280
|
+
{{ index + 1 }}. {{ section.label }}
|
|
4281
|
+
</li>
|
|
4282
|
+
}
|
|
4283
|
+
</ol>
|
|
4284
|
+
}
|
|
4285
|
+
|
|
4286
|
+
@if (layoutMode() === 'tabs' || layoutMode() === 'steps') {
|
|
4287
|
+
@for (section of visibleSections(); track section.id) {
|
|
4288
|
+
<div
|
|
4289
|
+
role="tabpanel"
|
|
4290
|
+
[attr.id]="layoutMode() === 'steps' ? 'step-panel' : null"
|
|
4291
|
+
class="grid grid-cols-1 gap-x-4 sm:grid-cols-12"
|
|
4292
|
+
>
|
|
4293
|
+
@for (field of section.fields; track field.key) {
|
|
4294
|
+
<div [class]="colSpan(field)">
|
|
4295
|
+
<app-field-host
|
|
4296
|
+
[field]="field"
|
|
4297
|
+
[tree]="tree"
|
|
4298
|
+
[exists]="exists()"
|
|
4299
|
+
[serverErrors]="serverErrors().fields"
|
|
4300
|
+
[resource]="resourceId()"
|
|
4301
|
+
/>
|
|
4302
|
+
</div>
|
|
4303
|
+
}
|
|
4304
|
+
</div>
|
|
4305
|
+
}
|
|
4306
|
+
@if (layoutMode() === 'steps' && sectionViews().length > 1) {
|
|
4307
|
+
<div class="mt-4 flex justify-between gap-3">
|
|
4308
|
+
<button
|
|
4309
|
+
app-button
|
|
4310
|
+
variant="outline"
|
|
4311
|
+
type="button"
|
|
4312
|
+
[disabled]="isFirstStep()"
|
|
4313
|
+
(click)="previousStep()"
|
|
4314
|
+
>
|
|
4315
|
+
Anterior
|
|
4316
|
+
</button>
|
|
4317
|
+
@if (!isLastStep()) {
|
|
4318
|
+
<button app-button type="button" (click)="nextStep()">Continuar</button>
|
|
4319
|
+
}
|
|
4320
|
+
</div>
|
|
4321
|
+
}
|
|
4322
|
+
} @else if (layoutMode() === 'columns') {
|
|
4323
|
+
<div
|
|
4324
|
+
[class]="
|
|
4325
|
+
layoutColumns() === 3
|
|
4326
|
+
? 'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3'
|
|
4327
|
+
: 'grid grid-cols-1 gap-4 md:grid-cols-2'
|
|
4328
|
+
"
|
|
4329
|
+
>
|
|
4330
|
+
@for (section of sectionViews(); track section.id) {
|
|
4331
|
+
<section class="rounded-xl border border-neutral/20 bg-surface p-4">
|
|
4332
|
+
<h2 class="mb-3 text-section font-semibold">{{ section.label }}</h2>
|
|
4333
|
+
<div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
|
|
4334
|
+
@for (field of section.fields; track field.key) {
|
|
4335
|
+
<div [class]="colSpan(field)">
|
|
4336
|
+
<app-field-host
|
|
4337
|
+
[field]="field"
|
|
4338
|
+
[tree]="tree"
|
|
4339
|
+
[exists]="exists()"
|
|
4340
|
+
[serverErrors]="serverErrors().fields"
|
|
4341
|
+
[resource]="resourceId()"
|
|
4342
|
+
/>
|
|
4343
|
+
</div>
|
|
4344
|
+
}
|
|
4345
|
+
</div>
|
|
4346
|
+
</section>
|
|
4347
|
+
}
|
|
4348
|
+
</div>
|
|
4349
|
+
} @else if (layoutMode() === 'sections') {
|
|
4350
|
+
<!-- Índice interno (§5.3): en escritorio, índice sticky a la
|
|
4351
|
+
izquierda; en móvil, chips horizontales. En tabs las
|
|
4352
|
+
pestañas ya hacen de índice, así que esto no se dibuja. -->
|
|
4353
|
+
<div class="flex flex-col gap-4 lg:flex-row lg:gap-6">
|
|
4354
|
+
<nav
|
|
4355
|
+
aria-label="Secciones"
|
|
4356
|
+
class="sticky top-4 hidden shrink-0 self-start lg:block lg:w-44"
|
|
4357
|
+
>
|
|
4358
|
+
<p
|
|
4359
|
+
class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral"
|
|
4360
|
+
aria-hidden="true"
|
|
4361
|
+
>
|
|
4362
|
+
Secciones
|
|
4363
|
+
</p>
|
|
4364
|
+
<ul class="space-y-1 border-l border-neutral/20 pl-3">
|
|
4365
|
+
@for (section of visibleSections(); track section.id) {
|
|
4366
|
+
<li>
|
|
4367
|
+
<button
|
|
4368
|
+
type="button"
|
|
4369
|
+
class="w-full rounded px-2 py-1 text-left text-sm hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
4370
|
+
[class.font-medium]="sectionProgress(section) === 'problem'"
|
|
4371
|
+
(click)="goToSection(section.id)"
|
|
4372
|
+
>
|
|
4373
|
+
{{ section.label }}
|
|
4374
|
+
@switch (sectionProgress(section)) {
|
|
4375
|
+
@case ('problem') {
|
|
4376
|
+
<span class="ml-1 text-danger" aria-hidden="true">⚠</span>
|
|
4377
|
+
<span class="sr-only"> — con errores</span>
|
|
4378
|
+
}
|
|
4379
|
+
@case ('complete') {
|
|
4380
|
+
<span class="ml-1 text-success" aria-hidden="true">✓</span>
|
|
4381
|
+
<span class="sr-only"> — completa</span>
|
|
4382
|
+
}
|
|
4383
|
+
}
|
|
4384
|
+
</button>
|
|
4385
|
+
</li>
|
|
4386
|
+
}
|
|
4387
|
+
</ul>
|
|
4388
|
+
</nav>
|
|
4389
|
+
|
|
4390
|
+
<div class="min-w-0 flex-1" [class.lg:grid]="layoutMode() === 'columns'">
|
|
4391
|
+
<nav
|
|
4392
|
+
aria-label="Secciones"
|
|
4393
|
+
class="-mx-1 mb-4 flex gap-2 overflow-x-auto px-1 pb-1 lg:hidden"
|
|
4394
|
+
>
|
|
4395
|
+
@for (section of visibleSections(); track section.id) {
|
|
4396
|
+
<button
|
|
4397
|
+
type="button"
|
|
4398
|
+
class="shrink-0 rounded-full border border-neutral/30 px-3 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
4399
|
+
[class.border-danger]="sectionProgress(section) === 'problem'"
|
|
4400
|
+
[class.text-danger]="sectionProgress(section) === 'problem'"
|
|
4401
|
+
[class.font-medium]="sectionProgress(section) === 'problem'"
|
|
4402
|
+
(click)="goToSection(section.id)"
|
|
4403
|
+
>
|
|
4404
|
+
{{ section.label }}
|
|
4405
|
+
</button>
|
|
4406
|
+
}
|
|
4407
|
+
</nav>
|
|
4408
|
+
|
|
4409
|
+
@for (section of visibleSections(); track section.id) {
|
|
4410
|
+
<details
|
|
4411
|
+
id="section-{{ section.id }}"
|
|
4412
|
+
class="mb-4 rounded-xl border border-neutral/20 bg-surface scroll-mt-4"
|
|
4413
|
+
[open]="isSectionOpen(section.id)"
|
|
4414
|
+
(toggle)="setSectionOpen(section.id, $event)"
|
|
4415
|
+
>
|
|
4416
|
+
<summary
|
|
4417
|
+
tabindex="-1"
|
|
4418
|
+
class="flex cursor-pointer items-center justify-between gap-2 rounded-xl px-4 py-3 text-section font-semibold hover:bg-neutral/5 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
|
|
4419
|
+
>
|
|
4420
|
+
<span>{{ section.label }}</span>
|
|
4421
|
+
@if (sectionProgress(section) === 'problem') {
|
|
4422
|
+
<span class="text-xs font-medium text-danger">Revisar</span>
|
|
4423
|
+
}
|
|
4424
|
+
</summary>
|
|
4425
|
+
<div class="grid grid-cols-1 gap-x-4 px-4 pb-4 sm:grid-cols-12">
|
|
4426
|
+
@for (field of section.fields; track field.key) {
|
|
4427
|
+
<div [class]="colSpan(field)">
|
|
4428
|
+
<app-field-host
|
|
4429
|
+
[field]="field"
|
|
4430
|
+
[tree]="tree"
|
|
4431
|
+
[exists]="exists()"
|
|
4432
|
+
[serverErrors]="serverErrors().fields"
|
|
4433
|
+
[resource]="resourceId()"
|
|
4434
|
+
/>
|
|
4435
|
+
</div>
|
|
4436
|
+
}
|
|
4437
|
+
</div>
|
|
4438
|
+
</details>
|
|
4439
|
+
}
|
|
4440
|
+
</div>
|
|
4441
|
+
</div>
|
|
4442
|
+
}
|
|
4443
|
+
|
|
4444
|
+
<!-- Sticky (§5.1): en products son 19 campos y Cancelar/Guardar
|
|
4445
|
+
desaparecían al final del scroll. bg-surface es opaco: sin
|
|
4446
|
+
él el contenido vería pasar por detrás. -->
|
|
4447
|
+
<div
|
|
4448
|
+
class="sticky bottom-0 z-10 mt-6 flex flex-wrap items-center justify-end gap-3 border-t border-neutral/20 bg-surface pt-4 pb-[calc(1rem+env(safe-area-inset-bottom))]"
|
|
4449
|
+
>
|
|
4450
|
+
@if (canDelete()) {
|
|
4451
|
+
<div class="mr-auto">
|
|
4452
|
+
<button app-button variant="danger" type="button" (click)="remove()">
|
|
4453
|
+
Eliminar
|
|
4454
|
+
</button>
|
|
4455
|
+
</div>
|
|
4456
|
+
}
|
|
4457
|
+
@if (mode() !== 'create') {
|
|
4458
|
+
<!-- §5: descartar lo escrito y volver a lo del servidor. -->
|
|
4459
|
+
<button app-button variant="ghost" type="button" (click)="restablecer()">
|
|
4460
|
+
Restablecer
|
|
4461
|
+
</button>
|
|
4462
|
+
}
|
|
4463
|
+
<button app-button variant="outline" type="button" (click)="back()">
|
|
4464
|
+
Cancelar
|
|
4465
|
+
</button>
|
|
4466
|
+
<button app-button type="submit" [loading]="saving()" [disabled]="!canSave()">
|
|
4467
|
+
{{ saving() ? 'Guardando…' : 'Guardar' }}
|
|
4468
|
+
</button>
|
|
4469
|
+
</div>
|
|
4470
|
+
</form>
|
|
4471
|
+
}
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
} @else {
|
|
4475
|
+
<app-empty-state
|
|
4476
|
+
title="Recurso no encontrado"
|
|
4477
|
+
message="No existe un schema con ese id en el registry."
|
|
4478
|
+
/>
|
|
4479
|
+
}
|
|
4480
|
+
|
|
4481
|
+
<!-- «Crear nuevo» desde el dropdown de una relación (plan 6.4). Se monta
|
|
4482
|
+
y desmonta con el pedido: el Drawer devuelve el foco al botón que lo
|
|
4483
|
+
abrió al destruirse. -->
|
|
4484
|
+
@if (relationDrawer.request(); as createRequest) {
|
|
4485
|
+
@if (drawerSchema(createRequest.resource); as target) {
|
|
4486
|
+
<app-drawer-form
|
|
4487
|
+
[open]="true"
|
|
4488
|
+
[schema]="target"
|
|
4489
|
+
mode="create"
|
|
4490
|
+
[title]="'Crear ' + target.label"
|
|
4491
|
+
(close)="relationDrawer.clear()"
|
|
4492
|
+
(saved)="relationCreated($event, createRequest)"
|
|
4493
|
+
/>
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
@if (createdWithSummary(); as createdRec) {
|
|
4498
|
+
@if (schema()?.postCreate; as postCreateConfig) {
|
|
4499
|
+
<app-post-create-modal
|
|
4500
|
+
[schema]="schema()!"
|
|
4501
|
+
[config]="postCreateConfig"
|
|
4502
|
+
[record]="createdRec"
|
|
4503
|
+
(closed)="onPostCreateClosed()"
|
|
4504
|
+
/>
|
|
4505
|
+
}
|
|
4506
|
+
}
|
|
4507
|
+
`
|
|
4508
|
+
}]
|
|
4509
|
+
}],
|
|
4510
|
+
ctorParameters: () => []
|
|
4511
|
+
});
|
|
4512
|
+
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 };
|
|
4513
|
+
|
|
4514
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map
|