@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,149 @@
|
|
|
1
|
+
import { Component, signal } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
|
|
4
|
+
import { FOCUSABLE, isVisibleNow } from '../../utils/dialog-focus';
|
|
5
|
+
import { Modal } from './modal';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fase 9.3 — contrato de foco y cierre del diálogo.
|
|
9
|
+
*
|
|
10
|
+
* Mismo escenario que `drawer.spec.ts`: el disparador real está fuera del
|
|
11
|
+
* diálogo, así que se puede comprobar que el foco vuelve a él — también
|
|
12
|
+
* cuando el consumidor destruye el componente con el diálogo todavía abierto
|
|
13
|
+
* (se monta bajo un `@if`).
|
|
14
|
+
*/
|
|
15
|
+
@Component({
|
|
16
|
+
template: `
|
|
17
|
+
<button type="button" data-testid="trigger">Abrir</button>
|
|
18
|
+
<app-modal [open]="open()" [title]="title()" (close)="open.set(false)">
|
|
19
|
+
<button type="button" data-testid="body-action">Dentro del diálogo</button>
|
|
20
|
+
<button type="button" data-testid="secondary-action">Cancelar</button>
|
|
21
|
+
</app-modal>
|
|
22
|
+
`,
|
|
23
|
+
imports: [Modal],
|
|
24
|
+
})
|
|
25
|
+
class ModalHost {
|
|
26
|
+
readonly open = signal(false);
|
|
27
|
+
readonly title = signal('Confirmar borrado');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('Modal — 9.3: foco, Escape y restauración', () => {
|
|
31
|
+
let fixture: ComponentFixture<ModalHost>;
|
|
32
|
+
|
|
33
|
+
beforeEach(async () => {
|
|
34
|
+
await TestBed.configureTestingModule({ imports: [ModalHost] }).compileComponents();
|
|
35
|
+
fixture = TestBed.createComponent(ModalHost);
|
|
36
|
+
fixture.detectChanges();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const dialog = (): HTMLElement | null => fixture.nativeElement.querySelector('[role="dialog"]');
|
|
40
|
+
const trigger = (): HTMLButtonElement =>
|
|
41
|
+
fixture.nativeElement.querySelector('[data-testid="trigger"]');
|
|
42
|
+
|
|
43
|
+
const open = async (): Promise<void> => {
|
|
44
|
+
trigger().focus();
|
|
45
|
+
fixture.componentInstance.open.set(true);
|
|
46
|
+
fixture.detectChanges();
|
|
47
|
+
await fixture.whenStable();
|
|
48
|
+
fixture.detectChanges();
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
it('cerrado no hay diálogo; abierto hay uno con nombre y aria-modal', async () => {
|
|
52
|
+
expect(dialog()).toBeNull();
|
|
53
|
+
|
|
54
|
+
await open();
|
|
55
|
+
|
|
56
|
+
const panel = dialog();
|
|
57
|
+
expect(panel).not.toBeNull();
|
|
58
|
+
expect(panel!.getAttribute('aria-modal')).toBe('true');
|
|
59
|
+
const labelledBy = panel!.getAttribute('aria-labelledby');
|
|
60
|
+
expect(document.getElementById(labelledBy!)?.textContent).toContain('Confirmar borrado');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('Escape y el botón ✕ piden cerrar', async () => {
|
|
64
|
+
await open();
|
|
65
|
+
|
|
66
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
67
|
+
fixture.detectChanges();
|
|
68
|
+
expect(fixture.componentInstance.open()).toBe(false);
|
|
69
|
+
|
|
70
|
+
await open();
|
|
71
|
+
(fixture.nativeElement.querySelector('[aria-label="Cerrar"]') as HTMLElement).click();
|
|
72
|
+
fixture.detectChanges();
|
|
73
|
+
expect(fixture.componentInstance.open()).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('el clic en el fondo pide cerrar', async () => {
|
|
77
|
+
await open();
|
|
78
|
+
|
|
79
|
+
(fixture.nativeElement.querySelector('.absolute.inset-0') as HTMLElement).click();
|
|
80
|
+
fixture.detectChanges();
|
|
81
|
+
expect(fixture.componentInstance.open()).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('el foco entra al abrir y vuelve al disparador al cerrar', async () => {
|
|
85
|
+
await open();
|
|
86
|
+
expect(dialog()!.contains(document.activeElement)).toBe(true);
|
|
87
|
+
|
|
88
|
+
fixture.componentInstance.open.set(false);
|
|
89
|
+
fixture.detectChanges();
|
|
90
|
+
await fixture.whenStable();
|
|
91
|
+
|
|
92
|
+
expect(document.activeElement).toBe(trigger());
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('destruirse con el diálogo abierto devuelve el foco (no se pierde detrás del fondo)', async () => {
|
|
96
|
+
await open();
|
|
97
|
+
expect(document.activeElement).toBe(dialog());
|
|
98
|
+
|
|
99
|
+
// Es lo que pasa cuando el consumidor lo monta bajo un `@if` y borra el
|
|
100
|
+
// nodo sin que la señal pase por `false`: el effect no llega a correr.
|
|
101
|
+
fixture.componentInstance.open.set(false);
|
|
102
|
+
fixture.destroy();
|
|
103
|
+
|
|
104
|
+
expect(document.activeElement).toBe(trigger());
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('el Tab queda atrapado: del último salta al primero', async () => {
|
|
108
|
+
await open();
|
|
109
|
+
|
|
110
|
+
const focusable = Array.from(
|
|
111
|
+
dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
|
|
112
|
+
);
|
|
113
|
+
expect(focusable.length).toBeGreaterThan(1);
|
|
114
|
+
|
|
115
|
+
const last = focusable[focusable.length - 1];
|
|
116
|
+
last.focus();
|
|
117
|
+
expect(document.activeElement).toBe(last);
|
|
118
|
+
|
|
119
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', bubbles: true }));
|
|
120
|
+
expect(document.activeElement).toBe(focusable[0]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('el Shift+Tab queda atrapado: del primero salta al último', async () => {
|
|
124
|
+
await open();
|
|
125
|
+
|
|
126
|
+
const focusable = Array.from(
|
|
127
|
+
dialog()!.querySelectorAll<HTMLElement>('button, [href], input, select, textarea'),
|
|
128
|
+
);
|
|
129
|
+
expect(focusable.length).toBeGreaterThan(1);
|
|
130
|
+
|
|
131
|
+
// Diagnóstico: la lista que ve el trap debe ser la misma que la del test
|
|
132
|
+
// (si divergen, el selector de `FOCUSABLE` y el del test dejan de
|
|
133
|
+
// describir el mismo conjunto de elementos).
|
|
134
|
+
expect(
|
|
135
|
+
Array.from(dialog()!.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(isVisibleNow),
|
|
136
|
+
'el selector del trap difiere del del test',
|
|
137
|
+
).toEqual(focusable);
|
|
138
|
+
|
|
139
|
+
const first = focusable[0];
|
|
140
|
+
first.focus();
|
|
141
|
+
expect(document.activeElement).toBe(first);
|
|
142
|
+
|
|
143
|
+
document.dispatchEvent(
|
|
144
|
+
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, shiftKey: true }),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(document.activeElement).toBe(focusable[focusable.length - 1]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Component,
|
|
3
|
+
ElementRef,
|
|
4
|
+
OnDestroy,
|
|
5
|
+
afterRenderEffect,
|
|
6
|
+
input,
|
|
7
|
+
output,
|
|
8
|
+
viewChild,
|
|
9
|
+
} from '@angular/core';
|
|
10
|
+
import { LucideX } from '@lucide/angular';
|
|
11
|
+
|
|
12
|
+
import { FOCUSABLE, isVisibleNow } from '../../utils/dialog-focus';
|
|
13
|
+
|
|
14
|
+
let titleCounter = 0;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Diálogo modal accesible: `aria-modal` con nombre por `aria-labelledby`,
|
|
18
|
+
* foco al abrirse y restaurado al cerrarse — también si el consumidor lo
|
|
19
|
+
* destruye con el diálogo todavía abierto — Escape y botón ✕ cierran.
|
|
20
|
+
*
|
|
21
|
+
* ATRAPA el Tab dentro del diálogo: sin eso el foco se tabula hacia la página
|
|
22
|
+
* de atrás y se pierde detrás del fondo oscuro. Mismo contrato que `drawer`,
|
|
23
|
+
* con el que comparte el selector de elementos focables.
|
|
24
|
+
*
|
|
25
|
+
* El clic en el fondo es un atajo para puntero; el teclado usa ✕ o Escape.
|
|
26
|
+
* Componente tonto: `open`/`title` entran por `input()`, el estado vive
|
|
27
|
+
* fuera.
|
|
28
|
+
*/
|
|
29
|
+
@Component({
|
|
30
|
+
selector: 'app-modal',
|
|
31
|
+
imports: [LucideX],
|
|
32
|
+
host: {
|
|
33
|
+
'(document:keydown.escape)': 'onEscape()',
|
|
34
|
+
// Angular arma la tecla CON sus modificadores y exige igualdad exacta:
|
|
35
|
+
// sin esta segunda entrada, Shift+Tab llega como `shift.tab`, no casa con
|
|
36
|
+
// `tab` y el foco escapa del diálogo hacia atrás.
|
|
37
|
+
'(document:keydown.tab)': 'onTab($event)',
|
|
38
|
+
'(document:keydown.shift.tab)': 'onTab($event)',
|
|
39
|
+
},
|
|
40
|
+
template: `
|
|
41
|
+
@if (open()) {
|
|
42
|
+
<div class="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
43
|
+
<div class="absolute inset-0 bg-black/50" aria-hidden="true" (click)="close.emit()"></div>
|
|
44
|
+
<div
|
|
45
|
+
#dialog
|
|
46
|
+
role="dialog"
|
|
47
|
+
aria-modal="true"
|
|
48
|
+
[attr.aria-labelledby]="titleId"
|
|
49
|
+
tabindex="-1"
|
|
50
|
+
class="relative w-full max-w-lg rounded-xl bg-surface-raised p-6 shadow-xl focus:outline-none"
|
|
51
|
+
>
|
|
52
|
+
<div class="mb-4 flex items-start justify-between gap-4">
|
|
53
|
+
<h2 [id]="titleId" class="font-display text-section font-semibold">
|
|
54
|
+
{{ title() }}
|
|
55
|
+
</h2>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
|
|
59
|
+
[attr.aria-label]="closeLabel()"
|
|
60
|
+
(click)="close.emit()"
|
|
61
|
+
>
|
|
62
|
+
<svg lucideX size="18" />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
<ng-content />
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
}
|
|
69
|
+
`,
|
|
70
|
+
})
|
|
71
|
+
export class Modal implements OnDestroy {
|
|
72
|
+
readonly open = input(false);
|
|
73
|
+
readonly title = input.required<string>();
|
|
74
|
+
readonly closeLabel = input('Cerrar');
|
|
75
|
+
readonly close = output<void>();
|
|
76
|
+
|
|
77
|
+
readonly titleId = `modal-title-${++titleCounter}`;
|
|
78
|
+
|
|
79
|
+
private readonly dialogRef = viewChild<ElementRef<HTMLDivElement>>('dialog');
|
|
80
|
+
private lastFocused: HTMLElement | null = null;
|
|
81
|
+
|
|
82
|
+
private readonly focusEffect = afterRenderEffect(() => {
|
|
83
|
+
const open = this.open();
|
|
84
|
+
const dialog = this.dialogRef();
|
|
85
|
+
if (open && dialog) {
|
|
86
|
+
const active = document.activeElement;
|
|
87
|
+
if (active !== dialog.nativeElement && this.lastFocused === null) {
|
|
88
|
+
this.lastFocused = active instanceof HTMLElement ? active : null;
|
|
89
|
+
}
|
|
90
|
+
dialog.nativeElement.focus();
|
|
91
|
+
} else if (!open && this.lastFocused !== null) {
|
|
92
|
+
this.lastFocused.focus();
|
|
93
|
+
this.lastFocused = null;
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* El consumidor puede destruir el componente con el diálogo todavía abierto
|
|
99
|
+
* (se monta bajo un `@if`): entonces el effect nunca ve la transición a
|
|
100
|
+
* `false` y el foco se quedaría perdido en el body.
|
|
101
|
+
*/
|
|
102
|
+
ngOnDestroy(): void {
|
|
103
|
+
if (this.lastFocused?.isConnected) {
|
|
104
|
+
this.lastFocused.focus();
|
|
105
|
+
}
|
|
106
|
+
this.lastFocused = null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
onEscape(): void {
|
|
110
|
+
if (this.open()) {
|
|
111
|
+
this.close.emit();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Mantiene el Tab dentro del diálogo mientras está abierto. Recibe `Event`
|
|
117
|
+
* porque los host listeners de Angular tipean `$event` así; el teclado se
|
|
118
|
+
* relee acá dentro.
|
|
119
|
+
*/
|
|
120
|
+
onTab(rawEvent: Event): void {
|
|
121
|
+
const event = rawEvent as KeyboardEvent;
|
|
122
|
+
const dialog = this.dialogRef()?.nativeElement;
|
|
123
|
+
if (!this.open() || !dialog) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const focusable = Array.from(dialog.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
|
127
|
+
(element) => isVisibleNow(element),
|
|
128
|
+
);
|
|
129
|
+
if (focusable.length === 0) {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
dialog.focus();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const first = focusable[0];
|
|
135
|
+
const last = focusable[focusable.length - 1];
|
|
136
|
+
const active = document.activeElement;
|
|
137
|
+
if (event.shiftKey && (active === first || active === dialog)) {
|
|
138
|
+
event.preventDefault();
|
|
139
|
+
last.focus();
|
|
140
|
+
} else if (!event.shiftKey && active === last) {
|
|
141
|
+
event.preventDefault();
|
|
142
|
+
first.focus();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { By } from '@angular/platform-browser';
|
|
3
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type { PostCreateSchema, ResourceSchema } from '../../../core/models/schema.model';
|
|
6
|
+
import { CopyShare } from '../copy-share/copy-share';
|
|
7
|
+
import { PostCreateModal } from './post-create-modal';
|
|
8
|
+
|
|
9
|
+
describe('PostCreateModal', () => {
|
|
10
|
+
let fixture: ComponentFixture<PostCreateModal>;
|
|
11
|
+
|
|
12
|
+
const mockSchema: ResourceSchema = {
|
|
13
|
+
id: 'orders',
|
|
14
|
+
label: 'Pedido',
|
|
15
|
+
labelPlural: 'Pedidos',
|
|
16
|
+
kind: 'collection',
|
|
17
|
+
endpoint: { list: '/admin/orders' },
|
|
18
|
+
keyField: 'ref',
|
|
19
|
+
titleField: 'customer_name',
|
|
20
|
+
listColumns: [],
|
|
21
|
+
fields: [
|
|
22
|
+
{ key: 'ref', type: 'text', label: 'Referencia' },
|
|
23
|
+
{ key: 'customer_name', type: 'text', label: 'Cliente' },
|
|
24
|
+
{ key: 'pickup_date', type: 'date', label: 'Fecha de Retiro' },
|
|
25
|
+
],
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mockConfig: PostCreateSchema = {
|
|
29
|
+
mode: 'modal',
|
|
30
|
+
title: '¡Pedido {ref} Creado!',
|
|
31
|
+
description: 'El pedido de {customer_name} ya está listo para compartir.',
|
|
32
|
+
summaryFields: ['ref', 'customer_name', 'pickup_date'],
|
|
33
|
+
actions: [
|
|
34
|
+
{
|
|
35
|
+
id: 'share',
|
|
36
|
+
type: 'copy-share',
|
|
37
|
+
label: 'Copiar Enlace de Retiro',
|
|
38
|
+
urlTemplate: '{public_base_url}/p/{ref}',
|
|
39
|
+
shareTextTemplate: 'Hola {customer_name}, tu pedido {ref} está listo.',
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
beforeEach(async () => {
|
|
45
|
+
await TestBed.configureTestingModule({
|
|
46
|
+
imports: [PostCreateModal],
|
|
47
|
+
}).compileComponents();
|
|
48
|
+
fixture = TestBed.createComponent(PostCreateModal);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('1. interpola título y descripción con los datos del registro', () => {
|
|
52
|
+
fixture.componentRef.setInput('schema', mockSchema);
|
|
53
|
+
fixture.componentRef.setInput('config', mockConfig);
|
|
54
|
+
fixture.componentRef.setInput('record', {
|
|
55
|
+
ref: 'ORD-555',
|
|
56
|
+
customer_name: 'Esteban Quito',
|
|
57
|
+
pickup_date: '2026-10-10',
|
|
58
|
+
});
|
|
59
|
+
fixture.componentRef.setInput('configValues', {
|
|
60
|
+
public_base_url: 'https://pickpass.app',
|
|
61
|
+
});
|
|
62
|
+
fixture.detectChanges();
|
|
63
|
+
|
|
64
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
65
|
+
expect(root.textContent).toContain('¡Pedido ORD-555 Creado!');
|
|
66
|
+
expect(root.textContent).toContain('El pedido de Esteban Quito ya está listo para compartir.');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('2. renderiza los campos de resumen especificados en summaryFields', () => {
|
|
70
|
+
fixture.componentRef.setInput('schema', mockSchema);
|
|
71
|
+
fixture.componentRef.setInput('config', mockConfig);
|
|
72
|
+
fixture.componentRef.setInput('record', {
|
|
73
|
+
ref: 'ORD-555',
|
|
74
|
+
customer_name: 'Esteban Quito',
|
|
75
|
+
pickup_date: '2026-10-10',
|
|
76
|
+
});
|
|
77
|
+
fixture.detectChanges();
|
|
78
|
+
|
|
79
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
80
|
+
expect(root.textContent).toContain('Referencia');
|
|
81
|
+
expect(root.textContent).toContain('ORD-555');
|
|
82
|
+
expect(root.textContent).toContain('Cliente');
|
|
83
|
+
expect(root.textContent).toContain('Esteban Quito');
|
|
84
|
+
expect(root.textContent).toContain('Fecha de Retiro');
|
|
85
|
+
expect(root.textContent).toContain('2026-10-10');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('3. renderiza el componente CopyShare cuando hay una acción copy-share', () => {
|
|
89
|
+
fixture.componentRef.setInput('schema', mockSchema);
|
|
90
|
+
fixture.componentRef.setInput('config', mockConfig);
|
|
91
|
+
fixture.componentRef.setInput('record', {
|
|
92
|
+
ref: 'ORD-888',
|
|
93
|
+
customer_name: 'Lucia',
|
|
94
|
+
});
|
|
95
|
+
fixture.componentRef.setInput('configValues', {
|
|
96
|
+
public_base_url: 'https://retiro.com',
|
|
97
|
+
});
|
|
98
|
+
fixture.detectChanges();
|
|
99
|
+
|
|
100
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
101
|
+
expect(root.querySelector('app-copy-share')).not.toBeNull();
|
|
102
|
+
expect(root.textContent).toContain('Copiar Enlace de Retiro');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('4. emite evento closed al hacer clic en el botón Continuar', () => {
|
|
106
|
+
let closedEmitted = false;
|
|
107
|
+
fixture.componentInstance.closed.subscribe(() => {
|
|
108
|
+
closedEmitted = true;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
fixture.componentRef.setInput('schema', mockSchema);
|
|
112
|
+
fixture.componentRef.setInput('config', mockConfig);
|
|
113
|
+
fixture.componentRef.setInput('record', { ref: 'ORD-1' });
|
|
114
|
+
fixture.detectChanges();
|
|
115
|
+
|
|
116
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
117
|
+
const btns = Array.from(root.querySelectorAll<HTMLButtonElement>('button[app-button]'));
|
|
118
|
+
const btn = btns.find((b) => b.textContent?.trim() === 'Continuar');
|
|
119
|
+
btn?.click();
|
|
120
|
+
fixture.detectChanges();
|
|
121
|
+
|
|
122
|
+
expect(closedEmitted).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('5. el enlace copiado lleva la URL pública sin que nadie pase configValues', () => {
|
|
126
|
+
// Réplica EXACTA del binding de producción: `form-view.ts:420-425` sólo
|
|
127
|
+
// pasa `schema`, `config`, `record` y `closed`. Nadie bindea
|
|
128
|
+
// `[configValues]`, así que la única fuente posible de
|
|
129
|
+
// `{public_base_url}` es `ConfigService` inyectado en el componente
|
|
130
|
+
// (D18): antes no lo estaba y el enlace salía sin dominio.
|
|
131
|
+
const configConUrl: PostCreateSchema = {
|
|
132
|
+
...mockConfig,
|
|
133
|
+
actions: [
|
|
134
|
+
{
|
|
135
|
+
id: 'share',
|
|
136
|
+
type: 'copy-share',
|
|
137
|
+
label: 'Copiar Enlace de Retiro',
|
|
138
|
+
urlTemplate: '{public_base_url}/p/{ref}',
|
|
139
|
+
shareTextTemplate:
|
|
140
|
+
'Hola {customer_name}, puede retirar su pedido {ref} aquí: {public_base_url}/p/{ref}.',
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
fixture.componentRef.setInput('schema', mockSchema);
|
|
146
|
+
fixture.componentRef.setInput('config', configConUrl);
|
|
147
|
+
fixture.componentRef.setInput('record', { ref: 'ORD-999', customer_name: 'Ana Pérez' });
|
|
148
|
+
fixture.detectChanges();
|
|
149
|
+
|
|
150
|
+
const share = fixture.debugElement.query(By.directive(CopyShare))
|
|
151
|
+
.componentInstance as CopyShare;
|
|
152
|
+
|
|
153
|
+
// Sin `configValues`, el dominio lo aporta `ConfigService` (fallback a
|
|
154
|
+
// `window.location.origin`, config.service.ts:30).
|
|
155
|
+
const esperado = `${window.location.origin}/p/ORD-999`;
|
|
156
|
+
expect(share.url(), 'el enlace quedó sin dominio').toBe(esperado);
|
|
157
|
+
expect(share.shareText(), 'el mensaje quedó con la URL rota').toContain(esperado);
|
|
158
|
+
|
|
159
|
+
// Y el input sigue mandando cuando alguien lo pasa: es un override,
|
|
160
|
+
// no la fuente principal.
|
|
161
|
+
fixture.componentRef.setInput('configValues', { public_base_url: 'https://retiro.com' });
|
|
162
|
+
fixture.detectChanges();
|
|
163
|
+
expect(share.url(), 'el override dejó de aplicar').toBe('https://retiro.com/p/ORD-999');
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { Component, computed, inject, input, output } from '@angular/core';
|
|
2
|
+
import { LucideCheckCircle2 } from '@lucide/angular';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CopyShareAction,
|
|
6
|
+
PostCreateSchema,
|
|
7
|
+
ResourceAction,
|
|
8
|
+
ResourceSchema,
|
|
9
|
+
} from '../../../core/models/schema.model';
|
|
10
|
+
import { isShareAction, resolveShare, type ResolvedShare } from '../../../actions/action-registry';
|
|
11
|
+
import { ConfigService } from '../../../core/services/config.service';
|
|
12
|
+
import { interpolateTemplate } from '../../../core/utils/template-interpolator';
|
|
13
|
+
import { Button } from '../button/button';
|
|
14
|
+
import { CopyShare } from '../copy-share/copy-share';
|
|
15
|
+
import { Modal } from '../modal/modal';
|
|
16
|
+
|
|
17
|
+
interface SummaryItem {
|
|
18
|
+
key: string;
|
|
19
|
+
label: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Modal de éxito y resumen tras crear un registro.
|
|
25
|
+
* Permite visualizar datos clave y acciones directas (como CopyShare o compartir por WhatsApp).
|
|
26
|
+
*/
|
|
27
|
+
@Component({
|
|
28
|
+
selector: 'app-post-create-modal',
|
|
29
|
+
imports: [Modal, Button, CopyShare, LucideCheckCircle2],
|
|
30
|
+
template: `
|
|
31
|
+
<app-modal [open]="true" [title]="title()" (close)="closed.emit()">
|
|
32
|
+
<div class="flex flex-col gap-4">
|
|
33
|
+
<!-- Encabezado con icono de éxito -->
|
|
34
|
+
<div class="flex items-center gap-3">
|
|
35
|
+
<div
|
|
36
|
+
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-emerald-500/10 text-emerald-500"
|
|
37
|
+
>
|
|
38
|
+
<svg lucideCheckCircle2 size="22" />
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<p class="text-sm text-neutral-light">{{ description() }}</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<!-- Tarjeta de campos de resumen -->
|
|
46
|
+
@if (summaryItems().length > 0) {
|
|
47
|
+
<div class="rounded-lg border border-neutral/15 bg-surface p-3.5">
|
|
48
|
+
<dl class="grid grid-cols-1 gap-x-4 gap-y-2.5 sm:grid-cols-2">
|
|
49
|
+
@for (item of summaryItems(); track item.key) {
|
|
50
|
+
<div class="flex flex-col">
|
|
51
|
+
<dt class="text-xs font-medium text-neutral">{{ item.label }}</dt>
|
|
52
|
+
<dd class="text-sm font-semibold text-neutral-light truncate">
|
|
53
|
+
{{ item.value }}
|
|
54
|
+
</dd>
|
|
55
|
+
</div>
|
|
56
|
+
}
|
|
57
|
+
</dl>
|
|
58
|
+
</div>
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
<!-- Acciones declaradas (ej. CopyShare) -->
|
|
62
|
+
@if (copyShareAction(); as action) {
|
|
63
|
+
<div class="rounded-lg border border-primary/20 bg-primary/5 p-3.5">
|
|
64
|
+
<p class="mb-2 text-xs font-medium text-primary">Enlace de Seguimiento</p>
|
|
65
|
+
@let share = shareOf(action);
|
|
66
|
+
<app-copy-share
|
|
67
|
+
[url]="share.url"
|
|
68
|
+
[shareText]="share.shareText"
|
|
69
|
+
[label]="action.label"
|
|
70
|
+
/>
|
|
71
|
+
</div>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
<!-- Botón de cierre primario -->
|
|
75
|
+
<div class="mt-2 flex justify-end">
|
|
76
|
+
<button app-button type="button" variant="primary" (click)="closed.emit()">
|
|
77
|
+
Continuar
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</app-modal>
|
|
82
|
+
`,
|
|
83
|
+
})
|
|
84
|
+
export class PostCreateModal {
|
|
85
|
+
readonly schema = input.required<ResourceSchema>();
|
|
86
|
+
readonly config = input.required<PostCreateSchema>();
|
|
87
|
+
readonly record = input.required<Record<string, unknown>>();
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Override puntual del contexto de interpolación (tests, o quien quiera
|
|
91
|
+
* forzar un valor). **No** es la vía normal: la config global la aporta
|
|
92
|
+
* `ConfigService`, igual que en `list-view`.
|
|
93
|
+
*/
|
|
94
|
+
readonly configValues = input<Record<string, unknown>>({});
|
|
95
|
+
|
|
96
|
+
readonly closed = output<void>();
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Inyectado y NO opcional a propósito. El bug que motivó esto (D18): con
|
|
100
|
+
* el input `configValues` como única fuente y nadie bindeándolo, la
|
|
101
|
+
* plantilla salía con `{public_base_url}` vacío y el operador copiaba
|
|
102
|
+
* `…aquí: /p/ORD-0001.` — un enlace inpegable.
|
|
103
|
+
*
|
|
104
|
+
* `buildInterpolationContext` mezcla config global + fila y fija
|
|
105
|
+
* `public_base_url` (`config.service.ts:64-73`); `configValues` se aplica
|
|
106
|
+
* encima, sólo como override.
|
|
107
|
+
*/
|
|
108
|
+
private readonly configService = inject(ConfigService);
|
|
109
|
+
|
|
110
|
+
constructor() {
|
|
111
|
+
// El modal se abre con la sesión ya iniciada: si el load del bootstrap
|
|
112
|
+
// falló (arranque sin sesión), éste es el momento de reintentarlo para
|
|
113
|
+
// que el mensaje de compartir salga del singleton y no «sólo la URL».
|
|
114
|
+
// `globalConfig` es signal: cuando llegue, el contexto y `shareOf` se
|
|
115
|
+
// recalculan solos.
|
|
116
|
+
void this.configService.ensureLoaded();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Contexto único con el que se resuelven título, descripción, URL y mensaje. */
|
|
120
|
+
private readonly context = computed(() => ({
|
|
121
|
+
...this.configService.buildInterpolationContext(this.record()),
|
|
122
|
+
...this.configValues(),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
protected readonly title = computed(() => {
|
|
126
|
+
const raw = this.config().title || '¡Registro Creado con Éxito!';
|
|
127
|
+
return interpolateTemplate(raw, this.context());
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
protected readonly description = computed(() => {
|
|
131
|
+
const raw =
|
|
132
|
+
this.config().description ||
|
|
133
|
+
'El registro fue creado correctamente y ya se encuentra guardado en el sistema.';
|
|
134
|
+
return interpolateTemplate(raw, this.context());
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
protected readonly summaryItems = computed<SummaryItem[]>(() => {
|
|
138
|
+
const fields = this.config().summaryFields || [];
|
|
139
|
+
const rec = this.record();
|
|
140
|
+
const schemaFields = this.schema().fields || [];
|
|
141
|
+
|
|
142
|
+
return fields.map((key) => {
|
|
143
|
+
const fieldDef = schemaFields.find((f) => f.key === key);
|
|
144
|
+
const label = fieldDef?.label || key.replace(/_/g, ' ');
|
|
145
|
+
const rawVal = rec[key];
|
|
146
|
+
const value = rawVal === null || rawVal === undefined || rawVal === '' ? '—' : String(rawVal);
|
|
147
|
+
return { key, label, value };
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
protected readonly copyShareAction = computed<CopyShareAction | null>(
|
|
152
|
+
() => (this.config().actions || []).find(isShareAction) ?? null,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
/** Enlace y mensaje, resueltos por `resolveShare` (R3.10: una sola copia). */
|
|
156
|
+
protected shareOf(action: CopyShareAction): ResolvedShare {
|
|
157
|
+
return resolveShare(action, this.context());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Bloque de carga decorativo: oculto para lectores (`aria-hidden`); la
|
|
5
|
+
* región que lo contiene debe exponer `aria-busy="true"`.
|
|
6
|
+
*/
|
|
7
|
+
@Component({
|
|
8
|
+
selector: 'app-skeleton',
|
|
9
|
+
host: { class: 'block space-y-2', 'aria-hidden': 'true' },
|
|
10
|
+
template: `
|
|
11
|
+
@for (line of lineCount(); track line) {
|
|
12
|
+
<div
|
|
13
|
+
class="animate-pulse rounded bg-neutral/15"
|
|
14
|
+
[style.height.px]="height()"
|
|
15
|
+
[style.width]="width()"
|
|
16
|
+
></div>
|
|
17
|
+
}
|
|
18
|
+
`,
|
|
19
|
+
})
|
|
20
|
+
export class Skeleton {
|
|
21
|
+
/** Cantidad de barras (mínimo 1). */
|
|
22
|
+
readonly lines = input(1);
|
|
23
|
+
readonly height = input(16);
|
|
24
|
+
readonly width = input('100%');
|
|
25
|
+
|
|
26
|
+
readonly lineCount = computed(() => {
|
|
27
|
+
const lines = Math.floor(this.lines());
|
|
28
|
+
const count = Number.isFinite(lines) && lines > 0 ? lines : 1;
|
|
29
|
+
return Array.from({ length: count }, (_, index) => index);
|
|
30
|
+
});
|
|
31
|
+
}
|