@tinya-code/gas-admin-ui 0.2.0-alpha.12 → 0.2.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +222 -0
- package/ng-package.json +7 -0
- package/package.json +3 -15
- package/src/lib/actions/action-registry.spec.ts +52 -0
- package/src/lib/actions/action-registry.ts +126 -0
- package/src/lib/actions/action-service.spec.ts +215 -0
- package/src/lib/actions/action-service.ts +87 -0
- package/src/lib/actions/descriptors.spec.ts +155 -0
- package/src/lib/actions/descriptors.ts +154 -0
- package/src/lib/columns/column-cell.ts +33 -0
- package/src/lib/columns/column-defaults.spec.ts +99 -0
- package/src/lib/columns/column-defaults.ts +84 -0
- package/src/lib/columns/column-format.spec.ts +274 -0
- package/src/lib/columns/descriptors.ts +113 -0
- package/src/lib/columns/format-badge.ts +51 -0
- package/src/lib/columns/format-boolean.ts +58 -0
- package/src/lib/columns/format-currency.ts +30 -0
- package/src/lib/columns/format-date.ts +35 -0
- package/src/lib/columns/format-number.ts +32 -0
- package/src/lib/columns/format-text.ts +23 -0
- package/src/lib/columns/format-thumbnail.ts +63 -0
- package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
- package/src/lib/core/guards/auth.guard.spec.ts +88 -0
- package/src/lib/core/guards/auth.guard.ts +48 -0
- package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
- package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
- package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
- package/src/lib/core/models/api.model.ts +104 -0
- package/src/lib/core/models/schema-descriptions.ts +118 -0
- package/src/lib/core/models/schema.model.ts +1097 -0
- package/src/lib/core/registry/composition-validator.spec.ts +358 -0
- package/src/lib/core/registry/composition-validator.ts +270 -0
- package/src/lib/core/registry/field-validators.spec.ts +199 -0
- package/src/lib/core/registry/field-validators.ts +440 -0
- package/src/lib/core/registry/render-host.spec.ts +208 -0
- package/src/lib/core/registry/render-host.ts +646 -0
- package/src/lib/core/registry/render-registry.spec.ts +96 -0
- package/src/lib/core/registry/render-registry.ts +312 -0
- package/src/lib/core/registry/schema-catalog.ts +156 -0
- package/src/lib/core/services/api.service.spec.ts +1142 -0
- package/src/lib/core/services/api.service.ts +371 -0
- package/src/lib/core/services/auth.service.spec.ts +653 -0
- package/src/lib/core/services/auth.service.ts +503 -0
- package/src/lib/core/services/config.service.spec.ts +121 -0
- package/src/lib/core/services/config.service.ts +119 -0
- package/src/lib/core/services/data-source.service.spec.ts +179 -0
- package/src/lib/core/services/data-source.service.ts +142 -0
- package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
- package/src/lib/core/services/error-mapper.service.ts +49 -0
- package/src/lib/core/services/notification.service.ts +73 -0
- package/src/lib/core/services/relation-drawer.service.ts +41 -0
- package/src/lib/core/services/theme.service.spec.ts +106 -0
- package/src/lib/core/services/theme.service.ts +86 -0
- package/src/lib/core/services/transport.spec.ts +198 -0
- package/src/lib/core/services/transport.ts +164 -0
- package/src/lib/core/services/upload.service.ts +81 -0
- package/src/lib/core/testing/categories.schema.ts +125 -0
- package/src/lib/core/testing/orders.schema.ts +372 -0
- package/src/lib/core/testing/products.schema.ts +258 -0
- package/src/lib/core/testing/site.schema.ts +195 -0
- package/src/lib/core/testing/test-catalog.ts +21 -0
- package/src/lib/core/tokens.ts +112 -0
- package/src/lib/core/utils/clipboard.spec.ts +55 -0
- package/src/lib/core/utils/clipboard.ts +90 -0
- package/src/lib/core/utils/context-resolver.spec.ts +95 -0
- package/src/lib/core/utils/context-resolver.ts +101 -0
- package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
- package/src/lib/core/utils/response-unwrapper.ts +103 -0
- package/src/lib/core/utils/source-options.spec.ts +96 -0
- package/src/lib/core/utils/source-options.ts +54 -0
- package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
- package/src/lib/core/utils/template-interpolator.ts +56 -0
- package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
- package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
- package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
- package/src/lib/fields/boolean/boolean.ts +40 -0
- package/src/lib/fields/color/color.descriptor.ts +12 -0
- package/src/lib/fields/color/color.ts +34 -0
- package/src/lib/fields/currency/currency.descriptor.ts +12 -0
- package/src/lib/fields/currency/currency.ts +67 -0
- package/src/lib/fields/date/date.descriptor.ts +12 -0
- package/src/lib/fields/date/date.ts +36 -0
- package/src/lib/fields/descriptors.ts +122 -0
- package/src/lib/fields/display/display-badge.ts +65 -0
- package/src/lib/fields/display/display-date.ts +39 -0
- package/src/lib/fields/display/display-defaults.ts +84 -0
- package/src/lib/fields/display/display-file.ts +41 -0
- package/src/lib/fields/display/display-image.ts +54 -0
- package/src/lib/fields/display/display-list.ts +107 -0
- package/src/lib/fields/display/display-number.ts +41 -0
- package/src/lib/fields/display/display-text.ts +42 -0
- package/src/lib/fields/display/display.spec.ts +242 -0
- package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
- package/src/lib/fields/dropdown/dropdown.ts +45 -0
- package/src/lib/fields/email/email.descriptor.ts +12 -0
- package/src/lib/fields/email/email.ts +28 -0
- package/src/lib/fields/field-aria.ts +60 -0
- package/src/lib/fields/field-host/field-host.spec.ts +118 -0
- package/src/lib/fields/field-host/field-host.ts +47 -0
- package/src/lib/fields/field-node.ts +88 -0
- package/src/lib/fields/file/file.descriptor.ts +14 -0
- package/src/lib/fields/file/file.spec.ts +134 -0
- package/src/lib/fields/file/file.ts +281 -0
- package/src/lib/fields/group/group.descriptor.ts +21 -0
- package/src/lib/fields/group/group.ts +110 -0
- package/src/lib/fields/image/image.descriptor.ts +12 -0
- package/src/lib/fields/image/image.spec.ts +117 -0
- package/src/lib/fields/image/image.ts +281 -0
- package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
- package/src/lib/fields/key-value/key-value.ts +283 -0
- package/src/lib/fields/list/list.descriptor.ts +21 -0
- package/src/lib/fields/list/list.ts +376 -0
- package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
- package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
- package/src/lib/fields/multiselect/multiselect.ts +152 -0
- package/src/lib/fields/number/number.descriptor.ts +12 -0
- package/src/lib/fields/number/number.ts +40 -0
- package/src/lib/fields/phone/phone.descriptor.ts +12 -0
- package/src/lib/fields/phone/phone.ts +46 -0
- package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
- package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
- package/src/lib/fields/relation/relation.descriptor.ts +12 -0
- package/src/lib/fields/relation/relation.spec.ts +193 -0
- package/src/lib/fields/relation/relation.ts +224 -0
- package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
- package/src/lib/fields/rich-select/rich-select.ts +175 -0
- package/src/lib/fields/select/radio-mode.ts +25 -0
- package/src/lib/fields/select/select.descriptor.ts +12 -0
- package/src/lib/fields/select/select.spec.ts +162 -0
- package/src/lib/fields/select/select.ts +210 -0
- package/src/lib/fields/slug/slug.descriptor.ts +19 -0
- package/src/lib/fields/slug/slug.ts +67 -0
- package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
- package/src/lib/fields/string-list/string-list.ts +222 -0
- package/src/lib/fields/text/text.descriptor.ts +12 -0
- package/src/lib/fields/text/text.ts +35 -0
- package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
- package/src/lib/fields/textarea/textarea.spec.ts +117 -0
- package/src/lib/fields/textarea/textarea.ts +126 -0
- package/src/lib/fields/time/time.descriptor.ts +12 -0
- package/src/lib/fields/time/time.ts +28 -0
- package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
- package/src/lib/fields/url/url.descriptor.ts +12 -0
- package/src/lib/fields/url/url.ts +53 -0
- package/src/lib/gas-admin-ui.spec.ts +21 -0
- package/src/lib/gas-admin-ui.ts +9 -0
- package/src/lib/layouts/descriptors.ts +9 -0
- package/src/lib/pages/dashboard/dashboard.ts +167 -0
- package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
- package/src/lib/pages/detail-view/detail-view.ts +570 -0
- package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
- package/src/lib/pages/form-view/form-model.ts +88 -0
- package/src/lib/pages/form-view/form-schema.ts +255 -0
- package/src/lib/pages/form-view/form-view.ts +1622 -0
- package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
- package/src/lib/pages/list-view/list-view.ts +882 -0
- package/src/lib/pages/login/login.spec.ts +109 -0
- package/src/lib/pages/login/login.ts +227 -0
- package/src/lib/pages/resource-page/resource-page.ts +234 -0
- package/src/lib/shared/components/badge/badge.ts +35 -0
- package/src/lib/shared/components/button/button.ts +65 -0
- package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
- package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
- package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
- package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
- package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
- package/src/lib/shared/components/drawer/drawer.ts +172 -0
- package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
- package/src/lib/shared/components/modal/modal.spec.ts +149 -0
- package/src/lib/shared/components/modal/modal.ts +145 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
- package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
- package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
- package/src/lib/shared/components/toast/toast.ts +58 -0
- package/src/lib/shared/utils/col-span.ts +25 -0
- package/src/lib/shared/utils/condition-evaluator.ts +53 -0
- package/src/lib/shared/utils/depends-on.ts +48 -0
- package/src/lib/shared/utils/dialog-focus.ts +28 -0
- package/src/lib/shared/utils/field-path.ts +49 -0
- package/src/lib/shared/utils/gap-sorting.ts +144 -0
- package/src/lib/shared/utils/seed.spec.ts +64 -0
- package/src/lib/shared/utils/seed.ts +149 -0
- package/src/lib/shared/utils/slugify.ts +13 -0
- package/src/lib/shared/utils/validators.ts +41 -0
- package/src/lib/shell/layout/layout.ts +63 -0
- package/src/lib/shell/shell.routes.spec.ts +59 -0
- package/src/lib/shell/shell.routes.ts +122 -0
- package/src/lib/shell/sidebar/sidebar.ts +139 -0
- package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
- package/src/lib/views/calendar-view/calendar-model.ts +426 -0
- package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
- package/src/lib/views/calendar-view/calendar-view.ts +568 -0
- package/src/lib/views/composer/composer-host.spec.ts +301 -0
- package/src/lib/views/composer/composer-host.ts +248 -0
- package/src/lib/views/composer/composer-imports.spec.ts +54 -0
- package/src/lib/views/composer/view-context.spec.ts +117 -0
- package/src/lib/views/composer/view-context.ts +109 -0
- package/src/lib/views/composer/view-slot.spec.ts +130 -0
- package/src/lib/views/composer/view-slot.ts +178 -0
- package/src/lib/views/descriptors.ts +46 -0
- package/src/lib/views/kanban-view/kanban-model.ts +173 -0
- package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
- package/src/lib/views/kanban-view/kanban-view.ts +327 -0
- package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
- package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
- package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
- package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
- package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
- package/src/lib/widgets/chart-line/chart-line.ts +219 -0
- package/src/lib/widgets/descriptors.ts +29 -0
- package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
- package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
- package/src/lib/widgets/metric-card/metric-card.ts +48 -0
- package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
- package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
- package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
- package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
- package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
- package/src/lib/widgets/record-list/record-list.ts +115 -0
- package/src/lib/widgets/series.spec.ts +42 -0
- package/src/lib/widgets/series.ts +56 -0
- package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
- package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
- package/src/lib/widgets/status-progress/status-progress.ts +104 -0
- package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
- package/src/lib/widgets/widget-host/widget-host.ts +99 -0
- package/src/public-api.ts +104 -0
- package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
- package/tsconfig.lib.json +13 -0
- package/tsconfig.lib.prod.json +11 -0
- package/tsconfig.spec.json +10 -0
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
- package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
- package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
- package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
- package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
- package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
- package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
- package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
- package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
- package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
- package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
- package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
- package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
- package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
- package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
- package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
- package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
- package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
- package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
- package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
- package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
- package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
- package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
- package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
- package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
- package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
- package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
- package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
- package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
- package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
- package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
- package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
- package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
- package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
- package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
- package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
- package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
- package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
- package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
- package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
- package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
- package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
- package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
- package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
- package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
- package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
- package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
- package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
- package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
- package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
- package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
- package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
- package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
- package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
- package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
- package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
- package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
- package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
- package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
- package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
- package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
3
|
+
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
|
4
|
+
import { provideRouter, Router } from '@angular/router';
|
|
5
|
+
import { of } from 'rxjs';
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
|
|
8
|
+
import type { AuthProvider } from '../../core/models/schema.model';
|
|
9
|
+
import { setSchemaCatalog } from '../../core/registry/schema-catalog';
|
|
10
|
+
import { AuthService } from '../../core/services/auth.service';
|
|
11
|
+
import { API_URL } from '../../core/tokens';
|
|
12
|
+
import { Login } from './login';
|
|
13
|
+
|
|
14
|
+
describe('Login component', () => {
|
|
15
|
+
let fixture: ComponentFixture<Login>;
|
|
16
|
+
let component: Login;
|
|
17
|
+
let auth: AuthService;
|
|
18
|
+
let router: Router;
|
|
19
|
+
|
|
20
|
+
function setup(providers: AuthProvider[], hasFirebase = false) {
|
|
21
|
+
TestBed.resetTestingModule();
|
|
22
|
+
setSchemaCatalog([], {
|
|
23
|
+
providers,
|
|
24
|
+
loginFields: [
|
|
25
|
+
{ key: 'email', type: 'email', required: true },
|
|
26
|
+
{ key: 'password', type: 'password', required: true },
|
|
27
|
+
],
|
|
28
|
+
redirectAfterLogin: '/dashboard',
|
|
29
|
+
rememberMe: false,
|
|
30
|
+
...(hasFirebase ? { firebase: { exchangeEndpoint: '/auth/firebase' } } : {}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
TestBed.configureTestingModule({
|
|
34
|
+
imports: [Login],
|
|
35
|
+
providers: [
|
|
36
|
+
provideHttpClient(),
|
|
37
|
+
provideHttpClientTesting(),
|
|
38
|
+
provideRouter([]),
|
|
39
|
+
{ provide: API_URL, useValue: '/api' },
|
|
40
|
+
],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
auth = TestBed.inject(AuthService);
|
|
44
|
+
router = TestBed.inject(Router);
|
|
45
|
+
vi.spyOn(router, 'navigateByUrl').mockResolvedValue(true);
|
|
46
|
+
fixture = TestBed.createComponent(Login);
|
|
47
|
+
component = fixture.componentInstance;
|
|
48
|
+
fixture.detectChanges();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
TestBed.resetTestingModule();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('providers = [password], sin firebase: no muestra botón de Google y submit() llama a auth.login()', () => {
|
|
56
|
+
setup(['password'], false);
|
|
57
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
58
|
+
expect(compiled.querySelector('#btn-login-google')).toBeNull();
|
|
59
|
+
|
|
60
|
+
const loginSpy = vi.spyOn(auth, 'login').mockReturnValue(
|
|
61
|
+
of({ id: 'u_1', email: 'test@example.com', role: 'admin' }),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
component.setValue('email', 'test@example.com');
|
|
65
|
+
component.setValue('password', 'secret');
|
|
66
|
+
|
|
67
|
+
const form = compiled.querySelector('form')!;
|
|
68
|
+
form.dispatchEvent(new Event('submit'));
|
|
69
|
+
|
|
70
|
+
expect(loginSpy).toHaveBeenCalledWith('test@example.com', 'secret');
|
|
71
|
+
expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('providers = [google, password]: muestra botón de Google en el DOM', () => {
|
|
75
|
+
setup(['google', 'password'], false);
|
|
76
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
77
|
+
expect(compiled.querySelector('#btn-login-google')).not.toBeNull();
|
|
78
|
+
expect(compiled.querySelector('#btn-login-google')?.textContent).toContain('Continuar con Google');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('loginWithGoogle() llama a auth.loginWithGoogle() y navega al tener éxito', () => {
|
|
82
|
+
setup(['google', 'password'], false);
|
|
83
|
+
const googleSpy = vi.spyOn(auth, 'loginWithGoogle').mockReturnValue(
|
|
84
|
+
of({ id: 'u_google', email: 'google@test.com', role: 'admin' }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
component.loginWithGoogle();
|
|
88
|
+
|
|
89
|
+
expect(googleSpy).toHaveBeenCalled();
|
|
90
|
+
expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('providers = [password] con firebase: submit() delega a loginWithFirebasePassword()', () => {
|
|
94
|
+
setup(['password'], true);
|
|
95
|
+
const fbPwSpy = vi.spyOn(auth, 'loginWithFirebasePassword').mockReturnValue(
|
|
96
|
+
of({ id: 'u_pw', email: 'pw@test.com', role: 'admin' }),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
component.setValue('email', 'pw@test.com');
|
|
100
|
+
component.setValue('password', 'secret');
|
|
101
|
+
|
|
102
|
+
const compiled = fixture.nativeElement as HTMLElement;
|
|
103
|
+
const form = compiled.querySelector('form')!;
|
|
104
|
+
form.dispatchEvent(new Event('submit'));
|
|
105
|
+
|
|
106
|
+
expect(fbPwSpy).toHaveBeenCalledWith('pw@test.com', 'secret');
|
|
107
|
+
expect(router.navigateByUrl).toHaveBeenCalledWith('/dashboard');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { Component, inject, signal } from '@angular/core';
|
|
2
|
+
import { Router } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
import { ApiError } from '../../core/models/api.model';
|
|
5
|
+
import { AuthService, LOGIN_PATH } from '../../core/services/auth.service';
|
|
6
|
+
import { Button } from '../../shared/components/button/button';
|
|
7
|
+
import { getAuth } from '../../core/registry/schema-catalog';
|
|
8
|
+
|
|
9
|
+
/** Copy de las etiquetas — vive acá, no en el schema: una sola pantalla. */
|
|
10
|
+
const LABELS: Record<string, string> = {
|
|
11
|
+
email: 'Correo electrónico',
|
|
12
|
+
password: 'Contraseña',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const FIELD_BASE =
|
|
16
|
+
'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';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Login (auth.md §5.1).
|
|
20
|
+
*
|
|
21
|
+
* **Pintada a partir de `authSchema.loginFields`**: si mañana cambian los
|
|
22
|
+
* campos, éste componente no se toca — sólo se mueven claves y etiquetas.
|
|
23
|
+
* No usa `FieldHost` porque el tipo `password` todavía no existe en
|
|
24
|
+
* `FieldSchema` (es pieza nueva de Fase 3, R3.3); el `@switch` de abajo es
|
|
25
|
+
* exactamente el reemplazo que R3.x hará por el registro de render.
|
|
26
|
+
*
|
|
27
|
+
* **No hay casilla «recordarme»**: D10-3 negó `localStorage` (§9.3) y el
|
|
28
|
+
* token muere con la pestaña. Poner un checkbox sería prometer que la
|
|
29
|
+
* sesión sobrevive a cerrar el navegador.
|
|
30
|
+
*/
|
|
31
|
+
@Component({
|
|
32
|
+
selector: 'app-login',
|
|
33
|
+
imports: [Button],
|
|
34
|
+
template: `
|
|
35
|
+
<main class="flex min-h-[100svh] items-center justify-center bg-surface px-4">
|
|
36
|
+
<div
|
|
37
|
+
class="w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm"
|
|
38
|
+
>
|
|
39
|
+
<h1 class="font-display text-lg font-semibold">Admin</h1>
|
|
40
|
+
<p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
|
|
41
|
+
|
|
42
|
+
@if (providers.includes('google')) {
|
|
43
|
+
<button
|
|
44
|
+
type="button"
|
|
45
|
+
id="btn-login-google"
|
|
46
|
+
app-button
|
|
47
|
+
variant="outline"
|
|
48
|
+
[loading]="providerLoading() === 'google'"
|
|
49
|
+
(click)="loginWithGoogle()"
|
|
50
|
+
class="mt-6 w-full"
|
|
51
|
+
>
|
|
52
|
+
Continuar con Google
|
|
53
|
+
</button>
|
|
54
|
+
<div class="my-4 flex items-center gap-3">
|
|
55
|
+
<hr class="flex-1 border-neutral/20" />
|
|
56
|
+
<span class="text-xs text-neutral/50">o</span>
|
|
57
|
+
<hr class="flex-1 border-neutral/20" />
|
|
58
|
+
</div>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
<form
|
|
62
|
+
[class]="providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'"
|
|
63
|
+
(submit)="submit($event)"
|
|
64
|
+
novalidate
|
|
65
|
+
>
|
|
66
|
+
@for (field of schema.loginFields; track field.key) {
|
|
67
|
+
<div>
|
|
68
|
+
<label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
|
|
69
|
+
{{ label(field.key) }}
|
|
70
|
+
</label>
|
|
71
|
+
|
|
72
|
+
@switch (field.type) {
|
|
73
|
+
@case ('email') {
|
|
74
|
+
<input
|
|
75
|
+
[attr.id]="field.key"
|
|
76
|
+
[attr.name]="field.key"
|
|
77
|
+
type="email"
|
|
78
|
+
autocomplete="email"
|
|
79
|
+
inputmode="email"
|
|
80
|
+
[attr.required]="field.required ? '' : null"
|
|
81
|
+
[value]="value(field.key)"
|
|
82
|
+
(input)="setValue(field.key, $any($event.target).value)"
|
|
83
|
+
class="${FIELD_BASE}"
|
|
84
|
+
/>
|
|
85
|
+
}
|
|
86
|
+
@default {
|
|
87
|
+
<input
|
|
88
|
+
[attr.id]="field.key"
|
|
89
|
+
[attr.name]="field.key"
|
|
90
|
+
type="password"
|
|
91
|
+
autocomplete="current-password"
|
|
92
|
+
[attr.required]="field.required ? '' : null"
|
|
93
|
+
[value]="value(field.key)"
|
|
94
|
+
(input)="setValue(field.key, $any($event.target).value)"
|
|
95
|
+
class="${FIELD_BASE}"
|
|
96
|
+
/>
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
</div>
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@if (error(); as message) {
|
|
103
|
+
<p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
|
|
104
|
+
{{ message }}
|
|
105
|
+
</p>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
<button app-button type="submit" [loading]="loading()" class="w-full">
|
|
109
|
+
Iniciar sesión
|
|
110
|
+
</button>
|
|
111
|
+
</form>
|
|
112
|
+
</div>
|
|
113
|
+
</main>
|
|
114
|
+
`,
|
|
115
|
+
})
|
|
116
|
+
export class Login {
|
|
117
|
+
private readonly auth = inject(AuthService);
|
|
118
|
+
private readonly router = inject(Router);
|
|
119
|
+
|
|
120
|
+
// Se resuelve en runtime: el catálogo lo fija `provideGasAdmin` en el bootstrap.
|
|
121
|
+
readonly schema = getAuth()!;
|
|
122
|
+
readonly providers = this.schema.providers;
|
|
123
|
+
readonly error = signal<string | null>(null);
|
|
124
|
+
readonly loading = signal(false);
|
|
125
|
+
readonly providerLoading = signal<string | null>(null);
|
|
126
|
+
|
|
127
|
+
/** Valores por clave: el set lo pinta el `@for` del schema. */
|
|
128
|
+
private readonly values = signal<Record<string, string>>({});
|
|
129
|
+
|
|
130
|
+
label(key: string): string {
|
|
131
|
+
return LABELS[key] ?? key;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
value(key: string): string {
|
|
135
|
+
return this.values()[key] ?? '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setValue(key: string, next: string): void {
|
|
139
|
+
this.values.update((current) => ({ ...current, [key]: next }));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
loginWithGoogle(): void {
|
|
143
|
+
if (this.providerLoading()) return;
|
|
144
|
+
this.providerLoading.set('google');
|
|
145
|
+
this.error.set(null);
|
|
146
|
+
this.auth.loginWithGoogle().subscribe({
|
|
147
|
+
next: () => {
|
|
148
|
+
this.providerLoading.set(null);
|
|
149
|
+
this.goAfterLogin();
|
|
150
|
+
},
|
|
151
|
+
error: (err: unknown) => {
|
|
152
|
+
this.providerLoading.set(null);
|
|
153
|
+
this.error.set(
|
|
154
|
+
err instanceof ApiError && err.message !== ''
|
|
155
|
+
? err.message
|
|
156
|
+
: err instanceof Error && err.message !== ''
|
|
157
|
+
? err.message
|
|
158
|
+
: 'No se pudo iniciar sesión con Google.',
|
|
159
|
+
);
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
submit(event: Event): void {
|
|
165
|
+
event.preventDefault();
|
|
166
|
+
if (this.loading()) return;
|
|
167
|
+
|
|
168
|
+
const email = this.value('email').trim();
|
|
169
|
+
const password = this.value('password');
|
|
170
|
+
if (email === '' || password === '') {
|
|
171
|
+
this.error.set('Completa el correo y la contraseña.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (this.schema.firebase && this.schema.providers.includes('password')) {
|
|
176
|
+
this.loading.set(true);
|
|
177
|
+
this.error.set(null);
|
|
178
|
+
this.auth.loginWithFirebasePassword(email, password).subscribe({
|
|
179
|
+
next: () => {
|
|
180
|
+
this.loading.set(false);
|
|
181
|
+
this.goAfterLogin();
|
|
182
|
+
},
|
|
183
|
+
error: (err: unknown) => {
|
|
184
|
+
this.loading.set(false);
|
|
185
|
+
this.error.set(
|
|
186
|
+
err instanceof ApiError && err.message !== ''
|
|
187
|
+
? err.message
|
|
188
|
+
: err instanceof Error && err.message !== ''
|
|
189
|
+
? err.message
|
|
190
|
+
: 'No se pudo iniciar sesión.',
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.loading.set(true);
|
|
198
|
+
this.error.set(null);
|
|
199
|
+
this.auth.login(email, password).subscribe({
|
|
200
|
+
next: () => {
|
|
201
|
+
this.loading.set(false);
|
|
202
|
+
this.goAfterLogin();
|
|
203
|
+
},
|
|
204
|
+
error: (err: unknown) => {
|
|
205
|
+
this.loading.set(false);
|
|
206
|
+
// El mensaje del backend ya viene en español y ya describe el
|
|
207
|
+
// caso («credenciales inválidas»): reformatearlo acá sólo
|
|
208
|
+
// duplicaría la verdad y se perdería si cambia el texto del back.
|
|
209
|
+
this.error.set(
|
|
210
|
+
err instanceof ApiError && err.message !== ''
|
|
211
|
+
? err.message
|
|
212
|
+
: 'No se pudo iniciar sesión.',
|
|
213
|
+
);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private goAfterLogin(): void {
|
|
219
|
+
const previous = this.auth.consumeReturnUrl();
|
|
220
|
+
const target =
|
|
221
|
+
previous !== null && previous !== '' && !previous.startsWith(LOGIN_PATH)
|
|
222
|
+
? previous
|
|
223
|
+
: this.schema.redirectAfterLogin;
|
|
224
|
+
void this.router.navigateByUrl(target).catch(() => undefined);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { Component, computed, inject, signal } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
4
|
+
import { distinctUntilChanged, map, of } from 'rxjs';
|
|
5
|
+
|
|
6
|
+
import type { ResourceSchema, SelectionBinding } from '../../core/models/schema.model';
|
|
7
|
+
import { AuthService } from '../../core/services/auth.service';
|
|
8
|
+
import type { SelectionEmitter } from '../../views/composer/view-context';
|
|
9
|
+
import { getSchema } from '../../core/registry/schema-catalog';
|
|
10
|
+
import { EmptyState } from '../../shared/components/empty-state/empty-state';
|
|
11
|
+
import { Dashboard } from '../dashboard/dashboard';
|
|
12
|
+
import { DetailView } from '../detail-view/detail-view';
|
|
13
|
+
import { FormView } from '../form-view/form-view';
|
|
14
|
+
import { ListView } from '../list-view/list-view';
|
|
15
|
+
|
|
16
|
+
type View = 'list' | 'detail';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Página del recurso: resuelve el `id` de la ruta contra el registry y
|
|
20
|
+
* despacha según `kind` — colección → list view, singleton → form view,
|
|
21
|
+
* dashboard → panel de sólo lectura — aplicando `permissions` (estado
|
|
22
|
+
* «sin permisos»).
|
|
23
|
+
*
|
|
24
|
+
* Cuando `schema.detail.enabled === true`, intercepta el click de fila del
|
|
25
|
+
* `ListView` y muestra el `DetailView` en lugar de navegar al formulario.
|
|
26
|
+
* «Editar» desde el `DetailView` sí navega al formulario de edición.
|
|
27
|
+
*/
|
|
28
|
+
@Component({
|
|
29
|
+
selector: 'app-resource-page',
|
|
30
|
+
imports: [Dashboard, DetailView, EmptyState, FormView, ListView],
|
|
31
|
+
template: `
|
|
32
|
+
@if (schema(); as schema) {
|
|
33
|
+
@if (hasNoAccess(schema)) {
|
|
34
|
+
<app-empty-state
|
|
35
|
+
title="Sin permisos"
|
|
36
|
+
message="No tiene permisos para gestionar este recurso."
|
|
37
|
+
/>
|
|
38
|
+
} @else {
|
|
39
|
+
@switch (schema.kind) {
|
|
40
|
+
@case ('collection') {
|
|
41
|
+
@switch (currentView()) {
|
|
42
|
+
@case ('detail') {
|
|
43
|
+
@if (selectedRecord(); as rec) {
|
|
44
|
+
<app-detail-view
|
|
45
|
+
[schema]="schema"
|
|
46
|
+
[record]="rec"
|
|
47
|
+
(back)="switchToList()"
|
|
48
|
+
(edit)="goEdit($event)"
|
|
49
|
+
(delete)="switchToList()"
|
|
50
|
+
/>
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
@default {
|
|
54
|
+
<header class="mb-6">
|
|
55
|
+
<h1 class="font-display text-2xl font-semibold">
|
|
56
|
+
{{ schema.labelPlural }}
|
|
57
|
+
</h1>
|
|
58
|
+
<p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
|
|
59
|
+
</header>
|
|
60
|
+
<app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
@case ('dashboard') {
|
|
65
|
+
<app-dashboard [schema]="schema" />
|
|
66
|
+
}
|
|
67
|
+
@default {
|
|
68
|
+
<!-- singleton: formulario directo -->
|
|
69
|
+
<app-form-view />
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} @else {
|
|
74
|
+
<app-empty-state
|
|
75
|
+
title="Recurso no encontrado"
|
|
76
|
+
message="No existe un schema con ese id en el registry."
|
|
77
|
+
/>
|
|
78
|
+
}
|
|
79
|
+
`,
|
|
80
|
+
})
|
|
81
|
+
export class ResourcePage {
|
|
82
|
+
private readonly route = inject(ActivatedRoute);
|
|
83
|
+
private readonly router = inject(Router);
|
|
84
|
+
private readonly auth = inject(AuthService);
|
|
85
|
+
// paramMap emite síncrono (ConvertToParamMap) → requireSync evita `| undefined`.
|
|
86
|
+
private readonly params = toSignal(this.route.paramMap, { requireSync: true });
|
|
87
|
+
private readonly routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
|
|
88
|
+
|
|
89
|
+
readonly schema = computed(() => {
|
|
90
|
+
const resourceId = this.routeData()['resourceId'] ?? this.params().get('id');
|
|
91
|
+
return getSchema(typeof resourceId === 'string' ? resourceId : '');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
/** Vista activa: 'list' (por defecto) o 'detail' (al hacer clic en una fila con detail.enabled). */
|
|
95
|
+
readonly currentView = signal<View>('list');
|
|
96
|
+
/** Registro seleccionado para la vista de detalle. */
|
|
97
|
+
readonly selectedRecord = signal<Record<string, unknown> | null>(null);
|
|
98
|
+
|
|
99
|
+
constructor() {
|
|
100
|
+
// La ficha es ENLAZABLE: `/:id/:key` (p. ej. el «Ver Ficha» del
|
|
101
|
+
// dashboard) entra directo al detalle. Al navegar — desde una fila o con
|
|
102
|
+
// el botón atrás — la URL es la única fuente de verdad de la vista.
|
|
103
|
+
this.route.paramMap
|
|
104
|
+
.pipe(
|
|
105
|
+
map((params) => params.get('key')),
|
|
106
|
+
distinctUntilChanged(),
|
|
107
|
+
takeUntilDestroyed(),
|
|
108
|
+
)
|
|
109
|
+
.subscribe((key) => this.openFromRoute(key));
|
|
110
|
+
this.route.data
|
|
111
|
+
?.pipe(takeUntilDestroyed())
|
|
112
|
+
.subscribe(() => this.openFromRoute(this.params().get('key')));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private openFromRoute(key: string | null): void {
|
|
116
|
+
const schema = this.schema();
|
|
117
|
+
if (!schema || schema.kind !== 'collection') return;
|
|
118
|
+
|
|
119
|
+
const requestedView = this.routeData()['viewType'];
|
|
120
|
+
if ((requestedView === 'detail' || key) && key && schema.detail?.enabled) {
|
|
121
|
+
// Si el registro ya está cargado (clic en una fila) se conserva tal
|
|
122
|
+
// cual; si llegamos por URL sólo hay clave, y DetailView trae el resto.
|
|
123
|
+
const current = this.selectedRecord();
|
|
124
|
+
if (!current || String(current[schema.keyField]) !== key) {
|
|
125
|
+
this.selectedRecord.set({ [schema.keyField]: key });
|
|
126
|
+
}
|
|
127
|
+
this.currentView.set('detail');
|
|
128
|
+
} else if (this.currentView() === 'detail') {
|
|
129
|
+
this.selectedRecord.set(null);
|
|
130
|
+
this.currentView.set('list');
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clic de fila. R9.9.4 · decision table: si el schema declara `select`,
|
|
136
|
+
* el contrato declarado REEMPLAZA el comportamiento local (`route` ⇒
|
|
137
|
+
* emisión al dispatcher, `inline` ⇒ selección local sin URL, `none` ⇒
|
|
138
|
+
* no-op); sin declaración, la navegación local actual sigue intacta
|
|
139
|
+
* (backward compat — R9.9 «existing routes untouched»).
|
|
140
|
+
*/
|
|
141
|
+
switchToDetail(record: Record<string, unknown>): void {
|
|
142
|
+
const schema = this.schema();
|
|
143
|
+
const data = this.routeData();
|
|
144
|
+
const binding = data['select'] as SelectionBinding | undefined;
|
|
145
|
+
|
|
146
|
+
if (binding !== undefined) {
|
|
147
|
+
const key =
|
|
148
|
+
schema && record[schema.keyField] !== undefined && record[schema.keyField] !== null
|
|
149
|
+
? String(record[schema.keyField])
|
|
150
|
+
: '';
|
|
151
|
+
if (binding.mode === 'route') {
|
|
152
|
+
const emit = data['emit'] as SelectionEmitter | undefined;
|
|
153
|
+
emit?.({
|
|
154
|
+
resource: schema?.id ?? String(data['resourceId'] ?? ''),
|
|
155
|
+
key,
|
|
156
|
+
action: binding.on,
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (binding.mode === 'none') return;
|
|
161
|
+
// inline ⇒ selección local (ficha embebida), sin tocar la URL
|
|
162
|
+
this.selectedRecord.set(record);
|
|
163
|
+
this.currentView.set('detail');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
this.selectedRecord.set(record);
|
|
168
|
+
this.currentView.set('detail');
|
|
169
|
+
const k = schema ? record[schema.keyField] : undefined;
|
|
170
|
+
if (schema && k !== undefined && k !== null && String(k) !== '') {
|
|
171
|
+
void this.router.navigate(['/', schema.id, String(k)]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
switchToList(): void {
|
|
176
|
+
this.selectedRecord.set(null);
|
|
177
|
+
this.currentView.set('list');
|
|
178
|
+
const schema = this.schema();
|
|
179
|
+
if (schema && this.params().get('key')) {
|
|
180
|
+
void this.router.navigate(['/', schema.id]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
goEdit(record: Record<string, unknown>): void {
|
|
185
|
+
const schema = this.schema();
|
|
186
|
+
if (!schema) return;
|
|
187
|
+
const key = record[schema.keyField] as string | undefined;
|
|
188
|
+
if (key) {
|
|
189
|
+
void this.router.navigate(['/', schema.id, key, 'edit']);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Colección sin ninguna operación posible u singleton sin `update`: no hay
|
|
195
|
+
* nada que mostrar con sentido (base.md §10 «Sin permisos»).
|
|
196
|
+
*
|
|
197
|
+
* R7.5: usa policies cuando existan, fallback a permissions.
|
|
198
|
+
*
|
|
199
|
+
* Un dashboard nunca entra aquí: es un panel de sólo lectura, `ResourcePermissions`
|
|
200
|
+
* no tiene `read` y el acceso real a los datos lo decide el endpoint
|
|
201
|
+
* (A4: el cliente es interfaz, no autoridad).
|
|
202
|
+
*/
|
|
203
|
+
hasNoAccess(schema: ResourceSchema): boolean {
|
|
204
|
+
// R7.5: intentar policies primero
|
|
205
|
+
if (schema.policies?.access) {
|
|
206
|
+
// Con policies, ocultar si el usuario no puede hacer NINGUNA operación
|
|
207
|
+
// relevante para el tipo de recurso
|
|
208
|
+
if (schema.kind === 'singleton') {
|
|
209
|
+
return !this.auth.canDoOperation(schema, 'update');
|
|
210
|
+
}
|
|
211
|
+
// collection: ocultar si no puede hacer list, create, update ni remove
|
|
212
|
+
return (
|
|
213
|
+
!this.auth.canDoOperation(schema, 'list') &&
|
|
214
|
+
!this.auth.canDoOperation(schema, 'create') &&
|
|
215
|
+
!this.auth.canDoOperation(schema, 'update') &&
|
|
216
|
+
!this.auth.canDoOperation(schema, 'remove')
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Fallback a permissions (legacy)
|
|
221
|
+
const permissions = schema.permissions;
|
|
222
|
+
if (!permissions || schema.kind === 'dashboard') {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
if (schema.kind === 'singleton') {
|
|
226
|
+
const hasReadEndpoint = !!schema.endpoint?.get;
|
|
227
|
+
return !hasReadEndpoint && permissions.update === false;
|
|
228
|
+
}
|
|
229
|
+
const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
|
|
230
|
+
const canWrite =
|
|
231
|
+
permissions.create !== false || permissions.update !== false || permissions.remove !== false;
|
|
232
|
+
return !hasReadEndpoint && !canWrite;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type BadgeKind = 'success' | 'danger' | 'warning' | 'info' | 'neutral';
|
|
4
|
+
|
|
5
|
+
const KINDS: Record<BadgeKind, string> = {
|
|
6
|
+
success: 'bg-success text-surface',
|
|
7
|
+
danger: 'bg-danger text-surface',
|
|
8
|
+
warning: 'bg-accent/20 text-accent-text font-semibold',
|
|
9
|
+
info: 'bg-primary text-surface',
|
|
10
|
+
neutral: 'bg-neutral text-surface',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Etiqueta compacta (booleanos, estados).
|
|
15
|
+
*
|
|
16
|
+
* El texto va con `text-surface` y no `text-white` porque el fondo lo da
|
|
17
|
+
* el token y en oscuro se aclara: blanco sobre `--color-danger` oscuro
|
|
18
|
+
* sería 2,8:1. `--color-surface` vale #fff en claro, así que el render
|
|
19
|
+
* actual no cambia, y en oscuro el texto se vuelve oscuro sobre el tono
|
|
20
|
+
* aclarado (6,4–9,2:1).
|
|
21
|
+
*/
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'app-badge',
|
|
24
|
+
host: { '[class]': 'classes()' },
|
|
25
|
+
template: `{{ label() }}`,
|
|
26
|
+
})
|
|
27
|
+
export class Badge {
|
|
28
|
+
readonly label = input.required<string>();
|
|
29
|
+
readonly kind = input<BadgeKind>('neutral');
|
|
30
|
+
|
|
31
|
+
readonly classes = computed(
|
|
32
|
+
() =>
|
|
33
|
+
`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${KINDS[this.kind()]}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type ButtonVariant = 'primary' | 'outline' | 'danger' | 'ghost';
|
|
4
|
+
export type ButtonSize = 'md' | 'sm' | 'icon';
|
|
5
|
+
|
|
6
|
+
const BASE =
|
|
7
|
+
'inline-flex cursor-pointer items-center justify-center gap-2 rounded-lg font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary disabled:cursor-not-allowed disabled:opacity-50';
|
|
8
|
+
|
|
9
|
+
const VARIANTS: Record<ButtonVariant, string> = {
|
|
10
|
+
primary: 'bg-primary text-surface hover:bg-primary/90',
|
|
11
|
+
outline: 'border border-neutral/40 bg-surface text-neutral hover:bg-neutral/10',
|
|
12
|
+
danger: 'bg-danger text-surface hover:bg-danger/90',
|
|
13
|
+
ghost: 'text-neutral hover:bg-neutral/10',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Tamaño: el padding y el tip-size viven AQUÍ, no en `BASE`. Dos botones
|
|
18
|
+
* con la misma propiedad (px-4 vs px-2) los resolvería el orden del CSS
|
|
19
|
+
* generado, no el código — el motivo por el que los chips del textarea
|
|
20
|
+
* acabaron con `!important`.
|
|
21
|
+
*
|
|
22
|
+
* Mobile-first: todas arrancan en `min-h-11` (44 px, objetivo táctil de la
|
|
23
|
+
* guía mobile §8.2) y en `lg` relajan al tamaño de escritorio, así el mismo
|
|
24
|
+
* componente cumple la guía táctil sin engordar la UI de escritorio.
|
|
25
|
+
* - `md` → botón de texto (Guardar, Cancelar…)
|
|
26
|
+
* - `sm` → chips y acciones compactas (texto `xs`)
|
|
27
|
+
* - `icon` → sólo glifo, cuadrado (toolbars de filas, arrastre)
|
|
28
|
+
*/
|
|
29
|
+
const SIZES: Record<ButtonSize, string> = {
|
|
30
|
+
md: 'px-4 py-2 text-sm min-h-11 lg:min-h-0',
|
|
31
|
+
sm: 'px-2 py-1 text-xs min-h-11 lg:min-h-0',
|
|
32
|
+
icon: 'p-1 min-h-11 min-w-11 lg:min-h-0 lg:min-w-0',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Botón nativo (selector por atributo, patrón Material `button[mat-button]`):
|
|
37
|
+
* conserva semántica y estilos de `<button>` y las clases del consumidor.
|
|
38
|
+
*/
|
|
39
|
+
@Component({
|
|
40
|
+
selector: 'button[app-button]',
|
|
41
|
+
host: {
|
|
42
|
+
'[class]': 'classes()',
|
|
43
|
+
'[type]': 'type()',
|
|
44
|
+
'[disabled]': 'disabled() || loading()',
|
|
45
|
+
'[attr.aria-busy]': 'loading() || null',
|
|
46
|
+
},
|
|
47
|
+
template: `
|
|
48
|
+
@if (loading()) {
|
|
49
|
+
<span
|
|
50
|
+
class="size-4 animate-spin rounded-full border-2 border-current border-t-transparent"
|
|
51
|
+
aria-hidden="true"
|
|
52
|
+
></span>
|
|
53
|
+
}
|
|
54
|
+
<ng-content />
|
|
55
|
+
`,
|
|
56
|
+
})
|
|
57
|
+
export class Button {
|
|
58
|
+
readonly variant = input<ButtonVariant>('primary');
|
|
59
|
+
readonly size = input<ButtonSize>('md');
|
|
60
|
+
readonly type = input<'button' | 'submit'>('button');
|
|
61
|
+
readonly disabled = input(false);
|
|
62
|
+
readonly loading = input(false);
|
|
63
|
+
|
|
64
|
+
readonly classes = computed(() => `${BASE} ${VARIANTS[this.variant()]} ${SIZES[this.size()]}`);
|
|
65
|
+
}
|