@tinya-code/gas-admin-ui 0.2.0-alpha.13 → 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/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,219 @@
|
|
|
1
|
+
import { Component, OnDestroy, computed, effect, input, viewChild } from '@angular/core';
|
|
2
|
+
import type { ElementRef } from '@angular/core';
|
|
3
|
+
import {
|
|
4
|
+
CategoryScale,
|
|
5
|
+
Chart,
|
|
6
|
+
LineController,
|
|
7
|
+
LineElement,
|
|
8
|
+
LinearScale,
|
|
9
|
+
PointElement,
|
|
10
|
+
Tooltip,
|
|
11
|
+
type ChartConfiguration,
|
|
12
|
+
} from 'chart.js';
|
|
13
|
+
|
|
14
|
+
import { Skeleton } from '../../shared/components/skeleton/skeleton';
|
|
15
|
+
import type { BarDatum } from '../bar-chart/bar-chart';
|
|
16
|
+
|
|
17
|
+
// Chart.js usa registro explícito en vez de `chart.js/auto`: sólo entra lo que
|
|
18
|
+
// esta serie necesita (sin DoughnutController, sin Filler, sin Legend).
|
|
19
|
+
Chart.register(CategoryScale, LineController, LineElement, LinearScale, PointElement, Tooltip);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Serie temporal con Chart.js (viabilidad B2.1).
|
|
23
|
+
*
|
|
24
|
+
* La librería —61,4 K gz medidos— sólo se arrastra con ESTE componente y se
|
|
25
|
+
* descarga en su propio chunk: el descriptor del widget la trae con
|
|
26
|
+
* `import('./chart-line')` y es esa llamada la que hace que esbuild la
|
|
27
|
+
* aparte del bundle inicial (aquí se importa estáticamente, así que ningún
|
|
28
|
+
* `@defer` de otro fichero podría separarla).
|
|
29
|
+
*
|
|
30
|
+
* El `@defer (on viewport)` de abajo cumple el otro objetivo y vive aquí, y
|
|
31
|
+
* no en un host: el chunk llega con el panel, pero el lienzo y la instancia
|
|
32
|
+
* de `Chart` sólo se crean cuando el gráfico entra en pantalla. Un host no
|
|
33
|
+
* puede decidir eso — desde R3.6 el motor no conoce ningún tipo concreto
|
|
34
|
+
* (R3.6 · `doc/plan-refactor-motor.md`).
|
|
35
|
+
*
|
|
36
|
+
* La condición `on viewport` necesita `IntersectionObserver`, que jsdom no
|
|
37
|
+
* trae: los tests configuran `DeferBlockBehavior.Manual` y pintan el bloque
|
|
38
|
+
* a mano, igual que hacía `widget-host.spec`.
|
|
39
|
+
*/
|
|
40
|
+
@Component({
|
|
41
|
+
selector: 'app-chart-line',
|
|
42
|
+
imports: [Skeleton],
|
|
43
|
+
template: `
|
|
44
|
+
<div class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
45
|
+
<p class="mb-3 text-sm text-neutral">{{ label() }}</p>
|
|
46
|
+
|
|
47
|
+
@if (data().length === 0) {
|
|
48
|
+
<p class="text-sm text-neutral/60" data-testid="empty">Sin datos.</p>
|
|
49
|
+
} @else {
|
|
50
|
+
<div aria-live="polite" aria-atomic="true">
|
|
51
|
+
@defer (on viewport) {
|
|
52
|
+
<div role="img" [attr.aria-label]="summary()" class="relative h-32">
|
|
53
|
+
<canvas #canvas class="absolute inset-0 h-full w-full"></canvas>
|
|
54
|
+
</div>
|
|
55
|
+
} @placeholder {
|
|
56
|
+
<div class="relative h-32" aria-hidden="true">
|
|
57
|
+
<svg
|
|
58
|
+
viewBox="0 0 100 40"
|
|
59
|
+
preserveAspectRatio="none"
|
|
60
|
+
class="absolute inset-0 h-full w-full text-primary"
|
|
61
|
+
>
|
|
62
|
+
<polyline
|
|
63
|
+
fill="none"
|
|
64
|
+
stroke="currentColor"
|
|
65
|
+
stroke-width="2"
|
|
66
|
+
vector-effect="non-scaling-stroke"
|
|
67
|
+
[attr.points]="sparkPoints()"
|
|
68
|
+
/>
|
|
69
|
+
</svg>
|
|
70
|
+
</div>
|
|
71
|
+
} @loading {
|
|
72
|
+
<app-skeleton [lines]="3" [height]="24" />
|
|
73
|
+
} @error {
|
|
74
|
+
<p class="text-sm text-neutral/60" data-testid="chart-error">
|
|
75
|
+
No se pudo cargar el gráfico.
|
|
76
|
+
</p>
|
|
77
|
+
}
|
|
78
|
+
</div>
|
|
79
|
+
}
|
|
80
|
+
</div>
|
|
81
|
+
`,
|
|
82
|
+
})
|
|
83
|
+
export class ChartLine implements OnDestroy {
|
|
84
|
+
/** Título del gráfico. */
|
|
85
|
+
readonly label = input.required<string>();
|
|
86
|
+
/** Series a dibujar; vacío ⇒ estado «Sin datos». */
|
|
87
|
+
readonly data = input<BarDatum[]>([]);
|
|
88
|
+
/** Sufijo de las cifras del eje («unidades», «S/»). */
|
|
89
|
+
readonly unit = input('');
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Opcional y no `required`: el lienzo sólo existe una vez completado el
|
|
93
|
+
* `@defer`, así que el `effect` de abajo se re-ejecuta solo al aparecer y
|
|
94
|
+
* dibuja en ese momento. Un `viewChild.required` reventaría en el primer
|
|
95
|
+
* ciclo, cuando el bloque todavía está en placeholder.
|
|
96
|
+
*/
|
|
97
|
+
private readonly canvas = viewChild<ElementRef<HTMLCanvasElement>>('canvas');
|
|
98
|
+
|
|
99
|
+
private chart: Chart<'line'> | null = null;
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
effect(() => {
|
|
103
|
+
const lienzo = this.canvas();
|
|
104
|
+
this.data();
|
|
105
|
+
this.label();
|
|
106
|
+
this.unit();
|
|
107
|
+
|
|
108
|
+
if (lienzo) {
|
|
109
|
+
this.render(lienzo.nativeElement);
|
|
110
|
+
} else {
|
|
111
|
+
// Sin lienzo (sin datos, o el bloque aún en placeholder) no hay nada
|
|
112
|
+
// que mantener vivo: una instancia huérfana seguiría redibujando.
|
|
113
|
+
this.destroy();
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
ngOnDestroy(): void {
|
|
119
|
+
this.destroy();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
protected readonly summary = computed(() => {
|
|
123
|
+
const rows = this.data();
|
|
124
|
+
if (rows.length === 0) {
|
|
125
|
+
return 'Sin datos.';
|
|
126
|
+
}
|
|
127
|
+
const unit = this.unit();
|
|
128
|
+
const first = rows[0];
|
|
129
|
+
const last = rows[rows.length - 1];
|
|
130
|
+
const delta = last.value - first.value;
|
|
131
|
+
const trend = delta > 0 ? 'sube' : delta < 0 ? 'baja' : 'se mantiene';
|
|
132
|
+
return (
|
|
133
|
+
`Serie de ${rows.length} puntos, de ${first.label} (${first.value}${unit}) ` +
|
|
134
|
+
`a ${last.label} (${last.value}${unit}): ${trend}.`
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
/** Miniatura del placeholder: la misma serie, dibujada con SVG y sin JS. */
|
|
139
|
+
protected readonly sparkPoints = computed(() => {
|
|
140
|
+
const rows = this.data();
|
|
141
|
+
if (rows.length === 0) {
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
const max = Math.max(0, ...rows.map((row) => row.value));
|
|
145
|
+
const last = rows.length - 1;
|
|
146
|
+
return rows
|
|
147
|
+
.map((row, index) => {
|
|
148
|
+
const x = last === 0 ? 50 : (index / last) * 100;
|
|
149
|
+
const y = max === 0 ? 40 : 40 - (row.value / max) * 36;
|
|
150
|
+
return `${x.toFixed(2)},${y.toFixed(2)}`;
|
|
151
|
+
})
|
|
152
|
+
.join(' ');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
private render(canvas: HTMLCanvasElement): void {
|
|
156
|
+
const rows = this.data();
|
|
157
|
+
|
|
158
|
+
// Sin datos no hay lienzo en el template: crear un `Chart` sobre él
|
|
159
|
+
// reventaría (y el `@if` ya se lo habría llevado por delante).
|
|
160
|
+
if (rows.length === 0) {
|
|
161
|
+
this.destroy();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const config: ChartConfiguration<'line'> = {
|
|
166
|
+
type: 'line',
|
|
167
|
+
data: {
|
|
168
|
+
labels: rows.map((row) => row.label),
|
|
169
|
+
datasets: [
|
|
170
|
+
{
|
|
171
|
+
label: this.label(),
|
|
172
|
+
data: rows.map((row) => row.value),
|
|
173
|
+
borderColor: this.cssColor('--color-primary'),
|
|
174
|
+
borderWidth: 2,
|
|
175
|
+
pointRadius: 3,
|
|
176
|
+
pointHoverRadius: 5,
|
|
177
|
+
tension: 0.35,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
},
|
|
181
|
+
options: {
|
|
182
|
+
responsive: true,
|
|
183
|
+
maintainAspectRatio: false,
|
|
184
|
+
plugins: {
|
|
185
|
+
legend: { display: false },
|
|
186
|
+
tooltip: { enabled: true, mode: 'index', intersect: false },
|
|
187
|
+
},
|
|
188
|
+
scales: {
|
|
189
|
+
x: { grid: { display: false } },
|
|
190
|
+
y: {
|
|
191
|
+
beginAtZero: true,
|
|
192
|
+
ticks: {
|
|
193
|
+
callback: (value) =>
|
|
194
|
+
typeof value === 'number' ? `${value}${this.unit()}` : String(value),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
this.destroy();
|
|
202
|
+
this.chart = new Chart<'line'>(canvas, config);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private destroy(): void {
|
|
206
|
+
this.chart?.destroy();
|
|
207
|
+
this.chart = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Chart.js pinta en el lienzo y NO entiende `var(--…)`, así que el token se
|
|
212
|
+
* resuelve en tiempo de dibujo. Se lee en cada render —no al arrancar—
|
|
213
|
+
* porque el modo oscuro sobre-escribe `--color-primary` (`styles/theme.css`).
|
|
214
|
+
*/
|
|
215
|
+
private cssColor(token: string): string {
|
|
216
|
+
const value = getComputedStyle(document.documentElement).getPropertyValue(token).trim();
|
|
217
|
+
return value || '#1d69f0';
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
import { descriptor as barChartDescriptor } from './bar-chart/bar-chart.descriptor';
|
|
4
|
+
import { descriptor as chartLineDescriptor } from './chart-line/chart-line.descriptor';
|
|
5
|
+
import { descriptor as metricCardDescriptor } from './metric-card/metric-card.descriptor';
|
|
6
|
+
import { descriptor as quickActionsDescriptor } from './quick-actions/quick-actions.descriptor';
|
|
7
|
+
import { descriptor as recordListDescriptor } from './record-list/record-list.descriptor';
|
|
8
|
+
import { descriptor as statusProgressDescriptor } from './status-progress/status-progress.descriptor';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Manifiesto de descriptores de widget (R3.6 · registro único de render).
|
|
12
|
+
*
|
|
13
|
+
* Mismo criterio que `fields/descriptors.ts`: una línea por tipo y el
|
|
14
|
+
* registro los recoge al importar este módulo. Es la **única** lista manual
|
|
15
|
+
* del catálogo de widgets — `import.meta.glob` no sobrevive a `ng build`
|
|
16
|
+
* (D9-1), así que se mantiene el «registro manual de una línea».
|
|
17
|
+
*
|
|
18
|
+
* Añadir un widget ⇒ crear la carpeta + su `.descriptor.ts` + una línea
|
|
19
|
+
* acá. **Ningún host se toca** (R3.8): si el componente sigue la forma
|
|
20
|
+
* estándar `{widget, data}` ni siquiera hay que escribir `inputs`.
|
|
21
|
+
*/
|
|
22
|
+
export const widgetDescriptors: readonly RenderDescriptor[] = [
|
|
23
|
+
barChartDescriptor,
|
|
24
|
+
chartLineDescriptor,
|
|
25
|
+
metricCardDescriptor,
|
|
26
|
+
quickActionsDescriptor,
|
|
27
|
+
recordListDescriptor,
|
|
28
|
+
statusProgressDescriptor,
|
|
29
|
+
];
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { MetricCardWidget } from '../../core/models/schema.model';
|
|
2
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* El dato de un KPI es un ESCALAR y puede llegar de dos formas:
|
|
6
|
+
*
|
|
7
|
+
* - **payload**: `data[widget.key]` ya es la cifra (la resuelve el handler
|
|
8
|
+
* del panel);
|
|
9
|
+
* - **fuente declarada** (F8 · R6.5): una vista con `aggregate` devuelve
|
|
10
|
+
* `[{ value: … }]` y la cifra es ese `value`.
|
|
11
|
+
*
|
|
12
|
+
* Cualquier otra forma ⇒ `null`, que `MetricCard` pinta como vacío. Se
|
|
13
|
+
* traduce acá, en el borde — el componente no debería saber de dónde salió
|
|
14
|
+
* el número.
|
|
15
|
+
*/
|
|
16
|
+
function valorDeKpi(data: unknown): number | string | null {
|
|
17
|
+
if (Array.isArray(data)) {
|
|
18
|
+
const first = data[0];
|
|
19
|
+
if (typeof first !== 'object' || first === null) return null;
|
|
20
|
+
const value = (first as { value?: unknown }).value;
|
|
21
|
+
return value === undefined || value === null ? null : (value as number | string);
|
|
22
|
+
}
|
|
23
|
+
if (data === undefined) return null;
|
|
24
|
+
return data as number | string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Descriptor del widget `metric-card` (R3.6). */
|
|
28
|
+
export const descriptor: RenderDescriptor = {
|
|
29
|
+
kind: 'widget',
|
|
30
|
+
type: 'metric-card',
|
|
31
|
+
since: '1.0',
|
|
32
|
+
load: () => import('./metric-card').then((module) => module.MetricCard),
|
|
33
|
+
surfaces: ['display'],
|
|
34
|
+
// El KPI no recibe el widget entero: sólo vive de su rótulo, del dato y de
|
|
35
|
+
// la nota al pie. Pasa el dato ya traducido — `MetricCard` distingue el
|
|
36
|
+
// `0` de un vacío, y el `0` de un agregado llega como `[{ value: 0 }]`.
|
|
37
|
+
inputs: (config, extras) => {
|
|
38
|
+
const cfg = config as MetricCardWidget;
|
|
39
|
+
return { label: cfg.label, value: valorDeKpi(extras.data), hint: cfg.hint ?? '' };
|
|
40
|
+
},
|
|
41
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
|
|
3
|
+
import { MetricCard } from './metric-card';
|
|
4
|
+
|
|
5
|
+
describe('MetricCard', () => {
|
|
6
|
+
let fixture: ComponentFixture<MetricCard>;
|
|
7
|
+
|
|
8
|
+
const root = (): HTMLElement => fixture.nativeElement as HTMLElement;
|
|
9
|
+
const value = (): HTMLElement | null => root().querySelector('[data-testid="value"]');
|
|
10
|
+
const empty = (): HTMLElement | null => root().querySelector('[data-testid="value-empty"]');
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
await TestBed.configureTestingModule({ imports: [MetricCard] }).compileComponents();
|
|
14
|
+
fixture = TestBed.createComponent(MetricCard);
|
|
15
|
+
fixture.componentRef.setInput('label', 'Pendientes');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('renderiza la etiqueta y la cifra', () => {
|
|
19
|
+
fixture.componentRef.setInput('value', 3);
|
|
20
|
+
fixture.detectChanges();
|
|
21
|
+
|
|
22
|
+
expect(root().textContent).toContain('Pendientes');
|
|
23
|
+
expect(value()?.textContent?.trim()).toBe('3');
|
|
24
|
+
expect(empty()).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('value null ⇒ estado vacío con el placeholder', () => {
|
|
28
|
+
fixture.componentRef.setInput('value', null);
|
|
29
|
+
fixture.detectChanges();
|
|
30
|
+
|
|
31
|
+
expect(empty()).not.toBeNull();
|
|
32
|
+
expect(value()).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('0 es un valor, no un vacío', () => {
|
|
36
|
+
fixture.componentRef.setInput('value', 0);
|
|
37
|
+
fixture.detectChanges();
|
|
38
|
+
|
|
39
|
+
expect(value()?.textContent?.trim()).toBe('0');
|
|
40
|
+
expect(empty()).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('sin hint no pinta la línea secundaria', () => {
|
|
44
|
+
fixture.componentRef.setInput('value', 3);
|
|
45
|
+
fixture.detectChanges();
|
|
46
|
+
expect(root().textContent).not.toContain('hint');
|
|
47
|
+
|
|
48
|
+
fixture.componentRef.setInput('hint', 'de 12 totales');
|
|
49
|
+
fixture.detectChanges();
|
|
50
|
+
expect(root().textContent).toContain('de 12 totales');
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* KPI de una cifra suelta («3 pendientes»).
|
|
5
|
+
*
|
|
6
|
+
* Cero dependencias: es sólo presentación — el valor lo trae el
|
|
7
|
+
* orquestador del panel desde el endpoint de métricas.
|
|
8
|
+
*
|
|
9
|
+
* Sin `icon`: el schema no tiene hoy un registro de nombres de icono
|
|
10
|
+
* (los Lucide se importan estáticamente), así que un `icon?: string` no
|
|
11
|
+
* podría resolverse declarativamente. Se añade cuando exista ese registro.
|
|
12
|
+
*/
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'app-metric-card',
|
|
15
|
+
template: `
|
|
16
|
+
<article class="rounded-lg border border-neutral/20 bg-surface p-4">
|
|
17
|
+
<p class="text-sm text-neutral">{{ label() }}</p>
|
|
18
|
+
|
|
19
|
+
@if (empty()) {
|
|
20
|
+
<p class="font-display text-3xl font-semibold text-neutral/40" data-testid="value-empty">
|
|
21
|
+
—
|
|
22
|
+
</p>
|
|
23
|
+
} @else {
|
|
24
|
+
<p class="font-display text-3xl font-semibold" data-testid="value">{{ display() }}</p>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@if (hint()) {
|
|
28
|
+
<p class="mt-1 text-xs text-neutral">{{ hint() }}</p>
|
|
29
|
+
}
|
|
30
|
+
</article>
|
|
31
|
+
`,
|
|
32
|
+
})
|
|
33
|
+
export class MetricCard {
|
|
34
|
+
/** Rótulo del KPI. */
|
|
35
|
+
readonly label = input.required<string>();
|
|
36
|
+
/** Cifra a mostrar; `null`/`undefined` ⇒ estado vacío. */
|
|
37
|
+
readonly value = input<unknown>(null);
|
|
38
|
+
/** Texto secundario estático bajo la cifra. */
|
|
39
|
+
readonly hint = input('');
|
|
40
|
+
|
|
41
|
+
/** `0` es un valor, no un vacío: sólo `null`/`undefined` vacían la tarjeta. */
|
|
42
|
+
protected readonly empty = computed(() => this.value() === null || this.value() === undefined);
|
|
43
|
+
|
|
44
|
+
protected readonly display = computed(() => {
|
|
45
|
+
const value = this.value();
|
|
46
|
+
return typeof value === 'number' ? new Intl.NumberFormat('es-PE').format(value) : String(value);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del widget `quick-actions` (R3.6). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'widget',
|
|
6
|
+
type: 'quick-actions',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./quick-actions').then((module) => module.QuickActions),
|
|
9
|
+
surfaces: ['display'],
|
|
10
|
+
// El único widget sin dato: sus botones salen todos del schema. Por eso no
|
|
11
|
+
// recibe `data` — `ngComponentOutletInputs` no admite inputs inexistentes.
|
|
12
|
+
inputs: (config) => ({ widget: config }),
|
|
13
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { provideRouter } from '@angular/router';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type { QuickActionsWidget } from '../../core/models/schema.model';
|
|
6
|
+
import { QuickActions } from './quick-actions';
|
|
7
|
+
|
|
8
|
+
describe('QuickActions', () => {
|
|
9
|
+
let fixture: ComponentFixture<QuickActions>;
|
|
10
|
+
|
|
11
|
+
const widget: QuickActionsWidget = {
|
|
12
|
+
type: 'quick-actions',
|
|
13
|
+
key: 'acciones_rapidas',
|
|
14
|
+
label: 'Accesos Directos',
|
|
15
|
+
actions: [
|
|
16
|
+
{ label: 'Nuevo Pedido', icon: 'plus', navigateTo: '/admin/orders?action=create' },
|
|
17
|
+
{ label: 'Configuración', icon: 'settings', navigateTo: '/admin/pickpass' },
|
|
18
|
+
{ label: 'Guía Externa', icon: 'external', navigateTo: 'https://ejemplo.com/guia' },
|
|
19
|
+
],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await TestBed.configureTestingModule({
|
|
24
|
+
imports: [QuickActions],
|
|
25
|
+
providers: [provideRouter([])],
|
|
26
|
+
}).compileComponents();
|
|
27
|
+
fixture = TestBed.createComponent(QuickActions);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('1. renderiza todas las acciones declaradas con sus textos', () => {
|
|
31
|
+
fixture.componentRef.setInput('widget', widget);
|
|
32
|
+
fixture.detectChanges();
|
|
33
|
+
|
|
34
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
35
|
+
expect(root.textContent).toContain('Accesos Directos');
|
|
36
|
+
expect(root.textContent).toContain('Nuevo Pedido');
|
|
37
|
+
expect(root.textContent).toContain('Configuración');
|
|
38
|
+
expect(root.textContent).toContain('Guía Externa');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('2. genera enlaces internos con routerLink', () => {
|
|
42
|
+
fixture.componentRef.setInput('widget', widget);
|
|
43
|
+
fixture.detectChanges();
|
|
44
|
+
|
|
45
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
46
|
+
const links = root.querySelectorAll('a');
|
|
47
|
+
expect(links.length).toBe(3);
|
|
48
|
+
expect(links[0].getAttribute('href')).toBe('/admin/orders?action=create');
|
|
49
|
+
expect(links[1].getAttribute('href')).toBe('/admin/pickpass');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('3. genera enlaces externos con target blank y rel noopener', () => {
|
|
53
|
+
fixture.componentRef.setInput('widget', widget);
|
|
54
|
+
fixture.detectChanges();
|
|
55
|
+
|
|
56
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
57
|
+
const links = root.querySelectorAll('a');
|
|
58
|
+
expect(links[2].getAttribute('href')).toBe('https://ejemplo.com/guia');
|
|
59
|
+
expect(links[2].getAttribute('target')).toBe('_blank');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Component, input } from '@angular/core';
|
|
2
|
+
import { RouterLink } from '@angular/router';
|
|
3
|
+
import {
|
|
4
|
+
LucideExternalLink,
|
|
5
|
+
LucideFileText,
|
|
6
|
+
LucidePlus,
|
|
7
|
+
LucideSettings,
|
|
8
|
+
LucideZap,
|
|
9
|
+
} from '@lucide/angular';
|
|
10
|
+
|
|
11
|
+
import type { QuickActionsWidget } from '../../core/models/schema.model';
|
|
12
|
+
import { parseNavigationUrl, type ParsedRoute } from '../../core/utils/template-interpolator';
|
|
13
|
+
|
|
14
|
+
const BUTTON_CLASSES: Record<string, string> = {
|
|
15
|
+
primary:
|
|
16
|
+
'bg-primary text-surface hover:bg-primary/90 font-medium px-4 py-2.5 rounded-lg inline-flex items-center gap-2 text-sm transition-colors',
|
|
17
|
+
outline:
|
|
18
|
+
'border border-neutral/40 bg-surface text-neutral hover:bg-neutral/10 font-medium px-4 py-2.5 rounded-lg inline-flex items-center gap-2 text-sm transition-colors',
|
|
19
|
+
secondary:
|
|
20
|
+
'bg-neutral/10 text-neutral-light hover:bg-neutral/20 font-medium px-4 py-2.5 rounded-lg inline-flex items-center gap-2 text-sm transition-colors',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Grid de accesos directos y atajos operativos para el Dashboard.
|
|
25
|
+
*/
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'app-quick-actions',
|
|
28
|
+
imports: [RouterLink, LucidePlus, LucideSettings, LucideFileText, LucideExternalLink, LucideZap],
|
|
29
|
+
template: `
|
|
30
|
+
<article class="flex h-full flex-col rounded-xl border border-neutral/20 bg-surface p-4">
|
|
31
|
+
<h2 class="mb-3 text-sm font-semibold text-neutral-light">{{ widget().label }}</h2>
|
|
32
|
+
|
|
33
|
+
<div class="grid flex-1 grid-cols-1 gap-2.5 sm:grid-cols-2">
|
|
34
|
+
@for (action of widget().actions; track action.label) {
|
|
35
|
+
@if (isExternal(action.navigateTo)) {
|
|
36
|
+
<a
|
|
37
|
+
[href]="action.navigateTo"
|
|
38
|
+
target="_blank"
|
|
39
|
+
rel="noopener noreferrer"
|
|
40
|
+
[class]="buttonClass(action.variant)"
|
|
41
|
+
>
|
|
42
|
+
@switch (action.icon) {
|
|
43
|
+
@case ('plus') {
|
|
44
|
+
<svg lucidePlus size="16" class="shrink-0" />
|
|
45
|
+
}
|
|
46
|
+
@case ('settings') {
|
|
47
|
+
<svg lucideSettings size="16" class="shrink-0" />
|
|
48
|
+
}
|
|
49
|
+
@case ('document') {
|
|
50
|
+
<svg lucideFileText size="16" class="shrink-0" />
|
|
51
|
+
}
|
|
52
|
+
@default {
|
|
53
|
+
<svg lucideExternalLink size="16" class="shrink-0" />
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
<span class="truncate">{{ action.label }}</span>
|
|
57
|
+
</a>
|
|
58
|
+
} @else {
|
|
59
|
+
<a
|
|
60
|
+
[routerLink]="parseRoute(action.navigateTo).path"
|
|
61
|
+
[queryParams]="parseRoute(action.navigateTo).queryParams"
|
|
62
|
+
[class]="buttonClass(action.variant)"
|
|
63
|
+
>
|
|
64
|
+
@switch (action.icon) {
|
|
65
|
+
@case ('plus') {
|
|
66
|
+
<svg lucidePlus size="16" class="shrink-0" />
|
|
67
|
+
}
|
|
68
|
+
@case ('settings') {
|
|
69
|
+
<svg lucideSettings size="16" class="shrink-0" />
|
|
70
|
+
}
|
|
71
|
+
@case ('document') {
|
|
72
|
+
<svg lucideFileText size="16" class="shrink-0" />
|
|
73
|
+
}
|
|
74
|
+
@default {
|
|
75
|
+
<svg lucideZap size="16" class="shrink-0" />
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
<span class="truncate">{{ action.label }}</span>
|
|
79
|
+
</a>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
</div>
|
|
83
|
+
</article>
|
|
84
|
+
`,
|
|
85
|
+
})
|
|
86
|
+
export class QuickActions {
|
|
87
|
+
readonly widget = input.required<QuickActionsWidget>();
|
|
88
|
+
|
|
89
|
+
protected isExternal(url: string): boolean {
|
|
90
|
+
return /^https?:\/\//i.test(url);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
protected parseRoute(url: string): ParsedRoute {
|
|
94
|
+
return parseNavigationUrl(url);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
protected buttonClass(variant?: string): string {
|
|
98
|
+
return BUTTON_CLASSES[variant || 'secondary'] || BUTTON_CLASSES['secondary'];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del widget `record-list` (R3.6). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'widget',
|
|
6
|
+
type: 'record-list',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./record-list').then((module) => module.RecordList),
|
|
9
|
+
surfaces: ['display'],
|
|
10
|
+
// `RecordList` dibuja filas: fuera de un array no hay nada que listar, y un
|
|
11
|
+
// objeto suelto no debe colarse como si fuera una única fila.
|
|
12
|
+
inputs: (config, extras) => ({
|
|
13
|
+
widget: config,
|
|
14
|
+
data: Array.isArray(extras.data) ? (extras.data as Record<string, unknown>[]) : null,
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { provideRouter } from '@angular/router';
|
|
2
|
+
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
3
|
+
import { describe, expect, it, beforeEach } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type { RecordListWidget } from '../../core/models/schema.model';
|
|
6
|
+
import { RecordList } from './record-list';
|
|
7
|
+
|
|
8
|
+
describe('RecordList', () => {
|
|
9
|
+
let fixture: ComponentFixture<RecordList>;
|
|
10
|
+
|
|
11
|
+
const widget: RecordListWidget = {
|
|
12
|
+
type: 'record-list',
|
|
13
|
+
key: 'pedidos_pendientes',
|
|
14
|
+
label: 'Pedidos que Requieren Atención',
|
|
15
|
+
columns: ['ref', 'customer_name', 'status'],
|
|
16
|
+
action: {
|
|
17
|
+
label: 'Ver Ficha',
|
|
18
|
+
routeTemplate: '/admin/orders?id={ref}',
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
await TestBed.configureTestingModule({
|
|
24
|
+
imports: [RecordList],
|
|
25
|
+
providers: [provideRouter([])],
|
|
26
|
+
}).compileComponents();
|
|
27
|
+
fixture = TestBed.createComponent(RecordList);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('1. renderiza las filas y columnas declaradas', () => {
|
|
31
|
+
const data = [
|
|
32
|
+
{ ref: 'ORD-001', customer_name: 'Cliente Uno', status: 'PENDIENTE' },
|
|
33
|
+
{ ref: 'ORD-002', customer_name: 'Cliente Dos', status: 'PENDIENTE' },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
fixture.componentRef.setInput('widget', widget);
|
|
37
|
+
fixture.componentRef.setInput('data', data);
|
|
38
|
+
fixture.detectChanges();
|
|
39
|
+
|
|
40
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
41
|
+
expect(root.textContent).toContain('Pedidos que Requieren Atención');
|
|
42
|
+
expect(root.textContent).toContain('ORD-001');
|
|
43
|
+
expect(root.textContent).toContain('Cliente Uno');
|
|
44
|
+
expect(root.textContent).toContain('ORD-002');
|
|
45
|
+
expect(root.textContent).toContain('Cliente Dos');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('2. renderiza estado vacío cuando no hay datos', () => {
|
|
49
|
+
fixture.componentRef.setInput('widget', widget);
|
|
50
|
+
fixture.componentRef.setInput('data', []);
|
|
51
|
+
fixture.detectChanges();
|
|
52
|
+
|
|
53
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
54
|
+
expect(root.textContent).toContain('Sin registros pendientes');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('3. genera el enlace de acción con la ruta interpolada', () => {
|
|
58
|
+
const data = [{ ref: 'ORD-777', customer_name: 'Carlos', status: 'PENDIENTE' }];
|
|
59
|
+
|
|
60
|
+
fixture.componentRef.setInput('widget', widget);
|
|
61
|
+
fixture.componentRef.setInput('data', data);
|
|
62
|
+
fixture.detectChanges();
|
|
63
|
+
|
|
64
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
65
|
+
const link = root.querySelector('a');
|
|
66
|
+
expect(link).not.toBeNull();
|
|
67
|
+
expect(link?.getAttribute('href')).toBe('/admin/orders?id=ORD-777');
|
|
68
|
+
expect(link?.textContent?.trim()).toBe('Ver Ficha');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('4. maneja datos nulos sin romper', () => {
|
|
72
|
+
fixture.componentRef.setInput('widget', widget);
|
|
73
|
+
fixture.componentRef.setInput('data', null);
|
|
74
|
+
fixture.detectChanges();
|
|
75
|
+
|
|
76
|
+
const root = fixture.nativeElement as HTMLElement;
|
|
77
|
+
expect(root.textContent).toContain('Sin registros pendientes');
|
|
78
|
+
});
|
|
79
|
+
});
|