@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,87 @@
|
|
|
1
|
+
import { Service, inject } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
actionType,
|
|
5
|
+
getAction,
|
|
6
|
+
type ActionContext,
|
|
7
|
+
type ActionRequest,
|
|
8
|
+
type ActionResult,
|
|
9
|
+
} from './action-registry';
|
|
10
|
+
import type { ResourceAction } from '../core/models/schema.model';
|
|
11
|
+
import { ConfigService } from '../core/services/config.service';
|
|
12
|
+
import { ErrorMapperService } from '../core/services/error-mapper.service';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Ejecuta una acción declarada en el schema (R3.9 · `refactormotor.md`).
|
|
16
|
+
*
|
|
17
|
+
* Sustituye el dispatch por tipo que hoy vive en `list-view` y en las
|
|
18
|
+
* copias de `detail-view` y `post-create-modal` (R3.11). El camino siempre
|
|
19
|
+
* es el mismo:
|
|
20
|
+
*
|
|
21
|
+
* 1. resolver el descriptor (si no existe, **rompe** — nada de «disponible
|
|
22
|
+
* próximamente», que hoy oculta esquemas mal declarados);
|
|
23
|
+
* 2. pedir confirmación si el descriptor la exige;
|
|
24
|
+
* 3. montar el `ActionContext` (petición + `vars` de interpolación);
|
|
25
|
+
* 4. ejecutar y, si el descriptor lanza, notificar el error y devolver
|
|
26
|
+
* `{ ok: false }` para que la vista no trate el fallo como éxito.
|
|
27
|
+
*/
|
|
28
|
+
@Service()
|
|
29
|
+
export class ActionService {
|
|
30
|
+
private readonly config = inject(ConfigService);
|
|
31
|
+
private readonly errores = inject(ErrorMapperService);
|
|
32
|
+
|
|
33
|
+
async run(action: ResourceAction, request: ActionRequest): Promise<ActionResult> {
|
|
34
|
+
const type = actionType(action);
|
|
35
|
+
const descriptor = getAction(type);
|
|
36
|
+
if (!descriptor) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[actions] «${action.label}» no tiene descriptor registrado (type "${type}").`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (descriptor.confirm) {
|
|
43
|
+
const aceptado = await request.toast.confirm({
|
|
44
|
+
title: action.label,
|
|
45
|
+
message: confirmMessage(action),
|
|
46
|
+
confirmLabel: 'Confirmar',
|
|
47
|
+
cancelLabel: 'Cancelar',
|
|
48
|
+
});
|
|
49
|
+
if (!aceptado) {
|
|
50
|
+
return { ok: false };
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// `copy-share` (R8.7) resuelve su mensaje contra la config global
|
|
55
|
+
// (`share_message_template`). Si el load del bootstrap no llegó (arranque
|
|
56
|
+
// sin sesión o red lenta), lo pedimos ahora —con la sesión ya iniciada—
|
|
57
|
+
// ANTES de congelar `vars`: si no, el mensaje copiado degradaría a
|
|
58
|
+
// «sólo la URL» hasta el próximo F5.
|
|
59
|
+
if (descriptor.type === 'copy-share') {
|
|
60
|
+
await this.config.ensureLoaded();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const ctx: ActionContext = { ...request, vars: this.vars(request.record) };
|
|
64
|
+
try {
|
|
65
|
+
return await descriptor.run(ctx, action);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
request.toast.error(this.errores.messageOf(err));
|
|
68
|
+
return { ok: false };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Config global + registro. Sin registro (`header`, `bulk`) el contexto de
|
|
74
|
+
* interpolación queda vacío: el descriptor no tiene nada que interpolar.
|
|
75
|
+
*
|
|
76
|
+
* Público porque `detail-view` sólo **presenta** una acción (pinta
|
|
77
|
+
* `<app-copy-share>`) en vez de ejecutarla, y aun así necesita el mismo
|
|
78
|
+
* contexto: R3.11 acabó con la tercera copia de esta lógica.
|
|
79
|
+
*/
|
|
80
|
+
vars(record: Record<string, unknown> | null): Record<string, unknown> {
|
|
81
|
+
return record ? this.config.buildInterpolationContext(record) : {};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function confirmMessage(action: ResourceAction): string {
|
|
86
|
+
return 'confirmMessage' in action && action.confirmMessage ? action.confirmMessage : action.label;
|
|
87
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi, type MockInstance } from 'vitest';
|
|
2
|
+
import { of } from 'rxjs';
|
|
3
|
+
|
|
4
|
+
import { getAction, type ActionContext } from './action-registry';
|
|
5
|
+
import { resolveShare } from './descriptors';
|
|
6
|
+
import type { CopyShareAction, NavigateAction, TriggerAction } from '../core/models/schema.model';
|
|
7
|
+
import type { ApiService } from '../core/services/api.service';
|
|
8
|
+
import type { NotificationService } from '../core/services/notification.service';
|
|
9
|
+
import type { Router } from '@angular/router';
|
|
10
|
+
|
|
11
|
+
const VARS = { public_base_url: 'https://demo', ref: 'p1', customer_name: 'Ana' };
|
|
12
|
+
|
|
13
|
+
const COMPARTIR: CopyShareAction = {
|
|
14
|
+
id: 'share',
|
|
15
|
+
label: 'Compartir',
|
|
16
|
+
type: 'copy-share',
|
|
17
|
+
urlTemplate: '{public_base_url}/p/{ref}',
|
|
18
|
+
shareTextTemplate: 'Hola {customer_name}, tu pedido {ref}',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function contexto(): ActionContext {
|
|
22
|
+
return {
|
|
23
|
+
record: { ref: 'p1' },
|
|
24
|
+
resource: 'orders',
|
|
25
|
+
api: { request: vi.fn().mockReturnValue(of({ ok: true })) } as unknown as ApiService,
|
|
26
|
+
router: { navigateByUrl: vi.fn().mockResolvedValue(true) } as unknown as Router,
|
|
27
|
+
toast: {
|
|
28
|
+
success: vi.fn(),
|
|
29
|
+
error: vi.fn(),
|
|
30
|
+
info: vi.fn(),
|
|
31
|
+
confirm: vi.fn(),
|
|
32
|
+
dismiss: vi.fn(),
|
|
33
|
+
} as unknown as NotificationService,
|
|
34
|
+
user: null,
|
|
35
|
+
keyField: 'ref',
|
|
36
|
+
vars: VARS,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** El `run` de un tipo registrado; lanza si no existe (estado incoherente). */
|
|
41
|
+
function ejecutar(type: string, action: CopyShareAction | NavigateAction | TriggerAction) {
|
|
42
|
+
const descriptor = getAction(type);
|
|
43
|
+
if (!descriptor) throw new Error(`sin descriptor «${type}»`);
|
|
44
|
+
return descriptor.run;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('resolveShare', () => {
|
|
48
|
+
it('interpola enlace y mensaje con el contexto del registro', () => {
|
|
49
|
+
expect(resolveShare(COMPARTIR, VARS)).toEqual({
|
|
50
|
+
url: 'https://demo/p/p1',
|
|
51
|
+
shareText: 'Hola Ana, tu pedido p1',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('sin shareTextTemplate cae a share_message_template (config global)', () => {
|
|
56
|
+
const action: CopyShareAction = { ...COMPARTIR, shareTextTemplate: undefined };
|
|
57
|
+
expect(resolveShare(action, { ...VARS, share_message_template: 'Hola {ref}' })).toEqual({
|
|
58
|
+
url: 'https://demo/p/p1',
|
|
59
|
+
shareText: 'Hola p1',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('sin plantilla ninguna el mensaje queda vacío', () => {
|
|
64
|
+
const action: CopyShareAction = { ...COMPARTIR, shareTextTemplate: undefined };
|
|
65
|
+
expect(resolveShare(action, VARS).shareText).toBe('');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('descriptores de acción', () => {
|
|
70
|
+
const escritor = vi.fn().mockResolvedValue(undefined);
|
|
71
|
+
let abrir: MockInstance;
|
|
72
|
+
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
escritor.mockClear();
|
|
75
|
+
Object.defineProperty(window.navigator, 'clipboard', {
|
|
76
|
+
value: { writeText: escritor },
|
|
77
|
+
configurable: true,
|
|
78
|
+
});
|
|
79
|
+
abrir = vi.spyOn(window, 'open').mockReturnValue(null);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
vi.restoreAllMocks();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('copy-share copia el mensaje completo y lo anuncia', async () => {
|
|
87
|
+
const ctx = contexto();
|
|
88
|
+
const resultado = await ejecutar('copy-share', COMPARTIR)(ctx, COMPARTIR);
|
|
89
|
+
|
|
90
|
+
expect(escritor).toHaveBeenCalledWith('Hola Ana, tu pedido p1 https://demo/p/p1');
|
|
91
|
+
expect(ctx.toast.success).toHaveBeenCalledWith('Mensaje copiado al portapapeles.');
|
|
92
|
+
expect(resultado).toEqual({ ok: true });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('navigate abre una URL absoluta en otra pestaña', async () => {
|
|
96
|
+
const action: NavigateAction = {
|
|
97
|
+
id: 'ver',
|
|
98
|
+
label: 'Ver público',
|
|
99
|
+
urlTemplate: '{public_base_url}/p/{ref}',
|
|
100
|
+
};
|
|
101
|
+
const ctx = contexto();
|
|
102
|
+
|
|
103
|
+
await ejecutar('navigate', action)(ctx, action);
|
|
104
|
+
|
|
105
|
+
expect(abrir).toHaveBeenCalledWith('https://demo/p/p1', '_blank', 'noopener');
|
|
106
|
+
expect(ctx.router.navigateByUrl).not.toHaveBeenCalled();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('navigate entrega una ruta interna al router', async () => {
|
|
110
|
+
const action: NavigateAction = { id: 'editar', label: 'Editar', urlTemplate: '/orders/p1' };
|
|
111
|
+
const ctx = contexto();
|
|
112
|
+
|
|
113
|
+
await ejecutar('navigate', action)(ctx, action);
|
|
114
|
+
|
|
115
|
+
expect(ctx.router.navigateByUrl).toHaveBeenCalledWith('/orders/p1');
|
|
116
|
+
expect(abrir).not.toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('trigger arma la ruta lógica de la acción declarada y pide recarga', async () => {
|
|
120
|
+
const action: TriggerAction = {
|
|
121
|
+
id: 'markDelivered',
|
|
122
|
+
label: 'Marcar entregado',
|
|
123
|
+
type: 'trigger',
|
|
124
|
+
};
|
|
125
|
+
const ctx = contexto();
|
|
126
|
+
|
|
127
|
+
const resultado = await ejecutar('trigger', action)(ctx, action);
|
|
128
|
+
|
|
129
|
+
expect(ctx.api.request).toHaveBeenCalledWith('POST', '/admin/orders/p1/actions/markDelivered');
|
|
130
|
+
expect(resultado).toEqual({ ok: true, reload: true });
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('trigger usa `name` cuando en el back se llama distinto que el `id`', async () => {
|
|
134
|
+
const action: TriggerAction = {
|
|
135
|
+
id: 'reenviar',
|
|
136
|
+
label: 'Reenviar pin',
|
|
137
|
+
type: 'trigger',
|
|
138
|
+
name: 'resendPin',
|
|
139
|
+
};
|
|
140
|
+
const ctx = contexto();
|
|
141
|
+
|
|
142
|
+
await ejecutar('trigger', action)(ctx, action);
|
|
143
|
+
|
|
144
|
+
expect(ctx.api.request).toHaveBeenCalledWith('POST', '/admin/orders/p1/actions/resendPin');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('sin clave de registro no dispara nada y lo dice (fail-loud)', async () => {
|
|
148
|
+
const action: TriggerAction = { id: 'markDelivered', label: 'Marcar', type: 'trigger' };
|
|
149
|
+
const ctx = contexto();
|
|
150
|
+
ctx.record = null;
|
|
151
|
+
|
|
152
|
+
await expect(ejecutar('trigger', action)(ctx, action)).rejects.toThrow('necesita la clave ref');
|
|
153
|
+
expect(ctx.api.request).not.toHaveBeenCalled();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { firstValueFrom } from 'rxjs';
|
|
2
|
+
import { LucideShare2 } from '@lucide/angular';
|
|
3
|
+
|
|
4
|
+
import type { ActionDescriptor } from './action-registry';
|
|
5
|
+
import type {
|
|
6
|
+
CopyShareAction,
|
|
7
|
+
NavigateAction,
|
|
8
|
+
ResourceAction,
|
|
9
|
+
TriggerAction,
|
|
10
|
+
} from '../core/models/schema.model';
|
|
11
|
+
import { buildShareMessage, writeClipboard } from '../core/utils/clipboard';
|
|
12
|
+
import { interpolateTemplate } from '../core/utils/template-interpolator';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Descriptores de las tres acciones que hoy declara el modelo
|
|
16
|
+
* (R3.10 · `doc/plan-refactor-motor.md` §3.2).
|
|
17
|
+
*
|
|
18
|
+
* Éste es el fichero que **cierra la triplicación de `copy-share`**: la
|
|
19
|
+
* resolución de enlace y de mensaje vivía en `list-view.runAction`,
|
|
20
|
+
* `detail-view.resolveUrl/resolveShareText` y
|
|
21
|
+
* `post-create-modal.resolvedShareUrl/resolvedShareText`. Aquí se resuelve
|
|
22
|
+
* una sola vez (`resolveShare`) y las vistas consumen ese resultado en R3.11.
|
|
23
|
+
*
|
|
24
|
+
* Se registra al importar, desde `action-registry.ts`, con el mismo criterio
|
|
25
|
+
* que el registro de render: sin esperar a DI, porque el registro es la única
|
|
26
|
+
* fuente de verdad en runtime.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** Enlace y mensaje ya interpolados, listos para copiar o compartir. */
|
|
30
|
+
export interface ResolvedShare {
|
|
31
|
+
url: string;
|
|
32
|
+
shareText: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `true` cuando la acción es de las que las vistas presentan como botón de
|
|
37
|
+
* compartir (`detail-view`, `post-create-modal`).
|
|
38
|
+
*
|
|
39
|
+
* Vive acá y no en la vista porque es **el único sitio donde se puede nombrar
|
|
40
|
+
* `'copy-share'`**: fuera de `descriptors.ts` ninguna vista decide por tipo
|
|
41
|
+
* (criterio de R3.11).
|
|
42
|
+
*/
|
|
43
|
+
export function isShareAction(action: ResourceAction): action is CopyShareAction {
|
|
44
|
+
return action.type === 'copy-share';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `true` cuando la acción es un `trigger` (acción de dominio del back).
|
|
49
|
+
*
|
|
50
|
+
* Mismo criterio que `isShareAction`: el único sitio que nombra el tipo,
|
|
51
|
+
* para que las vistas no decidan por string literal (R3.11). `detail-view`
|
|
52
|
+
* lo usa en R8.8 para renderizar «Regenerar PIN» junto a los botones de
|
|
53
|
+
* compartir.
|
|
54
|
+
*/
|
|
55
|
+
export function isTriggerAction(action: ResourceAction): action is TriggerAction {
|
|
56
|
+
return action.type === 'trigger';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resuelve `urlTemplate` y el mensaje de compartir contra `vars`
|
|
61
|
+
* (config global + registro).
|
|
62
|
+
*
|
|
63
|
+
* **Única implementación** de esta lógica en el admin. El fallback a
|
|
64
|
+
* `share_message_template` (la plantilla que declara el singleton de
|
|
65
|
+
* configuración) lo aplican los tres puntos por igual: hasta ahora la fila
|
|
66
|
+
* del listado no lo honraba y se quedaba sin saludo.
|
|
67
|
+
*/
|
|
68
|
+
export function resolveShare(
|
|
69
|
+
action: CopyShareAction,
|
|
70
|
+
vars: Record<string, unknown>,
|
|
71
|
+
): ResolvedShare {
|
|
72
|
+
const template =
|
|
73
|
+
action.shareTextTemplate || (vars['share_message_template'] as string | undefined);
|
|
74
|
+
return {
|
|
75
|
+
url: interpolateTemplate(action.urlTemplate, vars),
|
|
76
|
+
shareText: template ? interpolateTemplate(template, vars) : '',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** `copy-share` — copia (o comparte) el mensaje completo del enlace público. */
|
|
81
|
+
const copyShare: ActionDescriptor = {
|
|
82
|
+
type: 'copy-share',
|
|
83
|
+
surfaces: ['row', 'detail', 'post-create'],
|
|
84
|
+
// Icono de la fila del listado: la vista sólo decide entre «icono» y
|
|
85
|
+
// «rótulo», nunca por tipo. Se pasa la CLASE, no el nombre: sin
|
|
86
|
+
// `provideLucideIcons` no hay riesgo de `Unable to resolve icon`.
|
|
87
|
+
icon: LucideShare2,
|
|
88
|
+
run: async (ctx, action) => {
|
|
89
|
+
const { url, shareText } = resolveShare(action as CopyShareAction, ctx.vars);
|
|
90
|
+
const message = buildShareMessage(url, shareText);
|
|
91
|
+
const copied = await writeClipboard(message);
|
|
92
|
+
if (copied) {
|
|
93
|
+
ctx.toast.success('Mensaje copiado al portapapeles.');
|
|
94
|
+
} else {
|
|
95
|
+
ctx.toast.info(`Copiar manualmente: ${message}`);
|
|
96
|
+
}
|
|
97
|
+
return { ok: true };
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* `navigate` — abre la plantilla resuelta: URL absoluta en otra pestaña,
|
|
103
|
+
* ruta interna en el router. `NavigateAction.type` es opcional, así que el
|
|
104
|
+
* servicio llega acá también con una acción que no declara `type`.
|
|
105
|
+
*/
|
|
106
|
+
const navigate: ActionDescriptor = {
|
|
107
|
+
type: 'navigate',
|
|
108
|
+
surfaces: ['row', 'detail'],
|
|
109
|
+
run: async (ctx, action) => {
|
|
110
|
+
const url = interpolateTemplate((action as NavigateAction).urlTemplate, ctx.vars);
|
|
111
|
+
if (/^https?:\/\//.test(url)) {
|
|
112
|
+
window.open(url, '_blank', 'noopener');
|
|
113
|
+
} else {
|
|
114
|
+
await ctx.router.navigateByUrl(url);
|
|
115
|
+
}
|
|
116
|
+
return { ok: true };
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `trigger` — dispara una **acción de dominio declarada en el back** (B3).
|
|
122
|
+
*
|
|
123
|
+
* **R3.12:** el destino ya no lo declara el schema (`endpoint` y `method`
|
|
124
|
+
* murieron en este cambio). Se deriva de `resource` + clave del registro +
|
|
125
|
+
* `name` (o `id`), que es exactamente la ruta que el back publica solo en
|
|
126
|
+
* R5.4: `POST /admin/<recurso>/:id/actions/<name>`.
|
|
127
|
+
*
|
|
128
|
+
* ⚠️ **O2:** la ruta lógica viaja en el `path` del envelope, no en la URL —
|
|
129
|
+
* el HTTP sigue siendo el `POST API_URL` de siempre.
|
|
130
|
+
*
|
|
131
|
+
* Siempre pide confirmación: es la única acción de las tres que puede dejar
|
|
132
|
+
* una cola de trabajo hecha.
|
|
133
|
+
*/
|
|
134
|
+
const trigger: ActionDescriptor = {
|
|
135
|
+
type: 'trigger',
|
|
136
|
+
surfaces: ['row', 'detail'],
|
|
137
|
+
confirm: true,
|
|
138
|
+
run: async (ctx, action) => {
|
|
139
|
+
const declared = action as TriggerAction;
|
|
140
|
+
const rawKey = ctx.record?.[ctx.keyField];
|
|
141
|
+
if (rawKey === null || rawKey === undefined || rawKey === '') {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`La acción «${declared.label}» necesita la clave ${ctx.keyField} del registro.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
const name = declared.name ?? declared.id;
|
|
147
|
+
const route = `/admin/${ctx.resource}/${String(rawKey)}/actions/${name}`;
|
|
148
|
+
await firstValueFrom(ctx.api.request('POST', route));
|
|
149
|
+
return { ok: true, reload: true };
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Catálogo — una línea por acción, en el orden en que se registran. */
|
|
154
|
+
export const actionDescriptors: readonly ActionDescriptor[] = [copyShare, navigate, trigger];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FieldSchema, ListColumn } from '../core/models/schema.model';
|
|
2
|
+
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Contexto de una celda de listado (R4.3 · `kind: 'column-format'`).
|
|
7
|
+
*
|
|
8
|
+
* Es lo que antes cada `@case` de `list-view` pedía a la vista como
|
|
9
|
+
* `row`/`column` y lo que el descriptor cuelga de `extras.data`: el
|
|
10
|
+
* componente de formato no conoce a la vista, sólo a su celda.
|
|
11
|
+
*
|
|
12
|
+
* `fields`, `canUpdate`, `saving`, `currency` y `toggle` viajan acá porque
|
|
13
|
+
* son decisiones de la vista (permisos, moneda del schema, escritura de la
|
|
14
|
+
* fila); un formato nuevo no necesita tocar `list-view` para leerlas.
|
|
15
|
+
*/
|
|
16
|
+
export interface ColumnCell {
|
|
17
|
+
/** Fila completa. */
|
|
18
|
+
row: Row;
|
|
19
|
+
/** Columna que se pinta (`key`, `label`, `format`). */
|
|
20
|
+
column: ListColumn;
|
|
21
|
+
/** Campos del recurso: de acá salen `trueLabel`/`falseLabel`. */
|
|
22
|
+
fields: FieldSchema[];
|
|
23
|
+
/** `titleField` de la fila, para el `aria-label` del interruptor. */
|
|
24
|
+
title: string;
|
|
25
|
+
/** ¿La fila admite escritura? (`ListView.canUpdate`) */
|
|
26
|
+
canUpdate: boolean;
|
|
27
|
+
/** ¿La fila está guardando? (`ListView.savingKey === keyOf(row)`) */
|
|
28
|
+
saving: boolean;
|
|
29
|
+
/** Moneda del schema, para el formato `currency`. */
|
|
30
|
+
currency: string;
|
|
31
|
+
/** Escribe el valor en la fila. Sólo lo usa el interruptor `active`. */
|
|
32
|
+
toggle?: (checked: boolean) => void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { SUPPORTED_FIELD_TYPE_LIST } from '../fields/descriptors';
|
|
4
|
+
import type { FieldSchema, ListColumn } from '../core/models/schema.model';
|
|
5
|
+
import { FORMATO_POR_DEFECTO, formatoDeColumna, formatoPorTipo } from './column-defaults';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* R4.4 — «declarada, no implícita»: la tabla de formato por defecto.
|
|
9
|
+
*
|
|
10
|
+
* Dos promesas: que la tabla cubra **todos** los tipos del catálogo, y que
|
|
11
|
+
* una columna sin `format` pinta lo que la tabla dice — no un `?? 'text'`
|
|
12
|
+
* implícito que dependía de la memoria de quien escribía el schema.
|
|
13
|
+
*/
|
|
14
|
+
describe('R4.4 — formato por defecto según field.type', () => {
|
|
15
|
+
const columna = (parcial: Partial<ListColumn> & Pick<ListColumn, 'key'>): ListColumn => ({
|
|
16
|
+
label: parcial.key,
|
|
17
|
+
...parcial,
|
|
18
|
+
});
|
|
19
|
+
const campo = (key: string, type: FieldSchema['type']): FieldSchema =>
|
|
20
|
+
({ key, label: key, type }) as FieldSchema;
|
|
21
|
+
|
|
22
|
+
it('cubre exactamente el catálogo de tipos de campo', () => {
|
|
23
|
+
// El `Record<FieldType, …>` ya lo exige en compilación; el test además
|
|
24
|
+
// fija que ningún tipo se quede sin fila ni aparezca una fantasma.
|
|
25
|
+
expect(Object.keys(FORMATO_POR_DEFECTO).sort()).toEqual([...SUPPORTED_FIELD_TYPE_LIST].sort());
|
|
26
|
+
expect(SUPPORTED_FIELD_TYPE_LIST.length).toBe(24);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('repasa la paridad tipo a tipo con el display del detalle', () => {
|
|
30
|
+
const esperado: Record<string, string> = {
|
|
31
|
+
text: 'text',
|
|
32
|
+
textarea: 'text',
|
|
33
|
+
slug: 'text',
|
|
34
|
+
url: 'text',
|
|
35
|
+
email: 'text',
|
|
36
|
+
phone: 'text',
|
|
37
|
+
'readonly-text': 'text',
|
|
38
|
+
relation: 'text',
|
|
39
|
+
group: 'text',
|
|
40
|
+
'key-value': 'text',
|
|
41
|
+
'string-list': 'text',
|
|
42
|
+
multiselect: 'text',
|
|
43
|
+
time: 'text',
|
|
44
|
+
list: 'text',
|
|
45
|
+
date: 'date',
|
|
46
|
+
number: 'number',
|
|
47
|
+
currency: 'currency',
|
|
48
|
+
boolean: 'boolean',
|
|
49
|
+
select: 'badge',
|
|
50
|
+
image: 'thumbnail',
|
|
51
|
+
file: 'text',
|
|
52
|
+
color: 'text',
|
|
53
|
+
'rich-select': 'badge',
|
|
54
|
+
dropdown: 'badge',
|
|
55
|
+
};
|
|
56
|
+
expect(Object.keys(FORMATO_POR_DEFECTO).sort()).toEqual(Object.keys(esperado).sort());
|
|
57
|
+
for (const [tipo, formato] of Object.entries(esperado)) {
|
|
58
|
+
expect(formatoPorTipo(tipo), `tipo ${tipo}`).toBe(formato);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('la declaración del schema manda sobre la tabla', () => {
|
|
63
|
+
expect(
|
|
64
|
+
formatoDeColumna(columna({ key: 'status', format: 'text' }), [campo('status', 'select')]),
|
|
65
|
+
).toBe('text');
|
|
66
|
+
expect(
|
|
67
|
+
formatoDeColumna(columna({ key: 'price', format: 'badge' }), [campo('price', 'currency')]),
|
|
68
|
+
).toBe('badge');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('sin formato declarado usa el tipo del campo', () => {
|
|
72
|
+
const campos = [
|
|
73
|
+
campo('status', 'select'),
|
|
74
|
+
campo('pickup_date', 'date'),
|
|
75
|
+
campo('active', 'boolean'),
|
|
76
|
+
campo('photo', 'image'),
|
|
77
|
+
campo('price', 'currency'),
|
|
78
|
+
campo('name', 'slug'),
|
|
79
|
+
];
|
|
80
|
+
expect(formatoDeColumna(columna({ key: 'status' }), campos)).toBe('badge');
|
|
81
|
+
expect(formatoDeColumna(columna({ key: 'pickup_date' }), campos)).toBe('date');
|
|
82
|
+
expect(formatoDeColumna(columna({ key: 'active' }), campos)).toBe('boolean');
|
|
83
|
+
expect(formatoDeColumna(columna({ key: 'photo' }), campos)).toBe('thumbnail');
|
|
84
|
+
expect(formatoDeColumna(columna({ key: 'price' }), campos)).toBe('currency');
|
|
85
|
+
expect(formatoDeColumna(columna({ key: 'name' }), campos)).toBe('text');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('una columna sin campo asociado o con tipo mal escrito degrada a text', () => {
|
|
89
|
+
// El `@default` del `@switch` que R4.3 reemplazó: la degradación es la
|
|
90
|
+
// misma y nunca lanza.
|
|
91
|
+
expect(formatoDeColumna(columna({ key: 'inventada' }), [campo('otra', 'text')])).toBe('text');
|
|
92
|
+
expect(formatoPorTipo('holograma')).toBeUndefined();
|
|
93
|
+
expect(
|
|
94
|
+
formatoDeColumna(columna({ key: 'raro' }), [
|
|
95
|
+
campo('raro', 'holograma' as FieldSchema['type']),
|
|
96
|
+
]),
|
|
97
|
+
).toBe('text');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FieldSchema,
|
|
3
|
+
FieldType,
|
|
4
|
+
ListColumn,
|
|
5
|
+
ListColumnFormat,
|
|
6
|
+
} from '../core/models/schema.model';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* **Tabla de formato de columna por defecto según `field.type`** (R4.4 del
|
|
10
|
+
* plan): «declarada, no implícita» — `column.format` sólo sirve para
|
|
11
|
+
* sobreescribirla.
|
|
12
|
+
*
|
|
13
|
+
* Es la hermana de `fields/display/display-defaults.ts` (R4.1): un solo
|
|
14
|
+
* sitio decide qué pinta una columna cuando el schema no dice nada, en vez
|
|
15
|
+
* de un `?? 'text'` repartido por la vista. El vocabulario se alinea con el
|
|
16
|
+
* display del detalle — `select` es chip, `date` es fecha, `image` es
|
|
17
|
+
* miniatura — con las dos concesiones que sólo el listado puede hacer:
|
|
18
|
+
* `boolean` baja al interruptor (R4.3) en vez de al chip, y `list`, que en
|
|
19
|
+
* la ficha es timeline, acá sigue siendo texto porque el registro de
|
|
20
|
+
* formatos no tiene timeline (D41).
|
|
21
|
+
*
|
|
22
|
+
* `Record<FieldType, …>` es el **tripwire de exhaustividad en compilación**:
|
|
23
|
+
* un tipo nuevo obliga a elegir su formato de lista antes de que el build
|
|
24
|
+
* pase, y un tipo retirado obliga a limpiar la entrada.
|
|
25
|
+
*/
|
|
26
|
+
export const FORMATO_POR_DEFECTO: Record<FieldType, ListColumnFormat> = {
|
|
27
|
+
text: 'text',
|
|
28
|
+
textarea: 'text',
|
|
29
|
+
slug: 'text',
|
|
30
|
+
url: 'text',
|
|
31
|
+
email: 'text',
|
|
32
|
+
phone: 'text',
|
|
33
|
+
'readonly-text': 'text',
|
|
34
|
+
relation: 'text',
|
|
35
|
+
group: 'text',
|
|
36
|
+
'key-value': 'text',
|
|
37
|
+
'string-list': 'text',
|
|
38
|
+
multiselect: 'text',
|
|
39
|
+
time: 'text',
|
|
40
|
+
list: 'text',
|
|
41
|
+
date: 'date',
|
|
42
|
+
number: 'number',
|
|
43
|
+
currency: 'currency',
|
|
44
|
+
boolean: 'boolean',
|
|
45
|
+
select: 'badge',
|
|
46
|
+
image: 'thumbnail',
|
|
47
|
+
// Un archivo no tiene miniatura: en la lista va como URL en crudo (R9.5.3).
|
|
48
|
+
file: 'text',
|
|
49
|
+
// Un color se pinta como texto (el hex #RRGGBB) en el listado.
|
|
50
|
+
color: 'text',
|
|
51
|
+
// Un rich-select se pinta como badge con el label de la opción.
|
|
52
|
+
'rich-select': 'badge',
|
|
53
|
+
// Un dropdown se pinta como badge con el label de la opción.
|
|
54
|
+
dropdown: 'badge',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Formato por defecto para un `type`, o `undefined` si el schema lo escribió
|
|
59
|
+
* mal (tipo fuera de la unión). La degradación — igual que `displayLoader()` —
|
|
60
|
+
* es `text`, que era el `@default` del `@switch` que R4.3 reemplazó.
|
|
61
|
+
*/
|
|
62
|
+
export function formatoPorTipo(type: string): ListColumnFormat | undefined {
|
|
63
|
+
return Object.prototype.hasOwnProperty.call(FORMATO_POR_DEFECTO, type)
|
|
64
|
+
? FORMATO_POR_DEFECTO[type as FieldType]
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Formato final de una columna: la declaración del schema si existe, si no,
|
|
70
|
+
* la tabla de arriba según el `type` del campo con esa `key`, y si la columna
|
|
71
|
+
* ni siquiera aparece en `fields` (columna derivada o key inventada), `text`.
|
|
72
|
+
*
|
|
73
|
+
* Es la única puerta por la que `list-view` decide qué pinta una celda.
|
|
74
|
+
*/
|
|
75
|
+
export function formatoDeColumna(
|
|
76
|
+
column: ListColumn,
|
|
77
|
+
fields: readonly FieldSchema[],
|
|
78
|
+
): ListColumnFormat {
|
|
79
|
+
if (column.format) {
|
|
80
|
+
return column.format;
|
|
81
|
+
}
|
|
82
|
+
const field = fields.find((candidate) => candidate.key === column.key);
|
|
83
|
+
return (field ? formatoPorTipo(field.type) : undefined) ?? 'text';
|
|
84
|
+
}
|