@tinya-code/gas-admin-ui 0.2.0-alpha.15 → 0.2.0-alpha.17
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/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-DCY8bj2V.mjs +1027 -0
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.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-color-Dljh_By3.mjs +107 -0
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +101 -0
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.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-96FSbN72.mjs +88 -0
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.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-dropdown-C3blvmE1.mjs +122 -0
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-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-DY5y1FjE.mjs +45 -0
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.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-Dmxioog7.mjs +408 -0
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-CEe7aZ4m.mjs +4516 -0
- package/fesm2022/tinya-code-gas-admin-ui-form-view-CEe7aZ4m.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-7UhJ7nqI.mjs +416 -0
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +439 -0
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.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-DTtCYS4d.mjs +637 -0
- package/fesm2022/tinya-code-gas-admin-ui-list-DTtCYS4d.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-PB28e-jl.mjs +251 -0
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.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-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-relation-ykpJ3PYl.mjs +309 -0
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B88Pme-y.mjs +576 -0
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-B88Pme-y.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +161 -0
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +11 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +685 -0
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-DkVZbxPP.mjs +80 -0
- package/fesm2022/tinya-code-gas-admin-ui-seed-DkVZbxPP.mjs.map +1 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +296 -0
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.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-CZPEPCeN.mjs +60 -0
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.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 +4738 -0
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -0
- package/package.json +15 -3
- package/types/tinya-code-gas-admin-ui.d.ts +3634 -0
- package/ng-package.json +0 -7
- package/src/lib/actions/action-registry.spec.ts +0 -52
- package/src/lib/actions/action-registry.ts +0 -126
- package/src/lib/actions/action-service.spec.ts +0 -215
- package/src/lib/actions/action-service.ts +0 -87
- package/src/lib/actions/descriptors.spec.ts +0 -155
- package/src/lib/actions/descriptors.ts +0 -154
- package/src/lib/columns/column-cell.ts +0 -33
- package/src/lib/columns/column-defaults.spec.ts +0 -99
- package/src/lib/columns/column-defaults.ts +0 -84
- package/src/lib/columns/column-format.spec.ts +0 -274
- package/src/lib/columns/descriptors.ts +0 -113
- package/src/lib/columns/format-badge.ts +0 -51
- package/src/lib/columns/format-boolean.ts +0 -58
- package/src/lib/columns/format-currency.ts +0 -30
- package/src/lib/columns/format-date.ts +0 -35
- package/src/lib/columns/format-number.ts +0 -32
- package/src/lib/columns/format-text.ts +0 -23
- package/src/lib/columns/format-thumbnail.ts +0 -63
- package/src/lib/core/adapters/firebase-auth.adapter.ts +0 -6
- package/src/lib/core/guards/auth.guard.spec.ts +0 -88
- package/src/lib/core/guards/auth.guard.ts +0 -48
- package/src/lib/core/guards/unsaved-changes.guard.ts +0 -16
- package/src/lib/core/interceptors/api-error.interceptor.spec.ts +0 -182
- package/src/lib/core/interceptors/api-error.interceptor.ts +0 -87
- package/src/lib/core/models/api.model.ts +0 -104
- package/src/lib/core/models/schema-descriptions.ts +0 -118
- package/src/lib/core/models/schema.model.ts +0 -1097
- package/src/lib/core/registry/composition-validator.spec.ts +0 -358
- package/src/lib/core/registry/composition-validator.ts +0 -270
- package/src/lib/core/registry/field-validators.spec.ts +0 -199
- package/src/lib/core/registry/field-validators.ts +0 -440
- package/src/lib/core/registry/render-host.spec.ts +0 -208
- package/src/lib/core/registry/render-host.ts +0 -646
- package/src/lib/core/registry/render-registry.spec.ts +0 -96
- package/src/lib/core/registry/render-registry.ts +0 -312
- package/src/lib/core/registry/schema-catalog.ts +0 -156
- package/src/lib/core/services/api.service.spec.ts +0 -1142
- package/src/lib/core/services/api.service.ts +0 -371
- package/src/lib/core/services/auth.service.spec.ts +0 -653
- package/src/lib/core/services/auth.service.ts +0 -503
- package/src/lib/core/services/config.service.spec.ts +0 -121
- package/src/lib/core/services/config.service.ts +0 -119
- package/src/lib/core/services/data-source.service.spec.ts +0 -179
- package/src/lib/core/services/data-source.service.ts +0 -142
- package/src/lib/core/services/error-mapper.service.spec.ts +0 -50
- package/src/lib/core/services/error-mapper.service.ts +0 -49
- package/src/lib/core/services/notification.service.ts +0 -73
- package/src/lib/core/services/relation-drawer.service.ts +0 -41
- package/src/lib/core/services/theme.service.spec.ts +0 -106
- package/src/lib/core/services/theme.service.ts +0 -86
- package/src/lib/core/services/transport.spec.ts +0 -198
- package/src/lib/core/services/transport.ts +0 -164
- package/src/lib/core/services/upload.service.ts +0 -81
- package/src/lib/core/testing/categories.schema.ts +0 -125
- package/src/lib/core/testing/orders.schema.ts +0 -372
- package/src/lib/core/testing/products.schema.ts +0 -258
- package/src/lib/core/testing/site.schema.ts +0 -195
- package/src/lib/core/testing/test-catalog.ts +0 -21
- package/src/lib/core/tokens.ts +0 -112
- package/src/lib/core/utils/clipboard.spec.ts +0 -55
- package/src/lib/core/utils/clipboard.ts +0 -90
- package/src/lib/core/utils/context-resolver.spec.ts +0 -95
- package/src/lib/core/utils/context-resolver.ts +0 -101
- package/src/lib/core/utils/response-unwrapper.spec.ts +0 -70
- package/src/lib/core/utils/response-unwrapper.ts +0 -103
- package/src/lib/core/utils/source-options.spec.ts +0 -96
- package/src/lib/core/utils/source-options.ts +0 -54
- package/src/lib/core/utils/template-interpolator.spec.ts +0 -59
- package/src/lib/core/utils/template-interpolator.ts +0 -56
- package/src/lib/core/view-navigation/view-navigation.service.spec.ts +0 -207
- package/src/lib/core/view-navigation/view-navigation.service.ts +0 -132
- package/src/lib/fields/boolean/boolean.descriptor.ts +0 -11
- package/src/lib/fields/boolean/boolean.ts +0 -40
- package/src/lib/fields/color/color.descriptor.ts +0 -12
- package/src/lib/fields/color/color.ts +0 -34
- package/src/lib/fields/currency/currency.descriptor.ts +0 -12
- package/src/lib/fields/currency/currency.ts +0 -67
- package/src/lib/fields/date/date.descriptor.ts +0 -12
- package/src/lib/fields/date/date.ts +0 -36
- package/src/lib/fields/descriptors.ts +0 -122
- package/src/lib/fields/display/display-badge.ts +0 -65
- package/src/lib/fields/display/display-date.ts +0 -39
- package/src/lib/fields/display/display-defaults.ts +0 -84
- package/src/lib/fields/display/display-file.ts +0 -41
- package/src/lib/fields/display/display-image.ts +0 -54
- package/src/lib/fields/display/display-list.ts +0 -107
- package/src/lib/fields/display/display-number.ts +0 -41
- package/src/lib/fields/display/display-text.ts +0 -42
- package/src/lib/fields/display/display.spec.ts +0 -242
- package/src/lib/fields/dropdown/dropdown.descriptor.ts +0 -12
- package/src/lib/fields/dropdown/dropdown.ts +0 -45
- package/src/lib/fields/email/email.descriptor.ts +0 -12
- package/src/lib/fields/email/email.ts +0 -28
- package/src/lib/fields/field-aria.ts +0 -60
- package/src/lib/fields/field-host/field-host.spec.ts +0 -118
- package/src/lib/fields/field-host/field-host.ts +0 -47
- package/src/lib/fields/field-node.ts +0 -88
- package/src/lib/fields/file/file.descriptor.ts +0 -14
- package/src/lib/fields/file/file.spec.ts +0 -134
- package/src/lib/fields/file/file.ts +0 -281
- package/src/lib/fields/group/group.descriptor.ts +0 -21
- package/src/lib/fields/group/group.ts +0 -110
- package/src/lib/fields/image/image.descriptor.ts +0 -12
- package/src/lib/fields/image/image.spec.ts +0 -117
- package/src/lib/fields/image/image.ts +0 -281
- package/src/lib/fields/key-value/key-value.descriptor.ts +0 -11
- package/src/lib/fields/key-value/key-value.ts +0 -283
- package/src/lib/fields/list/list.descriptor.ts +0 -21
- package/src/lib/fields/list/list.ts +0 -376
- package/src/lib/fields/multiselect/multiselect.descriptor.ts +0 -11
- package/src/lib/fields/multiselect/multiselect.spec.ts +0 -142
- package/src/lib/fields/multiselect/multiselect.ts +0 -152
- package/src/lib/fields/number/number.descriptor.ts +0 -12
- package/src/lib/fields/number/number.ts +0 -40
- package/src/lib/fields/phone/phone.descriptor.ts +0 -12
- package/src/lib/fields/phone/phone.ts +0 -46
- package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +0 -12
- package/src/lib/fields/readonly-text/readonly-text.ts +0 -58
- package/src/lib/fields/relation/relation.descriptor.ts +0 -12
- package/src/lib/fields/relation/relation.spec.ts +0 -193
- package/src/lib/fields/relation/relation.ts +0 -224
- package/src/lib/fields/rich-select/rich-select.descriptor.ts +0 -12
- package/src/lib/fields/rich-select/rich-select.ts +0 -175
- package/src/lib/fields/select/radio-mode.ts +0 -25
- package/src/lib/fields/select/select.descriptor.ts +0 -12
- package/src/lib/fields/select/select.spec.ts +0 -162
- package/src/lib/fields/select/select.ts +0 -210
- package/src/lib/fields/slug/slug.descriptor.ts +0 -19
- package/src/lib/fields/slug/slug.ts +0 -67
- package/src/lib/fields/string-list/string-list.descriptor.ts +0 -11
- package/src/lib/fields/string-list/string-list.ts +0 -222
- package/src/lib/fields/text/text.descriptor.ts +0 -12
- package/src/lib/fields/text/text.ts +0 -35
- package/src/lib/fields/textarea/textarea.descriptor.ts +0 -12
- package/src/lib/fields/textarea/textarea.spec.ts +0 -117
- package/src/lib/fields/textarea/textarea.ts +0 -126
- package/src/lib/fields/time/time.descriptor.ts +0 -12
- package/src/lib/fields/time/time.ts +0 -28
- package/src/lib/fields/unknown-render/unknown-render.ts +0 -49
- package/src/lib/fields/url/url.descriptor.ts +0 -12
- package/src/lib/fields/url/url.ts +0 -53
- package/src/lib/gas-admin-ui.spec.ts +0 -21
- package/src/lib/gas-admin-ui.ts +0 -9
- package/src/lib/layouts/descriptors.ts +0 -9
- package/src/lib/pages/dashboard/dashboard.ts +0 -167
- package/src/lib/pages/detail-view/detail-view.spec.ts +0 -333
- package/src/lib/pages/detail-view/detail-view.ts +0 -570
- package/src/lib/pages/drawer-form/drawer-form.ts +0 -228
- package/src/lib/pages/form-view/form-model.ts +0 -88
- package/src/lib/pages/form-view/form-schema.ts +0 -255
- package/src/lib/pages/form-view/form-view.ts +0 -1622
- package/src/lib/pages/list-view/list-view.formats.spec.ts +0 -214
- package/src/lib/pages/list-view/list-view.ts +0 -882
- package/src/lib/pages/login/login.spec.ts +0 -109
- package/src/lib/pages/login/login.ts +0 -227
- package/src/lib/pages/resource-page/resource-page.ts +0 -234
- package/src/lib/shared/components/badge/badge.ts +0 -35
- package/src/lib/shared/components/button/button.ts +0 -65
- package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +0 -48
- package/src/lib/shared/components/copy-button/copy-button.ts +0 -56
- package/src/lib/shared/components/copy-share/copy-share.spec.ts +0 -122
- package/src/lib/shared/components/copy-share/copy-share.ts +0 -107
- package/src/lib/shared/components/drawer/drawer.spec.ts +0 -127
- package/src/lib/shared/components/drawer/drawer.ts +0 -172
- package/src/lib/shared/components/empty-state/empty-state.ts +0 -30
- package/src/lib/shared/components/modal/modal.spec.ts +0 -149
- package/src/lib/shared/components/modal/modal.ts +0 -145
- package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +0 -165
- package/src/lib/shared/components/post-create-modal/post-create-modal.ts +0 -159
- package/src/lib/shared/components/skeleton/skeleton.ts +0 -31
- package/src/lib/shared/components/toast/toast.ts +0 -58
- package/src/lib/shared/utils/col-span.ts +0 -25
- package/src/lib/shared/utils/condition-evaluator.ts +0 -53
- package/src/lib/shared/utils/depends-on.ts +0 -48
- package/src/lib/shared/utils/dialog-focus.ts +0 -28
- package/src/lib/shared/utils/field-path.ts +0 -49
- package/src/lib/shared/utils/gap-sorting.ts +0 -144
- package/src/lib/shared/utils/seed.spec.ts +0 -64
- package/src/lib/shared/utils/seed.ts +0 -149
- package/src/lib/shared/utils/slugify.ts +0 -13
- package/src/lib/shared/utils/validators.ts +0 -41
- package/src/lib/shell/layout/layout.ts +0 -63
- package/src/lib/shell/shell.routes.spec.ts +0 -59
- package/src/lib/shell/shell.routes.ts +0 -122
- package/src/lib/shell/sidebar/sidebar.ts +0 -139
- package/src/lib/shell/theme-toggle/theme-toggle.ts +0 -74
- package/src/lib/views/calendar-view/calendar-model.ts +0 -426
- package/src/lib/views/calendar-view/calendar-view.spec.ts +0 -557
- package/src/lib/views/calendar-view/calendar-view.ts +0 -568
- package/src/lib/views/composer/composer-host.spec.ts +0 -301
- package/src/lib/views/composer/composer-host.ts +0 -248
- package/src/lib/views/composer/composer-imports.spec.ts +0 -54
- package/src/lib/views/composer/view-context.spec.ts +0 -117
- package/src/lib/views/composer/view-context.ts +0 -109
- package/src/lib/views/composer/view-slot.spec.ts +0 -130
- package/src/lib/views/composer/view-slot.ts +0 -178
- package/src/lib/views/descriptors.ts +0 -46
- package/src/lib/views/kanban-view/kanban-model.ts +0 -173
- package/src/lib/views/kanban-view/kanban-view.spec.ts +0 -540
- package/src/lib/views/kanban-view/kanban-view.ts +0 -327
- package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +0 -17
- package/src/lib/widgets/bar-chart/bar-chart.spec.ts +0 -66
- package/src/lib/widgets/bar-chart/bar-chart.ts +0 -76
- package/src/lib/widgets/chart-line/chart-line.descriptor.ts +0 -17
- package/src/lib/widgets/chart-line/chart-line.spec.ts +0 -113
- package/src/lib/widgets/chart-line/chart-line.ts +0 -219
- package/src/lib/widgets/descriptors.ts +0 -29
- package/src/lib/widgets/metric-card/metric-card.descriptor.ts +0 -41
- package/src/lib/widgets/metric-card/metric-card.spec.ts +0 -52
- package/src/lib/widgets/metric-card/metric-card.ts +0 -48
- package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +0 -13
- package/src/lib/widgets/quick-actions/quick-actions.spec.ts +0 -61
- package/src/lib/widgets/quick-actions/quick-actions.ts +0 -100
- package/src/lib/widgets/record-list/record-list.descriptor.ts +0 -16
- package/src/lib/widgets/record-list/record-list.spec.ts +0 -79
- package/src/lib/widgets/record-list/record-list.ts +0 -115
- package/src/lib/widgets/series.spec.ts +0 -42
- package/src/lib/widgets/series.ts +0 -56
- package/src/lib/widgets/status-progress/status-progress.descriptor.ts +0 -19
- package/src/lib/widgets/status-progress/status-progress.spec.ts +0 -63
- package/src/lib/widgets/status-progress/status-progress.ts +0 -104
- package/src/lib/widgets/widget-host/widget-host.spec.ts +0 -179
- package/src/lib/widgets/widget-host/widget-host.ts +0 -99
- package/src/public-api.ts +0 -104
- package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
- package/tsconfig.lib.json +0 -13
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -10
|
@@ -0,0 +1,4738 @@
|
|
|
1
|
+
import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
|
|
2
|
+
import { _ as DEFAULT_RESPONSE_UNWRAP, a as provideFirebaseAdapter, b as ApiError, c as ConfigService, d as AuthService, f as LOGIN_PATH, g as API_URL, h as API_DRIVER, i as getSchema, l as ApiService, m as SESSION_USER_KEY, n as allSchemas, o as provideGasAdmin, p as SESSION_TOKEN_KEY, r as getAuth, s as setSchemaCatalog, t as GAS_SCHEMAS, u as apiErrorInterceptor, v as FIREBASE_AUTH_ADAPTER, x as renewedTokenOf, y as VALUE_SOURCES } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
|
|
3
|
+
import { a as register, c as unregister, d as SUPPORTED_FIELD_TYPE_LIST, f as fieldDescriptors, i as provideRenderDescriptors, l as viewDescriptors, m as columnFormatDescriptors, n as get, o as renderKinds, p as COLUMN_FORMAT_TYPES, r as isSupported, u as layoutDescriptors } from "./tinya-code-gas-admin-ui-render-registry-B88Pme-y.mjs";
|
|
4
|
+
import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs";
|
|
5
|
+
import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
|
|
6
|
+
import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
|
|
7
|
+
import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
|
|
8
|
+
import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs";
|
|
9
|
+
import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs";
|
|
10
|
+
import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs";
|
|
11
|
+
import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
|
|
12
|
+
import { a as stringItemTree, i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
|
|
13
|
+
import { _ as resolveShare, a as DrawerForm, c as buildFormSchema, d as actionTypes, f as actionsFor, g as isTriggerAction, h as isShareAction, i as CopyShare, l as FieldHost, m as registerAction, o as buildPayload, p as getAction, r as Modal, s as seedModel, t as FormView, u as actionType, v as writeClipboard, y as RenderHost } from "./tinya-code-gas-admin-ui-form-view-CEe7aZ4m.mjs";
|
|
14
|
+
import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
|
|
15
|
+
import { t as getValue } from "./tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs";
|
|
16
|
+
import * as i0 from "@angular/core";
|
|
17
|
+
import { ChangeDetectionStrategy, Component, EnvironmentInjector, Injector, Service, ViewContainerRef, computed, effect, inject, input, model, output, resource, signal, viewChild } from "@angular/core";
|
|
18
|
+
import { catchError, combineLatest, distinctUntilChanged, firstValueFrom, map, of, switchMap } from "rxjs";
|
|
19
|
+
import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet, convertToParamMap } from "@angular/router";
|
|
20
|
+
import { LucideArrowLeft, LucideCheck, LucideChevronDown, LucideChevronRight, LucideChevronUp, LucideCircleCheck, LucideCircleX, LucideCopy, LucideDynamicIcon, LucideExternalLink, LucideEye, LucideGripVertical, LucideInfo, LucideLogOut, LucideMonitor, LucideMoon, LucidePencil, LucidePlus, LucideRotateCw, LucideSun, LucideTrash2, LucideX, LucideZap } from "@lucide/angular";
|
|
21
|
+
import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop";
|
|
22
|
+
import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop";
|
|
23
|
+
const authGuard = () => {
|
|
24
|
+
if (inject(AuthService).isAuthenticated()) return true;
|
|
25
|
+
return inject(Router).createUrlTree([LOGIN_PATH]);
|
|
26
|
+
};
|
|
27
|
+
const roleGuard = (route) => {
|
|
28
|
+
const auth = inject(AuthService);
|
|
29
|
+
const router = inject(Router);
|
|
30
|
+
if (!auth.isAuthenticated()) return router.createUrlTree([LOGIN_PATH]);
|
|
31
|
+
const id = route.data?.["resourceId"] ?? route.paramMap.get("id");
|
|
32
|
+
if (id === null) return true;
|
|
33
|
+
const schema = getSchema(String(id));
|
|
34
|
+
if (schema === void 0) return true;
|
|
35
|
+
return auth.canAccess(schema.access);
|
|
36
|
+
};
|
|
37
|
+
const MAX_DEPTH = 3;
|
|
38
|
+
var SchemaCompositionError = class extends Error {
|
|
39
|
+
constructor(message) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = "SchemaCompositionError";
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
function configDe(vista) {
|
|
45
|
+
return "config" in vista ? vista.config : void 0;
|
|
46
|
+
}
|
|
47
|
+
function hijosDe(vista) {
|
|
48
|
+
const hijos = configDe(vista)?.children;
|
|
49
|
+
return Array.isArray(hijos) ? hijos : void 0;
|
|
50
|
+
}
|
|
51
|
+
function validateComposition(schema) {
|
|
52
|
+
const estado = {
|
|
53
|
+
esquema: schema,
|
|
54
|
+
nodos: 0,
|
|
55
|
+
contados: /* @__PURE__ */ new Set(),
|
|
56
|
+
camino: /* @__PURE__ */ new Set()
|
|
57
|
+
};
|
|
58
|
+
for (const [nombre, vista] of Object.entries(schema.uiViews ?? {})) {
|
|
59
|
+
estado.camino.add(nombre);
|
|
60
|
+
validarContenido(nombre, vista, 1, estado);
|
|
61
|
+
estado.camino.delete(nombre);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function validarContenido(nombre, vista, profundidadHijos, estado) {
|
|
65
|
+
const donde = `${estado.esquema.id}.${nombre}`;
|
|
66
|
+
validarSelect(donde, vista.select);
|
|
67
|
+
validarSource(donde, configDe(vista)?.source);
|
|
68
|
+
const hijos = hijosDe(vista);
|
|
69
|
+
if (!hijos) return;
|
|
70
|
+
validarHijos(nombre, hijos, profundidadHijos, estado);
|
|
71
|
+
}
|
|
72
|
+
function validarHijos(contenedor, hijos, profundidad, estado) {
|
|
73
|
+
const ids = /* @__PURE__ */ new Set();
|
|
74
|
+
for (const hijo of hijos) {
|
|
75
|
+
const id = hijo.id;
|
|
76
|
+
if (typeof id !== "string" || id === "") throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: hijo sin id`);
|
|
77
|
+
if (ids.has(id)) throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: id duplicado '${id}'`);
|
|
78
|
+
ids.add(id);
|
|
79
|
+
validarHijo(hijo, contenedor, id, profundidad, estado);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function validarHijo(hijo, contenedor, id, profundidad, estado) {
|
|
83
|
+
const donde = `${estado.esquema.id}.${contenedor} → child '${id}'`;
|
|
84
|
+
if (!estado.contados.has(hijo)) {
|
|
85
|
+
estado.contados.add(hijo);
|
|
86
|
+
estado.nodos += 1;
|
|
87
|
+
if (estado.nodos > 50) throw new SchemaCompositionError(`${estado.esquema.id}: la composición supera el límite de 50 nodos (observados ${estado.nodos})`);
|
|
88
|
+
}
|
|
89
|
+
if (profundidad > 3) throw new SchemaCompositionError(`${donde}: profundidad ${profundidad} supera el máximo 3`);
|
|
90
|
+
if ("ref" in hijo) {
|
|
91
|
+
validarRef(donde, hijo.ref, profundidad, estado);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const tipo = hijo.type;
|
|
95
|
+
if (!get("view", tipo)) throw new SchemaCompositionError(`${donde}: tipo desconocido '${tipo}'`);
|
|
96
|
+
validarSelect(donde, hijo.select);
|
|
97
|
+
validarSource(donde, configDe(hijo)?.source);
|
|
98
|
+
const internos = hijosDe(hijo);
|
|
99
|
+
if (internos !== void 0) validarHijos(id, internos, profundidad + 1, estado);
|
|
100
|
+
}
|
|
101
|
+
function validarRef(donde, target, profundidad, estado) {
|
|
102
|
+
if (estado.camino.has(target)) throw new SchemaCompositionError(`${donde}: ciclo detectado — '${target}' ya está en el camino`);
|
|
103
|
+
const vista = estado.esquema.uiViews?.[target];
|
|
104
|
+
if (!vista) throw new SchemaCompositionError(`${donde}: ref '${target}' no existe en ${estado.esquema.id}.uiViews`);
|
|
105
|
+
estado.camino.add(target);
|
|
106
|
+
validarContenido(target, vista, profundidad + 1, estado);
|
|
107
|
+
estado.camino.delete(target);
|
|
108
|
+
}
|
|
109
|
+
function validarSelect(donde, select) {
|
|
110
|
+
if (select === void 0 || select.mode !== "route") return;
|
|
111
|
+
const target = select.target;
|
|
112
|
+
if (target === void 0 || target === null) throw new SchemaCompositionError(`${donde}: select mode 'route' sin target`);
|
|
113
|
+
if (typeof target === "string") throw new SchemaCompositionError(`${donde}: destino con URL cruda '${target}' — declará resource + view`);
|
|
114
|
+
validarTarget(donde, target);
|
|
115
|
+
}
|
|
116
|
+
function validarTarget(donde, target) {
|
|
117
|
+
const { resource, view, params } = target;
|
|
118
|
+
if (typeof resource !== "string" || resource === "" || typeof view !== "string" || view === "") throw new SchemaCompositionError(`${donde}: destino sin resource + view`);
|
|
119
|
+
const destino = getSchema(resource);
|
|
120
|
+
if (!destino) throw new SchemaCompositionError(`${donde}: destino '${resource}' no existe`);
|
|
121
|
+
const vista = destino.uiViews?.[view];
|
|
122
|
+
if (!vista) throw new SchemaCompositionError(`${donde}: destino ${resource}.${view} no tiene uiView declarada`);
|
|
123
|
+
const paramsRuta = (vista.path ?? "").split("/").filter((segmento) => segmento.startsWith(":")).map((segmento) => segmento.slice(1));
|
|
124
|
+
const declarados = params ?? [];
|
|
125
|
+
for (const param of declarados) if (!paramsRuta.includes(param)) throw new SchemaCompositionError(`${donde}: param '${param}' no existe en la ruta ${resource}.${view}`);
|
|
126
|
+
for (const param of paramsRuta) if (!declarados.includes(param)) throw new SchemaCompositionError(`${donde}: falta declarar el param '${param}' de la ruta ${resource}.${view}`);
|
|
127
|
+
}
|
|
128
|
+
function validarSource(donde, source) {
|
|
129
|
+
if (source === void 0) return;
|
|
130
|
+
if (typeof source.resource !== "string" || source.resource === "") throw new SchemaCompositionError(`${donde}: source sin resource`);
|
|
131
|
+
const origen = getSchema(source.resource);
|
|
132
|
+
if (!origen) throw new SchemaCompositionError(`${donde}: source '${source.resource}' no existe`);
|
|
133
|
+
if (source.view !== void 0 && origen.views?.[source.view] === void 0) throw new SchemaCompositionError(`${donde}: la vista '${source.view}' no existe en ${source.resource}.views`);
|
|
134
|
+
}
|
|
135
|
+
const unsavedChangesGuard = (component) => {
|
|
136
|
+
return component?.canDeactivate?.() ?? true;
|
|
137
|
+
};
|
|
138
|
+
function instanceIdFor(parentId, childId) {
|
|
139
|
+
return `${parentId}/${childId}`;
|
|
140
|
+
}
|
|
141
|
+
function virtualSchemaFor(schema, instanceId, view) {
|
|
142
|
+
return {
|
|
143
|
+
...schema,
|
|
144
|
+
uiViews: {
|
|
145
|
+
...schema.uiViews,
|
|
146
|
+
[instanceId]: view
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function syntheticRoute(data) {
|
|
151
|
+
return {
|
|
152
|
+
data: of(data),
|
|
153
|
+
paramMap: of(convertToParamMap({}))
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function createViewContext(base, emit) {
|
|
157
|
+
return emit === void 0 ? { ...base } : {
|
|
158
|
+
...base,
|
|
159
|
+
emit
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
const STORAGE_KEY = "admin-theme";
|
|
163
|
+
const QUERY = "(prefers-color-scheme: dark)";
|
|
164
|
+
const MODES = [
|
|
165
|
+
"system",
|
|
166
|
+
"light",
|
|
167
|
+
"dark"
|
|
168
|
+
];
|
|
169
|
+
function storedMode() {
|
|
170
|
+
try {
|
|
171
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
172
|
+
return raw !== null && MODES.includes(raw) ? raw : "system";
|
|
173
|
+
} catch {
|
|
174
|
+
return "system";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function prefersDark() {
|
|
178
|
+
return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
|
|
179
|
+
}
|
|
180
|
+
function resolve(mode) {
|
|
181
|
+
return mode === "system" ? prefersDark() ? "dark" : "light" : mode;
|
|
182
|
+
}
|
|
183
|
+
var ThemeService = class ThemeService {
|
|
184
|
+
mode = signal(storedMode(), ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
|
|
185
|
+
resolved = signal(resolve(this.mode()), ...ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []);
|
|
186
|
+
constructor() {
|
|
187
|
+
this.apply();
|
|
188
|
+
(typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null)?.addEventListener("change", () => {
|
|
189
|
+
if (this.mode() === "system") this.apply();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
set(mode) {
|
|
193
|
+
this.mode.set(mode);
|
|
194
|
+
try {
|
|
195
|
+
localStorage.setItem(STORAGE_KEY, mode);
|
|
196
|
+
} catch {}
|
|
197
|
+
this.apply();
|
|
198
|
+
}
|
|
199
|
+
apply() {
|
|
200
|
+
const resolved = resolve(this.mode());
|
|
201
|
+
this.resolved.set(resolved);
|
|
202
|
+
document.documentElement.dataset["theme"] = resolved;
|
|
203
|
+
document.documentElement.dataset["themeMode"] = this.mode();
|
|
204
|
+
}
|
|
205
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
206
|
+
minVersion: "12.0.0",
|
|
207
|
+
version: "22.2.3",
|
|
208
|
+
ngImport: i0,
|
|
209
|
+
type: ThemeService,
|
|
210
|
+
deps: [],
|
|
211
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
212
|
+
});
|
|
213
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
214
|
+
minVersion: "22.0.0",
|
|
215
|
+
version: "22.2.3",
|
|
216
|
+
ngImport: i0,
|
|
217
|
+
type: ThemeService
|
|
218
|
+
});
|
|
219
|
+
};
|
|
220
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
221
|
+
minVersion: "12.0.0",
|
|
222
|
+
version: "22.2.3",
|
|
223
|
+
ngImport: i0,
|
|
224
|
+
type: ThemeService,
|
|
225
|
+
decorators: [{ type: Service }],
|
|
226
|
+
ctorParameters: () => []
|
|
227
|
+
});
|
|
228
|
+
const BASE = "rounded-md p-1.5 transition-colors min-h-11 min-w-11 lg:min-h-0 lg:min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
|
229
|
+
const ACTIVE = "bg-primary/10 text-primary";
|
|
230
|
+
const IDLE = "text-neutral hover:bg-neutral/10";
|
|
231
|
+
var ThemeToggle = class ThemeToggle {
|
|
232
|
+
theme = inject(ThemeService);
|
|
233
|
+
classes(mode) {
|
|
234
|
+
return `${BASE} ${this.theme.mode() === mode ? ACTIVE : IDLE}`;
|
|
235
|
+
}
|
|
236
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
237
|
+
minVersion: "12.0.0",
|
|
238
|
+
version: "22.2.3",
|
|
239
|
+
ngImport: i0,
|
|
240
|
+
type: ThemeToggle,
|
|
241
|
+
deps: [],
|
|
242
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
243
|
+
});
|
|
244
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
245
|
+
minVersion: "14.0.0",
|
|
246
|
+
version: "22.2.3",
|
|
247
|
+
type: ThemeToggle,
|
|
248
|
+
isStandalone: true,
|
|
249
|
+
selector: "app-theme-toggle",
|
|
250
|
+
ngImport: i0,
|
|
251
|
+
template: `
|
|
252
|
+
<div
|
|
253
|
+
role="group"
|
|
254
|
+
aria-label="Tema del panel"
|
|
255
|
+
class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
|
|
256
|
+
>
|
|
257
|
+
<button
|
|
258
|
+
type="button"
|
|
259
|
+
[class]="classes('system')"
|
|
260
|
+
[attr.aria-pressed]="theme.mode() === 'system'"
|
|
261
|
+
aria-label="Tema: seguir al sistema"
|
|
262
|
+
title="Seguir al sistema"
|
|
263
|
+
(click)="theme.set('system')"
|
|
264
|
+
>
|
|
265
|
+
<svg lucideMonitor size="16" />
|
|
266
|
+
</button>
|
|
267
|
+
<button
|
|
268
|
+
type="button"
|
|
269
|
+
[class]="classes('light')"
|
|
270
|
+
[attr.aria-pressed]="theme.mode() === 'light'"
|
|
271
|
+
aria-label="Tema: claro"
|
|
272
|
+
title="Tema claro"
|
|
273
|
+
(click)="theme.set('light')"
|
|
274
|
+
>
|
|
275
|
+
<svg lucideSun size="16" />
|
|
276
|
+
</button>
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
[class]="classes('dark')"
|
|
280
|
+
[attr.aria-pressed]="theme.mode() === 'dark'"
|
|
281
|
+
aria-label="Tema: oscuro"
|
|
282
|
+
title="Tema oscuro"
|
|
283
|
+
(click)="theme.set('dark')"
|
|
284
|
+
>
|
|
285
|
+
<svg lucideMoon size="16" />
|
|
286
|
+
</button>
|
|
287
|
+
</div>
|
|
288
|
+
`,
|
|
289
|
+
isInline: true,
|
|
290
|
+
dependencies: [
|
|
291
|
+
{
|
|
292
|
+
kind: "component",
|
|
293
|
+
type: LucideMonitor,
|
|
294
|
+
selector: "svg[lucideMonitor]"
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
kind: "component",
|
|
298
|
+
type: LucideSun,
|
|
299
|
+
selector: "svg[lucideSun]"
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
kind: "component",
|
|
303
|
+
type: LucideMoon,
|
|
304
|
+
selector: "svg[lucideMoon]"
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
310
|
+
minVersion: "12.0.0",
|
|
311
|
+
version: "22.2.3",
|
|
312
|
+
ngImport: i0,
|
|
313
|
+
type: ThemeToggle,
|
|
314
|
+
decorators: [{
|
|
315
|
+
type: Component,
|
|
316
|
+
args: [{
|
|
317
|
+
selector: "app-theme-toggle",
|
|
318
|
+
imports: [
|
|
319
|
+
LucideMonitor,
|
|
320
|
+
LucideSun,
|
|
321
|
+
LucideMoon
|
|
322
|
+
],
|
|
323
|
+
template: `
|
|
324
|
+
<div
|
|
325
|
+
role="group"
|
|
326
|
+
aria-label="Tema del panel"
|
|
327
|
+
class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
|
|
328
|
+
>
|
|
329
|
+
<button
|
|
330
|
+
type="button"
|
|
331
|
+
[class]="classes('system')"
|
|
332
|
+
[attr.aria-pressed]="theme.mode() === 'system'"
|
|
333
|
+
aria-label="Tema: seguir al sistema"
|
|
334
|
+
title="Seguir al sistema"
|
|
335
|
+
(click)="theme.set('system')"
|
|
336
|
+
>
|
|
337
|
+
<svg lucideMonitor size="16" />
|
|
338
|
+
</button>
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
[class]="classes('light')"
|
|
342
|
+
[attr.aria-pressed]="theme.mode() === 'light'"
|
|
343
|
+
aria-label="Tema: claro"
|
|
344
|
+
title="Tema claro"
|
|
345
|
+
(click)="theme.set('light')"
|
|
346
|
+
>
|
|
347
|
+
<svg lucideSun size="16" />
|
|
348
|
+
</button>
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
[class]="classes('dark')"
|
|
352
|
+
[attr.aria-pressed]="theme.mode() === 'dark'"
|
|
353
|
+
aria-label="Tema: oscuro"
|
|
354
|
+
title="Tema oscuro"
|
|
355
|
+
(click)="theme.set('dark')"
|
|
356
|
+
>
|
|
357
|
+
<svg lucideMoon size="16" />
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
`
|
|
361
|
+
}]
|
|
362
|
+
}]
|
|
363
|
+
});
|
|
364
|
+
var Sidebar = class Sidebar {
|
|
365
|
+
open = model(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
366
|
+
auth = inject(AuthService);
|
|
367
|
+
notifications = inject(NotificationService);
|
|
368
|
+
isAuthenticated = this.auth.isAuthenticated;
|
|
369
|
+
loggingOut = signal(false, ...ngDevMode ? [{ debugName: "loggingOut" }] : /* istanbul ignore next */ []);
|
|
370
|
+
menuItems = computed(() => allSchemas().filter((schema) => isVisible(schema) && this.auth.canAccess(schema.access)).map((schema) => ({
|
|
371
|
+
id: schema.id,
|
|
372
|
+
label: schema.labelPlural
|
|
373
|
+
})), ...ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []);
|
|
374
|
+
async closeSession() {
|
|
375
|
+
if (!await this.notifications.confirm({
|
|
376
|
+
title: "Cerrar sesión",
|
|
377
|
+
message: "¿Seguro que querés cerrar tu sesión?",
|
|
378
|
+
confirmLabel: "Cerrar sesión"
|
|
379
|
+
})) return;
|
|
380
|
+
this.loggingOut.set(true);
|
|
381
|
+
this.auth.logout().subscribe({ complete: () => this.loggingOut.set(false) });
|
|
382
|
+
}
|
|
383
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
384
|
+
minVersion: "12.0.0",
|
|
385
|
+
version: "22.2.3",
|
|
386
|
+
ngImport: i0,
|
|
387
|
+
type: Sidebar,
|
|
388
|
+
deps: [],
|
|
389
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
390
|
+
});
|
|
391
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
392
|
+
minVersion: "17.0.0",
|
|
393
|
+
version: "22.2.3",
|
|
394
|
+
type: Sidebar,
|
|
395
|
+
isStandalone: true,
|
|
396
|
+
selector: "app-sidebar",
|
|
397
|
+
inputs: { open: {
|
|
398
|
+
classPropertyName: "open",
|
|
399
|
+
publicName: "open",
|
|
400
|
+
isSignal: true,
|
|
401
|
+
isRequired: false,
|
|
402
|
+
transformFunction: null
|
|
403
|
+
} },
|
|
404
|
+
outputs: { open: "openChange" },
|
|
405
|
+
ngImport: i0,
|
|
406
|
+
template: `
|
|
407
|
+
<!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
|
|
408
|
+
y siempre visible en pantallas grandes. -->
|
|
409
|
+
<aside
|
|
410
|
+
class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
|
|
411
|
+
[class.-translate-x-full]="!open()"
|
|
412
|
+
>
|
|
413
|
+
<div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
|
|
414
|
+
<!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
|
|
415
|
+
trunca en vez de desbordar la sidebar de 240 px. -->
|
|
416
|
+
<span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
|
|
417
|
+
<app-theme-toggle />
|
|
418
|
+
</div>
|
|
419
|
+
<nav aria-label="Principal" class="p-3">
|
|
420
|
+
<ul class="space-y-2 lg:space-y-1">
|
|
421
|
+
@for (item of menuItems(); track item.id) {
|
|
422
|
+
<li>
|
|
423
|
+
<a
|
|
424
|
+
[routerLink]="['/', item.id]"
|
|
425
|
+
routerLinkActive="bg-primary/10 text-primary"
|
|
426
|
+
class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
|
|
427
|
+
(click)="open.set(false)"
|
|
428
|
+
>
|
|
429
|
+
{{ item.label }}
|
|
430
|
+
</a>
|
|
431
|
+
</li>
|
|
432
|
+
}
|
|
433
|
+
</ul>
|
|
434
|
+
</nav>
|
|
435
|
+
@if (isAuthenticated()) {
|
|
436
|
+
<div class="border-t border-neutral/20 p-3">
|
|
437
|
+
<button
|
|
438
|
+
app-button
|
|
439
|
+
type="button"
|
|
440
|
+
variant="danger"
|
|
441
|
+
[loading]="loggingOut()"
|
|
442
|
+
[disabled]="loggingOut()"
|
|
443
|
+
(click)="closeSession()"
|
|
444
|
+
class="w-full"
|
|
445
|
+
>
|
|
446
|
+
<svg lucideLogOut size="16" />
|
|
447
|
+
Cerrar sesión
|
|
448
|
+
</button>
|
|
449
|
+
</div>
|
|
450
|
+
}
|
|
451
|
+
</aside>
|
|
452
|
+
`,
|
|
453
|
+
isInline: true,
|
|
454
|
+
dependencies: [
|
|
455
|
+
{
|
|
456
|
+
kind: "directive",
|
|
457
|
+
type: RouterLink,
|
|
458
|
+
selector: "[routerLink]",
|
|
459
|
+
inputs: [
|
|
460
|
+
"target",
|
|
461
|
+
"queryParams",
|
|
462
|
+
"fragment",
|
|
463
|
+
"queryParamsHandling",
|
|
464
|
+
"state",
|
|
465
|
+
"info",
|
|
466
|
+
"relativeTo",
|
|
467
|
+
"preserveFragment",
|
|
468
|
+
"skipLocationChange",
|
|
469
|
+
"replaceUrl",
|
|
470
|
+
"browserUrl",
|
|
471
|
+
"routerLink"
|
|
472
|
+
]
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
kind: "directive",
|
|
476
|
+
type: RouterLinkActive,
|
|
477
|
+
selector: "[routerLinkActive]",
|
|
478
|
+
inputs: [
|
|
479
|
+
"routerLinkActiveOptions",
|
|
480
|
+
"ariaCurrentWhenActive",
|
|
481
|
+
"routerLinkActive"
|
|
482
|
+
],
|
|
483
|
+
outputs: ["isActiveChange"],
|
|
484
|
+
exportAs: ["routerLinkActive"]
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
kind: "component",
|
|
488
|
+
type: ThemeToggle,
|
|
489
|
+
selector: "app-theme-toggle"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
kind: "component",
|
|
493
|
+
type: Button,
|
|
494
|
+
selector: "button[app-button]",
|
|
495
|
+
inputs: [
|
|
496
|
+
"variant",
|
|
497
|
+
"size",
|
|
498
|
+
"type",
|
|
499
|
+
"disabled",
|
|
500
|
+
"loading"
|
|
501
|
+
]
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
kind: "component",
|
|
505
|
+
type: LucideLogOut,
|
|
506
|
+
selector: "svg[lucideLogOut]"
|
|
507
|
+
}
|
|
508
|
+
]
|
|
509
|
+
});
|
|
510
|
+
};
|
|
511
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
512
|
+
minVersion: "12.0.0",
|
|
513
|
+
version: "22.2.3",
|
|
514
|
+
ngImport: i0,
|
|
515
|
+
type: Sidebar,
|
|
516
|
+
decorators: [{
|
|
517
|
+
type: Component,
|
|
518
|
+
args: [{
|
|
519
|
+
selector: "app-sidebar",
|
|
520
|
+
imports: [
|
|
521
|
+
RouterLink,
|
|
522
|
+
RouterLinkActive,
|
|
523
|
+
ThemeToggle,
|
|
524
|
+
Button,
|
|
525
|
+
LucideLogOut
|
|
526
|
+
],
|
|
527
|
+
template: `
|
|
528
|
+
<!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
|
|
529
|
+
y siempre visible en pantallas grandes. -->
|
|
530
|
+
<aside
|
|
531
|
+
class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
|
|
532
|
+
[class.-translate-x-full]="!open()"
|
|
533
|
+
>
|
|
534
|
+
<div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
|
|
535
|
+
<!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
|
|
536
|
+
trunca en vez de desbordar la sidebar de 240 px. -->
|
|
537
|
+
<span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
|
|
538
|
+
<app-theme-toggle />
|
|
539
|
+
</div>
|
|
540
|
+
<nav aria-label="Principal" class="p-3">
|
|
541
|
+
<ul class="space-y-2 lg:space-y-1">
|
|
542
|
+
@for (item of menuItems(); track item.id) {
|
|
543
|
+
<li>
|
|
544
|
+
<a
|
|
545
|
+
[routerLink]="['/', item.id]"
|
|
546
|
+
routerLinkActive="bg-primary/10 text-primary"
|
|
547
|
+
class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
|
|
548
|
+
(click)="open.set(false)"
|
|
549
|
+
>
|
|
550
|
+
{{ item.label }}
|
|
551
|
+
</a>
|
|
552
|
+
</li>
|
|
553
|
+
}
|
|
554
|
+
</ul>
|
|
555
|
+
</nav>
|
|
556
|
+
@if (isAuthenticated()) {
|
|
557
|
+
<div class="border-t border-neutral/20 p-3">
|
|
558
|
+
<button
|
|
559
|
+
app-button
|
|
560
|
+
type="button"
|
|
561
|
+
variant="danger"
|
|
562
|
+
[loading]="loggingOut()"
|
|
563
|
+
[disabled]="loggingOut()"
|
|
564
|
+
(click)="closeSession()"
|
|
565
|
+
class="w-full"
|
|
566
|
+
>
|
|
567
|
+
<svg lucideLogOut size="16" />
|
|
568
|
+
Cerrar sesión
|
|
569
|
+
</button>
|
|
570
|
+
</div>
|
|
571
|
+
}
|
|
572
|
+
</aside>
|
|
573
|
+
`
|
|
574
|
+
}]
|
|
575
|
+
}],
|
|
576
|
+
propDecorators: { open: [{
|
|
577
|
+
type: i0.Input,
|
|
578
|
+
args: [{
|
|
579
|
+
isSignal: true,
|
|
580
|
+
alias: "open",
|
|
581
|
+
required: false
|
|
582
|
+
}]
|
|
583
|
+
}, {
|
|
584
|
+
type: i0.Output,
|
|
585
|
+
args: ["openChange"]
|
|
586
|
+
}] }
|
|
587
|
+
});
|
|
588
|
+
function isVisible(schema) {
|
|
589
|
+
if (schema.policies?.access) return true;
|
|
590
|
+
const permissions = schema.permissions;
|
|
591
|
+
if (!permissions) return true;
|
|
592
|
+
const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
|
|
593
|
+
const canWrite = permissions.create !== false || permissions.update !== false || permissions.remove !== false;
|
|
594
|
+
return hasReadEndpoint || canWrite;
|
|
595
|
+
}
|
|
596
|
+
var Layout = class Layout {
|
|
597
|
+
sidebarOpen = signal(false, ...ngDevMode ? [{ debugName: "sidebarOpen" }] : /* istanbul ignore next */ []);
|
|
598
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
599
|
+
minVersion: "12.0.0",
|
|
600
|
+
version: "22.2.3",
|
|
601
|
+
ngImport: i0,
|
|
602
|
+
type: Layout,
|
|
603
|
+
deps: [],
|
|
604
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
605
|
+
});
|
|
606
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
607
|
+
minVersion: "17.0.0",
|
|
608
|
+
version: "22.2.3",
|
|
609
|
+
type: Layout,
|
|
610
|
+
isStandalone: true,
|
|
611
|
+
selector: "app-layout",
|
|
612
|
+
ngImport: i0,
|
|
613
|
+
template: `
|
|
614
|
+
<div class="flex min-h-screen bg-neutral/5">
|
|
615
|
+
<button
|
|
616
|
+
app-button
|
|
617
|
+
type="button"
|
|
618
|
+
variant="outline"
|
|
619
|
+
size="icon"
|
|
620
|
+
class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
|
|
621
|
+
[attr.aria-expanded]="sidebarOpen()"
|
|
622
|
+
aria-controls="main-sidebar"
|
|
623
|
+
aria-label="Abrir menú"
|
|
624
|
+
(click)="sidebarOpen.set(true)"
|
|
625
|
+
>
|
|
626
|
+
<svg
|
|
627
|
+
aria-hidden="true"
|
|
628
|
+
class="size-5"
|
|
629
|
+
fill="none"
|
|
630
|
+
stroke="currentColor"
|
|
631
|
+
stroke-width="2"
|
|
632
|
+
viewBox="0 0 24 24"
|
|
633
|
+
>
|
|
634
|
+
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
635
|
+
</svg>
|
|
636
|
+
</button>
|
|
637
|
+
@if (sidebarOpen()) {
|
|
638
|
+
<div
|
|
639
|
+
class="fixed inset-0 z-30 bg-black/30 lg:hidden"
|
|
640
|
+
aria-hidden="true"
|
|
641
|
+
(click)="sidebarOpen.set(false)"
|
|
642
|
+
></div>
|
|
643
|
+
}
|
|
644
|
+
<app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
|
|
645
|
+
<!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
|
|
646
|
+
cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
|
|
647
|
+
base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
|
|
648
|
+
<main
|
|
649
|
+
class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
|
|
650
|
+
>
|
|
651
|
+
<router-outlet />
|
|
652
|
+
</main>
|
|
653
|
+
</div>
|
|
654
|
+
`,
|
|
655
|
+
isInline: true,
|
|
656
|
+
dependencies: [
|
|
657
|
+
{
|
|
658
|
+
kind: "directive",
|
|
659
|
+
type: RouterOutlet,
|
|
660
|
+
selector: "router-outlet",
|
|
661
|
+
inputs: ["name", "routerOutletData"],
|
|
662
|
+
outputs: [
|
|
663
|
+
"activate",
|
|
664
|
+
"deactivate",
|
|
665
|
+
"attach",
|
|
666
|
+
"detach"
|
|
667
|
+
],
|
|
668
|
+
exportAs: ["outlet"]
|
|
669
|
+
},
|
|
670
|
+
{
|
|
671
|
+
kind: "component",
|
|
672
|
+
type: Sidebar,
|
|
673
|
+
selector: "app-sidebar",
|
|
674
|
+
inputs: ["open"],
|
|
675
|
+
outputs: ["openChange"]
|
|
676
|
+
},
|
|
677
|
+
{
|
|
678
|
+
kind: "component",
|
|
679
|
+
type: Button,
|
|
680
|
+
selector: "button[app-button]",
|
|
681
|
+
inputs: [
|
|
682
|
+
"variant",
|
|
683
|
+
"size",
|
|
684
|
+
"type",
|
|
685
|
+
"disabled",
|
|
686
|
+
"loading"
|
|
687
|
+
]
|
|
688
|
+
}
|
|
689
|
+
]
|
|
690
|
+
});
|
|
691
|
+
};
|
|
692
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
693
|
+
minVersion: "12.0.0",
|
|
694
|
+
version: "22.2.3",
|
|
695
|
+
ngImport: i0,
|
|
696
|
+
type: Layout,
|
|
697
|
+
decorators: [{
|
|
698
|
+
type: Component,
|
|
699
|
+
args: [{
|
|
700
|
+
selector: "app-layout",
|
|
701
|
+
imports: [
|
|
702
|
+
RouterOutlet,
|
|
703
|
+
Sidebar,
|
|
704
|
+
Button
|
|
705
|
+
],
|
|
706
|
+
template: `
|
|
707
|
+
<div class="flex min-h-screen bg-neutral/5">
|
|
708
|
+
<button
|
|
709
|
+
app-button
|
|
710
|
+
type="button"
|
|
711
|
+
variant="outline"
|
|
712
|
+
size="icon"
|
|
713
|
+
class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
|
|
714
|
+
[attr.aria-expanded]="sidebarOpen()"
|
|
715
|
+
aria-controls="main-sidebar"
|
|
716
|
+
aria-label="Abrir menú"
|
|
717
|
+
(click)="sidebarOpen.set(true)"
|
|
718
|
+
>
|
|
719
|
+
<svg
|
|
720
|
+
aria-hidden="true"
|
|
721
|
+
class="size-5"
|
|
722
|
+
fill="none"
|
|
723
|
+
stroke="currentColor"
|
|
724
|
+
stroke-width="2"
|
|
725
|
+
viewBox="0 0 24 24"
|
|
726
|
+
>
|
|
727
|
+
<path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
728
|
+
</svg>
|
|
729
|
+
</button>
|
|
730
|
+
@if (sidebarOpen()) {
|
|
731
|
+
<div
|
|
732
|
+
class="fixed inset-0 z-30 bg-black/30 lg:hidden"
|
|
733
|
+
aria-hidden="true"
|
|
734
|
+
(click)="sidebarOpen.set(false)"
|
|
735
|
+
></div>
|
|
736
|
+
}
|
|
737
|
+
<app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
|
|
738
|
+
<!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
|
|
739
|
+
cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
|
|
740
|
+
base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
|
|
741
|
+
<main
|
|
742
|
+
class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
|
|
743
|
+
>
|
|
744
|
+
<router-outlet />
|
|
745
|
+
</main>
|
|
746
|
+
</div>
|
|
747
|
+
`
|
|
748
|
+
}]
|
|
749
|
+
}]
|
|
750
|
+
});
|
|
751
|
+
function pathRank(path) {
|
|
752
|
+
const segments = path.split("/");
|
|
753
|
+
return [segments.length, segments.filter((segment) => !segment.startsWith(":")).length];
|
|
754
|
+
}
|
|
755
|
+
function compareRoutes(a, b) {
|
|
756
|
+
const aRank = pathRank(a.path ?? "");
|
|
757
|
+
const bRank = pathRank(b.path ?? "");
|
|
758
|
+
return bRank[0] - aRank[0] || bRank[1] - aRank[1];
|
|
759
|
+
}
|
|
760
|
+
function resourceRoutes(catalog = allSchemas()) {
|
|
761
|
+
return catalog.flatMap((schema) => {
|
|
762
|
+
if (!schema.uiViews || Object.keys(schema.uiViews).length === 0) throw new Error(`El recurso '${schema.id}' no declara uiViews`);
|
|
763
|
+
validateComposition(schema);
|
|
764
|
+
return Object.entries(schema.uiViews).map(([name, view]) => {
|
|
765
|
+
const descriptor = get("view", view.type);
|
|
766
|
+
if (!descriptor) throw new Error(`No existe descriptor kind:view type '${view.type}' para ${schema.id}.${name}`);
|
|
767
|
+
const relativePath = view.path ?? "";
|
|
768
|
+
if (relativePath.startsWith("/") || relativePath.endsWith("/") || relativePath !== "" && relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Ruta uiViews inválida en ${schema.id}.${name}: '${relativePath}'`);
|
|
769
|
+
const route = {
|
|
770
|
+
path: [schema.id, relativePath].filter(Boolean).join("/"),
|
|
771
|
+
loadComponent: descriptor.load,
|
|
772
|
+
canActivate: [roleGuard],
|
|
773
|
+
data: {
|
|
774
|
+
resourceId: schema.id,
|
|
775
|
+
uiView: name,
|
|
776
|
+
viewType: view.type
|
|
777
|
+
},
|
|
778
|
+
resolve: { viewContext: () => {
|
|
779
|
+
const base = {
|
|
780
|
+
resourceId: schema.id,
|
|
781
|
+
uiView: name,
|
|
782
|
+
viewType: view.type,
|
|
783
|
+
...view.select !== void 0 ? { select: view.select } : {}
|
|
784
|
+
};
|
|
785
|
+
const select = view.select;
|
|
786
|
+
if (select?.mode !== "route") return createViewContext(base);
|
|
787
|
+
const nav = inject(ViewNavigationService);
|
|
788
|
+
return createViewContext(base, (event) => {
|
|
789
|
+
nav.dispatch(select, event);
|
|
790
|
+
});
|
|
791
|
+
} }
|
|
792
|
+
};
|
|
793
|
+
if (view.type === "form") route.canDeactivate = [unsavedChangesGuard];
|
|
794
|
+
return route;
|
|
795
|
+
});
|
|
796
|
+
}).sort(compareRoutes);
|
|
797
|
+
}
|
|
798
|
+
const shellRoutes = [{
|
|
799
|
+
path: "",
|
|
800
|
+
loadChildren: () => [{
|
|
801
|
+
path: "",
|
|
802
|
+
component: Layout,
|
|
803
|
+
children: resourceRoutes()
|
|
804
|
+
}]
|
|
805
|
+
}];
|
|
806
|
+
const LABELS = {
|
|
807
|
+
email: "Correo electrónico",
|
|
808
|
+
password: "Contraseña"
|
|
809
|
+
};
|
|
810
|
+
const FIELD_BASE = "mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
|
|
811
|
+
var Login = class Login {
|
|
812
|
+
auth = inject(AuthService);
|
|
813
|
+
router = inject(Router);
|
|
814
|
+
schema = getAuth();
|
|
815
|
+
providers = this.schema.providers;
|
|
816
|
+
error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
817
|
+
loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
|
|
818
|
+
providerLoading = signal(null, ...ngDevMode ? [{ debugName: "providerLoading" }] : /* istanbul ignore next */ []);
|
|
819
|
+
values = signal({}, ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
|
|
820
|
+
label(key) {
|
|
821
|
+
return LABELS[key] ?? key;
|
|
822
|
+
}
|
|
823
|
+
value(key) {
|
|
824
|
+
return this.values()[key] ?? "";
|
|
825
|
+
}
|
|
826
|
+
setValue(key, next) {
|
|
827
|
+
this.values.update((current) => ({
|
|
828
|
+
...current,
|
|
829
|
+
[key]: next
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
loginWithGoogle() {
|
|
833
|
+
if (this.providerLoading()) return;
|
|
834
|
+
this.providerLoading.set("google");
|
|
835
|
+
this.error.set(null);
|
|
836
|
+
this.auth.loginWithGoogle().subscribe({
|
|
837
|
+
next: () => {
|
|
838
|
+
this.providerLoading.set(null);
|
|
839
|
+
this.goAfterLogin();
|
|
840
|
+
},
|
|
841
|
+
error: (err) => {
|
|
842
|
+
this.providerLoading.set(null);
|
|
843
|
+
this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión con Google.");
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
submit(event) {
|
|
848
|
+
event.preventDefault();
|
|
849
|
+
if (this.loading()) return;
|
|
850
|
+
const email = this.value("email").trim();
|
|
851
|
+
const password = this.value("password");
|
|
852
|
+
if (email === "" || password === "") {
|
|
853
|
+
this.error.set("Completa el correo y la contraseña.");
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
if (this.schema.firebase && this.schema.providers.includes("password")) {
|
|
857
|
+
this.loading.set(true);
|
|
858
|
+
this.error.set(null);
|
|
859
|
+
this.auth.loginWithFirebasePassword(email, password).subscribe({
|
|
860
|
+
next: () => {
|
|
861
|
+
this.loading.set(false);
|
|
862
|
+
this.goAfterLogin();
|
|
863
|
+
},
|
|
864
|
+
error: (err) => {
|
|
865
|
+
this.loading.set(false);
|
|
866
|
+
this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
this.loading.set(true);
|
|
872
|
+
this.error.set(null);
|
|
873
|
+
this.auth.login(email, password).subscribe({
|
|
874
|
+
next: () => {
|
|
875
|
+
this.loading.set(false);
|
|
876
|
+
this.goAfterLogin();
|
|
877
|
+
},
|
|
878
|
+
error: (err) => {
|
|
879
|
+
this.loading.set(false);
|
|
880
|
+
this.error.set(err instanceof ApiError && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
goAfterLogin() {
|
|
885
|
+
const previous = this.auth.consumeReturnUrl();
|
|
886
|
+
const target = previous !== null && previous !== "" && !previous.startsWith("/login") ? previous : this.schema.redirectAfterLogin;
|
|
887
|
+
this.router.navigateByUrl(target).catch(() => void 0);
|
|
888
|
+
}
|
|
889
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
890
|
+
minVersion: "12.0.0",
|
|
891
|
+
version: "22.2.3",
|
|
892
|
+
ngImport: i0,
|
|
893
|
+
type: Login,
|
|
894
|
+
deps: [],
|
|
895
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
896
|
+
});
|
|
897
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
898
|
+
minVersion: "17.0.0",
|
|
899
|
+
version: "22.2.3",
|
|
900
|
+
type: Login,
|
|
901
|
+
isStandalone: true,
|
|
902
|
+
selector: "app-login",
|
|
903
|
+
ngImport: i0,
|
|
904
|
+
template: "\n <main class=\"flex min-h-[100svh] items-center justify-center bg-surface px-4\">\n <div\n class=\"w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm\"\n >\n <h1 class=\"font-display text-lg font-semibold\">Admin</h1>\n <p class=\"mt-1 text-sm text-neutral\">Inicia sesión para continuar</p>\n\n @if (providers.includes('google')) {\n <button\n type=\"button\"\n id=\"btn-login-google\"\n app-button\n variant=\"outline\"\n [loading]=\"providerLoading() === 'google'\"\n (click)=\"loginWithGoogle()\"\n class=\"mt-6 w-full\"\n >\n Continuar con Google\n </button>\n <div class=\"my-4 flex items-center gap-3\">\n <hr class=\"flex-1 border-neutral/20\" />\n <span class=\"text-xs text-neutral/50\">o</span>\n <hr class=\"flex-1 border-neutral/20\" />\n </div>\n }\n\n <form\n [class]=\"providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'\"\n (submit)=\"submit($event)\"\n novalidate\n >\n @for (field of schema.loginFields; track field.key) {\n <div>\n <label [attr.for]=\"field.key\" class=\"block text-sm font-medium text-neutral\">\n {{ label(field.key) }}\n </label>\n\n @switch (field.type) {\n @case ('email') {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"email\"\n autocomplete=\"email\"\n inputmode=\"email\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n @default {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"password\"\n autocomplete=\"current-password\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n }\n </div>\n }\n\n @if (error(); as message) {\n <p role=\"alert\" class=\"rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger\">\n {{ message }}\n </p>\n }\n\n <button app-button type=\"submit\" [loading]=\"loading()\" class=\"w-full\">\n Iniciar sesión\n </button>\n </form>\n </div>\n </main>\n ",
|
|
905
|
+
isInline: true,
|
|
906
|
+
dependencies: [{
|
|
907
|
+
kind: "component",
|
|
908
|
+
type: Button,
|
|
909
|
+
selector: "button[app-button]",
|
|
910
|
+
inputs: [
|
|
911
|
+
"variant",
|
|
912
|
+
"size",
|
|
913
|
+
"type",
|
|
914
|
+
"disabled",
|
|
915
|
+
"loading"
|
|
916
|
+
]
|
|
917
|
+
}]
|
|
918
|
+
});
|
|
919
|
+
};
|
|
920
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
921
|
+
minVersion: "12.0.0",
|
|
922
|
+
version: "22.2.3",
|
|
923
|
+
ngImport: i0,
|
|
924
|
+
type: Login,
|
|
925
|
+
decorators: [{
|
|
926
|
+
type: Component,
|
|
927
|
+
args: [{
|
|
928
|
+
selector: "app-login",
|
|
929
|
+
imports: [Button],
|
|
930
|
+
template: `
|
|
931
|
+
<main class="flex min-h-[100svh] items-center justify-center bg-surface px-4">
|
|
932
|
+
<div
|
|
933
|
+
class="w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm"
|
|
934
|
+
>
|
|
935
|
+
<h1 class="font-display text-lg font-semibold">Admin</h1>
|
|
936
|
+
<p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
|
|
937
|
+
|
|
938
|
+
@if (providers.includes('google')) {
|
|
939
|
+
<button
|
|
940
|
+
type="button"
|
|
941
|
+
id="btn-login-google"
|
|
942
|
+
app-button
|
|
943
|
+
variant="outline"
|
|
944
|
+
[loading]="providerLoading() === 'google'"
|
|
945
|
+
(click)="loginWithGoogle()"
|
|
946
|
+
class="mt-6 w-full"
|
|
947
|
+
>
|
|
948
|
+
Continuar con Google
|
|
949
|
+
</button>
|
|
950
|
+
<div class="my-4 flex items-center gap-3">
|
|
951
|
+
<hr class="flex-1 border-neutral/20" />
|
|
952
|
+
<span class="text-xs text-neutral/50">o</span>
|
|
953
|
+
<hr class="flex-1 border-neutral/20" />
|
|
954
|
+
</div>
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
<form
|
|
958
|
+
[class]="providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'"
|
|
959
|
+
(submit)="submit($event)"
|
|
960
|
+
novalidate
|
|
961
|
+
>
|
|
962
|
+
@for (field of schema.loginFields; track field.key) {
|
|
963
|
+
<div>
|
|
964
|
+
<label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
|
|
965
|
+
{{ label(field.key) }}
|
|
966
|
+
</label>
|
|
967
|
+
|
|
968
|
+
@switch (field.type) {
|
|
969
|
+
@case ('email') {
|
|
970
|
+
<input
|
|
971
|
+
[attr.id]="field.key"
|
|
972
|
+
[attr.name]="field.key"
|
|
973
|
+
type="email"
|
|
974
|
+
autocomplete="email"
|
|
975
|
+
inputmode="email"
|
|
976
|
+
[attr.required]="field.required ? '' : null"
|
|
977
|
+
[value]="value(field.key)"
|
|
978
|
+
(input)="setValue(field.key, $any($event.target).value)"
|
|
979
|
+
class="${FIELD_BASE}"
|
|
980
|
+
/>
|
|
981
|
+
}
|
|
982
|
+
@default {
|
|
983
|
+
<input
|
|
984
|
+
[attr.id]="field.key"
|
|
985
|
+
[attr.name]="field.key"
|
|
986
|
+
type="password"
|
|
987
|
+
autocomplete="current-password"
|
|
988
|
+
[attr.required]="field.required ? '' : null"
|
|
989
|
+
[value]="value(field.key)"
|
|
990
|
+
(input)="setValue(field.key, $any($event.target).value)"
|
|
991
|
+
class="${FIELD_BASE}"
|
|
992
|
+
/>
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
</div>
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
@if (error(); as message) {
|
|
999
|
+
<p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
|
|
1000
|
+
{{ message }}
|
|
1001
|
+
</p>
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
<button app-button type="submit" [loading]="loading()" class="w-full">
|
|
1005
|
+
Iniciar sesión
|
|
1006
|
+
</button>
|
|
1007
|
+
</form>
|
|
1008
|
+
</div>
|
|
1009
|
+
</main>
|
|
1010
|
+
`
|
|
1011
|
+
}]
|
|
1012
|
+
}]
|
|
1013
|
+
});
|
|
1014
|
+
const MOBILE_SPANS = {
|
|
1015
|
+
1: "col-span-1",
|
|
1016
|
+
2: "col-span-2",
|
|
1017
|
+
3: "col-span-3",
|
|
1018
|
+
4: "col-span-4",
|
|
1019
|
+
5: "col-span-5",
|
|
1020
|
+
6: "col-span-6",
|
|
1021
|
+
7: "col-span-7",
|
|
1022
|
+
8: "col-span-8",
|
|
1023
|
+
9: "col-span-9",
|
|
1024
|
+
10: "col-span-10",
|
|
1025
|
+
11: "col-span-11",
|
|
1026
|
+
12: "col-span-12"
|
|
1027
|
+
};
|
|
1028
|
+
const RESPONSIVE_SPANS = {
|
|
1029
|
+
12: "",
|
|
1030
|
+
11: "lg:col-span-11",
|
|
1031
|
+
10: "lg:col-span-10",
|
|
1032
|
+
9: "lg:col-span-9",
|
|
1033
|
+
8: "lg:col-span-8",
|
|
1034
|
+
7: "sm:col-span-6 lg:col-span-7",
|
|
1035
|
+
6: "sm:col-span-6",
|
|
1036
|
+
5: "sm:col-span-6 lg:col-span-5",
|
|
1037
|
+
4: "sm:col-span-6 lg:col-span-4",
|
|
1038
|
+
3: "sm:col-span-6 lg:col-span-3",
|
|
1039
|
+
2: "sm:col-span-6 lg:col-span-2",
|
|
1040
|
+
1: "sm:col-span-6 lg:col-span-1"
|
|
1041
|
+
};
|
|
1042
|
+
var WidgetHost = class WidgetHost {
|
|
1043
|
+
widget = input.required(...ngDevMode ? [{ debugName: "widget" }] : /* istanbul ignore next */ []);
|
|
1044
|
+
data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
|
|
1045
|
+
loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
|
|
1046
|
+
gridSpanClass = computed(() => {
|
|
1047
|
+
const w = this.widget().width ?? 4;
|
|
1048
|
+
const mw = this.widget().mobileWidth ?? 12;
|
|
1049
|
+
return [
|
|
1050
|
+
MOBILE_SPANS[mw] ?? "col-span-12",
|
|
1051
|
+
RESPONSIVE_SPANS[w] ?? "sm:col-span-6 lg:col-span-4",
|
|
1052
|
+
"block"
|
|
1053
|
+
].filter(Boolean).join(" ");
|
|
1054
|
+
}, ...ngDevMode ? [{ debugName: "gridSpanClass" }] : /* istanbul ignore next */ []);
|
|
1055
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1056
|
+
minVersion: "12.0.0",
|
|
1057
|
+
version: "22.2.3",
|
|
1058
|
+
ngImport: i0,
|
|
1059
|
+
type: WidgetHost,
|
|
1060
|
+
deps: [],
|
|
1061
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1062
|
+
});
|
|
1063
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1064
|
+
minVersion: "17.0.0",
|
|
1065
|
+
version: "22.2.3",
|
|
1066
|
+
type: WidgetHost,
|
|
1067
|
+
isStandalone: true,
|
|
1068
|
+
selector: "app-widget-host",
|
|
1069
|
+
inputs: {
|
|
1070
|
+
widget: {
|
|
1071
|
+
classPropertyName: "widget",
|
|
1072
|
+
publicName: "widget",
|
|
1073
|
+
isSignal: true,
|
|
1074
|
+
isRequired: true,
|
|
1075
|
+
transformFunction: null
|
|
1076
|
+
},
|
|
1077
|
+
data: {
|
|
1078
|
+
classPropertyName: "data",
|
|
1079
|
+
publicName: "data",
|
|
1080
|
+
isSignal: true,
|
|
1081
|
+
isRequired: false,
|
|
1082
|
+
transformFunction: null
|
|
1083
|
+
},
|
|
1084
|
+
loading: {
|
|
1085
|
+
classPropertyName: "loading",
|
|
1086
|
+
publicName: "loading",
|
|
1087
|
+
isSignal: true,
|
|
1088
|
+
isRequired: false,
|
|
1089
|
+
transformFunction: null
|
|
1090
|
+
}
|
|
1091
|
+
},
|
|
1092
|
+
host: { properties: { "class": "gridSpanClass()" } },
|
|
1093
|
+
ngImport: i0,
|
|
1094
|
+
template: `
|
|
1095
|
+
@if (loading()) {
|
|
1096
|
+
<!-- Reserva el espacio del widget mientras su dato viaja (la red
|
|
1097
|
+
Apps Script tarda ~1s): sin esto el panel se veía "completo"
|
|
1098
|
+
con valores vacíos que después aparecían solos (cargar ≠ vacío,
|
|
1099
|
+
doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
|
|
1100
|
+
span porque la clase la pone el host. -->
|
|
1101
|
+
<div class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
1102
|
+
<app-skeleton [lines]="1" [height]="14" width="40%" />
|
|
1103
|
+
<div class="mt-3">
|
|
1104
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
1105
|
+
</div>
|
|
1106
|
+
</div>
|
|
1107
|
+
} @else {
|
|
1108
|
+
<app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
|
|
1109
|
+
}
|
|
1110
|
+
`,
|
|
1111
|
+
isInline: true,
|
|
1112
|
+
dependencies: [{
|
|
1113
|
+
kind: "component",
|
|
1114
|
+
type: RenderHost,
|
|
1115
|
+
selector: "app-render-host",
|
|
1116
|
+
inputs: [
|
|
1117
|
+
"kind",
|
|
1118
|
+
"type",
|
|
1119
|
+
"config",
|
|
1120
|
+
"ctx",
|
|
1121
|
+
"data",
|
|
1122
|
+
"mode",
|
|
1123
|
+
"value",
|
|
1124
|
+
"chip"
|
|
1125
|
+
]
|
|
1126
|
+
}, {
|
|
1127
|
+
kind: "component",
|
|
1128
|
+
type: Skeleton,
|
|
1129
|
+
selector: "app-skeleton",
|
|
1130
|
+
inputs: [
|
|
1131
|
+
"lines",
|
|
1132
|
+
"height",
|
|
1133
|
+
"width"
|
|
1134
|
+
]
|
|
1135
|
+
}]
|
|
1136
|
+
});
|
|
1137
|
+
};
|
|
1138
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1139
|
+
minVersion: "12.0.0",
|
|
1140
|
+
version: "22.2.3",
|
|
1141
|
+
ngImport: i0,
|
|
1142
|
+
type: WidgetHost,
|
|
1143
|
+
decorators: [{
|
|
1144
|
+
type: Component,
|
|
1145
|
+
args: [{
|
|
1146
|
+
selector: "app-widget-host",
|
|
1147
|
+
imports: [RenderHost, Skeleton],
|
|
1148
|
+
host: { "[class]": "gridSpanClass()" },
|
|
1149
|
+
template: `
|
|
1150
|
+
@if (loading()) {
|
|
1151
|
+
<!-- Reserva el espacio del widget mientras su dato viaja (la red
|
|
1152
|
+
Apps Script tarda ~1s): sin esto el panel se veía "completo"
|
|
1153
|
+
con valores vacíos que después aparecían solos (cargar ≠ vacío,
|
|
1154
|
+
doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
|
|
1155
|
+
span porque la clase la pone el host. -->
|
|
1156
|
+
<div class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
1157
|
+
<app-skeleton [lines]="1" [height]="14" width="40%" />
|
|
1158
|
+
<div class="mt-3">
|
|
1159
|
+
<app-skeleton [lines]="2" [height]="32" />
|
|
1160
|
+
</div>
|
|
1161
|
+
</div>
|
|
1162
|
+
} @else {
|
|
1163
|
+
<app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
|
|
1164
|
+
}
|
|
1165
|
+
`
|
|
1166
|
+
}]
|
|
1167
|
+
}],
|
|
1168
|
+
propDecorators: {
|
|
1169
|
+
widget: [{
|
|
1170
|
+
type: i0.Input,
|
|
1171
|
+
args: [{
|
|
1172
|
+
isSignal: true,
|
|
1173
|
+
alias: "widget",
|
|
1174
|
+
required: true
|
|
1175
|
+
}]
|
|
1176
|
+
}],
|
|
1177
|
+
data: [{
|
|
1178
|
+
type: i0.Input,
|
|
1179
|
+
args: [{
|
|
1180
|
+
isSignal: true,
|
|
1181
|
+
alias: "data",
|
|
1182
|
+
required: false
|
|
1183
|
+
}]
|
|
1184
|
+
}],
|
|
1185
|
+
loading: [{
|
|
1186
|
+
type: i0.Input,
|
|
1187
|
+
args: [{
|
|
1188
|
+
isSignal: true,
|
|
1189
|
+
alias: "loading",
|
|
1190
|
+
required: false
|
|
1191
|
+
}]
|
|
1192
|
+
}]
|
|
1193
|
+
}
|
|
1194
|
+
});
|
|
1195
|
+
var Dashboard = class Dashboard {
|
|
1196
|
+
api = inject(ApiService, { optional: true });
|
|
1197
|
+
ds = inject(DataSourceService);
|
|
1198
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
1199
|
+
data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
|
|
1200
|
+
fetchedData = toSignal(toObservable(this.schema).pipe(switchMap((schema) => {
|
|
1201
|
+
const getEndpoint = schema.endpoint?.get;
|
|
1202
|
+
if (!getEndpoint || !this.api) return of({});
|
|
1203
|
+
return this.api.request("GET", getEndpoint).pipe(catchError(() => of({})));
|
|
1204
|
+
})), { initialValue: null });
|
|
1205
|
+
effectiveData = computed(() => {
|
|
1206
|
+
const override = this.data();
|
|
1207
|
+
if (override !== null && override !== void 0) return override;
|
|
1208
|
+
return this.fetchedData() ?? {};
|
|
1209
|
+
}, ...ngDevMode ? [{ debugName: "effectiveData" }] : /* istanbul ignore next */ []);
|
|
1210
|
+
widgets = computed(() => this.schema().widgets ?? [], ...ngDevMode ? [{ debugName: "widgets" }] : /* istanbul ignore next */ []);
|
|
1211
|
+
clavesFuente = computed(() => this.widgets().map((widget) => esFuente(widget.source) ? widget.key : "").filter((key) => key !== "").join("|"), ...ngDevMode ? [{ debugName: "clavesFuente" }] : /* istanbul ignore next */ []);
|
|
1212
|
+
fuentes = resource({
|
|
1213
|
+
...ngDevMode ? { debugName: "fuentes" } : /* istanbul ignore next */ {},
|
|
1214
|
+
params: () => this.clavesFuente() === "" ? void 0 : this.clavesFuente(),
|
|
1215
|
+
loader: async () => {
|
|
1216
|
+
const filas = {};
|
|
1217
|
+
for (const widget of this.widgets()) {
|
|
1218
|
+
if (!esFuente(widget.source)) continue;
|
|
1219
|
+
filas[widget.key] = await this.ds.rows(widget.source);
|
|
1220
|
+
}
|
|
1221
|
+
return filas;
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
falloFuentes = computed(() => this.fuentes.status() === "error", ...ngDevMode ? [{ debugName: "falloFuentes" }] : /* istanbul ignore next */ []);
|
|
1225
|
+
cargando = computed(() => {
|
|
1226
|
+
const payloadViaja = this.data() === null && this.fetchedData() === null;
|
|
1227
|
+
const fuentesViajan = this.fuentes.status() === "loading";
|
|
1228
|
+
return payloadViaja || fuentesViajan;
|
|
1229
|
+
}, ...ngDevMode ? [{ debugName: "cargando" }] : /* istanbul ignore next */ []);
|
|
1230
|
+
datum(widget) {
|
|
1231
|
+
if (esFuente(widget.source)) return this.fuentes.status() === "error" ? null : this.fuentes.value()?.[widget.key] ?? null;
|
|
1232
|
+
const allData = this.effectiveData();
|
|
1233
|
+
return widget.source?.kind === "payload" ? allData : allData[widget.key] ?? null;
|
|
1234
|
+
}
|
|
1235
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1236
|
+
minVersion: "12.0.0",
|
|
1237
|
+
version: "22.2.3",
|
|
1238
|
+
ngImport: i0,
|
|
1239
|
+
type: Dashboard,
|
|
1240
|
+
deps: [],
|
|
1241
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1242
|
+
});
|
|
1243
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1244
|
+
minVersion: "17.0.0",
|
|
1245
|
+
version: "22.2.3",
|
|
1246
|
+
type: Dashboard,
|
|
1247
|
+
isStandalone: true,
|
|
1248
|
+
selector: "app-dashboard",
|
|
1249
|
+
inputs: {
|
|
1250
|
+
schema: {
|
|
1251
|
+
classPropertyName: "schema",
|
|
1252
|
+
publicName: "schema",
|
|
1253
|
+
isSignal: true,
|
|
1254
|
+
isRequired: true,
|
|
1255
|
+
transformFunction: null
|
|
1256
|
+
},
|
|
1257
|
+
data: {
|
|
1258
|
+
classPropertyName: "data",
|
|
1259
|
+
publicName: "data",
|
|
1260
|
+
isSignal: true,
|
|
1261
|
+
isRequired: false,
|
|
1262
|
+
transformFunction: null
|
|
1263
|
+
}
|
|
1264
|
+
},
|
|
1265
|
+
ngImport: i0,
|
|
1266
|
+
template: `
|
|
1267
|
+
<header class="mb-6">
|
|
1268
|
+
<h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
|
|
1269
|
+
<p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
|
|
1270
|
+
</header>
|
|
1271
|
+
|
|
1272
|
+
@if (falloFuentes()) {
|
|
1273
|
+
<!-- Degradación visible (F9): sin este aviso el fallo de una fuente
|
|
1274
|
+
se confundiría con «el panel no tiene datos». -->
|
|
1275
|
+
<p class="mb-4 text-sm text-danger" role="alert">
|
|
1276
|
+
No se pudieron cargar los datos de algunos widgets.
|
|
1277
|
+
</p>
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
@if (widgets().length === 0) {
|
|
1281
|
+
<app-empty-state
|
|
1282
|
+
title="Sin widgets declarados"
|
|
1283
|
+
message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
|
|
1284
|
+
/>
|
|
1285
|
+
} @else {
|
|
1286
|
+
<div class="grid grid-cols-12 gap-4">
|
|
1287
|
+
@for (widget of widgets(); track widget.key) {
|
|
1288
|
+
<app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
|
|
1289
|
+
}
|
|
1290
|
+
</div>
|
|
1291
|
+
}
|
|
1292
|
+
`,
|
|
1293
|
+
isInline: true,
|
|
1294
|
+
dependencies: [{
|
|
1295
|
+
kind: "component",
|
|
1296
|
+
type: EmptyState,
|
|
1297
|
+
selector: "app-empty-state",
|
|
1298
|
+
inputs: ["title", "message"]
|
|
1299
|
+
}, {
|
|
1300
|
+
kind: "component",
|
|
1301
|
+
type: WidgetHost,
|
|
1302
|
+
selector: "app-widget-host",
|
|
1303
|
+
inputs: [
|
|
1304
|
+
"widget",
|
|
1305
|
+
"data",
|
|
1306
|
+
"loading"
|
|
1307
|
+
]
|
|
1308
|
+
}]
|
|
1309
|
+
});
|
|
1310
|
+
};
|
|
1311
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1312
|
+
minVersion: "12.0.0",
|
|
1313
|
+
version: "22.2.3",
|
|
1314
|
+
ngImport: i0,
|
|
1315
|
+
type: Dashboard,
|
|
1316
|
+
decorators: [{
|
|
1317
|
+
type: Component,
|
|
1318
|
+
args: [{
|
|
1319
|
+
selector: "app-dashboard",
|
|
1320
|
+
imports: [EmptyState, WidgetHost],
|
|
1321
|
+
template: `
|
|
1322
|
+
<header class="mb-6">
|
|
1323
|
+
<h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
|
|
1324
|
+
<p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
|
|
1325
|
+
</header>
|
|
1326
|
+
|
|
1327
|
+
@if (falloFuentes()) {
|
|
1328
|
+
<!-- Degradación visible (F9): sin este aviso el fallo de una fuente
|
|
1329
|
+
se confundiría con «el panel no tiene datos». -->
|
|
1330
|
+
<p class="mb-4 text-sm text-danger" role="alert">
|
|
1331
|
+
No se pudieron cargar los datos de algunos widgets.
|
|
1332
|
+
</p>
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
@if (widgets().length === 0) {
|
|
1336
|
+
<app-empty-state
|
|
1337
|
+
title="Sin widgets declarados"
|
|
1338
|
+
message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
|
|
1339
|
+
/>
|
|
1340
|
+
} @else {
|
|
1341
|
+
<div class="grid grid-cols-12 gap-4">
|
|
1342
|
+
@for (widget of widgets(); track widget.key) {
|
|
1343
|
+
<app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
|
|
1344
|
+
}
|
|
1345
|
+
</div>
|
|
1346
|
+
}
|
|
1347
|
+
`
|
|
1348
|
+
}]
|
|
1349
|
+
}],
|
|
1350
|
+
propDecorators: {
|
|
1351
|
+
schema: [{
|
|
1352
|
+
type: i0.Input,
|
|
1353
|
+
args: [{
|
|
1354
|
+
isSignal: true,
|
|
1355
|
+
alias: "schema",
|
|
1356
|
+
required: true
|
|
1357
|
+
}]
|
|
1358
|
+
}],
|
|
1359
|
+
data: [{
|
|
1360
|
+
type: i0.Input,
|
|
1361
|
+
args: [{
|
|
1362
|
+
isSignal: true,
|
|
1363
|
+
alias: "data",
|
|
1364
|
+
required: false
|
|
1365
|
+
}]
|
|
1366
|
+
}]
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
function esFuente(source) {
|
|
1370
|
+
return source !== void 0 && source !== null && "resource" in source;
|
|
1371
|
+
}
|
|
1372
|
+
var ActionService = class ActionService {
|
|
1373
|
+
config = inject(ConfigService);
|
|
1374
|
+
errores = inject(ErrorMapperService);
|
|
1375
|
+
async run(action, request) {
|
|
1376
|
+
const type = actionType(action);
|
|
1377
|
+
const descriptor = getAction(type);
|
|
1378
|
+
if (!descriptor) throw new Error(`[actions] «${action.label}» no tiene descriptor registrado (type "${type}").`);
|
|
1379
|
+
if (descriptor.confirm) {
|
|
1380
|
+
if (!await request.toast.confirm({
|
|
1381
|
+
title: action.label,
|
|
1382
|
+
message: confirmMessage(action),
|
|
1383
|
+
confirmLabel: "Confirmar",
|
|
1384
|
+
cancelLabel: "Cancelar"
|
|
1385
|
+
})) return { ok: false };
|
|
1386
|
+
}
|
|
1387
|
+
if (descriptor.type === "copy-share") await this.config.ensureLoaded();
|
|
1388
|
+
const ctx = {
|
|
1389
|
+
...request,
|
|
1390
|
+
vars: this.vars(request.record)
|
|
1391
|
+
};
|
|
1392
|
+
try {
|
|
1393
|
+
return await descriptor.run(ctx, action);
|
|
1394
|
+
} catch (err) {
|
|
1395
|
+
request.toast.error(this.errores.messageOf(err));
|
|
1396
|
+
return { ok: false };
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
vars(record) {
|
|
1400
|
+
return record ? this.config.buildInterpolationContext(record) : {};
|
|
1401
|
+
}
|
|
1402
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1403
|
+
minVersion: "12.0.0",
|
|
1404
|
+
version: "22.2.3",
|
|
1405
|
+
ngImport: i0,
|
|
1406
|
+
type: ActionService,
|
|
1407
|
+
deps: [],
|
|
1408
|
+
target: i0.ɵɵFactoryTarget.Service
|
|
1409
|
+
});
|
|
1410
|
+
static ɵprov = i0.ɵɵngDeclareService({
|
|
1411
|
+
minVersion: "22.0.0",
|
|
1412
|
+
version: "22.2.3",
|
|
1413
|
+
ngImport: i0,
|
|
1414
|
+
type: ActionService
|
|
1415
|
+
});
|
|
1416
|
+
};
|
|
1417
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
1418
|
+
minVersion: "12.0.0",
|
|
1419
|
+
version: "22.2.3",
|
|
1420
|
+
ngImport: i0,
|
|
1421
|
+
type: ActionService,
|
|
1422
|
+
decorators: [{ type: Service }]
|
|
1423
|
+
});
|
|
1424
|
+
function confirmMessage(action) {
|
|
1425
|
+
return "confirmMessage" in action && action.confirmMessage ? action.confirmMessage : action.label;
|
|
1426
|
+
}
|
|
1427
|
+
const FORMATO_POR_DEFECTO = {
|
|
1428
|
+
text: "text",
|
|
1429
|
+
textarea: "text",
|
|
1430
|
+
slug: "text",
|
|
1431
|
+
url: "text",
|
|
1432
|
+
email: "text",
|
|
1433
|
+
phone: "text",
|
|
1434
|
+
"readonly-text": "text",
|
|
1435
|
+
relation: "text",
|
|
1436
|
+
group: "text",
|
|
1437
|
+
"key-value": "text",
|
|
1438
|
+
"string-list": "text",
|
|
1439
|
+
multiselect: "text",
|
|
1440
|
+
time: "text",
|
|
1441
|
+
list: "text",
|
|
1442
|
+
date: "date",
|
|
1443
|
+
number: "number",
|
|
1444
|
+
currency: "currency",
|
|
1445
|
+
boolean: "boolean",
|
|
1446
|
+
select: "badge",
|
|
1447
|
+
image: "thumbnail",
|
|
1448
|
+
file: "text",
|
|
1449
|
+
color: "text",
|
|
1450
|
+
"rich-select": "badge",
|
|
1451
|
+
dropdown: "badge"
|
|
1452
|
+
};
|
|
1453
|
+
function formatoPorTipo(type) {
|
|
1454
|
+
return Object.prototype.hasOwnProperty.call(FORMATO_POR_DEFECTO, type) ? FORMATO_POR_DEFECTO[type] : void 0;
|
|
1455
|
+
}
|
|
1456
|
+
function formatoDeColumna(column, fields) {
|
|
1457
|
+
if (column.format) return column.format;
|
|
1458
|
+
const field = fields.find((candidate) => candidate.key === column.key);
|
|
1459
|
+
return (field ? formatoPorTipo(field.type) : void 0) ?? "text";
|
|
1460
|
+
}
|
|
1461
|
+
const BOOLEAN_FILTER_OPTIONS = [
|
|
1462
|
+
{
|
|
1463
|
+
value: "",
|
|
1464
|
+
label: "Todos"
|
|
1465
|
+
},
|
|
1466
|
+
{
|
|
1467
|
+
value: "yes",
|
|
1468
|
+
label: "Sí"
|
|
1469
|
+
},
|
|
1470
|
+
{
|
|
1471
|
+
value: "no",
|
|
1472
|
+
label: "No"
|
|
1473
|
+
}
|
|
1474
|
+
];
|
|
1475
|
+
function errorMessage(err, fallback) {
|
|
1476
|
+
return err instanceof ApiError ? err.message : fallback;
|
|
1477
|
+
}
|
|
1478
|
+
var ListView = class ListView {
|
|
1479
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
1480
|
+
viewRecord = output();
|
|
1481
|
+
api = inject(ApiService);
|
|
1482
|
+
auth = inject(AuthService);
|
|
1483
|
+
router = inject(Router);
|
|
1484
|
+
notifications = inject(NotificationService);
|
|
1485
|
+
actionService = inject(ActionService);
|
|
1486
|
+
status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
1487
|
+
rows = signal([], ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
1488
|
+
query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
|
|
1489
|
+
filterValues = signal({}, ...ngDevMode ? [{ debugName: "filterValues" }] : /* istanbul ignore next */ []);
|
|
1490
|
+
savingKey = signal(null, ...ngDevMode ? [{ debugName: "savingKey" }] : /* istanbul ignore next */ []);
|
|
1491
|
+
quickEditRow = signal(null, ...ngDevMode ? [{ debugName: "quickEditRow" }] : /* istanbul ignore next */ []);
|
|
1492
|
+
loadEpoch = 0;
|
|
1493
|
+
constructor() {
|
|
1494
|
+
effect(() => {
|
|
1495
|
+
const schema = this.schema();
|
|
1496
|
+
this.query.set("");
|
|
1497
|
+
this.filterValues.set({});
|
|
1498
|
+
this.load(schema);
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
visibleRows = computed(() => {
|
|
1502
|
+
const schema = this.schema();
|
|
1503
|
+
let rows = this.rows();
|
|
1504
|
+
const query = this.query().trim().toLowerCase();
|
|
1505
|
+
if (query !== "" && schema.search?.length) rows = rows.filter((row) => schema.search.some((key) => String(getValue(row, key) ?? "").toLowerCase().includes(query)));
|
|
1506
|
+
for (const filter of schema.filters ?? []) {
|
|
1507
|
+
const value = this.filterValues()[filter.key] ?? "";
|
|
1508
|
+
if (value === "") continue;
|
|
1509
|
+
rows = rows.filter((row) => {
|
|
1510
|
+
const cell = getValue(row, filter.key);
|
|
1511
|
+
if (filter.type === "boolean") return (cell === true || cell === "true") === (value === "yes");
|
|
1512
|
+
if (filter.type === "select") return String(cell ?? "") === value;
|
|
1513
|
+
return String(cell ?? "").toLowerCase().includes(value.toLowerCase());
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
const positionField = schema.positionField;
|
|
1517
|
+
if (positionField) {
|
|
1518
|
+
const positionOf = (row) => {
|
|
1519
|
+
const position = Number(getValue(row, positionField));
|
|
1520
|
+
return Number.isFinite(position) ? position : Number.POSITIVE_INFINITY;
|
|
1521
|
+
};
|
|
1522
|
+
rows = [...rows].sort((a, b) => positionOf(a) - positionOf(b));
|
|
1523
|
+
}
|
|
1524
|
+
return rows;
|
|
1525
|
+
}, ...ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []);
|
|
1526
|
+
currencyCode = computed(() => {
|
|
1527
|
+
const field = getSchema("site")?.fields.find((f) => f.key === "currency");
|
|
1528
|
+
return field?.type === "text" && field.default ? field.default : "USD";
|
|
1529
|
+
}, ...ngDevMode ? [{ debugName: "currencyCode" }] : /* istanbul ignore next */ []);
|
|
1530
|
+
permissions = computed(() => this.schema().permissions, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
|
|
1531
|
+
canCreate = computed(() => this.auth.canDoOperation(this.schema(), "create"), ...ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []);
|
|
1532
|
+
canUpdate = computed(() => this.auth.canDoOperation(this.schema(), "update"), ...ngDevMode ? [{ debugName: "canUpdate" }] : /* istanbul ignore next */ []);
|
|
1533
|
+
canRemove = computed(() => this.auth.canDoOperation(this.schema(), "remove"), ...ngDevMode ? [{ debugName: "canRemove" }] : /* istanbul ignore next */ []);
|
|
1534
|
+
canQuickEdit = computed(() => this.canUpdate() && (this.schema().quickEdit?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "canQuickEdit" }] : /* istanbul ignore next */ []);
|
|
1535
|
+
quickEditFields = computed(() => {
|
|
1536
|
+
const keys = this.schema().quickEdit ?? [];
|
|
1537
|
+
const byKey = new Map(this.schema().fields.map((field) => [field.key, field]));
|
|
1538
|
+
return keys.map((key) => byKey.get(key)).filter((field) => field !== void 0);
|
|
1539
|
+
}, ...ngDevMode ? [{ debugName: "quickEditFields" }] : /* istanbul ignore next */ []);
|
|
1540
|
+
hasRowActions = computed(() => this.canUpdate() || this.canRemove() || (this.schema().actions?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "hasRowActions" }] : /* istanbul ignore next */ []);
|
|
1541
|
+
reordering = signal(false, ...ngDevMode ? [{ debugName: "reordering" }] : /* istanbul ignore next */ []);
|
|
1542
|
+
hasActiveFilters = computed(() => {
|
|
1543
|
+
if (this.query().trim() !== "") return true;
|
|
1544
|
+
return Object.values(this.filterValues()).some((value) => value !== "");
|
|
1545
|
+
}, ...ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []);
|
|
1546
|
+
canReorder = computed(() => {
|
|
1547
|
+
const schema = this.schema();
|
|
1548
|
+
return schema.sortable === true && schema.positionField !== void 0 && schema.permissions?.reorder !== false && !this.hasActiveFilters();
|
|
1549
|
+
}, ...ngDevMode ? [{ debugName: "canReorder" }] : /* istanbul ignore next */ []);
|
|
1550
|
+
dropDisabled = computed(() => !this.canReorder() || this.reordering(), ...ngDevMode ? [{ debugName: "dropDisabled" }] : /* istanbul ignore next */ []);
|
|
1551
|
+
setQuery(value) {
|
|
1552
|
+
this.query.set(value);
|
|
1553
|
+
}
|
|
1554
|
+
setFilter(key, value) {
|
|
1555
|
+
this.filterValues.update((values) => ({
|
|
1556
|
+
...values,
|
|
1557
|
+
[key]: value
|
|
1558
|
+
}));
|
|
1559
|
+
}
|
|
1560
|
+
clearFilters() {
|
|
1561
|
+
this.query.set("");
|
|
1562
|
+
this.filterValues.set({});
|
|
1563
|
+
}
|
|
1564
|
+
retry() {
|
|
1565
|
+
this.load(this.schema());
|
|
1566
|
+
}
|
|
1567
|
+
isRowClickable() {
|
|
1568
|
+
return Boolean(this.schema().detail?.enabled || this.canUpdate());
|
|
1569
|
+
}
|
|
1570
|
+
goView(row) {
|
|
1571
|
+
this.viewRecord.emit(row);
|
|
1572
|
+
}
|
|
1573
|
+
onRowClick(row) {
|
|
1574
|
+
if (this.schema().detail?.enabled) this.goView(row);
|
|
1575
|
+
else if (this.canUpdate()) this.goEdit(row);
|
|
1576
|
+
}
|
|
1577
|
+
goCreate() {
|
|
1578
|
+
this.router.navigate([
|
|
1579
|
+
"/",
|
|
1580
|
+
this.schema().id,
|
|
1581
|
+
"new"
|
|
1582
|
+
]);
|
|
1583
|
+
}
|
|
1584
|
+
goEdit(row) {
|
|
1585
|
+
if (this.schema().detail?.enabled === true) {
|
|
1586
|
+
this.viewRecord.emit(row);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
this.router.navigate([
|
|
1590
|
+
"/",
|
|
1591
|
+
this.schema().id,
|
|
1592
|
+
this.keyOf(row),
|
|
1593
|
+
"edit"
|
|
1594
|
+
]);
|
|
1595
|
+
}
|
|
1596
|
+
quickEditSaved() {
|
|
1597
|
+
this.quickEditRow.set(null);
|
|
1598
|
+
this.load(this.schema());
|
|
1599
|
+
}
|
|
1600
|
+
async toggleActive(row, checked) {
|
|
1601
|
+
const schema = this.schema();
|
|
1602
|
+
const key = this.keyOf(row);
|
|
1603
|
+
this.savingKey.set(key);
|
|
1604
|
+
try {
|
|
1605
|
+
await firstValueFrom(this.api.update(schema.endpoint, key, {
|
|
1606
|
+
...row,
|
|
1607
|
+
active: checked
|
|
1608
|
+
}));
|
|
1609
|
+
this.rows.update((rows) => rows.map((candidate) => this.keyOf(candidate) === key ? {
|
|
1610
|
+
...candidate,
|
|
1611
|
+
active: checked
|
|
1612
|
+
} : candidate));
|
|
1613
|
+
} catch (err) {
|
|
1614
|
+
this.notifications.error(errorMessage(err, "No se pudo actualizar el registro."));
|
|
1615
|
+
} finally {
|
|
1616
|
+
this.savingKey.set(null);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
onDrop(event) {
|
|
1620
|
+
if (event.previousContainer !== event.container || event.previousIndex === event.currentIndex) return;
|
|
1621
|
+
const row = this.visibleRows()[event.previousIndex];
|
|
1622
|
+
if (row !== void 0) this.moveRow(this.keyOf(row), event.currentIndex);
|
|
1623
|
+
}
|
|
1624
|
+
moveBy(key, delta) {
|
|
1625
|
+
const index = this.visibleRows().findIndex((row) => this.keyOf(row) === key);
|
|
1626
|
+
if (index < 0) return;
|
|
1627
|
+
this.moveRow(key, index + delta);
|
|
1628
|
+
}
|
|
1629
|
+
async moveRow(key, targetIndex) {
|
|
1630
|
+
const schema = this.schema();
|
|
1631
|
+
const path = schema.endpoint.list;
|
|
1632
|
+
if (!path || !schema.positionField || this.reordering()) return;
|
|
1633
|
+
const rows = this.visibleRows();
|
|
1634
|
+
const from = rows.findIndex((row) => this.keyOf(row) === key);
|
|
1635
|
+
if (from < 0) return;
|
|
1636
|
+
const without = rows.filter((row) => this.keyOf(row) !== key);
|
|
1637
|
+
const target = Math.min(Math.max(targetIndex, 0), without.length);
|
|
1638
|
+
if (target === from) return;
|
|
1639
|
+
const reorder = target === 0 ? {
|
|
1640
|
+
key,
|
|
1641
|
+
toStart: true
|
|
1642
|
+
} : target >= without.length ? {
|
|
1643
|
+
key,
|
|
1644
|
+
toEnd: true
|
|
1645
|
+
} : {
|
|
1646
|
+
key,
|
|
1647
|
+
after: this.keyOf(without[target - 1])
|
|
1648
|
+
};
|
|
1649
|
+
this.reordering.set(true);
|
|
1650
|
+
try {
|
|
1651
|
+
const fresh = await firstValueFrom(this.api.request("PUT", path, { reorder }));
|
|
1652
|
+
if (Array.isArray(fresh)) this.rows.set(fresh);
|
|
1653
|
+
this.notifications.success("Orden actualizado.");
|
|
1654
|
+
} catch (err) {
|
|
1655
|
+
this.notifications.error(errorMessage(err, "No se pudo guardar el orden."));
|
|
1656
|
+
} finally {
|
|
1657
|
+
this.reordering.set(false);
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
async removeRow(row) {
|
|
1661
|
+
const schema = this.schema();
|
|
1662
|
+
const key = this.keyOf(row);
|
|
1663
|
+
if (!await this.notifications.confirm({
|
|
1664
|
+
title: `Eliminar «${this.titleOf(row)}»`,
|
|
1665
|
+
message: "Esta acción no se puede deshacer.",
|
|
1666
|
+
confirmLabel: "Eliminar",
|
|
1667
|
+
cancelLabel: "Cancelar"
|
|
1668
|
+
})) return;
|
|
1669
|
+
try {
|
|
1670
|
+
await firstValueFrom(this.api.remove(schema.endpoint, key));
|
|
1671
|
+
this.notifications.success("Registro eliminado.");
|
|
1672
|
+
this.rows.update((rows) => rows.filter((candidate) => this.keyOf(candidate) !== key));
|
|
1673
|
+
} catch (err) {
|
|
1674
|
+
this.notifications.error(errorMessage(err, "No se pudo eliminar el registro."));
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
async runAction(action, row) {
|
|
1678
|
+
await this.actionService.run(action, {
|
|
1679
|
+
record: row,
|
|
1680
|
+
resource: this.schema().id,
|
|
1681
|
+
api: this.api,
|
|
1682
|
+
router: this.router,
|
|
1683
|
+
toast: this.notifications,
|
|
1684
|
+
user: this.auth.user(),
|
|
1685
|
+
keyField: this.schema().keyField
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
iconOf(action) {
|
|
1689
|
+
return getAction(actionType(action))?.icon;
|
|
1690
|
+
}
|
|
1691
|
+
keyOf(row) {
|
|
1692
|
+
return String(row[this.schema().keyField] ?? "");
|
|
1693
|
+
}
|
|
1694
|
+
titleOf(row) {
|
|
1695
|
+
return String(row[this.schema().titleField] ?? "");
|
|
1696
|
+
}
|
|
1697
|
+
formatOf(column) {
|
|
1698
|
+
return formatoDeColumna(column, this.schema().fields);
|
|
1699
|
+
}
|
|
1700
|
+
cellOf(row, column) {
|
|
1701
|
+
return {
|
|
1702
|
+
row,
|
|
1703
|
+
column,
|
|
1704
|
+
fields: this.schema().fields,
|
|
1705
|
+
title: this.titleOf(row),
|
|
1706
|
+
canUpdate: this.canUpdate(),
|
|
1707
|
+
saving: this.savingKey() === this.keyOf(row),
|
|
1708
|
+
currency: this.currencyCode(),
|
|
1709
|
+
toggle: (checked) => void this.toggleActive(row, checked)
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
filterOptions(filter) {
|
|
1713
|
+
if (filter.type === "boolean") return BOOLEAN_FILTER_OPTIONS;
|
|
1714
|
+
if (filter.type === "select") return [{
|
|
1715
|
+
value: "",
|
|
1716
|
+
label: "Todos"
|
|
1717
|
+
}, ...filter.options ?? []];
|
|
1718
|
+
return [];
|
|
1719
|
+
}
|
|
1720
|
+
inputValue(event) {
|
|
1721
|
+
return event.target.value;
|
|
1722
|
+
}
|
|
1723
|
+
selectValue(event) {
|
|
1724
|
+
return event.target.value;
|
|
1725
|
+
}
|
|
1726
|
+
async load(schema) {
|
|
1727
|
+
const epoch = ++this.loadEpoch;
|
|
1728
|
+
this.status.set("loading");
|
|
1729
|
+
try {
|
|
1730
|
+
const rows = await firstValueFrom(this.api.list(schema.endpoint));
|
|
1731
|
+
if (epoch !== this.loadEpoch) return;
|
|
1732
|
+
this.rows.set(Array.isArray(rows) ? rows : []);
|
|
1733
|
+
this.status.set("ready");
|
|
1734
|
+
} catch {
|
|
1735
|
+
if (epoch !== this.loadEpoch) return;
|
|
1736
|
+
this.status.set("error");
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
1740
|
+
minVersion: "12.0.0",
|
|
1741
|
+
version: "22.2.3",
|
|
1742
|
+
ngImport: i0,
|
|
1743
|
+
type: ListView,
|
|
1744
|
+
deps: [],
|
|
1745
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
1746
|
+
});
|
|
1747
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
1748
|
+
minVersion: "17.0.0",
|
|
1749
|
+
version: "22.2.3",
|
|
1750
|
+
type: ListView,
|
|
1751
|
+
isStandalone: true,
|
|
1752
|
+
selector: "app-list-view",
|
|
1753
|
+
inputs: { schema: {
|
|
1754
|
+
classPropertyName: "schema",
|
|
1755
|
+
publicName: "schema",
|
|
1756
|
+
isSignal: true,
|
|
1757
|
+
isRequired: true,
|
|
1758
|
+
transformFunction: null
|
|
1759
|
+
} },
|
|
1760
|
+
outputs: { viewRecord: "viewRecord" },
|
|
1761
|
+
ngImport: i0,
|
|
1762
|
+
template: `
|
|
1763
|
+
<section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
|
|
1764
|
+
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
1765
|
+
<div class="flex flex-wrap items-end gap-3">
|
|
1766
|
+
@if (schema().search?.length) {
|
|
1767
|
+
<label class="flex flex-col gap-1 text-xs font-medium text-neutral">
|
|
1768
|
+
Buscar
|
|
1769
|
+
<input
|
|
1770
|
+
type="search"
|
|
1771
|
+
class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
1772
|
+
placeholder="Buscar…"
|
|
1773
|
+
[value]="query()"
|
|
1774
|
+
(input)="setQuery(inputValue($event))"
|
|
1775
|
+
/>
|
|
1776
|
+
</label>
|
|
1777
|
+
}
|
|
1778
|
+
@for (filter of schema().filters ?? []; track filter.key) {
|
|
1779
|
+
<label class="flex flex-col gap-1 text-xs font-medium text-neutral">
|
|
1780
|
+
{{ filter.label }}
|
|
1781
|
+
@if (filter.type === 'text') {
|
|
1782
|
+
<input
|
|
1783
|
+
type="search"
|
|
1784
|
+
class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
1785
|
+
[value]="filterValues()[filter.key] ?? ''"
|
|
1786
|
+
(input)="setFilter(filter.key, inputValue($event))"
|
|
1787
|
+
/>
|
|
1788
|
+
} @else {
|
|
1789
|
+
<select
|
|
1790
|
+
class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
1791
|
+
[value]="filterValues()[filter.key] ?? ''"
|
|
1792
|
+
(change)="setFilter(filter.key, selectValue($event))"
|
|
1793
|
+
>
|
|
1794
|
+
@for (option of filterOptions(filter); track option.value) {
|
|
1795
|
+
<option [value]="option.value">{{ option.label }}</option>
|
|
1796
|
+
}
|
|
1797
|
+
</select>
|
|
1798
|
+
}
|
|
1799
|
+
</label>
|
|
1800
|
+
}
|
|
1801
|
+
</div>
|
|
1802
|
+
@if (canCreate()) {
|
|
1803
|
+
<button app-button type="button" (click)="goCreate()">
|
|
1804
|
+
<svg lucidePlus size="16" />
|
|
1805
|
+
Crear {{ schema().label }}
|
|
1806
|
+
</button>
|
|
1807
|
+
}
|
|
1808
|
+
</div>
|
|
1809
|
+
|
|
1810
|
+
@switch (status()) {
|
|
1811
|
+
@case ('loading') {
|
|
1812
|
+
<app-skeleton [lines]="6" [height]="44" />
|
|
1813
|
+
}
|
|
1814
|
+
@case ('error') {
|
|
1815
|
+
<app-empty-state
|
|
1816
|
+
title="No se pudo cargar el listado"
|
|
1817
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
1818
|
+
>
|
|
1819
|
+
<button app-button variant="outline" type="button" (click)="retry()">
|
|
1820
|
+
<svg lucideRotateCw size="16" />
|
|
1821
|
+
Reintentar
|
|
1822
|
+
</button>
|
|
1823
|
+
</app-empty-state>
|
|
1824
|
+
}
|
|
1825
|
+
@case ('ready') {
|
|
1826
|
+
@if (visibleRows().length === 0) {
|
|
1827
|
+
<app-empty-state
|
|
1828
|
+
[title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
|
|
1829
|
+
[message]="
|
|
1830
|
+
rows().length === 0
|
|
1831
|
+
? 'Todavía no hay registros en este recurso.'
|
|
1832
|
+
: 'Ningún registro coincide con la búsqueda o los filtros.'
|
|
1833
|
+
"
|
|
1834
|
+
>
|
|
1835
|
+
@if (rows().length > 0) {
|
|
1836
|
+
<button app-button variant="outline" type="button" (click)="clearFilters()">
|
|
1837
|
+
Limpiar filtros
|
|
1838
|
+
</button>
|
|
1839
|
+
} @else if (canCreate()) {
|
|
1840
|
+
<button app-button type="button" (click)="goCreate()">
|
|
1841
|
+
<svg lucidePlus size="16" />
|
|
1842
|
+
Crear el primero
|
|
1843
|
+
</button>
|
|
1844
|
+
}
|
|
1845
|
+
</app-empty-state>
|
|
1846
|
+
} @else {
|
|
1847
|
+
<!-- La tabla es la vista de escritorio: se oculta bajo lg para
|
|
1848
|
+
que la lista de tarjetas de abajo sea la ÚNICA vista en
|
|
1849
|
+
móvil (con las dos visibles, el listado se duplicaba). -->
|
|
1850
|
+
<div
|
|
1851
|
+
class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
|
|
1852
|
+
>
|
|
1853
|
+
<table class="w-full text-sm">
|
|
1854
|
+
<thead>
|
|
1855
|
+
<tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
|
|
1856
|
+
@if (canReorder()) {
|
|
1857
|
+
<th scope="col" class="w-16 px-2 py-3 font-medium">
|
|
1858
|
+
<span class="sr-only">Orden</span>
|
|
1859
|
+
</th>
|
|
1860
|
+
}
|
|
1861
|
+
@for (column of schema().listColumns; track column.key) {
|
|
1862
|
+
<th scope="col" class="px-4 py-3 font-medium">
|
|
1863
|
+
{{ column.label }}
|
|
1864
|
+
</th>
|
|
1865
|
+
}
|
|
1866
|
+
@if (hasRowActions()) {
|
|
1867
|
+
<th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
|
|
1868
|
+
}
|
|
1869
|
+
</tr>
|
|
1870
|
+
</thead>
|
|
1871
|
+
<tbody
|
|
1872
|
+
cdkDropList
|
|
1873
|
+
[cdkDropListData]="visibleRows()"
|
|
1874
|
+
[cdkDropListDisabled]="dropDisabled()"
|
|
1875
|
+
(cdkDropListDropped)="onDrop($event)"
|
|
1876
|
+
>
|
|
1877
|
+
@for (row of visibleRows(); track keyOf(row); let i = $index) {
|
|
1878
|
+
<tr
|
|
1879
|
+
class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
|
|
1880
|
+
[class.cursor-pointer]="isRowClickable()"
|
|
1881
|
+
cdkDrag
|
|
1882
|
+
[cdkDragDisabled]="dropDisabled()"
|
|
1883
|
+
(click)="onRowClick(row)"
|
|
1884
|
+
>
|
|
1885
|
+
@if (canReorder()) {
|
|
1886
|
+
<td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
|
|
1887
|
+
<div class="flex items-center gap-1">
|
|
1888
|
+
<button
|
|
1889
|
+
type="button"
|
|
1890
|
+
class="cursor-grab text-neutral hover:text-primary"
|
|
1891
|
+
cdkDragHandle
|
|
1892
|
+
[attr.aria-label]="'Reordenar ' + titleOf(row)"
|
|
1893
|
+
title="Arrastrar para reordenar"
|
|
1894
|
+
>
|
|
1895
|
+
<svg lucideGripVertical size="16" />
|
|
1896
|
+
</button>
|
|
1897
|
+
<span class="flex flex-col leading-none">
|
|
1898
|
+
<button
|
|
1899
|
+
type="button"
|
|
1900
|
+
class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
|
|
1901
|
+
[disabled]="i === 0 || reordering()"
|
|
1902
|
+
[attr.aria-label]="'Subir ' + titleOf(row)"
|
|
1903
|
+
(click)="moveBy(keyOf(row), -1)"
|
|
1904
|
+
>
|
|
1905
|
+
<svg lucideChevronUp size="16" />
|
|
1906
|
+
</button>
|
|
1907
|
+
<button
|
|
1908
|
+
type="button"
|
|
1909
|
+
class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
|
|
1910
|
+
[disabled]="i === visibleRows().length - 1 || reordering()"
|
|
1911
|
+
[attr.aria-label]="'Bajar ' + titleOf(row)"
|
|
1912
|
+
(click)="moveBy(keyOf(row), 1)"
|
|
1913
|
+
>
|
|
1914
|
+
<svg lucideChevronDown size="16" />
|
|
1915
|
+
</button>
|
|
1916
|
+
</span>
|
|
1917
|
+
</div>
|
|
1918
|
+
</td>
|
|
1919
|
+
}
|
|
1920
|
+
@for (column of schema().listColumns; track column.key) {
|
|
1921
|
+
<td class="px-4 py-2.5 align-middle">
|
|
1922
|
+
<!-- Los 7 formatos salen del registro (R4.3):
|
|
1923
|
+
la vista sólo elige cuál y entrega la celda. -->
|
|
1924
|
+
<app-render-host
|
|
1925
|
+
kind="column-format"
|
|
1926
|
+
[type]="formatOf(column)"
|
|
1927
|
+
[config]="column"
|
|
1928
|
+
[data]="cellOf(row, column)"
|
|
1929
|
+
/>
|
|
1930
|
+
</td>
|
|
1931
|
+
}
|
|
1932
|
+
@if (hasRowActions()) {
|
|
1933
|
+
<td class="px-4 py-2.5" (click)="$event.stopPropagation()">
|
|
1934
|
+
<div class="flex items-center justify-end gap-1">
|
|
1935
|
+
@if (schema().detail?.enabled) {
|
|
1936
|
+
<button
|
|
1937
|
+
app-button
|
|
1938
|
+
variant="ghost"
|
|
1939
|
+
type="button"
|
|
1940
|
+
(click)="goView(row)"
|
|
1941
|
+
title="Ver ficha de detalle"
|
|
1942
|
+
class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
|
|
1943
|
+
>
|
|
1944
|
+
<svg lucideEye size="15" />
|
|
1945
|
+
<span>Ver</span>
|
|
1946
|
+
</button>
|
|
1947
|
+
}
|
|
1948
|
+
@for (action of schema().actions ?? []; track action.id) {
|
|
1949
|
+
<button
|
|
1950
|
+
app-button
|
|
1951
|
+
variant="ghost"
|
|
1952
|
+
type="button"
|
|
1953
|
+
[title]="action.label"
|
|
1954
|
+
[attr.aria-label]="action.label"
|
|
1955
|
+
(click)="runAction(action, row)"
|
|
1956
|
+
class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
|
|
1957
|
+
>
|
|
1958
|
+
@let actionIcon = iconOf(action);
|
|
1959
|
+
@if (actionIcon) {
|
|
1960
|
+
<svg [lucideIcon]="actionIcon" size="15" />
|
|
1961
|
+
} @else {
|
|
1962
|
+
{{ action.label }}
|
|
1963
|
+
}
|
|
1964
|
+
</button>
|
|
1965
|
+
}
|
|
1966
|
+
@if (canQuickEdit()) {
|
|
1967
|
+
<button
|
|
1968
|
+
app-button
|
|
1969
|
+
variant="ghost"
|
|
1970
|
+
type="button"
|
|
1971
|
+
(click)="quickEditRow.set(row)"
|
|
1972
|
+
title="Edición rápida"
|
|
1973
|
+
class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
|
|
1974
|
+
>
|
|
1975
|
+
<svg lucideZap size="15" />
|
|
1976
|
+
<span>Edición rápida</span>
|
|
1977
|
+
</button>
|
|
1978
|
+
}
|
|
1979
|
+
@if (canUpdate()) {
|
|
1980
|
+
<button
|
|
1981
|
+
app-button
|
|
1982
|
+
variant="ghost"
|
|
1983
|
+
type="button"
|
|
1984
|
+
(click)="goEdit(row)"
|
|
1985
|
+
title="Editar registro"
|
|
1986
|
+
class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
|
|
1987
|
+
>
|
|
1988
|
+
<svg lucidePencil size="15" />
|
|
1989
|
+
<span>Editar</span>
|
|
1990
|
+
</button>
|
|
1991
|
+
}
|
|
1992
|
+
@if (canRemove()) {
|
|
1993
|
+
<button
|
|
1994
|
+
app-button
|
|
1995
|
+
variant="ghost"
|
|
1996
|
+
type="button"
|
|
1997
|
+
(click)="removeRow(row)"
|
|
1998
|
+
title="Eliminar registro"
|
|
1999
|
+
class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
|
|
2000
|
+
>
|
|
2001
|
+
<svg lucideTrash2 size="15" />
|
|
2002
|
+
<span>Eliminar</span>
|
|
2003
|
+
</button>
|
|
2004
|
+
}
|
|
2005
|
+
</div>
|
|
2006
|
+
</td>
|
|
2007
|
+
}
|
|
2008
|
+
</tr>
|
|
2009
|
+
}
|
|
2010
|
+
</tbody>
|
|
2011
|
+
</table>
|
|
2012
|
+
</div>
|
|
2013
|
+
|
|
2014
|
+
<!-- Vista de tarjetas — mobile-first (guía mobile §6).
|
|
2015
|
+
La tabla de arriba necesita scroll horizontal con 5–7 columnas
|
|
2016
|
+
(el anti-patrón #2): en pantallas chicas se convierte en una
|
|
2017
|
+
lista de tarjetas de dos niveles (principal + secundario),
|
|
2018
|
+
donde cada fila completa es un objetivo táctil. lg:hidden la
|
|
2019
|
+
retira cuando la tabla vuelve a ser usable. -->
|
|
2020
|
+
<ul class="space-y-2 lg:hidden">
|
|
2021
|
+
@for (row of visibleRows(); track keyOf(row)) {
|
|
2022
|
+
<li
|
|
2023
|
+
class="rounded-xl border border-neutral/20 bg-surface p-3"
|
|
2024
|
+
[class.cursor-pointer]="isRowClickable()"
|
|
2025
|
+
(click)="onRowClick(row)"
|
|
2026
|
+
>
|
|
2027
|
+
<!-- Nivel principal: el título del registro, siempre visible
|
|
2028
|
+
(guía §6.2: el identificador nunca se esconde). -->
|
|
2029
|
+
<div class="flex items-start justify-between gap-2">
|
|
2030
|
+
<span class="text-sm font-medium">
|
|
2031
|
+
{{ titleOf(row) }}
|
|
2032
|
+
</span>
|
|
2033
|
+
@if (schema().detail?.enabled) {
|
|
2034
|
+
<svg
|
|
2035
|
+
lucideChevronRight
|
|
2036
|
+
size="16"
|
|
2037
|
+
class="mt-0.5 shrink-0 text-neutral"
|
|
2038
|
+
aria-hidden="true"
|
|
2039
|
+
/>
|
|
2040
|
+
}
|
|
2041
|
+
</div>
|
|
2042
|
+
<!-- Nivel secundario: el resto de las columnas como pares
|
|
2043
|
+
etiqueta/valor. Sin hover: todo lo accionable va en el
|
|
2044
|
+
pie de la tarjeta, al alcance del pulgar. -->
|
|
2045
|
+
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
|
2046
|
+
@for (column of schema().listColumns; track column.key) {
|
|
2047
|
+
<dt class="text-neutral">{{ column.label }}</dt>
|
|
2048
|
+
<dd>
|
|
2049
|
+
<app-render-host
|
|
2050
|
+
kind="column-format"
|
|
2051
|
+
[type]="formatOf(column)"
|
|
2052
|
+
[config]="column"
|
|
2053
|
+
[data]="cellOf(row, column)"
|
|
2054
|
+
/>
|
|
2055
|
+
</dd>
|
|
2056
|
+
}
|
|
2057
|
+
</dl>
|
|
2058
|
+
@if (hasRowActions()) {
|
|
2059
|
+
<!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
|
|
2060
|
+
<div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
|
|
2061
|
+
@if (schema().detail?.enabled) {
|
|
2062
|
+
<button
|
|
2063
|
+
app-button
|
|
2064
|
+
variant="outline"
|
|
2065
|
+
type="button"
|
|
2066
|
+
size="sm"
|
|
2067
|
+
class="flex-1"
|
|
2068
|
+
(click)="$event.stopPropagation(); goView(row)"
|
|
2069
|
+
>
|
|
2070
|
+
<svg lucideEye size="15" aria-hidden="true" />
|
|
2071
|
+
<span>Ver</span>
|
|
2072
|
+
</button>
|
|
2073
|
+
}
|
|
2074
|
+
@for (action of schema().actions ?? []; track action.id) {
|
|
2075
|
+
<button
|
|
2076
|
+
app-button
|
|
2077
|
+
variant="outline"
|
|
2078
|
+
type="button"
|
|
2079
|
+
size="sm"
|
|
2080
|
+
class="flex-1"
|
|
2081
|
+
(click)="$event.stopPropagation(); runAction(action, row)"
|
|
2082
|
+
>
|
|
2083
|
+
@let actionIcon = iconOf(action);
|
|
2084
|
+
@if (actionIcon) {
|
|
2085
|
+
<svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
|
|
2086
|
+
}
|
|
2087
|
+
<span>{{ action.label }}</span>
|
|
2088
|
+
</button>
|
|
2089
|
+
}
|
|
2090
|
+
<!-- Editar es O una vía u otra, como en la tabla: si el
|
|
2091
|
+
schema declara quickEdit va el atajo; si no, la
|
|
2092
|
+
pantalla completa. Apilados los dos sería ruido. -->
|
|
2093
|
+
@if (canQuickEdit()) {
|
|
2094
|
+
<button
|
|
2095
|
+
app-button
|
|
2096
|
+
variant="outline"
|
|
2097
|
+
type="button"
|
|
2098
|
+
size="sm"
|
|
2099
|
+
class="flex-1"
|
|
2100
|
+
(click)="$event.stopPropagation(); quickEditRow.set(row)"
|
|
2101
|
+
>
|
|
2102
|
+
<svg lucideZap size="15" aria-hidden="true" />
|
|
2103
|
+
<span>Editar</span>
|
|
2104
|
+
</button>
|
|
2105
|
+
} @else if (canUpdate()) {
|
|
2106
|
+
<button
|
|
2107
|
+
app-button
|
|
2108
|
+
variant="outline"
|
|
2109
|
+
type="button"
|
|
2110
|
+
size="sm"
|
|
2111
|
+
class="flex-1"
|
|
2112
|
+
(click)="$event.stopPropagation(); goEdit(row)"
|
|
2113
|
+
>
|
|
2114
|
+
<svg lucidePencil size="15" aria-hidden="true" />
|
|
2115
|
+
<span>Editar</span>
|
|
2116
|
+
</button>
|
|
2117
|
+
}
|
|
2118
|
+
@if (canRemove()) {
|
|
2119
|
+
<button
|
|
2120
|
+
app-button
|
|
2121
|
+
variant="danger"
|
|
2122
|
+
type="button"
|
|
2123
|
+
size="sm"
|
|
2124
|
+
class="flex-1"
|
|
2125
|
+
(click)="$event.stopPropagation(); removeRow(row)"
|
|
2126
|
+
>
|
|
2127
|
+
<svg lucideTrash2 size="15" aria-hidden="true" />
|
|
2128
|
+
<span>Eliminar</span>
|
|
2129
|
+
</button>
|
|
2130
|
+
}
|
|
2131
|
+
</div>
|
|
2132
|
+
}
|
|
2133
|
+
</li>
|
|
2134
|
+
}
|
|
2135
|
+
</ul>
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
@if (quickEditRow(); as row) {
|
|
2141
|
+
<app-drawer-form
|
|
2142
|
+
[open]="true"
|
|
2143
|
+
[schema]="schema()"
|
|
2144
|
+
mode="edit"
|
|
2145
|
+
[recordKey]="keyOf(row)"
|
|
2146
|
+
[fields]="quickEditFields()"
|
|
2147
|
+
title="Edición rápida"
|
|
2148
|
+
size="sm"
|
|
2149
|
+
(close)="quickEditRow.set(null)"
|
|
2150
|
+
(saved)="quickEditSaved()"
|
|
2151
|
+
/>
|
|
2152
|
+
}
|
|
2153
|
+
</section>
|
|
2154
|
+
`,
|
|
2155
|
+
isInline: true,
|
|
2156
|
+
dependencies: [
|
|
2157
|
+
{
|
|
2158
|
+
kind: "component",
|
|
2159
|
+
type: Button,
|
|
2160
|
+
selector: "button[app-button]",
|
|
2161
|
+
inputs: [
|
|
2162
|
+
"variant",
|
|
2163
|
+
"size",
|
|
2164
|
+
"type",
|
|
2165
|
+
"disabled",
|
|
2166
|
+
"loading"
|
|
2167
|
+
]
|
|
2168
|
+
},
|
|
2169
|
+
{
|
|
2170
|
+
kind: "directive",
|
|
2171
|
+
type: CdkDrag,
|
|
2172
|
+
selector: "[cdkDrag]",
|
|
2173
|
+
inputs: [
|
|
2174
|
+
"cdkDragData",
|
|
2175
|
+
"cdkDragLockAxis",
|
|
2176
|
+
"cdkDragRootElement",
|
|
2177
|
+
"cdkDragBoundary",
|
|
2178
|
+
"cdkDragStartDelay",
|
|
2179
|
+
"cdkDragFreeDragPosition",
|
|
2180
|
+
"cdkDragDisabled",
|
|
2181
|
+
"cdkDragConstrainPosition",
|
|
2182
|
+
"cdkDragPreviewClass",
|
|
2183
|
+
"cdkDragPreviewContainer",
|
|
2184
|
+
"cdkDragScale"
|
|
2185
|
+
],
|
|
2186
|
+
outputs: [
|
|
2187
|
+
"cdkDragStarted",
|
|
2188
|
+
"cdkDragReleased",
|
|
2189
|
+
"cdkDragEnded",
|
|
2190
|
+
"cdkDragEntered",
|
|
2191
|
+
"cdkDragExited",
|
|
2192
|
+
"cdkDragDropped",
|
|
2193
|
+
"cdkDragMoved"
|
|
2194
|
+
],
|
|
2195
|
+
exportAs: ["cdkDrag"]
|
|
2196
|
+
},
|
|
2197
|
+
{
|
|
2198
|
+
kind: "directive",
|
|
2199
|
+
type: CdkDragHandle,
|
|
2200
|
+
selector: "[cdkDragHandle]",
|
|
2201
|
+
inputs: ["cdkDragHandleDisabled"]
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
kind: "directive",
|
|
2205
|
+
type: CdkDropList,
|
|
2206
|
+
selector: "[cdkDropList], cdk-drop-list",
|
|
2207
|
+
inputs: [
|
|
2208
|
+
"cdkDropListConnectedTo",
|
|
2209
|
+
"cdkDropListData",
|
|
2210
|
+
"cdkDropListOrientation",
|
|
2211
|
+
"id",
|
|
2212
|
+
"cdkDropListLockAxis",
|
|
2213
|
+
"cdkDropListDisabled",
|
|
2214
|
+
"cdkDropListSortingDisabled",
|
|
2215
|
+
"cdkDropListEnterPredicate",
|
|
2216
|
+
"cdkDropListSortPredicate",
|
|
2217
|
+
"cdkDropListAutoScrollDisabled",
|
|
2218
|
+
"cdkDropListAutoScrollStep",
|
|
2219
|
+
"cdkDropListElementContainer",
|
|
2220
|
+
"cdkDropListHasAnchor"
|
|
2221
|
+
],
|
|
2222
|
+
outputs: [
|
|
2223
|
+
"cdkDropListDropped",
|
|
2224
|
+
"cdkDropListEntered",
|
|
2225
|
+
"cdkDropListExited",
|
|
2226
|
+
"cdkDropListSorted"
|
|
2227
|
+
],
|
|
2228
|
+
exportAs: ["cdkDropList"]
|
|
2229
|
+
},
|
|
2230
|
+
{
|
|
2231
|
+
kind: "component",
|
|
2232
|
+
type: DrawerForm,
|
|
2233
|
+
selector: "app-drawer-form",
|
|
2234
|
+
inputs: [
|
|
2235
|
+
"open",
|
|
2236
|
+
"schema",
|
|
2237
|
+
"fields",
|
|
2238
|
+
"mode",
|
|
2239
|
+
"recordKey",
|
|
2240
|
+
"title",
|
|
2241
|
+
"size"
|
|
2242
|
+
],
|
|
2243
|
+
outputs: ["saved", "close"]
|
|
2244
|
+
},
|
|
2245
|
+
{
|
|
2246
|
+
kind: "component",
|
|
2247
|
+
type: EmptyState,
|
|
2248
|
+
selector: "app-empty-state",
|
|
2249
|
+
inputs: ["title", "message"]
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
kind: "component",
|
|
2253
|
+
type: LucideChevronDown,
|
|
2254
|
+
selector: "svg[lucideChevronDown]"
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
kind: "component",
|
|
2258
|
+
type: LucideChevronRight,
|
|
2259
|
+
selector: "svg[lucideChevronRight]"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
kind: "component",
|
|
2263
|
+
type: LucideChevronUp,
|
|
2264
|
+
selector: "svg[lucideChevronUp]"
|
|
2265
|
+
},
|
|
2266
|
+
{
|
|
2267
|
+
kind: "component",
|
|
2268
|
+
type: LucideEye,
|
|
2269
|
+
selector: "svg[lucideEye]"
|
|
2270
|
+
},
|
|
2271
|
+
{
|
|
2272
|
+
kind: "component",
|
|
2273
|
+
type: LucideGripVertical,
|
|
2274
|
+
selector: "svg[lucideGripVertical]"
|
|
2275
|
+
},
|
|
2276
|
+
{
|
|
2277
|
+
kind: "component",
|
|
2278
|
+
type: LucidePencil,
|
|
2279
|
+
selector: "svg[lucidePencil]"
|
|
2280
|
+
},
|
|
2281
|
+
{
|
|
2282
|
+
kind: "component",
|
|
2283
|
+
type: LucidePlus,
|
|
2284
|
+
selector: "svg[lucidePlus]"
|
|
2285
|
+
},
|
|
2286
|
+
{
|
|
2287
|
+
kind: "component",
|
|
2288
|
+
type: LucideRotateCw,
|
|
2289
|
+
selector: "svg[lucideRotateCw]"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
kind: "component",
|
|
2293
|
+
type: LucideDynamicIcon,
|
|
2294
|
+
selector: "svg[lucideIcon]",
|
|
2295
|
+
inputs: ["lucideIcon"]
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
kind: "component",
|
|
2299
|
+
type: LucideTrash2,
|
|
2300
|
+
selector: "svg[lucideTrash], svg[lucideTrash2]"
|
|
2301
|
+
},
|
|
2302
|
+
{
|
|
2303
|
+
kind: "component",
|
|
2304
|
+
type: LucideZap,
|
|
2305
|
+
selector: "svg[lucideZap]"
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
kind: "component",
|
|
2309
|
+
type: RenderHost,
|
|
2310
|
+
selector: "app-render-host",
|
|
2311
|
+
inputs: [
|
|
2312
|
+
"kind",
|
|
2313
|
+
"type",
|
|
2314
|
+
"config",
|
|
2315
|
+
"ctx",
|
|
2316
|
+
"data",
|
|
2317
|
+
"mode",
|
|
2318
|
+
"value",
|
|
2319
|
+
"chip"
|
|
2320
|
+
]
|
|
2321
|
+
},
|
|
2322
|
+
{
|
|
2323
|
+
kind: "component",
|
|
2324
|
+
type: Skeleton,
|
|
2325
|
+
selector: "app-skeleton",
|
|
2326
|
+
inputs: [
|
|
2327
|
+
"lines",
|
|
2328
|
+
"height",
|
|
2329
|
+
"width"
|
|
2330
|
+
]
|
|
2331
|
+
}
|
|
2332
|
+
]
|
|
2333
|
+
});
|
|
2334
|
+
};
|
|
2335
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2336
|
+
minVersion: "12.0.0",
|
|
2337
|
+
version: "22.2.3",
|
|
2338
|
+
ngImport: i0,
|
|
2339
|
+
type: ListView,
|
|
2340
|
+
decorators: [{
|
|
2341
|
+
type: Component,
|
|
2342
|
+
args: [{
|
|
2343
|
+
selector: "app-list-view",
|
|
2344
|
+
imports: [
|
|
2345
|
+
Button,
|
|
2346
|
+
CdkDrag,
|
|
2347
|
+
CdkDragHandle,
|
|
2348
|
+
CdkDropList,
|
|
2349
|
+
DrawerForm,
|
|
2350
|
+
EmptyState,
|
|
2351
|
+
LucideChevronDown,
|
|
2352
|
+
LucideChevronRight,
|
|
2353
|
+
LucideChevronUp,
|
|
2354
|
+
LucideEye,
|
|
2355
|
+
LucideGripVertical,
|
|
2356
|
+
LucidePencil,
|
|
2357
|
+
LucidePlus,
|
|
2358
|
+
LucideRotateCw,
|
|
2359
|
+
LucideDynamicIcon,
|
|
2360
|
+
LucideTrash2,
|
|
2361
|
+
LucideZap,
|
|
2362
|
+
RenderHost,
|
|
2363
|
+
Skeleton
|
|
2364
|
+
],
|
|
2365
|
+
template: `
|
|
2366
|
+
<section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
|
|
2367
|
+
<div class="flex flex-wrap items-end justify-between gap-3">
|
|
2368
|
+
<div class="flex flex-wrap items-end gap-3">
|
|
2369
|
+
@if (schema().search?.length) {
|
|
2370
|
+
<label class="flex flex-col gap-1 text-xs font-medium text-neutral">
|
|
2371
|
+
Buscar
|
|
2372
|
+
<input
|
|
2373
|
+
type="search"
|
|
2374
|
+
class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
2375
|
+
placeholder="Buscar…"
|
|
2376
|
+
[value]="query()"
|
|
2377
|
+
(input)="setQuery(inputValue($event))"
|
|
2378
|
+
/>
|
|
2379
|
+
</label>
|
|
2380
|
+
}
|
|
2381
|
+
@for (filter of schema().filters ?? []; track filter.key) {
|
|
2382
|
+
<label class="flex flex-col gap-1 text-xs font-medium text-neutral">
|
|
2383
|
+
{{ filter.label }}
|
|
2384
|
+
@if (filter.type === 'text') {
|
|
2385
|
+
<input
|
|
2386
|
+
type="search"
|
|
2387
|
+
class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
2388
|
+
[value]="filterValues()[filter.key] ?? ''"
|
|
2389
|
+
(input)="setFilter(filter.key, inputValue($event))"
|
|
2390
|
+
/>
|
|
2391
|
+
} @else {
|
|
2392
|
+
<select
|
|
2393
|
+
class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
|
|
2394
|
+
[value]="filterValues()[filter.key] ?? ''"
|
|
2395
|
+
(change)="setFilter(filter.key, selectValue($event))"
|
|
2396
|
+
>
|
|
2397
|
+
@for (option of filterOptions(filter); track option.value) {
|
|
2398
|
+
<option [value]="option.value">{{ option.label }}</option>
|
|
2399
|
+
}
|
|
2400
|
+
</select>
|
|
2401
|
+
}
|
|
2402
|
+
</label>
|
|
2403
|
+
}
|
|
2404
|
+
</div>
|
|
2405
|
+
@if (canCreate()) {
|
|
2406
|
+
<button app-button type="button" (click)="goCreate()">
|
|
2407
|
+
<svg lucidePlus size="16" />
|
|
2408
|
+
Crear {{ schema().label }}
|
|
2409
|
+
</button>
|
|
2410
|
+
}
|
|
2411
|
+
</div>
|
|
2412
|
+
|
|
2413
|
+
@switch (status()) {
|
|
2414
|
+
@case ('loading') {
|
|
2415
|
+
<app-skeleton [lines]="6" [height]="44" />
|
|
2416
|
+
}
|
|
2417
|
+
@case ('error') {
|
|
2418
|
+
<app-empty-state
|
|
2419
|
+
title="No se pudo cargar el listado"
|
|
2420
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
2421
|
+
>
|
|
2422
|
+
<button app-button variant="outline" type="button" (click)="retry()">
|
|
2423
|
+
<svg lucideRotateCw size="16" />
|
|
2424
|
+
Reintentar
|
|
2425
|
+
</button>
|
|
2426
|
+
</app-empty-state>
|
|
2427
|
+
}
|
|
2428
|
+
@case ('ready') {
|
|
2429
|
+
@if (visibleRows().length === 0) {
|
|
2430
|
+
<app-empty-state
|
|
2431
|
+
[title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
|
|
2432
|
+
[message]="
|
|
2433
|
+
rows().length === 0
|
|
2434
|
+
? 'Todavía no hay registros en este recurso.'
|
|
2435
|
+
: 'Ningún registro coincide con la búsqueda o los filtros.'
|
|
2436
|
+
"
|
|
2437
|
+
>
|
|
2438
|
+
@if (rows().length > 0) {
|
|
2439
|
+
<button app-button variant="outline" type="button" (click)="clearFilters()">
|
|
2440
|
+
Limpiar filtros
|
|
2441
|
+
</button>
|
|
2442
|
+
} @else if (canCreate()) {
|
|
2443
|
+
<button app-button type="button" (click)="goCreate()">
|
|
2444
|
+
<svg lucidePlus size="16" />
|
|
2445
|
+
Crear el primero
|
|
2446
|
+
</button>
|
|
2447
|
+
}
|
|
2448
|
+
</app-empty-state>
|
|
2449
|
+
} @else {
|
|
2450
|
+
<!-- La tabla es la vista de escritorio: se oculta bajo lg para
|
|
2451
|
+
que la lista de tarjetas de abajo sea la ÚNICA vista en
|
|
2452
|
+
móvil (con las dos visibles, el listado se duplicaba). -->
|
|
2453
|
+
<div
|
|
2454
|
+
class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
|
|
2455
|
+
>
|
|
2456
|
+
<table class="w-full text-sm">
|
|
2457
|
+
<thead>
|
|
2458
|
+
<tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
|
|
2459
|
+
@if (canReorder()) {
|
|
2460
|
+
<th scope="col" class="w-16 px-2 py-3 font-medium">
|
|
2461
|
+
<span class="sr-only">Orden</span>
|
|
2462
|
+
</th>
|
|
2463
|
+
}
|
|
2464
|
+
@for (column of schema().listColumns; track column.key) {
|
|
2465
|
+
<th scope="col" class="px-4 py-3 font-medium">
|
|
2466
|
+
{{ column.label }}
|
|
2467
|
+
</th>
|
|
2468
|
+
}
|
|
2469
|
+
@if (hasRowActions()) {
|
|
2470
|
+
<th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
|
|
2471
|
+
}
|
|
2472
|
+
</tr>
|
|
2473
|
+
</thead>
|
|
2474
|
+
<tbody
|
|
2475
|
+
cdkDropList
|
|
2476
|
+
[cdkDropListData]="visibleRows()"
|
|
2477
|
+
[cdkDropListDisabled]="dropDisabled()"
|
|
2478
|
+
(cdkDropListDropped)="onDrop($event)"
|
|
2479
|
+
>
|
|
2480
|
+
@for (row of visibleRows(); track keyOf(row); let i = $index) {
|
|
2481
|
+
<tr
|
|
2482
|
+
class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
|
|
2483
|
+
[class.cursor-pointer]="isRowClickable()"
|
|
2484
|
+
cdkDrag
|
|
2485
|
+
[cdkDragDisabled]="dropDisabled()"
|
|
2486
|
+
(click)="onRowClick(row)"
|
|
2487
|
+
>
|
|
2488
|
+
@if (canReorder()) {
|
|
2489
|
+
<td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
|
|
2490
|
+
<div class="flex items-center gap-1">
|
|
2491
|
+
<button
|
|
2492
|
+
type="button"
|
|
2493
|
+
class="cursor-grab text-neutral hover:text-primary"
|
|
2494
|
+
cdkDragHandle
|
|
2495
|
+
[attr.aria-label]="'Reordenar ' + titleOf(row)"
|
|
2496
|
+
title="Arrastrar para reordenar"
|
|
2497
|
+
>
|
|
2498
|
+
<svg lucideGripVertical size="16" />
|
|
2499
|
+
</button>
|
|
2500
|
+
<span class="flex flex-col leading-none">
|
|
2501
|
+
<button
|
|
2502
|
+
type="button"
|
|
2503
|
+
class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
|
|
2504
|
+
[disabled]="i === 0 || reordering()"
|
|
2505
|
+
[attr.aria-label]="'Subir ' + titleOf(row)"
|
|
2506
|
+
(click)="moveBy(keyOf(row), -1)"
|
|
2507
|
+
>
|
|
2508
|
+
<svg lucideChevronUp size="16" />
|
|
2509
|
+
</button>
|
|
2510
|
+
<button
|
|
2511
|
+
type="button"
|
|
2512
|
+
class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
|
|
2513
|
+
[disabled]="i === visibleRows().length - 1 || reordering()"
|
|
2514
|
+
[attr.aria-label]="'Bajar ' + titleOf(row)"
|
|
2515
|
+
(click)="moveBy(keyOf(row), 1)"
|
|
2516
|
+
>
|
|
2517
|
+
<svg lucideChevronDown size="16" />
|
|
2518
|
+
</button>
|
|
2519
|
+
</span>
|
|
2520
|
+
</div>
|
|
2521
|
+
</td>
|
|
2522
|
+
}
|
|
2523
|
+
@for (column of schema().listColumns; track column.key) {
|
|
2524
|
+
<td class="px-4 py-2.5 align-middle">
|
|
2525
|
+
<!-- Los 7 formatos salen del registro (R4.3):
|
|
2526
|
+
la vista sólo elige cuál y entrega la celda. -->
|
|
2527
|
+
<app-render-host
|
|
2528
|
+
kind="column-format"
|
|
2529
|
+
[type]="formatOf(column)"
|
|
2530
|
+
[config]="column"
|
|
2531
|
+
[data]="cellOf(row, column)"
|
|
2532
|
+
/>
|
|
2533
|
+
</td>
|
|
2534
|
+
}
|
|
2535
|
+
@if (hasRowActions()) {
|
|
2536
|
+
<td class="px-4 py-2.5" (click)="$event.stopPropagation()">
|
|
2537
|
+
<div class="flex items-center justify-end gap-1">
|
|
2538
|
+
@if (schema().detail?.enabled) {
|
|
2539
|
+
<button
|
|
2540
|
+
app-button
|
|
2541
|
+
variant="ghost"
|
|
2542
|
+
type="button"
|
|
2543
|
+
(click)="goView(row)"
|
|
2544
|
+
title="Ver ficha de detalle"
|
|
2545
|
+
class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
|
|
2546
|
+
>
|
|
2547
|
+
<svg lucideEye size="15" />
|
|
2548
|
+
<span>Ver</span>
|
|
2549
|
+
</button>
|
|
2550
|
+
}
|
|
2551
|
+
@for (action of schema().actions ?? []; track action.id) {
|
|
2552
|
+
<button
|
|
2553
|
+
app-button
|
|
2554
|
+
variant="ghost"
|
|
2555
|
+
type="button"
|
|
2556
|
+
[title]="action.label"
|
|
2557
|
+
[attr.aria-label]="action.label"
|
|
2558
|
+
(click)="runAction(action, row)"
|
|
2559
|
+
class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
|
|
2560
|
+
>
|
|
2561
|
+
@let actionIcon = iconOf(action);
|
|
2562
|
+
@if (actionIcon) {
|
|
2563
|
+
<svg [lucideIcon]="actionIcon" size="15" />
|
|
2564
|
+
} @else {
|
|
2565
|
+
{{ action.label }}
|
|
2566
|
+
}
|
|
2567
|
+
</button>
|
|
2568
|
+
}
|
|
2569
|
+
@if (canQuickEdit()) {
|
|
2570
|
+
<button
|
|
2571
|
+
app-button
|
|
2572
|
+
variant="ghost"
|
|
2573
|
+
type="button"
|
|
2574
|
+
(click)="quickEditRow.set(row)"
|
|
2575
|
+
title="Edición rápida"
|
|
2576
|
+
class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
|
|
2577
|
+
>
|
|
2578
|
+
<svg lucideZap size="15" />
|
|
2579
|
+
<span>Edición rápida</span>
|
|
2580
|
+
</button>
|
|
2581
|
+
}
|
|
2582
|
+
@if (canUpdate()) {
|
|
2583
|
+
<button
|
|
2584
|
+
app-button
|
|
2585
|
+
variant="ghost"
|
|
2586
|
+
type="button"
|
|
2587
|
+
(click)="goEdit(row)"
|
|
2588
|
+
title="Editar registro"
|
|
2589
|
+
class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
|
|
2590
|
+
>
|
|
2591
|
+
<svg lucidePencil size="15" />
|
|
2592
|
+
<span>Editar</span>
|
|
2593
|
+
</button>
|
|
2594
|
+
}
|
|
2595
|
+
@if (canRemove()) {
|
|
2596
|
+
<button
|
|
2597
|
+
app-button
|
|
2598
|
+
variant="ghost"
|
|
2599
|
+
type="button"
|
|
2600
|
+
(click)="removeRow(row)"
|
|
2601
|
+
title="Eliminar registro"
|
|
2602
|
+
class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
|
|
2603
|
+
>
|
|
2604
|
+
<svg lucideTrash2 size="15" />
|
|
2605
|
+
<span>Eliminar</span>
|
|
2606
|
+
</button>
|
|
2607
|
+
}
|
|
2608
|
+
</div>
|
|
2609
|
+
</td>
|
|
2610
|
+
}
|
|
2611
|
+
</tr>
|
|
2612
|
+
}
|
|
2613
|
+
</tbody>
|
|
2614
|
+
</table>
|
|
2615
|
+
</div>
|
|
2616
|
+
|
|
2617
|
+
<!-- Vista de tarjetas — mobile-first (guía mobile §6).
|
|
2618
|
+
La tabla de arriba necesita scroll horizontal con 5–7 columnas
|
|
2619
|
+
(el anti-patrón #2): en pantallas chicas se convierte en una
|
|
2620
|
+
lista de tarjetas de dos niveles (principal + secundario),
|
|
2621
|
+
donde cada fila completa es un objetivo táctil. lg:hidden la
|
|
2622
|
+
retira cuando la tabla vuelve a ser usable. -->
|
|
2623
|
+
<ul class="space-y-2 lg:hidden">
|
|
2624
|
+
@for (row of visibleRows(); track keyOf(row)) {
|
|
2625
|
+
<li
|
|
2626
|
+
class="rounded-xl border border-neutral/20 bg-surface p-3"
|
|
2627
|
+
[class.cursor-pointer]="isRowClickable()"
|
|
2628
|
+
(click)="onRowClick(row)"
|
|
2629
|
+
>
|
|
2630
|
+
<!-- Nivel principal: el título del registro, siempre visible
|
|
2631
|
+
(guía §6.2: el identificador nunca se esconde). -->
|
|
2632
|
+
<div class="flex items-start justify-between gap-2">
|
|
2633
|
+
<span class="text-sm font-medium">
|
|
2634
|
+
{{ titleOf(row) }}
|
|
2635
|
+
</span>
|
|
2636
|
+
@if (schema().detail?.enabled) {
|
|
2637
|
+
<svg
|
|
2638
|
+
lucideChevronRight
|
|
2639
|
+
size="16"
|
|
2640
|
+
class="mt-0.5 shrink-0 text-neutral"
|
|
2641
|
+
aria-hidden="true"
|
|
2642
|
+
/>
|
|
2643
|
+
}
|
|
2644
|
+
</div>
|
|
2645
|
+
<!-- Nivel secundario: el resto de las columnas como pares
|
|
2646
|
+
etiqueta/valor. Sin hover: todo lo accionable va en el
|
|
2647
|
+
pie de la tarjeta, al alcance del pulgar. -->
|
|
2648
|
+
<dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
|
2649
|
+
@for (column of schema().listColumns; track column.key) {
|
|
2650
|
+
<dt class="text-neutral">{{ column.label }}</dt>
|
|
2651
|
+
<dd>
|
|
2652
|
+
<app-render-host
|
|
2653
|
+
kind="column-format"
|
|
2654
|
+
[type]="formatOf(column)"
|
|
2655
|
+
[config]="column"
|
|
2656
|
+
[data]="cellOf(row, column)"
|
|
2657
|
+
/>
|
|
2658
|
+
</dd>
|
|
2659
|
+
}
|
|
2660
|
+
</dl>
|
|
2661
|
+
@if (hasRowActions()) {
|
|
2662
|
+
<!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
|
|
2663
|
+
<div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
|
|
2664
|
+
@if (schema().detail?.enabled) {
|
|
2665
|
+
<button
|
|
2666
|
+
app-button
|
|
2667
|
+
variant="outline"
|
|
2668
|
+
type="button"
|
|
2669
|
+
size="sm"
|
|
2670
|
+
class="flex-1"
|
|
2671
|
+
(click)="$event.stopPropagation(); goView(row)"
|
|
2672
|
+
>
|
|
2673
|
+
<svg lucideEye size="15" aria-hidden="true" />
|
|
2674
|
+
<span>Ver</span>
|
|
2675
|
+
</button>
|
|
2676
|
+
}
|
|
2677
|
+
@for (action of schema().actions ?? []; track action.id) {
|
|
2678
|
+
<button
|
|
2679
|
+
app-button
|
|
2680
|
+
variant="outline"
|
|
2681
|
+
type="button"
|
|
2682
|
+
size="sm"
|
|
2683
|
+
class="flex-1"
|
|
2684
|
+
(click)="$event.stopPropagation(); runAction(action, row)"
|
|
2685
|
+
>
|
|
2686
|
+
@let actionIcon = iconOf(action);
|
|
2687
|
+
@if (actionIcon) {
|
|
2688
|
+
<svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
|
|
2689
|
+
}
|
|
2690
|
+
<span>{{ action.label }}</span>
|
|
2691
|
+
</button>
|
|
2692
|
+
}
|
|
2693
|
+
<!-- Editar es O una vía u otra, como en la tabla: si el
|
|
2694
|
+
schema declara quickEdit va el atajo; si no, la
|
|
2695
|
+
pantalla completa. Apilados los dos sería ruido. -->
|
|
2696
|
+
@if (canQuickEdit()) {
|
|
2697
|
+
<button
|
|
2698
|
+
app-button
|
|
2699
|
+
variant="outline"
|
|
2700
|
+
type="button"
|
|
2701
|
+
size="sm"
|
|
2702
|
+
class="flex-1"
|
|
2703
|
+
(click)="$event.stopPropagation(); quickEditRow.set(row)"
|
|
2704
|
+
>
|
|
2705
|
+
<svg lucideZap size="15" aria-hidden="true" />
|
|
2706
|
+
<span>Editar</span>
|
|
2707
|
+
</button>
|
|
2708
|
+
} @else if (canUpdate()) {
|
|
2709
|
+
<button
|
|
2710
|
+
app-button
|
|
2711
|
+
variant="outline"
|
|
2712
|
+
type="button"
|
|
2713
|
+
size="sm"
|
|
2714
|
+
class="flex-1"
|
|
2715
|
+
(click)="$event.stopPropagation(); goEdit(row)"
|
|
2716
|
+
>
|
|
2717
|
+
<svg lucidePencil size="15" aria-hidden="true" />
|
|
2718
|
+
<span>Editar</span>
|
|
2719
|
+
</button>
|
|
2720
|
+
}
|
|
2721
|
+
@if (canRemove()) {
|
|
2722
|
+
<button
|
|
2723
|
+
app-button
|
|
2724
|
+
variant="danger"
|
|
2725
|
+
type="button"
|
|
2726
|
+
size="sm"
|
|
2727
|
+
class="flex-1"
|
|
2728
|
+
(click)="$event.stopPropagation(); removeRow(row)"
|
|
2729
|
+
>
|
|
2730
|
+
<svg lucideTrash2 size="15" aria-hidden="true" />
|
|
2731
|
+
<span>Eliminar</span>
|
|
2732
|
+
</button>
|
|
2733
|
+
}
|
|
2734
|
+
</div>
|
|
2735
|
+
}
|
|
2736
|
+
</li>
|
|
2737
|
+
}
|
|
2738
|
+
</ul>
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
@if (quickEditRow(); as row) {
|
|
2744
|
+
<app-drawer-form
|
|
2745
|
+
[open]="true"
|
|
2746
|
+
[schema]="schema()"
|
|
2747
|
+
mode="edit"
|
|
2748
|
+
[recordKey]="keyOf(row)"
|
|
2749
|
+
[fields]="quickEditFields()"
|
|
2750
|
+
title="Edición rápida"
|
|
2751
|
+
size="sm"
|
|
2752
|
+
(close)="quickEditRow.set(null)"
|
|
2753
|
+
(saved)="quickEditSaved()"
|
|
2754
|
+
/>
|
|
2755
|
+
}
|
|
2756
|
+
</section>
|
|
2757
|
+
`
|
|
2758
|
+
}]
|
|
2759
|
+
}],
|
|
2760
|
+
ctorParameters: () => [],
|
|
2761
|
+
propDecorators: {
|
|
2762
|
+
schema: [{
|
|
2763
|
+
type: i0.Input,
|
|
2764
|
+
args: [{
|
|
2765
|
+
isSignal: true,
|
|
2766
|
+
alias: "schema",
|
|
2767
|
+
required: true
|
|
2768
|
+
}]
|
|
2769
|
+
}],
|
|
2770
|
+
viewRecord: [{
|
|
2771
|
+
type: i0.Output,
|
|
2772
|
+
args: ["viewRecord"]
|
|
2773
|
+
}]
|
|
2774
|
+
}
|
|
2775
|
+
});
|
|
2776
|
+
var CopyButton = class CopyButton {
|
|
2777
|
+
value = input.required(...ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []);
|
|
2778
|
+
notify = inject(NotificationService);
|
|
2779
|
+
copied = signal(false, ...ngDevMode ? [{ debugName: "copied" }] : /* istanbul ignore next */ []);
|
|
2780
|
+
copied$ = this.copied.asReadonly();
|
|
2781
|
+
async copy() {
|
|
2782
|
+
const text = String(this.value() ?? "");
|
|
2783
|
+
if (await writeClipboard(text)) {
|
|
2784
|
+
this.copied.set(true);
|
|
2785
|
+
this.notify.success("Copiado al portapapeles.");
|
|
2786
|
+
setTimeout(() => this.copied.set(false), 1500);
|
|
2787
|
+
} else this.notify.error("No se pudo copiar.");
|
|
2788
|
+
}
|
|
2789
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
2790
|
+
minVersion: "12.0.0",
|
|
2791
|
+
version: "22.2.3",
|
|
2792
|
+
ngImport: i0,
|
|
2793
|
+
type: CopyButton,
|
|
2794
|
+
deps: [],
|
|
2795
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
2796
|
+
});
|
|
2797
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
2798
|
+
minVersion: "17.0.0",
|
|
2799
|
+
version: "22.2.3",
|
|
2800
|
+
type: CopyButton,
|
|
2801
|
+
isStandalone: true,
|
|
2802
|
+
selector: "app-copy-button",
|
|
2803
|
+
inputs: { value: {
|
|
2804
|
+
classPropertyName: "value",
|
|
2805
|
+
publicName: "value",
|
|
2806
|
+
isSignal: true,
|
|
2807
|
+
isRequired: true,
|
|
2808
|
+
transformFunction: null
|
|
2809
|
+
} },
|
|
2810
|
+
ngImport: i0,
|
|
2811
|
+
template: `
|
|
2812
|
+
<button
|
|
2813
|
+
app-button
|
|
2814
|
+
type="button"
|
|
2815
|
+
variant="ghost"
|
|
2816
|
+
size="icon"
|
|
2817
|
+
[disabled]="copied()"
|
|
2818
|
+
(click)="copy()"
|
|
2819
|
+
[title]="'Copiar ' + value()"
|
|
2820
|
+
class="text-neutral hover:text-primary"
|
|
2821
|
+
>
|
|
2822
|
+
@if (copied()) {
|
|
2823
|
+
<svg lucideCheck size="16" class="text-primary" />
|
|
2824
|
+
} @else {
|
|
2825
|
+
<svg lucideCopy size="16" />
|
|
2826
|
+
}
|
|
2827
|
+
</button>
|
|
2828
|
+
`,
|
|
2829
|
+
isInline: true,
|
|
2830
|
+
dependencies: [
|
|
2831
|
+
{
|
|
2832
|
+
kind: "component",
|
|
2833
|
+
type: Button,
|
|
2834
|
+
selector: "button[app-button]",
|
|
2835
|
+
inputs: [
|
|
2836
|
+
"variant",
|
|
2837
|
+
"size",
|
|
2838
|
+
"type",
|
|
2839
|
+
"disabled",
|
|
2840
|
+
"loading"
|
|
2841
|
+
]
|
|
2842
|
+
},
|
|
2843
|
+
{
|
|
2844
|
+
kind: "component",
|
|
2845
|
+
type: LucideCopy,
|
|
2846
|
+
selector: "svg[lucideCopy]"
|
|
2847
|
+
},
|
|
2848
|
+
{
|
|
2849
|
+
kind: "component",
|
|
2850
|
+
type: LucideCheck,
|
|
2851
|
+
selector: "svg[lucideCheck]"
|
|
2852
|
+
}
|
|
2853
|
+
]
|
|
2854
|
+
});
|
|
2855
|
+
};
|
|
2856
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
2857
|
+
minVersion: "12.0.0",
|
|
2858
|
+
version: "22.2.3",
|
|
2859
|
+
ngImport: i0,
|
|
2860
|
+
type: CopyButton,
|
|
2861
|
+
decorators: [{
|
|
2862
|
+
type: Component,
|
|
2863
|
+
args: [{
|
|
2864
|
+
selector: "app-copy-button",
|
|
2865
|
+
imports: [
|
|
2866
|
+
Button,
|
|
2867
|
+
LucideCopy,
|
|
2868
|
+
LucideCheck
|
|
2869
|
+
],
|
|
2870
|
+
template: `
|
|
2871
|
+
<button
|
|
2872
|
+
app-button
|
|
2873
|
+
type="button"
|
|
2874
|
+
variant="ghost"
|
|
2875
|
+
size="icon"
|
|
2876
|
+
[disabled]="copied()"
|
|
2877
|
+
(click)="copy()"
|
|
2878
|
+
[title]="'Copiar ' + value()"
|
|
2879
|
+
class="text-neutral hover:text-primary"
|
|
2880
|
+
>
|
|
2881
|
+
@if (copied()) {
|
|
2882
|
+
<svg lucideCheck size="16" class="text-primary" />
|
|
2883
|
+
} @else {
|
|
2884
|
+
<svg lucideCopy size="16" />
|
|
2885
|
+
}
|
|
2886
|
+
</button>
|
|
2887
|
+
`
|
|
2888
|
+
}]
|
|
2889
|
+
}],
|
|
2890
|
+
propDecorators: { value: [{
|
|
2891
|
+
type: i0.Input,
|
|
2892
|
+
args: [{
|
|
2893
|
+
isSignal: true,
|
|
2894
|
+
alias: "value",
|
|
2895
|
+
required: true
|
|
2896
|
+
}]
|
|
2897
|
+
}] }
|
|
2898
|
+
});
|
|
2899
|
+
var DetailView = class DetailView {
|
|
2900
|
+
api = inject(ApiService, { optional: true });
|
|
2901
|
+
actionService = inject(ActionService);
|
|
2902
|
+
auth = inject(AuthService);
|
|
2903
|
+
router = inject(Router);
|
|
2904
|
+
nav = inject(ViewNavigationService);
|
|
2905
|
+
notifications = inject(NotificationService);
|
|
2906
|
+
configService = inject(ConfigService);
|
|
2907
|
+
constructor() {
|
|
2908
|
+
this.configService.ensureLoaded();
|
|
2909
|
+
}
|
|
2910
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
2911
|
+
record = input.required(...ngDevMode ? [{ debugName: "record" }] : /* istanbul ignore next */ []);
|
|
2912
|
+
edit = output();
|
|
2913
|
+
back = output();
|
|
2914
|
+
delete = output();
|
|
2915
|
+
refreshTick = signal(0, ...ngDevMode ? [{ debugName: "refreshTick" }] : /* istanbul ignore next */ []);
|
|
2916
|
+
fetched = toSignal(combineLatest([toObservable(this.record), toObservable(this.refreshTick)]).pipe(switchMap(([rec]) => {
|
|
2917
|
+
const schema = this.schema();
|
|
2918
|
+
const key = rec ? String(rec[schema.keyField] ?? "") : "";
|
|
2919
|
+
const getEndpoint = schema.endpoint?.get;
|
|
2920
|
+
if (!key || !getEndpoint || typeof this.api?.get !== "function") return of({
|
|
2921
|
+
phase: "done",
|
|
2922
|
+
data: null
|
|
2923
|
+
});
|
|
2924
|
+
return this.api.get(schema.endpoint, key).pipe(map((data) => ({
|
|
2925
|
+
phase: "done",
|
|
2926
|
+
data
|
|
2927
|
+
})), catchError((error) => of({
|
|
2928
|
+
phase: "error",
|
|
2929
|
+
error
|
|
2930
|
+
})));
|
|
2931
|
+
})), { initialValue: { phase: "pending" } });
|
|
2932
|
+
status = computed(() => {
|
|
2933
|
+
const fetched = this.fetched();
|
|
2934
|
+
if (fetched.phase === "pending") return "loading";
|
|
2935
|
+
if (fetched.phase === "error") return "error";
|
|
2936
|
+
return "ready";
|
|
2937
|
+
}, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
2938
|
+
retry() {
|
|
2939
|
+
this.refreshTick.update((tick) => tick + 1);
|
|
2940
|
+
}
|
|
2941
|
+
errorDeCarga() {
|
|
2942
|
+
const fetched = this.fetched();
|
|
2943
|
+
return fetched.phase === "error" ? fetched.error : void 0;
|
|
2944
|
+
}
|
|
2945
|
+
volverDeError() {
|
|
2946
|
+
this.back.emit();
|
|
2947
|
+
this.nav.back();
|
|
2948
|
+
}
|
|
2949
|
+
effectiveRecord = computed(() => {
|
|
2950
|
+
const initial = this.record();
|
|
2951
|
+
const fetched = this.fetched();
|
|
2952
|
+
if (fetched.phase === "done" && fetched.data && typeof fetched.data === "object" && Object.keys(fetched.data).length > 0) return {
|
|
2953
|
+
...initial,
|
|
2954
|
+
...fetched.data
|
|
2955
|
+
};
|
|
2956
|
+
return initial;
|
|
2957
|
+
}, ...ngDevMode ? [{ debugName: "effectiveRecord" }] : /* istanbul ignore next */ []);
|
|
2958
|
+
detail = computed(() => this.schema().detail ?? { enabled: true }, ...ngDevMode ? [{ debugName: "detail" }] : /* istanbul ignore next */ []);
|
|
2959
|
+
title = computed(() => {
|
|
2960
|
+
const rec = this.effectiveRecord();
|
|
2961
|
+
const schema = this.schema();
|
|
2962
|
+
const raw = rec[schema.titleField];
|
|
2963
|
+
return raw !== void 0 && raw !== null && raw !== "" ? String(raw) : schema.label;
|
|
2964
|
+
}, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
2965
|
+
recordKey = computed(() => {
|
|
2966
|
+
const rec = this.effectiveRecord();
|
|
2967
|
+
const schema = this.schema();
|
|
2968
|
+
return String(rec[schema.keyField] ?? "—");
|
|
2969
|
+
}, ...ngDevMode ? [{ debugName: "recordKey" }] : /* istanbul ignore next */ []);
|
|
2970
|
+
headerFields = computed(() => {
|
|
2971
|
+
return (this.detail().headerFields ?? []).map((key) => this.resolveFieldValue(key)).filter((f) => f !== null);
|
|
2972
|
+
}, ...ngDevMode ? [{ debugName: "headerFields" }] : /* istanbul ignore next */ []);
|
|
2973
|
+
sections = computed(() => {
|
|
2974
|
+
const schema = this.schema();
|
|
2975
|
+
const detailConfig = this.detail();
|
|
2976
|
+
if (detailConfig.sections && detailConfig.sections.length > 0) return detailConfig.sections.map((sec) => ({
|
|
2977
|
+
id: sec.id,
|
|
2978
|
+
label: sec.label,
|
|
2979
|
+
fields: sec.fields.map((key) => this.resolveFieldValue(key)).filter((f) => f !== null)
|
|
2980
|
+
})).filter((sec) => sec.fields.length > 0);
|
|
2981
|
+
const layoutSections = schema.layout?.sections ?? [];
|
|
2982
|
+
const sectionMap = /* @__PURE__ */ new Map();
|
|
2983
|
+
for (const layoutSection of layoutSections) sectionMap.set(layoutSection.id, {
|
|
2984
|
+
id: layoutSection.id,
|
|
2985
|
+
label: layoutSection.label,
|
|
2986
|
+
fields: []
|
|
2987
|
+
});
|
|
2988
|
+
const unsectioned = {
|
|
2989
|
+
id: "_unsectioned",
|
|
2990
|
+
label: schema.label,
|
|
2991
|
+
fields: []
|
|
2992
|
+
};
|
|
2993
|
+
for (const field of schema.fields) {
|
|
2994
|
+
const value = this.resolveFieldValue(field.key);
|
|
2995
|
+
if (value === null) continue;
|
|
2996
|
+
if (field.section && sectionMap.has(field.section)) sectionMap.get(field.section).fields.push(value);
|
|
2997
|
+
else unsectioned.fields.push(value);
|
|
2998
|
+
}
|
|
2999
|
+
const result = [];
|
|
3000
|
+
if (unsectioned.fields.length > 0) result.push(unsectioned);
|
|
3001
|
+
for (const sec of sectionMap.values()) if (sec.fields.length > 0) result.push(sec);
|
|
3002
|
+
return result;
|
|
3003
|
+
}, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
|
|
3004
|
+
copyShareActions = computed(() => {
|
|
3005
|
+
return (this.schema().actions ?? this.detail().actions ?? []).filter(isShareAction);
|
|
3006
|
+
}, ...ngDevMode ? [{ debugName: "copyShareActions" }] : /* istanbul ignore next */ []);
|
|
3007
|
+
triggerActions = computed(() => {
|
|
3008
|
+
return (this.schema().actions ?? this.detail().actions ?? []).filter(isTriggerAction);
|
|
3009
|
+
}, ...ngDevMode ? [{ debugName: "triggerActions" }] : /* istanbul ignore next */ []);
|
|
3010
|
+
async runTrigger(action) {
|
|
3011
|
+
if (!this.api) return;
|
|
3012
|
+
const result = await this.actionService.run(action, {
|
|
3013
|
+
record: this.effectiveRecord(),
|
|
3014
|
+
resource: this.schema().id,
|
|
3015
|
+
api: this.api,
|
|
3016
|
+
router: this.router,
|
|
3017
|
+
toast: this.notifications,
|
|
3018
|
+
user: this.auth.user(),
|
|
3019
|
+
keyField: this.schema().keyField
|
|
3020
|
+
});
|
|
3021
|
+
if (result.ok && result.reload) this.refreshTick.update((tick) => tick + 1);
|
|
3022
|
+
}
|
|
3023
|
+
resolveFieldValue(key) {
|
|
3024
|
+
const schema = this.schema();
|
|
3025
|
+
const rec = this.effectiveRecord();
|
|
3026
|
+
const field = schema.fields.find((f) => f.key === key);
|
|
3027
|
+
const raw = getValue(rec, key);
|
|
3028
|
+
const declarado = field ?? {
|
|
3029
|
+
key,
|
|
3030
|
+
label: key.replace(/_/g, " "),
|
|
3031
|
+
type: "text"
|
|
3032
|
+
};
|
|
3033
|
+
if (!this.auth.canReadField(declarado)) return null;
|
|
3034
|
+
return {
|
|
3035
|
+
key,
|
|
3036
|
+
label: field?.label ?? declarado.label,
|
|
3037
|
+
field: declarado,
|
|
3038
|
+
raw
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
vacio(valor) {
|
|
3042
|
+
return valor === null || valor === void 0 || valor === "";
|
|
3043
|
+
}
|
|
3044
|
+
esHistorial(campo) {
|
|
3045
|
+
return campo.field.type === "list" && !this.vacio(campo.raw);
|
|
3046
|
+
}
|
|
3047
|
+
regularFields(section) {
|
|
3048
|
+
return section.fields.filter((f) => !this.esHistorial(f));
|
|
3049
|
+
}
|
|
3050
|
+
listFields(section) {
|
|
3051
|
+
return section.fields.filter((f) => this.esHistorial(f));
|
|
3052
|
+
}
|
|
3053
|
+
hasLargeContent(section) {
|
|
3054
|
+
return section.fields.some((f) => this.esHistorial(f) || f.key === "description");
|
|
3055
|
+
}
|
|
3056
|
+
colSpanFor(width) {
|
|
3057
|
+
return `lg:col-span-${Math.min(width ?? 6, 9)}`;
|
|
3058
|
+
}
|
|
3059
|
+
shareOf(action) {
|
|
3060
|
+
return resolveShare(action, this.actionService.vars(this.effectiveRecord()));
|
|
3061
|
+
}
|
|
3062
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
3063
|
+
minVersion: "12.0.0",
|
|
3064
|
+
version: "22.2.3",
|
|
3065
|
+
ngImport: i0,
|
|
3066
|
+
type: DetailView,
|
|
3067
|
+
deps: [],
|
|
3068
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
3069
|
+
});
|
|
3070
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
3071
|
+
minVersion: "17.0.0",
|
|
3072
|
+
version: "22.2.3",
|
|
3073
|
+
type: DetailView,
|
|
3074
|
+
isStandalone: true,
|
|
3075
|
+
selector: "app-detail-view",
|
|
3076
|
+
inputs: {
|
|
3077
|
+
schema: {
|
|
3078
|
+
classPropertyName: "schema",
|
|
3079
|
+
publicName: "schema",
|
|
3080
|
+
isSignal: true,
|
|
3081
|
+
isRequired: true,
|
|
3082
|
+
transformFunction: null
|
|
3083
|
+
},
|
|
3084
|
+
record: {
|
|
3085
|
+
classPropertyName: "record",
|
|
3086
|
+
publicName: "record",
|
|
3087
|
+
isSignal: true,
|
|
3088
|
+
isRequired: true,
|
|
3089
|
+
transformFunction: null
|
|
3090
|
+
}
|
|
3091
|
+
},
|
|
3092
|
+
outputs: {
|
|
3093
|
+
edit: "edit",
|
|
3094
|
+
back: "back",
|
|
3095
|
+
delete: "delete"
|
|
3096
|
+
},
|
|
3097
|
+
ngImport: i0,
|
|
3098
|
+
template: `
|
|
3099
|
+
<div class="space-y-6">
|
|
3100
|
+
<!-- ── Cabecera Principal ── -->
|
|
3101
|
+
<header
|
|
3102
|
+
class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
|
|
3103
|
+
>
|
|
3104
|
+
<div class="space-y-2">
|
|
3105
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
3106
|
+
<h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
|
|
3107
|
+
{{ title() }}
|
|
3108
|
+
</h1>
|
|
3109
|
+
@for (campo of headerFields(); track campo.key) {
|
|
3110
|
+
@if (!vacio(campo.raw)) {
|
|
3111
|
+
<!-- La cabecera siempre mostró los headerFields como chips:
|
|
3112
|
+
el matiz viaja al host, no un @switch por tipo acá. -->
|
|
3113
|
+
<app-render-host
|
|
3114
|
+
mode="display"
|
|
3115
|
+
[chip]="true"
|
|
3116
|
+
[type]="campo.field.type"
|
|
3117
|
+
[config]="campo.field"
|
|
3118
|
+
[value]="campo.raw"
|
|
3119
|
+
/>
|
|
3120
|
+
}
|
|
3121
|
+
}
|
|
3122
|
+
</div>
|
|
3123
|
+
<p class="text-sm text-neutral">
|
|
3124
|
+
{{ schema().label }} · Identificador:
|
|
3125
|
+
<span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
|
|
3126
|
+
</p>
|
|
3127
|
+
</div>
|
|
3128
|
+
|
|
3129
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
3130
|
+
<button app-button variant="outline" type="button" (click)="back.emit()">
|
|
3131
|
+
<svg lucideArrowLeft size="16" />
|
|
3132
|
+
Volver
|
|
3133
|
+
</button>
|
|
3134
|
+
@if (detail().allowEdit !== false && status() !== 'error') {
|
|
3135
|
+
<button app-button variant="primary" type="button" (click)="edit.emit(record())">
|
|
3136
|
+
<svg lucidePencil size="16" />
|
|
3137
|
+
Editar
|
|
3138
|
+
</button>
|
|
3139
|
+
}
|
|
3140
|
+
@if (detail().allowDelete !== false && status() !== 'error') {
|
|
3141
|
+
<button app-button variant="danger" type="button" (click)="delete.emit(record())">
|
|
3142
|
+
<svg lucideTrash2 size="16" />
|
|
3143
|
+
Eliminar
|
|
3144
|
+
</button>
|
|
3145
|
+
}
|
|
3146
|
+
</div>
|
|
3147
|
+
</header>
|
|
3148
|
+
|
|
3149
|
+
<!-- ── Acciones Destacadas del Registro (si existen) ── -->
|
|
3150
|
+
@if (copyShareActions().length > 0 || triggerActions().length > 0) {
|
|
3151
|
+
<section
|
|
3152
|
+
class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
|
|
3153
|
+
>
|
|
3154
|
+
<div class="mb-3 flex items-center gap-2">
|
|
3155
|
+
<svg lucideExternalLink size="16" class="text-primary" />
|
|
3156
|
+
<h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
|
|
3157
|
+
</div>
|
|
3158
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
|
|
3159
|
+
@for (action of copyShareActions(); track action.id) {
|
|
3160
|
+
<div class="flex-1 min-w-[280px]">
|
|
3161
|
+
@let share = shareOf(action);
|
|
3162
|
+
<app-copy-share
|
|
3163
|
+
[url]="share.url"
|
|
3164
|
+
[shareText]="share.shareText"
|
|
3165
|
+
[label]="action.label"
|
|
3166
|
+
/>
|
|
3167
|
+
</div>
|
|
3168
|
+
}
|
|
3169
|
+
@for (action of triggerActions(); track action.id) {
|
|
3170
|
+
<button app-button variant="outline" type="button" (click)="runTrigger(action)">
|
|
3171
|
+
{{ action.label }}
|
|
3172
|
+
</button>
|
|
3173
|
+
}
|
|
3174
|
+
</div>
|
|
3175
|
+
</section>
|
|
3176
|
+
}
|
|
3177
|
+
|
|
3178
|
+
<!-- ── Secciones de Datos ── -->
|
|
3179
|
+
@switch (status()) {
|
|
3180
|
+
@case ('loading') {
|
|
3181
|
+
<!-- Skeleton con la forma de las tarjetas de sección: reserva el
|
|
3182
|
+
espacio (sin layout jump) y distingue «cargando» de «vacío».
|
|
3183
|
+
La cabecera y las acciones de arriba sí se pintan ya: son la
|
|
3184
|
+
fila de la lista, datos reales que no están en disputa. -->
|
|
3185
|
+
<div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
3186
|
+
@for (placeholder of [0, 1]; track placeholder) {
|
|
3187
|
+
<section
|
|
3188
|
+
class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
|
|
3189
|
+
>
|
|
3190
|
+
<header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
|
|
3191
|
+
<app-skeleton [lines]="1" [height]="14" width="30%" />
|
|
3192
|
+
</header>
|
|
3193
|
+
<div class="p-5">
|
|
3194
|
+
<app-skeleton [lines]="6" [height]="44" />
|
|
3195
|
+
</div>
|
|
3196
|
+
</section>
|
|
3197
|
+
}
|
|
3198
|
+
</div>
|
|
3199
|
+
}
|
|
3200
|
+
@case ('error') {
|
|
3201
|
+
<app-empty-state
|
|
3202
|
+
title="No se pudo cargar el registro"
|
|
3203
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
3204
|
+
>
|
|
3205
|
+
<button app-button variant="outline" type="button" (click)="retry()">
|
|
3206
|
+
<svg lucideRotateCw size="16" />
|
|
3207
|
+
Reintentar
|
|
3208
|
+
</button>
|
|
3209
|
+
<button app-button variant="outline" type="button" (click)="volverDeError()">
|
|
3210
|
+
<svg lucideArrowLeft size="16" />
|
|
3211
|
+
Volver
|
|
3212
|
+
</button>
|
|
3213
|
+
</app-empty-state>
|
|
3214
|
+
}
|
|
3215
|
+
@case ('ready') {
|
|
3216
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
3217
|
+
@for (section of sections(); track section.id) {
|
|
3218
|
+
<section
|
|
3219
|
+
class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
|
|
3220
|
+
[class.lg:col-span-2]="hasLargeContent(section)"
|
|
3221
|
+
>
|
|
3222
|
+
<header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
|
|
3223
|
+
<h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
|
|
3224
|
+
{{ section.label }}
|
|
3225
|
+
</h2>
|
|
3226
|
+
</header>
|
|
3227
|
+
|
|
3228
|
+
<div class="p-5 flex-1 space-y-4">
|
|
3229
|
+
<!-- Grid para campos regulares -->
|
|
3230
|
+
@if (regularFields(section).length > 0) {
|
|
3231
|
+
<dl class="grid grid-cols-1 gap-x-6 gap-y-4 lg:grid-cols-12">
|
|
3232
|
+
@for (campo of regularFields(section); track campo.key) {
|
|
3233
|
+
<div class="space-y-1">
|
|
3234
|
+
<dt class="col-span-12 sm:col-span-12 lg:col-span-3 text-xs font-medium text-neutral">{{ campo.label }}</dt>
|
|
3235
|
+
<dd class="col-span-12 sm:col-span-12 lg:col-span-{{ colSpanFor(campo.field.width) }} text-sm font-medium text-neutral-light break-words">
|
|
3236
|
+
<app-render-host
|
|
3237
|
+
mode="display"
|
|
3238
|
+
[type]="campo.field.type"
|
|
3239
|
+
[config]="campo.field"
|
|
3240
|
+
[value]="campo.raw"
|
|
3241
|
+
/>
|
|
3242
|
+
@if (campo.field.copyable && campo.raw !== null && campo.raw !== undefined && campo.raw !== '') {
|
|
3243
|
+
<app-copy-button [value]="campo.raw" />
|
|
3244
|
+
}
|
|
3245
|
+
</dd>
|
|
3246
|
+
</div>
|
|
3247
|
+
}
|
|
3248
|
+
</dl>
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
<!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
|
|
3252
|
+
@for (campo of listFields(section); track campo.key) {
|
|
3253
|
+
<div class="pt-2">
|
|
3254
|
+
<h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
|
|
3255
|
+
{{ campo.label }}
|
|
3256
|
+
</h3>
|
|
3257
|
+
<app-render-host
|
|
3258
|
+
mode="display"
|
|
3259
|
+
[type]="campo.field.type"
|
|
3260
|
+
[config]="campo.field"
|
|
3261
|
+
[value]="campo.raw"
|
|
3262
|
+
/>
|
|
3263
|
+
</div>
|
|
3264
|
+
}
|
|
3265
|
+
</div>
|
|
3266
|
+
</section>
|
|
3267
|
+
}
|
|
3268
|
+
</div>
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
3271
|
+
</div>
|
|
3272
|
+
`,
|
|
3273
|
+
isInline: true,
|
|
3274
|
+
dependencies: [
|
|
3275
|
+
{
|
|
3276
|
+
kind: "component",
|
|
3277
|
+
type: Button,
|
|
3278
|
+
selector: "button[app-button]",
|
|
3279
|
+
inputs: [
|
|
3280
|
+
"variant",
|
|
3281
|
+
"size",
|
|
3282
|
+
"type",
|
|
3283
|
+
"disabled",
|
|
3284
|
+
"loading"
|
|
3285
|
+
]
|
|
3286
|
+
},
|
|
3287
|
+
{
|
|
3288
|
+
kind: "component",
|
|
3289
|
+
type: CopyShare,
|
|
3290
|
+
selector: "app-copy-share",
|
|
3291
|
+
inputs: [
|
|
3292
|
+
"url",
|
|
3293
|
+
"label",
|
|
3294
|
+
"shareText"
|
|
3295
|
+
]
|
|
3296
|
+
},
|
|
3297
|
+
{
|
|
3298
|
+
kind: "component",
|
|
3299
|
+
type: CopyButton,
|
|
3300
|
+
selector: "app-copy-button",
|
|
3301
|
+
inputs: ["value"]
|
|
3302
|
+
},
|
|
3303
|
+
{
|
|
3304
|
+
kind: "component",
|
|
3305
|
+
type: EmptyState,
|
|
3306
|
+
selector: "app-empty-state",
|
|
3307
|
+
inputs: ["title", "message"]
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
kind: "component",
|
|
3311
|
+
type: LucideArrowLeft,
|
|
3312
|
+
selector: "svg[lucideArrowLeft]"
|
|
3313
|
+
},
|
|
3314
|
+
{
|
|
3315
|
+
kind: "component",
|
|
3316
|
+
type: LucideExternalLink,
|
|
3317
|
+
selector: "svg[lucideExternalLink]"
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
kind: "component",
|
|
3321
|
+
type: LucidePencil,
|
|
3322
|
+
selector: "svg[lucidePencil]"
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
kind: "component",
|
|
3326
|
+
type: LucideRotateCw,
|
|
3327
|
+
selector: "svg[lucideRotateCw]"
|
|
3328
|
+
},
|
|
3329
|
+
{
|
|
3330
|
+
kind: "component",
|
|
3331
|
+
type: LucideTrash2,
|
|
3332
|
+
selector: "svg[lucideTrash], svg[lucideTrash2]"
|
|
3333
|
+
},
|
|
3334
|
+
{
|
|
3335
|
+
kind: "component",
|
|
3336
|
+
type: RenderHost,
|
|
3337
|
+
selector: "app-render-host",
|
|
3338
|
+
inputs: [
|
|
3339
|
+
"kind",
|
|
3340
|
+
"type",
|
|
3341
|
+
"config",
|
|
3342
|
+
"ctx",
|
|
3343
|
+
"data",
|
|
3344
|
+
"mode",
|
|
3345
|
+
"value",
|
|
3346
|
+
"chip"
|
|
3347
|
+
]
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
kind: "component",
|
|
3351
|
+
type: Skeleton,
|
|
3352
|
+
selector: "app-skeleton",
|
|
3353
|
+
inputs: [
|
|
3354
|
+
"lines",
|
|
3355
|
+
"height",
|
|
3356
|
+
"width"
|
|
3357
|
+
]
|
|
3358
|
+
}
|
|
3359
|
+
]
|
|
3360
|
+
});
|
|
3361
|
+
};
|
|
3362
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
3363
|
+
minVersion: "12.0.0",
|
|
3364
|
+
version: "22.2.3",
|
|
3365
|
+
ngImport: i0,
|
|
3366
|
+
type: DetailView,
|
|
3367
|
+
decorators: [{
|
|
3368
|
+
type: Component,
|
|
3369
|
+
args: [{
|
|
3370
|
+
selector: "app-detail-view",
|
|
3371
|
+
imports: [
|
|
3372
|
+
Button,
|
|
3373
|
+
CopyShare,
|
|
3374
|
+
CopyButton,
|
|
3375
|
+
EmptyState,
|
|
3376
|
+
LucideArrowLeft,
|
|
3377
|
+
LucideExternalLink,
|
|
3378
|
+
LucidePencil,
|
|
3379
|
+
LucideRotateCw,
|
|
3380
|
+
LucideTrash2,
|
|
3381
|
+
RenderHost,
|
|
3382
|
+
Skeleton
|
|
3383
|
+
],
|
|
3384
|
+
template: `
|
|
3385
|
+
<div class="space-y-6">
|
|
3386
|
+
<!-- ── Cabecera Principal ── -->
|
|
3387
|
+
<header
|
|
3388
|
+
class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
|
|
3389
|
+
>
|
|
3390
|
+
<div class="space-y-2">
|
|
3391
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
3392
|
+
<h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
|
|
3393
|
+
{{ title() }}
|
|
3394
|
+
</h1>
|
|
3395
|
+
@for (campo of headerFields(); track campo.key) {
|
|
3396
|
+
@if (!vacio(campo.raw)) {
|
|
3397
|
+
<!-- La cabecera siempre mostró los headerFields como chips:
|
|
3398
|
+
el matiz viaja al host, no un @switch por tipo acá. -->
|
|
3399
|
+
<app-render-host
|
|
3400
|
+
mode="display"
|
|
3401
|
+
[chip]="true"
|
|
3402
|
+
[type]="campo.field.type"
|
|
3403
|
+
[config]="campo.field"
|
|
3404
|
+
[value]="campo.raw"
|
|
3405
|
+
/>
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
</div>
|
|
3409
|
+
<p class="text-sm text-neutral">
|
|
3410
|
+
{{ schema().label }} · Identificador:
|
|
3411
|
+
<span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
|
|
3412
|
+
</p>
|
|
3413
|
+
</div>
|
|
3414
|
+
|
|
3415
|
+
<div class="flex flex-wrap items-center gap-2.5">
|
|
3416
|
+
<button app-button variant="outline" type="button" (click)="back.emit()">
|
|
3417
|
+
<svg lucideArrowLeft size="16" />
|
|
3418
|
+
Volver
|
|
3419
|
+
</button>
|
|
3420
|
+
@if (detail().allowEdit !== false && status() !== 'error') {
|
|
3421
|
+
<button app-button variant="primary" type="button" (click)="edit.emit(record())">
|
|
3422
|
+
<svg lucidePencil size="16" />
|
|
3423
|
+
Editar
|
|
3424
|
+
</button>
|
|
3425
|
+
}
|
|
3426
|
+
@if (detail().allowDelete !== false && status() !== 'error') {
|
|
3427
|
+
<button app-button variant="danger" type="button" (click)="delete.emit(record())">
|
|
3428
|
+
<svg lucideTrash2 size="16" />
|
|
3429
|
+
Eliminar
|
|
3430
|
+
</button>
|
|
3431
|
+
}
|
|
3432
|
+
</div>
|
|
3433
|
+
</header>
|
|
3434
|
+
|
|
3435
|
+
<!-- ── Acciones Destacadas del Registro (si existen) ── -->
|
|
3436
|
+
@if (copyShareActions().length > 0 || triggerActions().length > 0) {
|
|
3437
|
+
<section
|
|
3438
|
+
class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
|
|
3439
|
+
>
|
|
3440
|
+
<div class="mb-3 flex items-center gap-2">
|
|
3441
|
+
<svg lucideExternalLink size="16" class="text-primary" />
|
|
3442
|
+
<h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
|
|
3443
|
+
</div>
|
|
3444
|
+
<div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
|
|
3445
|
+
@for (action of copyShareActions(); track action.id) {
|
|
3446
|
+
<div class="flex-1 min-w-[280px]">
|
|
3447
|
+
@let share = shareOf(action);
|
|
3448
|
+
<app-copy-share
|
|
3449
|
+
[url]="share.url"
|
|
3450
|
+
[shareText]="share.shareText"
|
|
3451
|
+
[label]="action.label"
|
|
3452
|
+
/>
|
|
3453
|
+
</div>
|
|
3454
|
+
}
|
|
3455
|
+
@for (action of triggerActions(); track action.id) {
|
|
3456
|
+
<button app-button variant="outline" type="button" (click)="runTrigger(action)">
|
|
3457
|
+
{{ action.label }}
|
|
3458
|
+
</button>
|
|
3459
|
+
}
|
|
3460
|
+
</div>
|
|
3461
|
+
</section>
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
<!-- ── Secciones de Datos ── -->
|
|
3465
|
+
@switch (status()) {
|
|
3466
|
+
@case ('loading') {
|
|
3467
|
+
<!-- Skeleton con la forma de las tarjetas de sección: reserva el
|
|
3468
|
+
espacio (sin layout jump) y distingue «cargando» de «vacío».
|
|
3469
|
+
La cabecera y las acciones de arriba sí se pintan ya: son la
|
|
3470
|
+
fila de la lista, datos reales que no están en disputa. -->
|
|
3471
|
+
<div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
3472
|
+
@for (placeholder of [0, 1]; track placeholder) {
|
|
3473
|
+
<section
|
|
3474
|
+
class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
|
|
3475
|
+
>
|
|
3476
|
+
<header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
|
|
3477
|
+
<app-skeleton [lines]="1" [height]="14" width="30%" />
|
|
3478
|
+
</header>
|
|
3479
|
+
<div class="p-5">
|
|
3480
|
+
<app-skeleton [lines]="6" [height]="44" />
|
|
3481
|
+
</div>
|
|
3482
|
+
</section>
|
|
3483
|
+
}
|
|
3484
|
+
</div>
|
|
3485
|
+
}
|
|
3486
|
+
@case ('error') {
|
|
3487
|
+
<app-empty-state
|
|
3488
|
+
title="No se pudo cargar el registro"
|
|
3489
|
+
message="Revisa tu conexión e intenta de nuevo."
|
|
3490
|
+
>
|
|
3491
|
+
<button app-button variant="outline" type="button" (click)="retry()">
|
|
3492
|
+
<svg lucideRotateCw size="16" />
|
|
3493
|
+
Reintentar
|
|
3494
|
+
</button>
|
|
3495
|
+
<button app-button variant="outline" type="button" (click)="volverDeError()">
|
|
3496
|
+
<svg lucideArrowLeft size="16" />
|
|
3497
|
+
Volver
|
|
3498
|
+
</button>
|
|
3499
|
+
</app-empty-state>
|
|
3500
|
+
}
|
|
3501
|
+
@case ('ready') {
|
|
3502
|
+
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
3503
|
+
@for (section of sections(); track section.id) {
|
|
3504
|
+
<section
|
|
3505
|
+
class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
|
|
3506
|
+
[class.lg:col-span-2]="hasLargeContent(section)"
|
|
3507
|
+
>
|
|
3508
|
+
<header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
|
|
3509
|
+
<h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
|
|
3510
|
+
{{ section.label }}
|
|
3511
|
+
</h2>
|
|
3512
|
+
</header>
|
|
3513
|
+
|
|
3514
|
+
<div class="p-5 flex-1 space-y-4">
|
|
3515
|
+
<!-- Grid para campos regulares -->
|
|
3516
|
+
@if (regularFields(section).length > 0) {
|
|
3517
|
+
<dl class="grid grid-cols-1 gap-x-6 gap-y-4 lg:grid-cols-12">
|
|
3518
|
+
@for (campo of regularFields(section); track campo.key) {
|
|
3519
|
+
<div class="space-y-1">
|
|
3520
|
+
<dt class="col-span-12 sm:col-span-12 lg:col-span-3 text-xs font-medium text-neutral">{{ campo.label }}</dt>
|
|
3521
|
+
<dd class="col-span-12 sm:col-span-12 lg:col-span-{{ colSpanFor(campo.field.width) }} text-sm font-medium text-neutral-light break-words">
|
|
3522
|
+
<app-render-host
|
|
3523
|
+
mode="display"
|
|
3524
|
+
[type]="campo.field.type"
|
|
3525
|
+
[config]="campo.field"
|
|
3526
|
+
[value]="campo.raw"
|
|
3527
|
+
/>
|
|
3528
|
+
@if (campo.field.copyable && campo.raw !== null && campo.raw !== undefined && campo.raw !== '') {
|
|
3529
|
+
<app-copy-button [value]="campo.raw" />
|
|
3530
|
+
}
|
|
3531
|
+
</dd>
|
|
3532
|
+
</div>
|
|
3533
|
+
}
|
|
3534
|
+
</dl>
|
|
3535
|
+
}
|
|
3536
|
+
|
|
3537
|
+
<!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
|
|
3538
|
+
@for (campo of listFields(section); track campo.key) {
|
|
3539
|
+
<div class="pt-2">
|
|
3540
|
+
<h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
|
|
3541
|
+
{{ campo.label }}
|
|
3542
|
+
</h3>
|
|
3543
|
+
<app-render-host
|
|
3544
|
+
mode="display"
|
|
3545
|
+
[type]="campo.field.type"
|
|
3546
|
+
[config]="campo.field"
|
|
3547
|
+
[value]="campo.raw"
|
|
3548
|
+
/>
|
|
3549
|
+
</div>
|
|
3550
|
+
}
|
|
3551
|
+
</div>
|
|
3552
|
+
</section>
|
|
3553
|
+
}
|
|
3554
|
+
</div>
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
</div>
|
|
3558
|
+
`
|
|
3559
|
+
}]
|
|
3560
|
+
}],
|
|
3561
|
+
ctorParameters: () => [],
|
|
3562
|
+
propDecorators: {
|
|
3563
|
+
schema: [{
|
|
3564
|
+
type: i0.Input,
|
|
3565
|
+
args: [{
|
|
3566
|
+
isSignal: true,
|
|
3567
|
+
alias: "schema",
|
|
3568
|
+
required: true
|
|
3569
|
+
}]
|
|
3570
|
+
}],
|
|
3571
|
+
record: [{
|
|
3572
|
+
type: i0.Input,
|
|
3573
|
+
args: [{
|
|
3574
|
+
isSignal: true,
|
|
3575
|
+
alias: "record",
|
|
3576
|
+
required: true
|
|
3577
|
+
}]
|
|
3578
|
+
}],
|
|
3579
|
+
edit: [{
|
|
3580
|
+
type: i0.Output,
|
|
3581
|
+
args: ["edit"]
|
|
3582
|
+
}],
|
|
3583
|
+
back: [{
|
|
3584
|
+
type: i0.Output,
|
|
3585
|
+
args: ["back"]
|
|
3586
|
+
}],
|
|
3587
|
+
delete: [{
|
|
3588
|
+
type: i0.Output,
|
|
3589
|
+
args: ["delete"]
|
|
3590
|
+
}]
|
|
3591
|
+
}
|
|
3592
|
+
});
|
|
3593
|
+
var resource_page_exports = /* @__PURE__ */ __exportAll({ ResourcePage: () => ResourcePage });
|
|
3594
|
+
var ResourcePage = class ResourcePage {
|
|
3595
|
+
route = inject(ActivatedRoute);
|
|
3596
|
+
router = inject(Router);
|
|
3597
|
+
auth = inject(AuthService);
|
|
3598
|
+
params = toSignal(this.route.paramMap, { requireSync: true });
|
|
3599
|
+
routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
|
|
3600
|
+
schema = computed(() => {
|
|
3601
|
+
const resourceId = this.routeData()["resourceId"] ?? this.params().get("id");
|
|
3602
|
+
return getSchema(typeof resourceId === "string" ? resourceId : "");
|
|
3603
|
+
}, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
3604
|
+
currentView = signal("list", ...ngDevMode ? [{ debugName: "currentView" }] : /* istanbul ignore next */ []);
|
|
3605
|
+
selectedRecord = signal(null, ...ngDevMode ? [{ debugName: "selectedRecord" }] : /* istanbul ignore next */ []);
|
|
3606
|
+
constructor() {
|
|
3607
|
+
this.route.paramMap.pipe(map((params) => params.get("key")), distinctUntilChanged(), takeUntilDestroyed()).subscribe((key) => this.openFromRoute(key));
|
|
3608
|
+
this.route.data?.pipe(takeUntilDestroyed()).subscribe(() => this.openFromRoute(this.params().get("key")));
|
|
3609
|
+
}
|
|
3610
|
+
openFromRoute(key) {
|
|
3611
|
+
const schema = this.schema();
|
|
3612
|
+
if (!schema || schema.kind !== "collection") return;
|
|
3613
|
+
if ((this.routeData()["viewType"] === "detail" || key) && key && schema.detail?.enabled) {
|
|
3614
|
+
const current = this.selectedRecord();
|
|
3615
|
+
if (!current || String(current[schema.keyField]) !== key) this.selectedRecord.set({ [schema.keyField]: key });
|
|
3616
|
+
this.currentView.set("detail");
|
|
3617
|
+
} else if (this.currentView() === "detail") {
|
|
3618
|
+
this.selectedRecord.set(null);
|
|
3619
|
+
this.currentView.set("list");
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
switchToDetail(record) {
|
|
3623
|
+
const schema = this.schema();
|
|
3624
|
+
const data = this.routeData();
|
|
3625
|
+
const binding = data["select"];
|
|
3626
|
+
if (binding !== void 0) {
|
|
3627
|
+
const key = schema && record[schema.keyField] !== void 0 && record[schema.keyField] !== null ? String(record[schema.keyField]) : "";
|
|
3628
|
+
if (binding.mode === "route") {
|
|
3629
|
+
const emit = data["emit"];
|
|
3630
|
+
emit?.({
|
|
3631
|
+
resource: schema?.id ?? String(data["resourceId"] ?? ""),
|
|
3632
|
+
key,
|
|
3633
|
+
action: binding.on
|
|
3634
|
+
});
|
|
3635
|
+
return;
|
|
3636
|
+
}
|
|
3637
|
+
if (binding.mode === "none") return;
|
|
3638
|
+
this.selectedRecord.set(record);
|
|
3639
|
+
this.currentView.set("detail");
|
|
3640
|
+
return;
|
|
3641
|
+
}
|
|
3642
|
+
this.selectedRecord.set(record);
|
|
3643
|
+
this.currentView.set("detail");
|
|
3644
|
+
const k = schema ? record[schema.keyField] : void 0;
|
|
3645
|
+
if (schema && k !== void 0 && k !== null && String(k) !== "") this.router.navigate([
|
|
3646
|
+
"/",
|
|
3647
|
+
schema.id,
|
|
3648
|
+
String(k)
|
|
3649
|
+
]);
|
|
3650
|
+
}
|
|
3651
|
+
switchToList() {
|
|
3652
|
+
this.selectedRecord.set(null);
|
|
3653
|
+
this.currentView.set("list");
|
|
3654
|
+
const schema = this.schema();
|
|
3655
|
+
if (schema && this.params().get("key")) this.router.navigate(["/", schema.id]);
|
|
3656
|
+
}
|
|
3657
|
+
goEdit(record) {
|
|
3658
|
+
const schema = this.schema();
|
|
3659
|
+
if (!schema) return;
|
|
3660
|
+
const key = record[schema.keyField];
|
|
3661
|
+
if (key) this.router.navigate([
|
|
3662
|
+
"/",
|
|
3663
|
+
schema.id,
|
|
3664
|
+
key,
|
|
3665
|
+
"edit"
|
|
3666
|
+
]);
|
|
3667
|
+
}
|
|
3668
|
+
hasNoAccess(schema) {
|
|
3669
|
+
if (schema.policies?.access) {
|
|
3670
|
+
if (schema.kind === "singleton") return !this.auth.canDoOperation(schema, "update");
|
|
3671
|
+
return !this.auth.canDoOperation(schema, "list") && !this.auth.canDoOperation(schema, "create") && !this.auth.canDoOperation(schema, "update") && !this.auth.canDoOperation(schema, "remove");
|
|
3672
|
+
}
|
|
3673
|
+
const permissions = schema.permissions;
|
|
3674
|
+
if (!permissions || schema.kind === "dashboard") return false;
|
|
3675
|
+
if (schema.kind === "singleton") return !!!schema.endpoint?.get && permissions.update === false;
|
|
3676
|
+
const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
|
|
3677
|
+
const canWrite = permissions.create !== false || permissions.update !== false || permissions.remove !== false;
|
|
3678
|
+
return !hasReadEndpoint && !canWrite;
|
|
3679
|
+
}
|
|
3680
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
3681
|
+
minVersion: "12.0.0",
|
|
3682
|
+
version: "22.2.3",
|
|
3683
|
+
ngImport: i0,
|
|
3684
|
+
type: ResourcePage,
|
|
3685
|
+
deps: [],
|
|
3686
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
3687
|
+
});
|
|
3688
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
3689
|
+
minVersion: "17.0.0",
|
|
3690
|
+
version: "22.2.3",
|
|
3691
|
+
type: ResourcePage,
|
|
3692
|
+
isStandalone: true,
|
|
3693
|
+
selector: "app-resource-page",
|
|
3694
|
+
ngImport: i0,
|
|
3695
|
+
template: `
|
|
3696
|
+
@if (schema(); as schema) {
|
|
3697
|
+
@if (hasNoAccess(schema)) {
|
|
3698
|
+
<app-empty-state
|
|
3699
|
+
title="Sin permisos"
|
|
3700
|
+
message="No tiene permisos para gestionar este recurso."
|
|
3701
|
+
/>
|
|
3702
|
+
} @else {
|
|
3703
|
+
@switch (schema.kind) {
|
|
3704
|
+
@case ('collection') {
|
|
3705
|
+
@switch (currentView()) {
|
|
3706
|
+
@case ('detail') {
|
|
3707
|
+
@if (selectedRecord(); as rec) {
|
|
3708
|
+
<app-detail-view
|
|
3709
|
+
[schema]="schema"
|
|
3710
|
+
[record]="rec"
|
|
3711
|
+
(back)="switchToList()"
|
|
3712
|
+
(edit)="goEdit($event)"
|
|
3713
|
+
(delete)="switchToList()"
|
|
3714
|
+
/>
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
@default {
|
|
3718
|
+
<header class="mb-6">
|
|
3719
|
+
<h1 class="font-display text-2xl font-semibold">
|
|
3720
|
+
{{ schema.labelPlural }}
|
|
3721
|
+
</h1>
|
|
3722
|
+
<p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
|
|
3723
|
+
</header>
|
|
3724
|
+
<app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
@case ('dashboard') {
|
|
3729
|
+
<app-dashboard [schema]="schema" />
|
|
3730
|
+
}
|
|
3731
|
+
@default {
|
|
3732
|
+
<!-- singleton: formulario directo -->
|
|
3733
|
+
<app-form-view />
|
|
3734
|
+
}
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
} @else {
|
|
3738
|
+
<app-empty-state
|
|
3739
|
+
title="Recurso no encontrado"
|
|
3740
|
+
message="No existe un schema con ese id en el registry."
|
|
3741
|
+
/>
|
|
3742
|
+
}
|
|
3743
|
+
`,
|
|
3744
|
+
isInline: true,
|
|
3745
|
+
dependencies: [
|
|
3746
|
+
{
|
|
3747
|
+
kind: "component",
|
|
3748
|
+
type: Dashboard,
|
|
3749
|
+
selector: "app-dashboard",
|
|
3750
|
+
inputs: ["schema", "data"]
|
|
3751
|
+
},
|
|
3752
|
+
{
|
|
3753
|
+
kind: "component",
|
|
3754
|
+
type: DetailView,
|
|
3755
|
+
selector: "app-detail-view",
|
|
3756
|
+
inputs: ["schema", "record"],
|
|
3757
|
+
outputs: [
|
|
3758
|
+
"edit",
|
|
3759
|
+
"back",
|
|
3760
|
+
"delete"
|
|
3761
|
+
]
|
|
3762
|
+
},
|
|
3763
|
+
{
|
|
3764
|
+
kind: "component",
|
|
3765
|
+
type: EmptyState,
|
|
3766
|
+
selector: "app-empty-state",
|
|
3767
|
+
inputs: ["title", "message"]
|
|
3768
|
+
},
|
|
3769
|
+
{
|
|
3770
|
+
kind: "component",
|
|
3771
|
+
type: FormView,
|
|
3772
|
+
selector: "app-form-view"
|
|
3773
|
+
},
|
|
3774
|
+
{
|
|
3775
|
+
kind: "component",
|
|
3776
|
+
type: ListView,
|
|
3777
|
+
selector: "app-list-view",
|
|
3778
|
+
inputs: ["schema"],
|
|
3779
|
+
outputs: ["viewRecord"]
|
|
3780
|
+
}
|
|
3781
|
+
]
|
|
3782
|
+
});
|
|
3783
|
+
};
|
|
3784
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
3785
|
+
minVersion: "12.0.0",
|
|
3786
|
+
version: "22.2.3",
|
|
3787
|
+
ngImport: i0,
|
|
3788
|
+
type: ResourcePage,
|
|
3789
|
+
decorators: [{
|
|
3790
|
+
type: Component,
|
|
3791
|
+
args: [{
|
|
3792
|
+
selector: "app-resource-page",
|
|
3793
|
+
imports: [
|
|
3794
|
+
Dashboard,
|
|
3795
|
+
DetailView,
|
|
3796
|
+
EmptyState,
|
|
3797
|
+
FormView,
|
|
3798
|
+
ListView
|
|
3799
|
+
],
|
|
3800
|
+
template: `
|
|
3801
|
+
@if (schema(); as schema) {
|
|
3802
|
+
@if (hasNoAccess(schema)) {
|
|
3803
|
+
<app-empty-state
|
|
3804
|
+
title="Sin permisos"
|
|
3805
|
+
message="No tiene permisos para gestionar este recurso."
|
|
3806
|
+
/>
|
|
3807
|
+
} @else {
|
|
3808
|
+
@switch (schema.kind) {
|
|
3809
|
+
@case ('collection') {
|
|
3810
|
+
@switch (currentView()) {
|
|
3811
|
+
@case ('detail') {
|
|
3812
|
+
@if (selectedRecord(); as rec) {
|
|
3813
|
+
<app-detail-view
|
|
3814
|
+
[schema]="schema"
|
|
3815
|
+
[record]="rec"
|
|
3816
|
+
(back)="switchToList()"
|
|
3817
|
+
(edit)="goEdit($event)"
|
|
3818
|
+
(delete)="switchToList()"
|
|
3819
|
+
/>
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
@default {
|
|
3823
|
+
<header class="mb-6">
|
|
3824
|
+
<h1 class="font-display text-2xl font-semibold">
|
|
3825
|
+
{{ schema.labelPlural }}
|
|
3826
|
+
</h1>
|
|
3827
|
+
<p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
|
|
3828
|
+
</header>
|
|
3829
|
+
<app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
@case ('dashboard') {
|
|
3834
|
+
<app-dashboard [schema]="schema" />
|
|
3835
|
+
}
|
|
3836
|
+
@default {
|
|
3837
|
+
<!-- singleton: formulario directo -->
|
|
3838
|
+
<app-form-view />
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
}
|
|
3842
|
+
} @else {
|
|
3843
|
+
<app-empty-state
|
|
3844
|
+
title="Recurso no encontrado"
|
|
3845
|
+
message="No existe un schema con ese id en el registry."
|
|
3846
|
+
/>
|
|
3847
|
+
}
|
|
3848
|
+
`
|
|
3849
|
+
}]
|
|
3850
|
+
}],
|
|
3851
|
+
ctorParameters: () => []
|
|
3852
|
+
});
|
|
3853
|
+
var ViewSlot = class ViewSlot {
|
|
3854
|
+
schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
3855
|
+
node = input.required(...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
|
|
3856
|
+
parentInstanceId = input.required(...ngDevMode ? [{ debugName: "parentInstanceId" }] : /* istanbul ignore next */ []);
|
|
3857
|
+
depth = input(0, ...ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []);
|
|
3858
|
+
status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
3859
|
+
error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
3860
|
+
payload = signal(null, ...ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []);
|
|
3861
|
+
nav = inject(ViewNavigationService);
|
|
3862
|
+
envInjector = inject(EnvironmentInjector);
|
|
3863
|
+
ancla = viewChild("host", {
|
|
3864
|
+
...ngDevMode ? { debugName: "ancla" } : /* istanbul ignore next */ {},
|
|
3865
|
+
read: ViewContainerRef
|
|
3866
|
+
});
|
|
3867
|
+
epoch = 0;
|
|
3868
|
+
pendiente = null;
|
|
3869
|
+
constructor() {
|
|
3870
|
+
effect(() => this.monar());
|
|
3871
|
+
effect(() => {
|
|
3872
|
+
if (this.status() !== "ready") return;
|
|
3873
|
+
const pend = this.pendiente;
|
|
3874
|
+
let ancla;
|
|
3875
|
+
try {
|
|
3876
|
+
ancla = this.ancla();
|
|
3877
|
+
} catch {
|
|
3878
|
+
ancla = void 0;
|
|
3879
|
+
}
|
|
3880
|
+
if (!pend || !ancla) return;
|
|
3881
|
+
this.pendiente = null;
|
|
3882
|
+
ancla.clear();
|
|
3883
|
+
ancla.createComponent(pend.tipo, { injector: pend.injector });
|
|
3884
|
+
});
|
|
3885
|
+
}
|
|
3886
|
+
async monar() {
|
|
3887
|
+
const esquema = this.schema();
|
|
3888
|
+
const nodo = this.node();
|
|
3889
|
+
if (!esquema || !nodo) return;
|
|
3890
|
+
const turno = ++this.epoch;
|
|
3891
|
+
this.pendiente = null;
|
|
3892
|
+
if (this.depth() > 3) {
|
|
3893
|
+
this.error.set(/* @__PURE__ */ new Error(`Límite de profundidad de composición excedido (3) en ${esquema.id}.${nodo.id}`));
|
|
3894
|
+
this.status.set("error");
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
const resuelto = "ref" in nodo ? esquema.uiViews?.[nodo.ref] : nodo;
|
|
3898
|
+
if (resuelto === void 0) {
|
|
3899
|
+
const ref = "ref" in nodo ? nodo.ref : nodo.id;
|
|
3900
|
+
this.error.set(/* @__PURE__ */ new Error(`Referencia '${ref}' inexistente en ${esquema.id}`));
|
|
3901
|
+
this.status.set("error");
|
|
3902
|
+
return;
|
|
3903
|
+
}
|
|
3904
|
+
const descriptor = get("view", resuelto.type);
|
|
3905
|
+
if (!descriptor) {
|
|
3906
|
+
this.error.set(/* @__PURE__ */ new Error(`No existe descriptor kind:view type '${resuelto.type}' en ${esquema.id}`));
|
|
3907
|
+
this.status.set("error");
|
|
3908
|
+
return;
|
|
3909
|
+
}
|
|
3910
|
+
const instanceId = instanceIdFor(this.parentInstanceId(), nodo.id);
|
|
3911
|
+
const select = "ref" in nodo ? resuelto.select : nodo.select;
|
|
3912
|
+
const virtual = virtualSchemaFor(esquema, instanceId, resuelto);
|
|
3913
|
+
const emit = select?.mode === "route" ? (event) => {
|
|
3914
|
+
this.nav.dispatch(select, event);
|
|
3915
|
+
} : void 0;
|
|
3916
|
+
this.payload.set(createViewContext({
|
|
3917
|
+
schema: virtual,
|
|
3918
|
+
resourceId: esquema.id,
|
|
3919
|
+
uiView: instanceId,
|
|
3920
|
+
viewType: resuelto.type,
|
|
3921
|
+
depth: this.depth(),
|
|
3922
|
+
...select !== void 0 ? { select } : {}
|
|
3923
|
+
}, emit));
|
|
3924
|
+
this.error.set(null);
|
|
3925
|
+
this.status.set("loading");
|
|
3926
|
+
const injector = Injector.create({
|
|
3927
|
+
providers: [{
|
|
3928
|
+
provide: ActivatedRoute,
|
|
3929
|
+
useValue: syntheticRoute(this.payload())
|
|
3930
|
+
}],
|
|
3931
|
+
parent: this.envInjector
|
|
3932
|
+
});
|
|
3933
|
+
try {
|
|
3934
|
+
const tipo = await descriptor.load();
|
|
3935
|
+
if (turno !== this.epoch) return;
|
|
3936
|
+
this.pendiente = {
|
|
3937
|
+
tipo,
|
|
3938
|
+
injector
|
|
3939
|
+
};
|
|
3940
|
+
this.status.set("ready");
|
|
3941
|
+
} catch (e) {
|
|
3942
|
+
if (turno !== this.epoch) return;
|
|
3943
|
+
this.error.set(e);
|
|
3944
|
+
this.status.set("error");
|
|
3945
|
+
}
|
|
3946
|
+
}
|
|
3947
|
+
mensaje() {
|
|
3948
|
+
const e = this.error();
|
|
3949
|
+
return e instanceof Error ? e.message : String(e);
|
|
3950
|
+
}
|
|
3951
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
3952
|
+
minVersion: "12.0.0",
|
|
3953
|
+
version: "22.2.3",
|
|
3954
|
+
ngImport: i0,
|
|
3955
|
+
type: ViewSlot,
|
|
3956
|
+
deps: [],
|
|
3957
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
3958
|
+
});
|
|
3959
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
3960
|
+
minVersion: "17.0.0",
|
|
3961
|
+
version: "22.2.3",
|
|
3962
|
+
type: ViewSlot,
|
|
3963
|
+
isStandalone: true,
|
|
3964
|
+
selector: "app-view-slot",
|
|
3965
|
+
inputs: {
|
|
3966
|
+
schema: {
|
|
3967
|
+
classPropertyName: "schema",
|
|
3968
|
+
publicName: "schema",
|
|
3969
|
+
isSignal: true,
|
|
3970
|
+
isRequired: true,
|
|
3971
|
+
transformFunction: null
|
|
3972
|
+
},
|
|
3973
|
+
node: {
|
|
3974
|
+
classPropertyName: "node",
|
|
3975
|
+
publicName: "node",
|
|
3976
|
+
isSignal: true,
|
|
3977
|
+
isRequired: true,
|
|
3978
|
+
transformFunction: null
|
|
3979
|
+
},
|
|
3980
|
+
parentInstanceId: {
|
|
3981
|
+
classPropertyName: "parentInstanceId",
|
|
3982
|
+
publicName: "parentInstanceId",
|
|
3983
|
+
isSignal: true,
|
|
3984
|
+
isRequired: true,
|
|
3985
|
+
transformFunction: null
|
|
3986
|
+
},
|
|
3987
|
+
depth: {
|
|
3988
|
+
classPropertyName: "depth",
|
|
3989
|
+
publicName: "depth",
|
|
3990
|
+
isSignal: true,
|
|
3991
|
+
isRequired: false,
|
|
3992
|
+
transformFunction: null
|
|
3993
|
+
}
|
|
3994
|
+
},
|
|
3995
|
+
viewQueries: [{
|
|
3996
|
+
propertyName: "ancla",
|
|
3997
|
+
first: true,
|
|
3998
|
+
predicate: ["host"],
|
|
3999
|
+
descendants: true,
|
|
4000
|
+
read: ViewContainerRef,
|
|
4001
|
+
isSignal: true
|
|
4002
|
+
}],
|
|
4003
|
+
ngImport: i0,
|
|
4004
|
+
template: `
|
|
4005
|
+
@if (status() === 'loading') {
|
|
4006
|
+
<div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
|
|
4007
|
+
} @else if (status() === 'error') {
|
|
4008
|
+
<app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
|
|
4009
|
+
} @else {
|
|
4010
|
+
<div #host></div>
|
|
4011
|
+
}
|
|
4012
|
+
`,
|
|
4013
|
+
isInline: true,
|
|
4014
|
+
dependencies: [{
|
|
4015
|
+
kind: "component",
|
|
4016
|
+
type: EmptyState,
|
|
4017
|
+
selector: "app-empty-state",
|
|
4018
|
+
inputs: ["title", "message"]
|
|
4019
|
+
}],
|
|
4020
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
4021
|
+
});
|
|
4022
|
+
};
|
|
4023
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
4024
|
+
minVersion: "12.0.0",
|
|
4025
|
+
version: "22.2.3",
|
|
4026
|
+
ngImport: i0,
|
|
4027
|
+
type: ViewSlot,
|
|
4028
|
+
decorators: [{
|
|
4029
|
+
type: Component,
|
|
4030
|
+
args: [{
|
|
4031
|
+
selector: "app-view-slot",
|
|
4032
|
+
standalone: true,
|
|
4033
|
+
imports: [EmptyState],
|
|
4034
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4035
|
+
template: `
|
|
4036
|
+
@if (status() === 'loading') {
|
|
4037
|
+
<div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
|
|
4038
|
+
} @else if (status() === 'error') {
|
|
4039
|
+
<app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
|
|
4040
|
+
} @else {
|
|
4041
|
+
<div #host></div>
|
|
4042
|
+
}
|
|
4043
|
+
`
|
|
4044
|
+
}]
|
|
4045
|
+
}],
|
|
4046
|
+
ctorParameters: () => [],
|
|
4047
|
+
propDecorators: {
|
|
4048
|
+
schema: [{
|
|
4049
|
+
type: i0.Input,
|
|
4050
|
+
args: [{
|
|
4051
|
+
isSignal: true,
|
|
4052
|
+
alias: "schema",
|
|
4053
|
+
required: true
|
|
4054
|
+
}]
|
|
4055
|
+
}],
|
|
4056
|
+
node: [{
|
|
4057
|
+
type: i0.Input,
|
|
4058
|
+
args: [{
|
|
4059
|
+
isSignal: true,
|
|
4060
|
+
alias: "node",
|
|
4061
|
+
required: true
|
|
4062
|
+
}]
|
|
4063
|
+
}],
|
|
4064
|
+
parentInstanceId: [{
|
|
4065
|
+
type: i0.Input,
|
|
4066
|
+
args: [{
|
|
4067
|
+
isSignal: true,
|
|
4068
|
+
alias: "parentInstanceId",
|
|
4069
|
+
required: true
|
|
4070
|
+
}]
|
|
4071
|
+
}],
|
|
4072
|
+
depth: [{
|
|
4073
|
+
type: i0.Input,
|
|
4074
|
+
args: [{
|
|
4075
|
+
isSignal: true,
|
|
4076
|
+
alias: "depth",
|
|
4077
|
+
required: false
|
|
4078
|
+
}]
|
|
4079
|
+
}],
|
|
4080
|
+
ancla: [{
|
|
4081
|
+
type: i0.ViewChild,
|
|
4082
|
+
args: ["host", {
|
|
4083
|
+
read: ViewContainerRef,
|
|
4084
|
+
isSignal: true
|
|
4085
|
+
}]
|
|
4086
|
+
}]
|
|
4087
|
+
}
|
|
4088
|
+
});
|
|
4089
|
+
var composer_host_exports = /* @__PURE__ */ __exportAll({ ComposerHost: () => ComposerHost });
|
|
4090
|
+
var ComposerHost = class ComposerHost {
|
|
4091
|
+
route = inject(ActivatedRoute);
|
|
4092
|
+
routeData = toSignal(this.route.data, { requireSync: true });
|
|
4093
|
+
schema = computed(() => {
|
|
4094
|
+
const data = this.routeData();
|
|
4095
|
+
const explicit = data["schema"];
|
|
4096
|
+
if (explicit !== void 0) return explicit;
|
|
4097
|
+
const resourceId = data["resourceId"];
|
|
4098
|
+
return typeof resourceId === "string" ? getSchema(resourceId) : void 0;
|
|
4099
|
+
}, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
|
|
4100
|
+
uiViewName = computed(() => {
|
|
4101
|
+
const v = this.routeData()["uiView"];
|
|
4102
|
+
return typeof v === "string" ? v : "";
|
|
4103
|
+
}, ...ngDevMode ? [{ debugName: "uiViewName" }] : /* istanbul ignore next */ []);
|
|
4104
|
+
profundidad = computed(() => {
|
|
4105
|
+
const d = this.routeData()["depth"];
|
|
4106
|
+
return typeof d === "number" ? d : 0;
|
|
4107
|
+
}, ...ngDevMode ? [{ debugName: "profundidad" }] : /* istanbul ignore next */ []);
|
|
4108
|
+
vista = computed(() => {
|
|
4109
|
+
const schema = this.schema();
|
|
4110
|
+
const nombre = this.uiViewName();
|
|
4111
|
+
if (schema === void 0 || nombre === "") return void 0;
|
|
4112
|
+
const vista = schema.uiViews?.[nombre];
|
|
4113
|
+
return vista !== void 0 && "config" in vista && vista.type === "tabs" ? vista : void 0;
|
|
4114
|
+
}, ...ngDevMode ? [{ debugName: "vista" }] : /* istanbul ignore next */ []);
|
|
4115
|
+
errorConfig = computed(() => {
|
|
4116
|
+
const data = this.routeData();
|
|
4117
|
+
const schema = this.schema();
|
|
4118
|
+
const nombre = this.uiViewName();
|
|
4119
|
+
if (schema === void 0) return `Schema no encontrado para '${String(data["resourceId"] ?? "")}'`;
|
|
4120
|
+
if (nombre === "") return "Ruta de composición sin uiView declarada";
|
|
4121
|
+
const vista = schema.uiViews?.[nombre];
|
|
4122
|
+
if (vista === void 0) return `Vista '${nombre}' no declarada en ${schema.id}`;
|
|
4123
|
+
if (vista.type !== "tabs") return `Configuración inválida: ${schema.id}.${nombre} declara '${vista.type}', no es una vista tabs`;
|
|
4124
|
+
return null;
|
|
4125
|
+
}, ...ngDevMode ? [{ debugName: "errorConfig" }] : /* istanbul ignore next */ []);
|
|
4126
|
+
errorValidacion = signal(null, ...ngDevMode ? [{ debugName: "errorValidacion" }] : /* istanbul ignore next */ []);
|
|
4127
|
+
error = computed(() => this.errorValidacion() ?? this.errorConfig(), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
|
|
4128
|
+
hijos = computed(() => this.vista()?.config.children ?? [], ...ngDevMode ? [{ debugName: "hijos" }] : /* istanbul ignore next */ []);
|
|
4129
|
+
visibles = computed(() => this.hijos().filter((h) => h.visibleWhen === void 0 || h.visibleWhen.every((cond) => evaluateCondition(cond, {}, {}))), ...ngDevMode ? [{ debugName: "visibles" }] : /* istanbul ignore next */ []);
|
|
4130
|
+
seleccion = signal("", ...ngDevMode ? [{ debugName: "seleccion" }] : /* istanbul ignore next */ []);
|
|
4131
|
+
activo = computed(() => {
|
|
4132
|
+
const vis = this.visibles();
|
|
4133
|
+
if (vis.length === 0) return "";
|
|
4134
|
+
const sel = this.seleccion();
|
|
4135
|
+
if (vis.some((h) => h.id === sel)) return sel;
|
|
4136
|
+
const inicial = this.vista()?.config.initial;
|
|
4137
|
+
return inicial !== void 0 && vis.some((h) => h.id === inicial) ? inicial : vis[0].id;
|
|
4138
|
+
}, ...ngDevMode ? [{ debugName: "activo" }] : /* istanbul ignore next */ []);
|
|
4139
|
+
visitados = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "visitados" }] : /* istanbul ignore next */ []);
|
|
4140
|
+
montados = computed(() => {
|
|
4141
|
+
const visibles = new Set(this.visibles().map((h) => h.id));
|
|
4142
|
+
const activo = this.activo();
|
|
4143
|
+
const visitados = this.visitados();
|
|
4144
|
+
return this.hijos().filter((h) => visibles.has(h.id) && (h.id === activo || h.retain === true && visitados.has(h.id)));
|
|
4145
|
+
}, ...ngDevMode ? [{ debugName: "montados" }] : /* istanbul ignore next */ []);
|
|
4146
|
+
constructor() {
|
|
4147
|
+
const schema = this.schema();
|
|
4148
|
+
if (schema !== void 0) try {
|
|
4149
|
+
validateComposition(schema);
|
|
4150
|
+
} catch (e) {
|
|
4151
|
+
this.errorValidacion.set(e instanceof Error ? e.message : String(e));
|
|
4152
|
+
}
|
|
4153
|
+
const inicial = this.activo();
|
|
4154
|
+
if (inicial !== "") this.visitados.set(/* @__PURE__ */ new Set([inicial]));
|
|
4155
|
+
}
|
|
4156
|
+
seleccionar(id, conFoco) {
|
|
4157
|
+
this.seleccion.set(id);
|
|
4158
|
+
this.visitados.update((v) => new Set(v).add(id));
|
|
4159
|
+
if (conFoco) setTimeout(() => document.getElementById(`tab-${id}`)?.focus(), 0);
|
|
4160
|
+
}
|
|
4161
|
+
onKeydown(event) {
|
|
4162
|
+
const vis = this.visibles();
|
|
4163
|
+
if (vis.length === 0) return;
|
|
4164
|
+
const i = vis.findIndex((h) => h.id === this.activo());
|
|
4165
|
+
let next = -1;
|
|
4166
|
+
if (event.key === "ArrowRight") next = (i + 1) % vis.length;
|
|
4167
|
+
else if (event.key === "ArrowLeft") next = (i - 1 + vis.length) % vis.length;
|
|
4168
|
+
else if (event.key === "Home") next = 0;
|
|
4169
|
+
else if (event.key === "End") next = vis.length - 1;
|
|
4170
|
+
if (next < 0) return;
|
|
4171
|
+
event.preventDefault();
|
|
4172
|
+
this.seleccionar(vis[next].id, true);
|
|
4173
|
+
}
|
|
4174
|
+
montado(id) {
|
|
4175
|
+
return this.montados().some((h) => h.id === id);
|
|
4176
|
+
}
|
|
4177
|
+
claseTab(id) {
|
|
4178
|
+
const base = "rounded-t-lg px-3 py-2 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary";
|
|
4179
|
+
return id === this.activo() ? `${base} bg-primary/10 font-medium text-primary` : `${base} text-neutral hover:text-primary`;
|
|
4180
|
+
}
|
|
4181
|
+
etiqueta() {
|
|
4182
|
+
return this.schema()?.labelPlural ?? "Secciones";
|
|
4183
|
+
}
|
|
4184
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
4185
|
+
minVersion: "12.0.0",
|
|
4186
|
+
version: "22.2.3",
|
|
4187
|
+
ngImport: i0,
|
|
4188
|
+
type: ComposerHost,
|
|
4189
|
+
deps: [],
|
|
4190
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
4191
|
+
});
|
|
4192
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
4193
|
+
minVersion: "17.0.0",
|
|
4194
|
+
version: "22.2.3",
|
|
4195
|
+
type: ComposerHost,
|
|
4196
|
+
isStandalone: true,
|
|
4197
|
+
selector: "app-composer-host",
|
|
4198
|
+
ngImport: i0,
|
|
4199
|
+
template: `
|
|
4200
|
+
@if (error() !== null) {
|
|
4201
|
+
<app-empty-state title="Composición no disponible" [message]="error()!" />
|
|
4202
|
+
} @else if (visibles().length === 0) {
|
|
4203
|
+
<app-empty-state
|
|
4204
|
+
title="Sin secciones disponibles"
|
|
4205
|
+
message="La vista no declara secciones visibles."
|
|
4206
|
+
/>
|
|
4207
|
+
} @else {
|
|
4208
|
+
<div
|
|
4209
|
+
role="tablist"
|
|
4210
|
+
class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
|
|
4211
|
+
[attr.aria-label]="etiqueta()"
|
|
4212
|
+
(keydown)="onKeydown($event)"
|
|
4213
|
+
>
|
|
4214
|
+
@for (h of visibles(); track h.id) {
|
|
4215
|
+
<button
|
|
4216
|
+
type="button"
|
|
4217
|
+
role="tab"
|
|
4218
|
+
[id]="'tab-' + h.id"
|
|
4219
|
+
[attr.aria-selected]="h.id === activo()"
|
|
4220
|
+
[attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
|
|
4221
|
+
[attr.tabindex]="h.id === activo() ? 0 : -1"
|
|
4222
|
+
[class]="claseTab(h.id)"
|
|
4223
|
+
(click)="seleccionar(h.id, true)"
|
|
4224
|
+
>
|
|
4225
|
+
{{ h.label }}
|
|
4226
|
+
</button>
|
|
4227
|
+
}
|
|
4228
|
+
</div>
|
|
4229
|
+
|
|
4230
|
+
@for (h of montados(); track h.id) {
|
|
4231
|
+
<div
|
|
4232
|
+
role="tabpanel"
|
|
4233
|
+
[id]="'panel-' + h.id"
|
|
4234
|
+
[attr.aria-labelledby]="'tab-' + h.id"
|
|
4235
|
+
[class.hidden]="h.id !== activo()"
|
|
4236
|
+
class="py-3 focus:outline-none"
|
|
4237
|
+
tabindex="0"
|
|
4238
|
+
>
|
|
4239
|
+
<app-view-slot
|
|
4240
|
+
[schema]="schema()!"
|
|
4241
|
+
[node]="h"
|
|
4242
|
+
[parentInstanceId]="uiViewName()"
|
|
4243
|
+
[depth]="profundidad() + 1"
|
|
4244
|
+
/>
|
|
4245
|
+
</div>
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
`,
|
|
4249
|
+
isInline: true,
|
|
4250
|
+
dependencies: [{
|
|
4251
|
+
kind: "component",
|
|
4252
|
+
type: ViewSlot,
|
|
4253
|
+
selector: "app-view-slot",
|
|
4254
|
+
inputs: [
|
|
4255
|
+
"schema",
|
|
4256
|
+
"node",
|
|
4257
|
+
"parentInstanceId",
|
|
4258
|
+
"depth"
|
|
4259
|
+
]
|
|
4260
|
+
}, {
|
|
4261
|
+
kind: "component",
|
|
4262
|
+
type: EmptyState,
|
|
4263
|
+
selector: "app-empty-state",
|
|
4264
|
+
inputs: ["title", "message"]
|
|
4265
|
+
}],
|
|
4266
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
4267
|
+
});
|
|
4268
|
+
};
|
|
4269
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
4270
|
+
minVersion: "12.0.0",
|
|
4271
|
+
version: "22.2.3",
|
|
4272
|
+
ngImport: i0,
|
|
4273
|
+
type: ComposerHost,
|
|
4274
|
+
decorators: [{
|
|
4275
|
+
type: Component,
|
|
4276
|
+
args: [{
|
|
4277
|
+
selector: "app-composer-host",
|
|
4278
|
+
standalone: true,
|
|
4279
|
+
imports: [ViewSlot, EmptyState],
|
|
4280
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
4281
|
+
template: `
|
|
4282
|
+
@if (error() !== null) {
|
|
4283
|
+
<app-empty-state title="Composición no disponible" [message]="error()!" />
|
|
4284
|
+
} @else if (visibles().length === 0) {
|
|
4285
|
+
<app-empty-state
|
|
4286
|
+
title="Sin secciones disponibles"
|
|
4287
|
+
message="La vista no declara secciones visibles."
|
|
4288
|
+
/>
|
|
4289
|
+
} @else {
|
|
4290
|
+
<div
|
|
4291
|
+
role="tablist"
|
|
4292
|
+
class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
|
|
4293
|
+
[attr.aria-label]="etiqueta()"
|
|
4294
|
+
(keydown)="onKeydown($event)"
|
|
4295
|
+
>
|
|
4296
|
+
@for (h of visibles(); track h.id) {
|
|
4297
|
+
<button
|
|
4298
|
+
type="button"
|
|
4299
|
+
role="tab"
|
|
4300
|
+
[id]="'tab-' + h.id"
|
|
4301
|
+
[attr.aria-selected]="h.id === activo()"
|
|
4302
|
+
[attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
|
|
4303
|
+
[attr.tabindex]="h.id === activo() ? 0 : -1"
|
|
4304
|
+
[class]="claseTab(h.id)"
|
|
4305
|
+
(click)="seleccionar(h.id, true)"
|
|
4306
|
+
>
|
|
4307
|
+
{{ h.label }}
|
|
4308
|
+
</button>
|
|
4309
|
+
}
|
|
4310
|
+
</div>
|
|
4311
|
+
|
|
4312
|
+
@for (h of montados(); track h.id) {
|
|
4313
|
+
<div
|
|
4314
|
+
role="tabpanel"
|
|
4315
|
+
[id]="'panel-' + h.id"
|
|
4316
|
+
[attr.aria-labelledby]="'tab-' + h.id"
|
|
4317
|
+
[class.hidden]="h.id !== activo()"
|
|
4318
|
+
class="py-3 focus:outline-none"
|
|
4319
|
+
tabindex="0"
|
|
4320
|
+
>
|
|
4321
|
+
<app-view-slot
|
|
4322
|
+
[schema]="schema()!"
|
|
4323
|
+
[node]="h"
|
|
4324
|
+
[parentInstanceId]="uiViewName()"
|
|
4325
|
+
[depth]="profundidad() + 1"
|
|
4326
|
+
/>
|
|
4327
|
+
</div>
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
`
|
|
4331
|
+
}]
|
|
4332
|
+
}],
|
|
4333
|
+
ctorParameters: () => []
|
|
4334
|
+
});
|
|
4335
|
+
var ConfirmDialog = class ConfirmDialog {
|
|
4336
|
+
options = input(null, ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
|
|
4337
|
+
danger = input(false, ...ngDevMode ? [{ debugName: "danger" }] : /* istanbul ignore next */ []);
|
|
4338
|
+
confirmed = output();
|
|
4339
|
+
cancelled = output();
|
|
4340
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
4341
|
+
minVersion: "12.0.0",
|
|
4342
|
+
version: "22.2.3",
|
|
4343
|
+
ngImport: i0,
|
|
4344
|
+
type: ConfirmDialog,
|
|
4345
|
+
deps: [],
|
|
4346
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
4347
|
+
});
|
|
4348
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
4349
|
+
minVersion: "17.0.0",
|
|
4350
|
+
version: "22.2.3",
|
|
4351
|
+
type: ConfirmDialog,
|
|
4352
|
+
isStandalone: true,
|
|
4353
|
+
selector: "app-confirm-dialog",
|
|
4354
|
+
inputs: {
|
|
4355
|
+
options: {
|
|
4356
|
+
classPropertyName: "options",
|
|
4357
|
+
publicName: "options",
|
|
4358
|
+
isSignal: true,
|
|
4359
|
+
isRequired: false,
|
|
4360
|
+
transformFunction: null
|
|
4361
|
+
},
|
|
4362
|
+
danger: {
|
|
4363
|
+
classPropertyName: "danger",
|
|
4364
|
+
publicName: "danger",
|
|
4365
|
+
isSignal: true,
|
|
4366
|
+
isRequired: false,
|
|
4367
|
+
transformFunction: null
|
|
4368
|
+
}
|
|
4369
|
+
},
|
|
4370
|
+
outputs: {
|
|
4371
|
+
confirmed: "confirmed",
|
|
4372
|
+
cancelled: "cancelled"
|
|
4373
|
+
},
|
|
4374
|
+
ngImport: i0,
|
|
4375
|
+
template: `
|
|
4376
|
+
@if (options(); as opts) {
|
|
4377
|
+
<app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
|
|
4378
|
+
<p class="text-sm text-neutral">{{ opts.message }}</p>
|
|
4379
|
+
<div class="mt-6 flex justify-end gap-3">
|
|
4380
|
+
<button app-button variant="outline" type="button" (click)="cancelled.emit()">
|
|
4381
|
+
{{ opts.cancelLabel ?? 'Cancel' }}
|
|
4382
|
+
</button>
|
|
4383
|
+
<button
|
|
4384
|
+
app-button
|
|
4385
|
+
type="button"
|
|
4386
|
+
[variant]="danger() ? 'danger' : 'primary'"
|
|
4387
|
+
(click)="confirmed.emit()"
|
|
4388
|
+
>
|
|
4389
|
+
{{ opts.confirmLabel ?? 'Confirm' }}
|
|
4390
|
+
</button>
|
|
4391
|
+
</div>
|
|
4392
|
+
</app-modal>
|
|
4393
|
+
}
|
|
4394
|
+
`,
|
|
4395
|
+
isInline: true,
|
|
4396
|
+
dependencies: [{
|
|
4397
|
+
kind: "component",
|
|
4398
|
+
type: Modal,
|
|
4399
|
+
selector: "app-modal",
|
|
4400
|
+
inputs: [
|
|
4401
|
+
"open",
|
|
4402
|
+
"title",
|
|
4403
|
+
"closeLabel"
|
|
4404
|
+
],
|
|
4405
|
+
outputs: ["close"]
|
|
4406
|
+
}, {
|
|
4407
|
+
kind: "component",
|
|
4408
|
+
type: Button,
|
|
4409
|
+
selector: "button[app-button]",
|
|
4410
|
+
inputs: [
|
|
4411
|
+
"variant",
|
|
4412
|
+
"size",
|
|
4413
|
+
"type",
|
|
4414
|
+
"disabled",
|
|
4415
|
+
"loading"
|
|
4416
|
+
]
|
|
4417
|
+
}]
|
|
4418
|
+
});
|
|
4419
|
+
};
|
|
4420
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
4421
|
+
minVersion: "12.0.0",
|
|
4422
|
+
version: "22.2.3",
|
|
4423
|
+
ngImport: i0,
|
|
4424
|
+
type: ConfirmDialog,
|
|
4425
|
+
decorators: [{
|
|
4426
|
+
type: Component,
|
|
4427
|
+
args: [{
|
|
4428
|
+
selector: "app-confirm-dialog",
|
|
4429
|
+
imports: [Modal, Button],
|
|
4430
|
+
template: `
|
|
4431
|
+
@if (options(); as opts) {
|
|
4432
|
+
<app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
|
|
4433
|
+
<p class="text-sm text-neutral">{{ opts.message }}</p>
|
|
4434
|
+
<div class="mt-6 flex justify-end gap-3">
|
|
4435
|
+
<button app-button variant="outline" type="button" (click)="cancelled.emit()">
|
|
4436
|
+
{{ opts.cancelLabel ?? 'Cancel' }}
|
|
4437
|
+
</button>
|
|
4438
|
+
<button
|
|
4439
|
+
app-button
|
|
4440
|
+
type="button"
|
|
4441
|
+
[variant]="danger() ? 'danger' : 'primary'"
|
|
4442
|
+
(click)="confirmed.emit()"
|
|
4443
|
+
>
|
|
4444
|
+
{{ opts.confirmLabel ?? 'Confirm' }}
|
|
4445
|
+
</button>
|
|
4446
|
+
</div>
|
|
4447
|
+
</app-modal>
|
|
4448
|
+
}
|
|
4449
|
+
`
|
|
4450
|
+
}]
|
|
4451
|
+
}],
|
|
4452
|
+
propDecorators: {
|
|
4453
|
+
options: [{
|
|
4454
|
+
type: i0.Input,
|
|
4455
|
+
args: [{
|
|
4456
|
+
isSignal: true,
|
|
4457
|
+
alias: "options",
|
|
4458
|
+
required: false
|
|
4459
|
+
}]
|
|
4460
|
+
}],
|
|
4461
|
+
danger: [{
|
|
4462
|
+
type: i0.Input,
|
|
4463
|
+
args: [{
|
|
4464
|
+
isSignal: true,
|
|
4465
|
+
alias: "danger",
|
|
4466
|
+
required: false
|
|
4467
|
+
}]
|
|
4468
|
+
}],
|
|
4469
|
+
confirmed: [{
|
|
4470
|
+
type: i0.Output,
|
|
4471
|
+
args: ["confirmed"]
|
|
4472
|
+
}],
|
|
4473
|
+
cancelled: [{
|
|
4474
|
+
type: i0.Output,
|
|
4475
|
+
args: ["cancelled"]
|
|
4476
|
+
}]
|
|
4477
|
+
}
|
|
4478
|
+
});
|
|
4479
|
+
var Toast = class Toast {
|
|
4480
|
+
toasts = input([], ...ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []);
|
|
4481
|
+
closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
|
|
4482
|
+
dismissed = output();
|
|
4483
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
4484
|
+
minVersion: "12.0.0",
|
|
4485
|
+
version: "22.2.3",
|
|
4486
|
+
ngImport: i0,
|
|
4487
|
+
type: Toast,
|
|
4488
|
+
deps: [],
|
|
4489
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
4490
|
+
});
|
|
4491
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
4492
|
+
minVersion: "17.0.0",
|
|
4493
|
+
version: "22.2.3",
|
|
4494
|
+
type: Toast,
|
|
4495
|
+
isStandalone: true,
|
|
4496
|
+
selector: "app-toast",
|
|
4497
|
+
inputs: {
|
|
4498
|
+
toasts: {
|
|
4499
|
+
classPropertyName: "toasts",
|
|
4500
|
+
publicName: "toasts",
|
|
4501
|
+
isSignal: true,
|
|
4502
|
+
isRequired: false,
|
|
4503
|
+
transformFunction: null
|
|
4504
|
+
},
|
|
4505
|
+
closeLabel: {
|
|
4506
|
+
classPropertyName: "closeLabel",
|
|
4507
|
+
publicName: "closeLabel",
|
|
4508
|
+
isSignal: true,
|
|
4509
|
+
isRequired: false,
|
|
4510
|
+
transformFunction: null
|
|
4511
|
+
}
|
|
4512
|
+
},
|
|
4513
|
+
outputs: { dismissed: "dismissed" },
|
|
4514
|
+
host: {
|
|
4515
|
+
attributes: {
|
|
4516
|
+
"role": "status",
|
|
4517
|
+
"aria-live": "polite"
|
|
4518
|
+
},
|
|
4519
|
+
classAttribute: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2"
|
|
4520
|
+
},
|
|
4521
|
+
ngImport: i0,
|
|
4522
|
+
template: `
|
|
4523
|
+
@for (toast of toasts(); track toast.id) {
|
|
4524
|
+
<div
|
|
4525
|
+
class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
|
|
4526
|
+
[class.border-l-success]="toast.kind === 'success'"
|
|
4527
|
+
[class.border-l-danger]="toast.kind === 'error'"
|
|
4528
|
+
[class.border-l-primary]="toast.kind === 'info'"
|
|
4529
|
+
>
|
|
4530
|
+
@if (toast.kind === 'success') {
|
|
4531
|
+
<svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
|
|
4532
|
+
} @else if (toast.kind === 'error') {
|
|
4533
|
+
<svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
|
|
4534
|
+
} @else {
|
|
4535
|
+
<svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
|
|
4536
|
+
}
|
|
4537
|
+
<p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
|
|
4538
|
+
<button
|
|
4539
|
+
type="button"
|
|
4540
|
+
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"
|
|
4541
|
+
[attr.aria-label]="closeLabel()"
|
|
4542
|
+
(click)="dismissed.emit(toast.id)"
|
|
4543
|
+
>
|
|
4544
|
+
<svg lucideX size="18" />
|
|
4545
|
+
</button>
|
|
4546
|
+
</div>
|
|
4547
|
+
}
|
|
4548
|
+
`,
|
|
4549
|
+
isInline: true,
|
|
4550
|
+
dependencies: [
|
|
4551
|
+
{
|
|
4552
|
+
kind: "component",
|
|
4553
|
+
type: LucideCircleCheck,
|
|
4554
|
+
selector: "svg[lucideCircleCheck], svg[lucideCheckCircle2]"
|
|
4555
|
+
},
|
|
4556
|
+
{
|
|
4557
|
+
kind: "component",
|
|
4558
|
+
type: LucideCircleX,
|
|
4559
|
+
selector: "svg[lucideCircleX], svg[lucideXCircle]"
|
|
4560
|
+
},
|
|
4561
|
+
{
|
|
4562
|
+
kind: "component",
|
|
4563
|
+
type: LucideInfo,
|
|
4564
|
+
selector: "svg[lucideInfo]"
|
|
4565
|
+
},
|
|
4566
|
+
{
|
|
4567
|
+
kind: "component",
|
|
4568
|
+
type: LucideX,
|
|
4569
|
+
selector: "svg[lucideX]"
|
|
4570
|
+
}
|
|
4571
|
+
]
|
|
4572
|
+
});
|
|
4573
|
+
};
|
|
4574
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
4575
|
+
minVersion: "12.0.0",
|
|
4576
|
+
version: "22.2.3",
|
|
4577
|
+
ngImport: i0,
|
|
4578
|
+
type: Toast,
|
|
4579
|
+
decorators: [{
|
|
4580
|
+
type: Component,
|
|
4581
|
+
args: [{
|
|
4582
|
+
selector: "app-toast",
|
|
4583
|
+
imports: [
|
|
4584
|
+
LucideCircleCheck,
|
|
4585
|
+
LucideCircleX,
|
|
4586
|
+
LucideInfo,
|
|
4587
|
+
LucideX
|
|
4588
|
+
],
|
|
4589
|
+
host: {
|
|
4590
|
+
class: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2",
|
|
4591
|
+
role: "status",
|
|
4592
|
+
"aria-live": "polite"
|
|
4593
|
+
},
|
|
4594
|
+
template: `
|
|
4595
|
+
@for (toast of toasts(); track toast.id) {
|
|
4596
|
+
<div
|
|
4597
|
+
class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
|
|
4598
|
+
[class.border-l-success]="toast.kind === 'success'"
|
|
4599
|
+
[class.border-l-danger]="toast.kind === 'error'"
|
|
4600
|
+
[class.border-l-primary]="toast.kind === 'info'"
|
|
4601
|
+
>
|
|
4602
|
+
@if (toast.kind === 'success') {
|
|
4603
|
+
<svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
|
|
4604
|
+
} @else if (toast.kind === 'error') {
|
|
4605
|
+
<svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
|
|
4606
|
+
} @else {
|
|
4607
|
+
<svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
|
|
4608
|
+
}
|
|
4609
|
+
<p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
|
|
4610
|
+
<button
|
|
4611
|
+
type="button"
|
|
4612
|
+
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"
|
|
4613
|
+
[attr.aria-label]="closeLabel()"
|
|
4614
|
+
(click)="dismissed.emit(toast.id)"
|
|
4615
|
+
>
|
|
4616
|
+
<svg lucideX size="18" />
|
|
4617
|
+
</button>
|
|
4618
|
+
</div>
|
|
4619
|
+
}
|
|
4620
|
+
`
|
|
4621
|
+
}]
|
|
4622
|
+
}],
|
|
4623
|
+
propDecorators: {
|
|
4624
|
+
toasts: [{
|
|
4625
|
+
type: i0.Input,
|
|
4626
|
+
args: [{
|
|
4627
|
+
isSignal: true,
|
|
4628
|
+
alias: "toasts",
|
|
4629
|
+
required: false
|
|
4630
|
+
}]
|
|
4631
|
+
}],
|
|
4632
|
+
closeLabel: [{
|
|
4633
|
+
type: i0.Input,
|
|
4634
|
+
args: [{
|
|
4635
|
+
isSignal: true,
|
|
4636
|
+
alias: "closeLabel",
|
|
4637
|
+
required: false
|
|
4638
|
+
}]
|
|
4639
|
+
}],
|
|
4640
|
+
dismissed: [{
|
|
4641
|
+
type: i0.Output,
|
|
4642
|
+
args: ["dismissed"]
|
|
4643
|
+
}]
|
|
4644
|
+
}
|
|
4645
|
+
});
|
|
4646
|
+
const schemaDescriptions = {
|
|
4647
|
+
key: "Nombre de la propiedad en el JSON que envía/recibe el back.",
|
|
4648
|
+
label: "Rótulo visible para el usuario en el formulario y la ficha.",
|
|
4649
|
+
help: "Texto corto de ayuda que se muestra debajo del campo.",
|
|
4650
|
+
placeholder: "Texto sugerente que se muestra dentro del input cuando está vacío.",
|
|
4651
|
+
required: "Si es obligatorio. El back también lo valida (R7.4).",
|
|
4652
|
+
default: "Valor por defecto si el registro no trae este campo (ej. `default: \"none\"` en un `select`/`dropdown`).",
|
|
4653
|
+
readonly: "Si el campo es solo lectura (no editable en el formulario).",
|
|
4654
|
+
readonlyOn: "Bloquea el campo solo en creación, solo en edición o siempre.",
|
|
4655
|
+
readonlyWhen: "Condición bajo la cual el campo es solo lectura (expresión sobre otros campos).",
|
|
4656
|
+
visibleWhen: "Condición bajo la cual el campo se muestra u oculta.",
|
|
4657
|
+
hiddenOn: "Oculta el campo en creación, en edición o siempre.",
|
|
4658
|
+
dependsOn: "Selección dependiente (guía §1: país → ciudad). Clave del campo padre del mismo nivel.",
|
|
4659
|
+
validators: "Reglas de validación (mínimo, máximo, patrón, unicidad, etc.).",
|
|
4660
|
+
width: "Ancho en la grilla del formulario (1–12), igual que un widget del dashboard.",
|
|
4661
|
+
section: "Id de la sección/pestaña del layout a la que pertenece (base.md §4).",
|
|
4662
|
+
read: "Código de nivel de acceso requerido para leer el campo.",
|
|
4663
|
+
write: "Código de nivel de acceso requerido para escribir el campo.",
|
|
4664
|
+
description: "Descripción corta de para qué sirve este campo. Se muestra como tooltip al pasar el cursor sobre el rótulo.",
|
|
4665
|
+
docLink: "URL de documentación externa para este campo (se abre al hacer clic en el tooltip).",
|
|
4666
|
+
value: "Valor que se envía al back cuando se elige esta opción.",
|
|
4667
|
+
labelOption: "Texto visible de la opción.",
|
|
4668
|
+
disabled: "Si la opción está deshabilitada (no se puede elegir).",
|
|
4669
|
+
type: "Tipo de campo: `text`, `color`, `number`, `currency`, `boolean`, `select`, `dropdown`, `rich-select`, `multiselect`, `url`, `email`, `phone`, `date`, `time`, `readonly-text`, `slug`, `textarea`, `relation`, `image`, `file`, `group`, `list`, `string-list`, `key-value`.",
|
|
4670
|
+
maxLength: "Longitud máxima de caracteres permitida.",
|
|
4671
|
+
pattern: "Expresión regular que debe cumplir el valor.",
|
|
4672
|
+
rows: "Número de filas visibles del textarea.",
|
|
4673
|
+
minWords: "Cantidad mínima de palabras.",
|
|
4674
|
+
maxWords: "Cantidad máxima de palabras.",
|
|
4675
|
+
tokens: "Marcadores insertables como chips bajo el área (plantillas de mensaje).",
|
|
4676
|
+
from: "Campo del registro del que se autogenera el slug.",
|
|
4677
|
+
lockAfterCreate: "Bloquea el campo una vez creado el registro (por defecto: sí).",
|
|
4678
|
+
min: "Valor mínimo permitido.",
|
|
4679
|
+
max: "Valor máximo permitido.",
|
|
4680
|
+
step: "Incremento entre valores permitidos.",
|
|
4681
|
+
decimals: "Número de decimales mostrados.",
|
|
4682
|
+
currencyFrom: "Campo del registro del que se toma la moneda (ej. `currency`).",
|
|
4683
|
+
trueLabel: "Texto que se muestra cuando el valor es verdadero.",
|
|
4684
|
+
falseLabel: "Texto que se muestra cuando el valor es falso.",
|
|
4685
|
+
source: "De dónde salen las opciones: `{ resource, view?, params?, … }` (F8 · R6.5).",
|
|
4686
|
+
resource: "Recurso declarado (`resource.id`): su listado es el de las opciones.",
|
|
4687
|
+
view: "Vista declarada en `resource.views` (opcional): filtra en el back.",
|
|
4688
|
+
params: "Valores interpolados para la vista (`{campo}` ← contexto).",
|
|
4689
|
+
labelField: "Etiqueta por defecto de las filas (si no, `titleField` del recurso).",
|
|
4690
|
+
valueField: "Valor por defecto de las filas (si no, `keyField` del recurso).",
|
|
4691
|
+
secondaryField: "Segundo dato a mostrar junto a la etiqueta (ej. \"Juan Pérez · DNI 123\").",
|
|
4692
|
+
onlyActive: "Cargar solo registros activos (lo exige además la FK en el back).",
|
|
4693
|
+
accept: "Tipos aceptados (ej. `image/*`, `image/png,image/jpeg` o `pdf,docx`).",
|
|
4694
|
+
maxSizeMB: "Tamaño máximo del archivo en megabytes.",
|
|
4695
|
+
aspectRatio: "Relación de aspecto de la vista previa (ej. `16/9`).",
|
|
4696
|
+
previewSize: "Tamaño de la vista previa.",
|
|
4697
|
+
formatCurrency: "Campo del registro del que se toma la moneda (ej. `currency`).",
|
|
4698
|
+
display: "Presentación del grupo: `card`, `collapsible` o `inline`.",
|
|
4699
|
+
fields: "Campos de cada item (cualquier tipo, incluso `group` o `list`).",
|
|
4700
|
+
minItems: "Cantidad mínima de items.",
|
|
4701
|
+
maxItems: "Cantidad máxima de items.",
|
|
4702
|
+
sortable: "Si se puede reordenar (por defecto: sí).",
|
|
4703
|
+
positionField: "Campo donde se guarda el orden dentro de cada item (`position`).",
|
|
4704
|
+
addLabel: "Etiqueta del botón para agregar un item nuevo.",
|
|
4705
|
+
emptyText: "Texto que se muestra cuando no hay items.",
|
|
4706
|
+
itemDisplay: "Cómo se muestra cada item: `card`, `row` o `accordion`.",
|
|
4707
|
+
itemFields: "Campos de cada item (cualquier tipo, incluso `group` o `list`).",
|
|
4708
|
+
itemTitle: "Campo del item usado como título cuando está colapsado.",
|
|
4709
|
+
primaryFirst: "El primer item tiene significado especial (ej. imagen principal).",
|
|
4710
|
+
itemType: "Tipo permitido de cada item (base.md §9: solo `text`, `url` o `email`).",
|
|
4711
|
+
sort: "Si la lista se ordena alfabéticamente o por posición.",
|
|
4712
|
+
filter: "Si el listado permite filtrado por texto.",
|
|
4713
|
+
search: "Campos buscables en el listado.",
|
|
4714
|
+
filters: "Filtros declarados para el listado.",
|
|
4715
|
+
listColumns: "Columnas que se muestran en el listado.",
|
|
4716
|
+
searchable: "Si el campo es buscable.",
|
|
4717
|
+
filterable: "Si el campo es filtrable.",
|
|
4718
|
+
sortableItems: "Si los items de la lista se pueden reordenar.",
|
|
4719
|
+
groupBy: "Campo por el que agrupar los items.",
|
|
4720
|
+
cardWidth: "Ancho de cada tarjeta de opción en la grilla (1–12). Por defecto `4` (3 tarjetas por fila en escritorio).",
|
|
4721
|
+
cardCols: "Cuántas tarjetas mostrar por fila en el punto de ruptura `lg`. Sobrescribe el cálculo automático basado en `cardWidth`.",
|
|
4722
|
+
hintMetric: "Texto secundario bajo la cifra (estático, va en el schema).",
|
|
4723
|
+
unitChart: "Sufijo de las cifras del eje («unidades», «S/»).",
|
|
4724
|
+
segments: "Segmentos de la barra de distribución porcentual.",
|
|
4725
|
+
actions: "Acciones de navegación por fila.",
|
|
4726
|
+
labelAction: "Etiqueta de la acción.",
|
|
4727
|
+
icon: "Icono de la acción.",
|
|
4728
|
+
navigateTo: "Ruta de destino de la acción.",
|
|
4729
|
+
variant: "Variante visual: `primary`, `secondary` u `outline`.",
|
|
4730
|
+
widgets: "Widgets del panel. Se declara en `ResourceSchema.widgets` — no en `fields`, porque un KPI no es un campo.",
|
|
4731
|
+
mobileWidth: "Ancho en la grilla en mobile (1–12). Por defecto `12` (ancho completo, apilado).",
|
|
4732
|
+
sourceWidget: "De dónde saca sus datos: `DataSource` o `{ kind: \"payload\" }`.",
|
|
4733
|
+
hintWidget: "Texto secundario bajo la cifra (estático, va en el schema).",
|
|
4734
|
+
unitWidget: "Sufijo de las cifras del eje."
|
|
4735
|
+
};
|
|
4736
|
+
export { API_DRIVER, API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, DEFAULT_RESPONSE_UNWRAP, Dashboard, DataSourceService, ErrorMapperService, FIREBASE_AUTH_ADAPTER, FieldHost, FormView, GAS_SCHEMAS, ListView, Login, MAX_DEPTH, NotificationService, ResourcePage, SESSION_TOKEN_KEY, SESSION_USER_KEY, SUPPORTED_FIELD_TYPE_LIST, SchemaCompositionError, Sidebar, Toast, UploadService, VALUE_SOURCES, actionType, actionTypes, actionsFor, allSchemas, apiErrorInterceptor, authGuard, buildFormSchema, buildPayload, childTree, columnFormatDescriptors, fieldDescriptors, getAction, getAuth, get as getRenderDescriptor, getSchema, groupTree, interpolateTemplate, isSupported as isRenderSupported, isSupported, isShareAction, isTriggerAction, itemTree, layoutDescriptors, listTree, resource_page_exports as n, provideFirebaseAdapter, provideGasAdmin, provideRenderDescriptors, registerAction, register as registerRenderDescriptor, renderKinds, renewedTokenOf, resolveShare, resourceRoutes, schemaDescriptions, seedModel, setSchemaCatalog, shellRoutes, stringItemTree, composer_host_exports as t, unregister as unregisterRenderDescriptor, validateComposition, viewDescriptors };
|
|
4737
|
+
|
|
4738
|
+
//# sourceMappingURL=tinya-code-gas-admin-ui.mjs.map
|