@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
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
|
|
3
|
-
import { ThemeService } from './theme.service';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* jsdom no implementa `matchMedia`, así que se instala un doble controlable
|
|
7
|
-
* antes de cualquier construcción del servicio: `matches` decide el estado
|
|
8
|
-
* del sistema y `listeners` permite disparar el cambio en vivo.
|
|
9
|
-
*/
|
|
10
|
-
describe('ThemeService — 8.6 selector Sistema / Claro / Oscuro', () => {
|
|
11
|
-
let matches = false;
|
|
12
|
-
const listeners: Array<() => void> = [];
|
|
13
|
-
|
|
14
|
-
beforeAll(() => {
|
|
15
|
-
Object.defineProperty(window, 'matchMedia', {
|
|
16
|
-
writable: true,
|
|
17
|
-
configurable: true,
|
|
18
|
-
value: (query: string) => ({
|
|
19
|
-
matches: query.includes('prefers-color-scheme') && matches,
|
|
20
|
-
media: query,
|
|
21
|
-
onchange: null,
|
|
22
|
-
addEventListener: (_type: string, listener: () => void) => listeners.push(listener),
|
|
23
|
-
removeEventListener: () => undefined,
|
|
24
|
-
dispatchEvent: () => false,
|
|
25
|
-
}),
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
beforeEach(() => {
|
|
30
|
-
localStorage.clear();
|
|
31
|
-
document.documentElement.removeAttribute('data-theme');
|
|
32
|
-
document.documentElement.removeAttribute('data-theme-mode');
|
|
33
|
-
matches = false;
|
|
34
|
-
listeners.length = 0;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
const fireSystemChange = (): void => {
|
|
38
|
-
for (const listener of listeners) {
|
|
39
|
-
listener();
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
it('arranca en «system» cuando no hay elección guardada', () => {
|
|
44
|
-
const theme = new ThemeService();
|
|
45
|
-
|
|
46
|
-
expect(theme.mode()).toBe('system');
|
|
47
|
-
expect(theme.resolved()).toBe('light');
|
|
48
|
-
expect(document.documentElement.dataset['theme']).toBe('light');
|
|
49
|
-
expect(document.documentElement.dataset['themeMode']).toBe('system');
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('una elección corrupta en localStorage vuelve a «system»', () => {
|
|
53
|
-
localStorage.setItem('admin-theme', 'naranja');
|
|
54
|
-
|
|
55
|
-
expect(new ThemeService().mode()).toBe('system');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('aplica y persiste una elección concreta', () => {
|
|
59
|
-
const theme = new ThemeService();
|
|
60
|
-
|
|
61
|
-
theme.set('dark');
|
|
62
|
-
|
|
63
|
-
expect(theme.mode()).toBe('dark');
|
|
64
|
-
expect(theme.resolved()).toBe('dark');
|
|
65
|
-
expect(document.documentElement.dataset['theme']).toBe('dark');
|
|
66
|
-
expect(document.documentElement.dataset['themeMode']).toBe('dark');
|
|
67
|
-
expect(localStorage.getItem('admin-theme')).toBe('dark');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('recupera la elección guardada al arrancar y pinta ese modo', () => {
|
|
71
|
-
matches = true;
|
|
72
|
-
localStorage.setItem('admin-theme', 'light');
|
|
73
|
-
|
|
74
|
-
const theme = new ThemeService();
|
|
75
|
-
|
|
76
|
-
expect(theme.mode()).toBe('light');
|
|
77
|
-
expect(theme.resolved()).toBe('light');
|
|
78
|
-
expect(document.documentElement.dataset['theme']).toBe('light');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('«system» reacciona en vivo a un cambio del tema del sistema', () => {
|
|
82
|
-
const theme = new ThemeService();
|
|
83
|
-
expect(theme.resolved()).toBe('light');
|
|
84
|
-
|
|
85
|
-
matches = true;
|
|
86
|
-
fireSystemChange();
|
|
87
|
-
|
|
88
|
-
expect(theme.resolved()).toBe('dark');
|
|
89
|
-
expect(document.documentElement.dataset['theme']).toBe('dark');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('ignora el SO cuando la elección es concreta', () => {
|
|
93
|
-
const theme = new ThemeService();
|
|
94
|
-
theme.set('light');
|
|
95
|
-
|
|
96
|
-
matches = true;
|
|
97
|
-
fireSystemChange();
|
|
98
|
-
|
|
99
|
-
expect(theme.resolved()).toBe('light');
|
|
100
|
-
expect(document.documentElement.dataset['theme']).toBe('light');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('queda disponible por DI como servicio raíz', () => {
|
|
104
|
-
expect(TestBed.inject(ThemeService)).toBeTruthy();
|
|
105
|
-
});
|
|
106
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { Service, signal } from '@angular/core';
|
|
2
|
-
|
|
3
|
-
/** Los tres estados del selector (§8.6). */
|
|
4
|
-
export type ThemeMode = 'system' | 'light' | 'dark';
|
|
5
|
-
|
|
6
|
-
/** Lo que acaba pintado en `<html data-theme>`. */
|
|
7
|
-
export type ResolvedTheme = 'light' | 'dark';
|
|
8
|
-
|
|
9
|
-
const STORAGE_KEY = 'admin-theme';
|
|
10
|
-
const QUERY = '(prefers-color-scheme: dark)';
|
|
11
|
-
const MODES: string[] = ['system', 'light', 'dark'];
|
|
12
|
-
|
|
13
|
-
/** Lee la elección guardada; cualquier basura vuelve a `system`. */
|
|
14
|
-
function storedMode(): ThemeMode {
|
|
15
|
-
try {
|
|
16
|
-
const raw = localStorage.getItem(STORAGE_KEY);
|
|
17
|
-
return raw !== null && MODES.includes(raw) ? (raw as ThemeMode) : 'system';
|
|
18
|
-
} catch {
|
|
19
|
-
// localStorage puede tirar en modo privado o con cuota agotada.
|
|
20
|
-
return 'system';
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* `system` → lo que diga el sistema operativo.
|
|
26
|
-
* jsdom no implementa `matchMedia`: sin él se asume claro, que es además
|
|
27
|
-
* el estado por defecto antes de que corra el script de `index.html`.
|
|
28
|
-
*/
|
|
29
|
-
function prefersDark(): boolean {
|
|
30
|
-
return typeof window.matchMedia === 'function' && window.matchMedia(QUERY).matches;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function resolve(mode: ThemeMode): ResolvedTheme {
|
|
34
|
-
return mode === 'system' ? (prefersDark() ? 'dark' : 'light') : mode;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Selector de tema con tres estados: `system`, `light`, `dark` (Fase 8.6).
|
|
39
|
-
*
|
|
40
|
-
* `system` NO se traduce en CSS con una media query sino que se resuelve a
|
|
41
|
-
* un valor concreto, porque con tres estados un único `prefers-color-scheme`
|
|
42
|
-
* no alcanza para distinguir "oscuro elegido a mano" de "oscuro heredado".
|
|
43
|
-
*
|
|
44
|
-
* El arranque sin flash vive en `index.html`: un script síncrono en el
|
|
45
|
-
* `<head>` escribe `data-theme` y `data-theme-mode` antes del primer
|
|
46
|
-
* pintado. Este servicio toma el relevo después y además escucha cambios
|
|
47
|
-
* en vivo del sistema, para que cambiar el tema del SO con la app abierta
|
|
48
|
-
* la recoloree — pero sólo mientras la elección siga siendo `system`: si el
|
|
49
|
-
* usuario eligió un modo concreto, el SO deja de mandar.
|
|
50
|
-
*/
|
|
51
|
-
@Service()
|
|
52
|
-
export class ThemeService {
|
|
53
|
-
/** Elección persistida. */
|
|
54
|
-
readonly mode = signal<ThemeMode>(storedMode());
|
|
55
|
-
/** Lo que está pintado ahora mismo. */
|
|
56
|
-
readonly resolved = signal<ResolvedTheme>(resolve(this.mode()));
|
|
57
|
-
|
|
58
|
-
constructor() {
|
|
59
|
-
this.apply();
|
|
60
|
-
|
|
61
|
-
const media = typeof window.matchMedia === 'function' ? window.matchMedia(QUERY) : null;
|
|
62
|
-
media?.addEventListener('change', () => {
|
|
63
|
-
if (this.mode() === 'system') {
|
|
64
|
-
this.apply();
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
set(mode: ThemeMode): void {
|
|
70
|
-
this.mode.set(mode);
|
|
71
|
-
try {
|
|
72
|
-
localStorage.setItem(STORAGE_KEY, mode);
|
|
73
|
-
} catch {
|
|
74
|
-
// Sin persistencia el tema sigue funcionando durante la sesión.
|
|
75
|
-
}
|
|
76
|
-
this.apply();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** Escribe el resultado en `<html>`; es lo que lee `theme.css`. */
|
|
80
|
-
private apply(): void {
|
|
81
|
-
const resolved = resolve(this.mode());
|
|
82
|
-
this.resolved.set(resolved);
|
|
83
|
-
document.documentElement.dataset['theme'] = resolved;
|
|
84
|
-
document.documentElement.dataset['themeMode'] = this.mode();
|
|
85
|
-
}
|
|
86
|
-
}
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { HttpClient, HttpParams, provideHttpClient } from '@angular/common/http';
|
|
2
|
-
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
|
3
|
-
import { TestBed } from '@angular/core/testing';
|
|
4
|
-
import { firstValueFrom } from 'rxjs';
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
buildUrl,
|
|
8
|
-
doRestRequest,
|
|
9
|
-
sendGas,
|
|
10
|
-
sendRest,
|
|
11
|
-
toHttpParams,
|
|
12
|
-
type TransportDeps,
|
|
13
|
-
} from './transport';
|
|
14
|
-
|
|
15
|
-
// Transporte desacoplado (plan doc/engine-refactor-plan.md · Fase 4).
|
|
16
|
-
// Funciones puras con dependencias por parámetro; sin DI, sin ciclo (R6).
|
|
17
|
-
describe('transport — funciones puras (Fase 4)', () => {
|
|
18
|
-
let http: HttpClient;
|
|
19
|
-
let httpMock: HttpTestingController;
|
|
20
|
-
|
|
21
|
-
const deps = (overrides: Partial<TransportDeps> = {}): TransportDeps => ({
|
|
22
|
-
http,
|
|
23
|
-
apiUrl: '/api',
|
|
24
|
-
sessionToken: () => 'tok',
|
|
25
|
-
...overrides,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
TestBed.configureTestingModule({
|
|
30
|
-
providers: [provideHttpClient(), provideHttpClientTesting()],
|
|
31
|
-
});
|
|
32
|
-
http = TestBed.inject(HttpClient);
|
|
33
|
-
httpMock = TestBed.inject(HttpTestingController);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
httpMock.verify();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
// ── buildUrl ────────────────────────────────────────────────────────────
|
|
41
|
-
describe('buildUrl', () => {
|
|
42
|
-
it('concatena apiUrl + path con slash inicial', () => {
|
|
43
|
-
expect(buildUrl('/api', '/admin/items')).toBe('/api/admin/items');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('tolera path sin slash inicial', () => {
|
|
47
|
-
expect(buildUrl('/api', 'admin/items')).toBe('/api/admin/items');
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('recorta slashes finales de la base', () => {
|
|
51
|
-
expect(buildUrl('https://x.example/', '/admin')).toBe('https://x.example/admin');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('apiUrl vacía ⇒ path con slash inicial', () => {
|
|
55
|
-
expect(buildUrl('', '/admin')).toBe('/admin');
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// ── toHttpParams ────────────────────────────────────────────────────────
|
|
60
|
-
describe('toHttpParams', () => {
|
|
61
|
-
it('plana objetos anidados un nivel (contrato histórico)', () => {
|
|
62
|
-
const params = toHttpParams({ view: 'calendar', params: { orden: 'es' } });
|
|
63
|
-
expect(params.get('view')).toBe('calendar');
|
|
64
|
-
expect(params.get('orden')).toBe('es');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('omite null y undefined', () => {
|
|
68
|
-
const params = toHttpParams({ a: null, b: undefined, c: 'x' });
|
|
69
|
-
expect(params.has('a')).toBe(false);
|
|
70
|
-
expect(params.has('b')).toBe(false);
|
|
71
|
-
expect(params.get('c')).toBe('x');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('arrays viajan como repetición de la clave', () => {
|
|
75
|
-
const params = toHttpParams({ tag: ['a', 'b'] });
|
|
76
|
-
expect(params.getAll('tag')).toEqual(['a', 'b']);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('payload vacío ⇒ HttpParams vacío', () => {
|
|
80
|
-
expect(toHttpParams(undefined) instanceof HttpParams).toBe(true);
|
|
81
|
-
expect(toHttpParams({}).keys().length).toBe(0);
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
// ── sendGas ─────────────────────────────────────────────────────────────
|
|
86
|
-
describe('sendGas', () => {
|
|
87
|
-
it('POST text/plain a apiUrl con envelope { token, method, path, payload }', async () => {
|
|
88
|
-
const pending = firstValueFrom(sendGas(deps(), '/admin/items', 'GET', { view: 'v' }));
|
|
89
|
-
const req = httpMock.expectOne('/api');
|
|
90
|
-
expect(req.request.method).toBe('POST');
|
|
91
|
-
expect(req.request.headers.get('Content-Type')).toBe('text/plain');
|
|
92
|
-
expect(req.request.body).toEqual({
|
|
93
|
-
token: 'tok',
|
|
94
|
-
method: 'GET',
|
|
95
|
-
path: '/admin/items',
|
|
96
|
-
payload: { view: 'v' },
|
|
97
|
-
});
|
|
98
|
-
req.flush({ items: [] });
|
|
99
|
-
await pending;
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('payload ausente ⇒ {} en el envelope', async () => {
|
|
103
|
-
const pending = firstValueFrom(sendGas(deps(), '/admin/x', 'POST'));
|
|
104
|
-
const req = httpMock.expectOne('/api');
|
|
105
|
-
expect((req.request.body as { payload: unknown }).payload).toEqual({});
|
|
106
|
-
req.flush({});
|
|
107
|
-
await pending;
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('lanza si se pide multipart en el driver gas', () => {
|
|
111
|
-
expect(() => sendGas(deps(), '/admin/x', 'POST', undefined, true)).toThrow(
|
|
112
|
-
/only supported with the REST driver/i,
|
|
113
|
-
);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('lee el token de sesión en cada petición (getter), no al construir', async () => {
|
|
117
|
-
let token = 't1';
|
|
118
|
-
const pending = firstValueFrom(sendGas(deps({ sessionToken: () => token }), '/x', 'GET'));
|
|
119
|
-
const req = httpMock.expectOne('/api');
|
|
120
|
-
expect((req.request.body as { token: string }).token).toBe('t1');
|
|
121
|
-
token = 't2'; // cambio de sesión antes del flush no afecta lo ya enviado
|
|
122
|
-
req.flush({});
|
|
123
|
-
await pending;
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// ── sendRest ────────────────────────────────────────────────────────────
|
|
128
|
-
describe('sendRest', () => {
|
|
129
|
-
it('GET con Authorization bearer del getter de sesión y params en query', async () => {
|
|
130
|
-
const pending = firstValueFrom(sendRest(deps(), '/admin/items', 'GET', { page: 2 }));
|
|
131
|
-
const req = httpMock.expectOne('/api/admin/items?page=2');
|
|
132
|
-
expect(req.request.method).toBe('GET');
|
|
133
|
-
expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
|
|
134
|
-
req.flush([]);
|
|
135
|
-
await pending;
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('POST JSON: Content-Type application/json y body = payload', async () => {
|
|
139
|
-
const pending = firstValueFrom(sendRest(deps(), '/admin/items', 'POST', { slug: 'a' }));
|
|
140
|
-
const req = httpMock.expectOne('/api/admin/items');
|
|
141
|
-
expect(req.request.headers.get('Content-Type')).toBe('application/json');
|
|
142
|
-
expect(req.request.body).toEqual({ slug: 'a' });
|
|
143
|
-
req.flush({});
|
|
144
|
-
await pending;
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('multipart: FormData sin Content-Type application/json (boundary del navegador)', async () => {
|
|
148
|
-
const fd = new FormData();
|
|
149
|
-
fd.append('file', new Blob(['x']), 'x.txt');
|
|
150
|
-
const pending = firstValueFrom(sendRest(deps(), '/admin/upload', 'POST', fd, true));
|
|
151
|
-
const req = httpMock.expectOne('/api/admin/upload');
|
|
152
|
-
expect(req.request.body instanceof FormData).toBe(true);
|
|
153
|
-
expect(req.request.headers.get('Content-Type')).not.toBe('application/json');
|
|
154
|
-
req.flush({});
|
|
155
|
-
await pending;
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('sin getIdToken usa el token de sesión almacenado', async () => {
|
|
159
|
-
const pending = firstValueFrom(sendRest(deps(), '/admin/x', 'GET'));
|
|
160
|
-
const req = httpMock.expectOne('/api/admin/x');
|
|
161
|
-
expect(req.request.headers.get('Authorization')).toBe('Bearer tok');
|
|
162
|
-
req.flush({});
|
|
163
|
-
await pending;
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('con getIdToken fresco se usa el token devuelto por el adaptador', async () => {
|
|
167
|
-
const d = deps({ getIdToken: () => Promise.resolve('fresh-123') });
|
|
168
|
-
const pending = firstValueFrom(sendRest(d, '/admin/x', 'GET'));
|
|
169
|
-
// La promesa resuelve en un microtask: esperar antes de buscar la petición.
|
|
170
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
171
|
-
const req = httpMock.expectOne('/api/admin/x');
|
|
172
|
-
expect(req.request.headers.get('Authorization')).toBe('Bearer fresh-123');
|
|
173
|
-
req.flush({});
|
|
174
|
-
await pending;
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('un rechazo de getIdToken viaja por el canal de errores (sin petición)', async () => {
|
|
178
|
-
const d = deps({ getIdToken: () => Promise.reject(new Error('token expirado')) });
|
|
179
|
-
const pending = firstValueFrom(sendRest(d, '/admin/x', 'GET'));
|
|
180
|
-
await expect(pending).rejects.toThrow('token expirado');
|
|
181
|
-
httpMock.expectNone('/api/admin/x');
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// ── doRestRequest ───────────────────────────────────────────────────────
|
|
186
|
-
describe('doRestRequest', () => {
|
|
187
|
-
it('DELETE va con params, no con body', async () => {
|
|
188
|
-
const pending = firstValueFrom(
|
|
189
|
-
doRestRequest(deps(), '/api/admin/x/1', 'DELETE', { reason: 'r' }, undefined),
|
|
190
|
-
);
|
|
191
|
-
const req = httpMock.expectOne('/api/admin/x/1?reason=r');
|
|
192
|
-
expect(req.request.method).toBe('DELETE');
|
|
193
|
-
expect(req.request.body).toBeNull();
|
|
194
|
-
req.flush(null);
|
|
195
|
-
await pending;
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
});
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Transporte HTTP puro del motor (plan doc/engine-refactor-plan.md · Fase 4).
|
|
3
|
-
*
|
|
4
|
-
* Funciones sin estado ni DI: todas las dependencias (HttpClient, apiUrl,
|
|
5
|
-
* getter de token de sesión, adaptador opcional de Firebase) entran por
|
|
6
|
-
* parámetro en `TransportDeps`. `ApiService` queda como fachada que arma los
|
|
7
|
-
* deps y delega acá — mismo comportamiento observable, cero ciclos de DI (R6).
|
|
8
|
-
*
|
|
9
|
-
* Los contratos de transporte (R7) viven en este módulo y no cambian:
|
|
10
|
-
*
|
|
11
|
-
* - **gas**: `POST text/plain` a `apiUrl` con envelope
|
|
12
|
-
* `{ token, method, path, payload }` (contrato Google Apps Script).
|
|
13
|
-
* - **rest**: verbos reales con `Authorization: Bearer`, params en query
|
|
14
|
-
* (GET/DELETE) o body JSON (POST/PUT/PATCH), multipart sin Content-Type.
|
|
15
|
-
*/
|
|
16
|
-
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
|
|
17
|
-
import { from, Observable, switchMap } from 'rxjs';
|
|
18
|
-
|
|
19
|
-
/** Métodos que entiende el envelope del backend (baseapi §2.3). */
|
|
20
|
-
export type EnvelopeMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
21
|
-
|
|
22
|
-
/** Dependencias del transporte, aportadas por la fachada (no hay singleton acá). */
|
|
23
|
-
export interface TransportDeps {
|
|
24
|
-
http: HttpClient;
|
|
25
|
-
/** URL base (`API_URL`): el Web App de Apps Script o el origen REST. */
|
|
26
|
-
apiUrl: string;
|
|
27
|
-
/** Getter del token de SESIÓN, leído en CADA petición (R2.8). */
|
|
28
|
-
sessionToken: () => string | null;
|
|
29
|
-
/**
|
|
30
|
-
* Refresco opcional de ID token (Fase Firebase): si existe, se llama antes
|
|
31
|
-
* de armar la cabecera Authorization. Un rechazo viaja por el canal de
|
|
32
|
-
* errores — nunca se manda una petición con token inválido.
|
|
33
|
-
*/
|
|
34
|
-
getIdToken?: () => Promise<string>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** `POST text/plain` a `apiUrl` con el envelope GAS. */
|
|
38
|
-
export function sendGas<T>(
|
|
39
|
-
deps: TransportDeps,
|
|
40
|
-
path: string,
|
|
41
|
-
method: EnvelopeMethod,
|
|
42
|
-
payload?: Record<string, unknown> | FormData,
|
|
43
|
-
multipart = false,
|
|
44
|
-
): Observable<T> {
|
|
45
|
-
if (multipart) {
|
|
46
|
-
throw new Error('Multipart payload mode is only supported with the REST driver');
|
|
47
|
-
}
|
|
48
|
-
// Token de la SESIÓN, leído en cada petición (R2.8): no hay credencial
|
|
49
|
-
// fija que inyectar en el arranque.
|
|
50
|
-
const body = {
|
|
51
|
-
token: deps.sessionToken(),
|
|
52
|
-
method,
|
|
53
|
-
path,
|
|
54
|
-
payload: payload ?? {},
|
|
55
|
-
};
|
|
56
|
-
return deps.http.post<T>(deps.apiUrl, body, {
|
|
57
|
-
headers: { 'Content-Type': 'text/plain' },
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Verbo HTTP real con `Authorization: Bearer`. Si el adaptador provee
|
|
63
|
-
* `getIdToken()`, se resuelve el token fresco ANTES de la cabecera; si no,
|
|
64
|
-
* se usa el de sesión almacenado (fallback). Cualquier error de `getIdToken()`
|
|
65
|
-
* se propaga por el canal de errores.
|
|
66
|
-
*/
|
|
67
|
-
export function sendRest<T>(
|
|
68
|
-
deps: TransportDeps,
|
|
69
|
-
path: string,
|
|
70
|
-
method: EnvelopeMethod,
|
|
71
|
-
payload?: Record<string, unknown> | FormData,
|
|
72
|
-
multipart = false,
|
|
73
|
-
): Observable<T> {
|
|
74
|
-
const url = buildUrl(deps.apiUrl, path);
|
|
75
|
-
const headers = new HttpHeaders();
|
|
76
|
-
|
|
77
|
-
if (deps.getIdToken) {
|
|
78
|
-
return from(deps.getIdToken()).pipe(
|
|
79
|
-
switchMap((freshToken) =>
|
|
80
|
-
doRestRequest<T>(
|
|
81
|
-
deps,
|
|
82
|
-
url,
|
|
83
|
-
method,
|
|
84
|
-
payload,
|
|
85
|
-
headers.set('Authorization', `Bearer ${freshToken}`),
|
|
86
|
-
multipart,
|
|
87
|
-
),
|
|
88
|
-
),
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const token = deps.sessionToken();
|
|
93
|
-
const finalHeaders = token ? headers.set('Authorization', `Bearer ${token}`) : headers;
|
|
94
|
-
return doRestRequest<T>(deps, url, method, payload, finalHeaders, multipart);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Despacho REST concreto sobre una URL ya construida. */
|
|
98
|
-
export function doRestRequest<T>(
|
|
99
|
-
deps: TransportDeps,
|
|
100
|
-
url: string,
|
|
101
|
-
method: EnvelopeMethod,
|
|
102
|
-
payload: Record<string, unknown> | FormData | undefined,
|
|
103
|
-
headers: HttpHeaders = new HttpHeaders(),
|
|
104
|
-
multipart = false,
|
|
105
|
-
): Observable<T> {
|
|
106
|
-
if (method === 'GET' || method === 'DELETE') {
|
|
107
|
-
const params = toHttpParams(
|
|
108
|
-
payload instanceof FormData ? undefined : (payload as Record<string, unknown> | undefined),
|
|
109
|
-
);
|
|
110
|
-
return deps.http.request<T>(method, url, { headers, params });
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (multipart || payload instanceof FormData) {
|
|
114
|
-
// Sin Content-Type: application/json para que el navegador ponga el boundary.
|
|
115
|
-
return deps.http.request<T>(method, url, { headers, body: payload ?? {} });
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return deps.http.request<T>(method, url, {
|
|
119
|
-
headers: headers.set('Content-Type', 'application/json'),
|
|
120
|
-
body: payload ?? {},
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/** `${apiUrl}${path}` normalizada: sin dobles slashes ni path sin slash inicial. */
|
|
125
|
-
export function buildUrl(apiUrl: string, path: string): string {
|
|
126
|
-
const base = apiUrl ? apiUrl.replace(/\/+$/, '') : '';
|
|
127
|
-
const cleanPath = path.startsWith('/') ? path : `/${path}`;
|
|
128
|
-
return `${base}${cleanPath}`;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Convierte un payload en HttpParams para peticiones GET y DELETE en modo REST.
|
|
133
|
-
* Si un valor es un objeto plano (ej: `{ view: 'calendar', params: { orden: 'es' } }`),
|
|
134
|
-
* lo aplana un nivel para que sus claves viajen como query params reales
|
|
135
|
-
* en lugar de serializarse como `params=[object Object]`.
|
|
136
|
-
*/
|
|
137
|
-
export function toHttpParams(payload?: Record<string, unknown>): HttpParams {
|
|
138
|
-
let params = new HttpParams();
|
|
139
|
-
if (!payload) {
|
|
140
|
-
return params;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
for (const [key, value] of Object.entries(payload)) {
|
|
144
|
-
if (value === undefined || value === null) {
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
148
|
-
for (const [nestedKey, nestedValue] of Object.entries(value as Record<string, unknown>)) {
|
|
149
|
-
if (nestedValue !== undefined && nestedValue !== null) {
|
|
150
|
-
params = params.set(nestedKey, String(nestedValue));
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
} else if (Array.isArray(value)) {
|
|
154
|
-
for (const item of value) {
|
|
155
|
-
if (item !== undefined && item !== null) {
|
|
156
|
-
params = params.append(key, String(item));
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
} else {
|
|
160
|
-
params = params.set(key, String(value));
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
return params;
|
|
164
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { HttpClient, HttpEvent, HttpEventType, HttpResponse } from '@angular/common/http';
|
|
2
|
-
import { inject, Service } from '@angular/core';
|
|
3
|
-
import { map, Observable, switchMap } from 'rxjs';
|
|
4
|
-
|
|
5
|
-
import { ApiService } from './api.service';
|
|
6
|
-
|
|
7
|
-
/** Firma que emite `/admin/upload-signature` (baseapi §11.3). NUNCA `api_secret`. */
|
|
8
|
-
interface UploadSignature {
|
|
9
|
-
cloud_name: string;
|
|
10
|
-
api_key: string;
|
|
11
|
-
timestamp: number;
|
|
12
|
-
signature: string;
|
|
13
|
-
folder: string;
|
|
14
|
-
allowed_formats: string;
|
|
15
|
-
upload_url: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Respuesta de Cloudinary en un upload normal (sólo interesa `secure_url`). */
|
|
19
|
-
interface CloudinaryResponse {
|
|
20
|
-
secure_url?: unknown;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Subida de imágenes en DOS PASOS (baseapi §11.2, opción B):
|
|
25
|
-
* 1. Firma firmada por el backend → `POST /admin/upload-signature`
|
|
26
|
-
* con `{ resource }` (mismo envelope text/plain que todo `/admin/*`).
|
|
27
|
-
* 2. El navegador sube directo a Cloudinary (FormData a `upload_url`) con
|
|
28
|
-
* los parámetros que fijó y firmó el backend; el API secret jamás sale
|
|
29
|
-
* de acá (§11.1–§11.3).
|
|
30
|
-
*
|
|
31
|
-
* Se guarda SOLO `secure_url` (base.md §7: la URL pública es el valor del
|
|
32
|
-
* campo; la imagen anterior no se borra al reemplazar — §11.6).
|
|
33
|
-
*
|
|
34
|
-
* Los errores (firma caducada, rate-limit 429, red) llegan por el canal de
|
|
35
|
-
* error; el componente conserva la imagen anterior (§11.2 paso 6).
|
|
36
|
-
*/
|
|
37
|
-
@Service()
|
|
38
|
-
export class UploadService {
|
|
39
|
-
private readonly http = inject(HttpClient);
|
|
40
|
-
private readonly api = inject(ApiService);
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Misma subida con eventos de progreso (base.md §7, estado «Subiendo»):
|
|
44
|
-
* emite `UploadProgress` durante la subida a Cloudinary y termina con
|
|
45
|
-
* `Response` llevando la URL en el body. El campo sólo cambia cuando
|
|
46
|
-
* llega la respuesta.
|
|
47
|
-
*/
|
|
48
|
-
uploadWithProgress(file: File, resource: string, field?: string): Observable<HttpEvent<string>> {
|
|
49
|
-
// `field` es opcional (R9.5.3): si el campo declara `formats`, esos son
|
|
50
|
-
// los formatos que el backend firma. Sin campo firma la lista global.
|
|
51
|
-
const payload: { resource: string; field?: string } = field
|
|
52
|
-
? { resource, field }
|
|
53
|
-
: { resource };
|
|
54
|
-
return this.api.request<UploadSignature>('POST', '/admin/upload-signature', payload).pipe(
|
|
55
|
-
switchMap((signature) => {
|
|
56
|
-
const form = new FormData();
|
|
57
|
-
form.append('file', file, file.name);
|
|
58
|
-
form.append('api_key', signature.api_key);
|
|
59
|
-
form.append('timestamp', String(signature.timestamp));
|
|
60
|
-
form.append('signature', signature.signature);
|
|
61
|
-
form.append('folder', signature.folder);
|
|
62
|
-
form.append('allowed_formats', signature.allowed_formats);
|
|
63
|
-
return this.http.post<CloudinaryResponse>(signature.upload_url, form, {
|
|
64
|
-
reportProgress: true,
|
|
65
|
-
observe: 'events',
|
|
66
|
-
});
|
|
67
|
-
}),
|
|
68
|
-
map((event) => {
|
|
69
|
-
if (event.type !== HttpEventType.Response) {
|
|
70
|
-
return event;
|
|
71
|
-
}
|
|
72
|
-
const url = typeof event.body?.secure_url === 'string' ? event.body.secure_url : '';
|
|
73
|
-
return new HttpResponse({
|
|
74
|
-
body: url,
|
|
75
|
-
status: event.status,
|
|
76
|
-
statusText: event.statusText,
|
|
77
|
-
});
|
|
78
|
-
}),
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|