@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,222 @@
|
|
|
1
|
+
import { Component, computed, inject, input } from '@angular/core';
|
|
2
|
+
import { FormField } from '@angular/forms/signals';
|
|
3
|
+
import {
|
|
4
|
+
CdkDrag,
|
|
5
|
+
CdkDragDrop,
|
|
6
|
+
CdkDragHandle,
|
|
7
|
+
CdkDropList,
|
|
8
|
+
moveItemInArray,
|
|
9
|
+
} from '@angular/cdk/drag-drop';
|
|
10
|
+
|
|
11
|
+
import { LucideArrowDown, LucideArrowUp, LucideGripVertical, LucideTrash2 } from '@lucide/angular';
|
|
12
|
+
import type { StringListField } from '../../core/models/schema.model';
|
|
13
|
+
import { NotificationService } from '../../core/services/notification.service';
|
|
14
|
+
import { Button } from '../../shared/components/button/button';
|
|
15
|
+
import { FieldAria } from '../field-aria';
|
|
16
|
+
import { childTree, stringItemTree, type RootTree } from '../field-node';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Lista de cadenas (base.md §9: `text`, `url` o `email` por ítem).
|
|
20
|
+
*
|
|
21
|
+
* Hoja: renderiza sus propios `<input [formField]>` (el nodo de cada ítem es
|
|
22
|
+
* `FieldTree<string>`) y no toca FieldHost. Los ítems escalares no pueden
|
|
23
|
+
* llevar `position`: si el schema la declara, el orden viaja como orden del
|
|
24
|
+
* arreglo y el backend renumera (api.md R3).
|
|
25
|
+
*/
|
|
26
|
+
@Component({
|
|
27
|
+
selector: 'app-field-string-list',
|
|
28
|
+
imports: [
|
|
29
|
+
FieldAria,
|
|
30
|
+
Button,
|
|
31
|
+
CdkDrag,
|
|
32
|
+
CdkDragHandle,
|
|
33
|
+
CdkDropList,
|
|
34
|
+
FormField,
|
|
35
|
+
LucideArrowDown,
|
|
36
|
+
LucideArrowUp,
|
|
37
|
+
LucideGripVertical,
|
|
38
|
+
LucideTrash2,
|
|
39
|
+
],
|
|
40
|
+
template: `
|
|
41
|
+
@if (items().length === 0) {
|
|
42
|
+
<p
|
|
43
|
+
class="rounded-lg border border-dashed border-neutral/30 px-4 py-6 text-center text-sm text-neutral"
|
|
44
|
+
>
|
|
45
|
+
{{ field().emptyText ?? 'Sin elementos.' }}
|
|
46
|
+
</p>
|
|
47
|
+
} @else {
|
|
48
|
+
<div
|
|
49
|
+
cdkDropList
|
|
50
|
+
class="space-y-2"
|
|
51
|
+
[cdkDropListData]="items()"
|
|
52
|
+
[cdkDropListDisabled]="!sortable()"
|
|
53
|
+
(cdkDropListDropped)="onDrop($event)"
|
|
54
|
+
>
|
|
55
|
+
@for (item of items(); track $index; let i = $index) {
|
|
56
|
+
<div class="flex items-center gap-2" cdkDrag [cdkDragDisabled]="!sortable()">
|
|
57
|
+
@if (sortable()) {
|
|
58
|
+
<button
|
|
59
|
+
type="button"
|
|
60
|
+
app-button
|
|
61
|
+
variant="ghost"
|
|
62
|
+
size="icon"
|
|
63
|
+
class="cursor-grab"
|
|
64
|
+
cdkDragHandle
|
|
65
|
+
[attr.aria-label]="'Reordenar elemento ' + (i + 1)"
|
|
66
|
+
title="Arrastrar para reordenar"
|
|
67
|
+
>
|
|
68
|
+
<svg lucideGripVertical size="16" />
|
|
69
|
+
</button>
|
|
70
|
+
}
|
|
71
|
+
<input
|
|
72
|
+
class="field-input min-w-0 grow"
|
|
73
|
+
[type]="inputType()"
|
|
74
|
+
[placeholder]="field().placeholder ?? placeholderFor()"
|
|
75
|
+
[attr.aria-label]="field().label + ', elemento ' + (i + 1)"
|
|
76
|
+
[id]="idPrefix() + '-' + i"
|
|
77
|
+
[formField]="itemNode(i)"
|
|
78
|
+
/>
|
|
79
|
+
<span class="flex shrink-0 items-center gap-2 lg:gap-1">
|
|
80
|
+
<button
|
|
81
|
+
type="button"
|
|
82
|
+
app-button
|
|
83
|
+
variant="ghost"
|
|
84
|
+
size="icon"
|
|
85
|
+
[disabled]="i === 0"
|
|
86
|
+
[attr.aria-label]="'Subir elemento ' + (i + 1)"
|
|
87
|
+
(click)="move(i, -1)"
|
|
88
|
+
>
|
|
89
|
+
<svg lucideArrowUp size="16" />
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
app-button
|
|
94
|
+
variant="ghost"
|
|
95
|
+
size="icon"
|
|
96
|
+
[disabled]="i === items().length - 1"
|
|
97
|
+
[attr.aria-label]="'Bajar elemento ' + (i + 1)"
|
|
98
|
+
(click)="move(i, 1)"
|
|
99
|
+
>
|
|
100
|
+
<svg lucideArrowDown size="16" />
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
type="button"
|
|
104
|
+
app-button
|
|
105
|
+
variant="ghost"
|
|
106
|
+
size="icon"
|
|
107
|
+
[disabled]="!canRemove()"
|
|
108
|
+
[attr.aria-label]="'Eliminar elemento ' + (i + 1)"
|
|
109
|
+
(click)="remove(i)"
|
|
110
|
+
>
|
|
111
|
+
<svg lucideTrash2 size="16" />
|
|
112
|
+
</button>
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
}
|
|
116
|
+
</div>
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
<div class="mt-3">
|
|
120
|
+
<button app-button variant="outline" type="button" [disabled]="!canAdd()" (click)="add()">
|
|
121
|
+
+ {{ field().addLabel ?? 'Agregar elemento' }}
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
`,
|
|
125
|
+
})
|
|
126
|
+
export class FieldStringList {
|
|
127
|
+
readonly field = input.required<StringListField>();
|
|
128
|
+
readonly tree = input.required<RootTree>();
|
|
129
|
+
|
|
130
|
+
private readonly notifications = inject(NotificationService);
|
|
131
|
+
|
|
132
|
+
private readonly itemsNode = computed(() => childTree<string[]>(this.tree(), this.field().key));
|
|
133
|
+
private readonly state = computed(() => this.itemsNode()());
|
|
134
|
+
/** Prefijo estable del campo para los id de cada ítem (§8). */
|
|
135
|
+
protected readonly idPrefix = computed(() => this.state().name());
|
|
136
|
+
protected readonly items = computed(() => this.state().value() ?? []);
|
|
137
|
+
|
|
138
|
+
protected readonly sortable = computed(() => this.field().sortable ?? true);
|
|
139
|
+
protected readonly inputType = computed(() => this.field().itemType ?? 'text');
|
|
140
|
+
|
|
141
|
+
protected itemNode(index: number): ReturnType<typeof stringItemTree> {
|
|
142
|
+
return stringItemTree(this.itemsNode(), index);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected canAdd(): boolean {
|
|
146
|
+
const max = this.field().max;
|
|
147
|
+
return max === undefined || this.items().length < max;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
protected canRemove(): boolean {
|
|
151
|
+
return this.items().length > (this.field().min ?? 0);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
protected add(): void {
|
|
155
|
+
if (!this.canAdd()) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
this.updateItems((next) => {
|
|
159
|
+
next.push('');
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
protected move(index: number, direction: -1 | 1): void {
|
|
164
|
+
const target = index + direction;
|
|
165
|
+
if (target < 0 || target >= this.items().length) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.updateItems((next) => {
|
|
169
|
+
moveItemInArray(next, index, target);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected onDrop(event: CdkDragDrop<string[]>): void {
|
|
174
|
+
if (event.previousIndex === event.currentIndex) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
this.updateItems((next) => {
|
|
178
|
+
moveItemInArray(next, event.previousIndex, event.currentIndex);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
protected async remove(index: number): Promise<void> {
|
|
183
|
+
if (!this.canRemove()) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const value = this.items()[index] ?? '';
|
|
187
|
+
if (value.trim() !== '') {
|
|
188
|
+
const accepted = await this.notifications.confirm({
|
|
189
|
+
title: `Eliminar elemento ${index + 1}`,
|
|
190
|
+
message: 'Esta acción no se puede deshacer.',
|
|
191
|
+
confirmLabel: 'Eliminar',
|
|
192
|
+
cancelLabel: 'Cancelar',
|
|
193
|
+
});
|
|
194
|
+
if (!accepted) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
this.updateItems((next) => {
|
|
199
|
+
next.splice(index, 1);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private updateItems(mutator: (next: string[]) => void): void {
|
|
204
|
+
this.state().controlValue.update((current) => {
|
|
205
|
+
const next = [...current];
|
|
206
|
+
mutator(next);
|
|
207
|
+
return next;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private placeholderFor(): string {
|
|
212
|
+
switch (this.field().itemType) {
|
|
213
|
+
case 'url':
|
|
214
|
+
return 'https://…';
|
|
215
|
+
case 'email':
|
|
216
|
+
return 'nombre@ejemplo.com';
|
|
217
|
+
default:
|
|
218
|
+
// §2: placeholder siempre ejemplo, nunca instrucción.
|
|
219
|
+
return 'Ej. Opción 1';
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del campo `text` (R3.3). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'field',
|
|
6
|
+
type: 'text',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./text').then((module) => module.FieldText),
|
|
9
|
+
// Nace como cadena: los textos, fechas, urls, selects y relacionales.
|
|
10
|
+
seed: 'string',
|
|
11
|
+
surfaces: ['form', 'display'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
import { FormField } from '@angular/forms/signals';
|
|
3
|
+
|
|
4
|
+
import type { TextField } from '../../core/models/schema.model';
|
|
5
|
+
import { FieldAria } from '../field-aria';
|
|
6
|
+
import { childTree, type RootTree } from '../field-node';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input de una línea (base.md §5.1 `text`).
|
|
10
|
+
*
|
|
11
|
+
* `readonly`, `maxLength` y `pattern` NO se bindean como atributos: el
|
|
12
|
+
* directive `FormField` los gestiona desde el estado del campo y el compilador
|
|
13
|
+
* lo exige (NG8022). La Fase 10 los traduce a reglas (`readonly()`,
|
|
14
|
+
* `maxLength()`, `pattern()`) en el schema del formulario.
|
|
15
|
+
*/
|
|
16
|
+
@Component({
|
|
17
|
+
selector: 'app-field-text',
|
|
18
|
+
imports: [FieldAria, FormField],
|
|
19
|
+
template: `
|
|
20
|
+
<input
|
|
21
|
+
type="text"
|
|
22
|
+
class="field-input"
|
|
23
|
+
[id]="state().name()"
|
|
24
|
+
[formField]="node()"
|
|
25
|
+
[placeholder]="field().placeholder ?? ''"
|
|
26
|
+
/>
|
|
27
|
+
`,
|
|
28
|
+
})
|
|
29
|
+
export class FieldText {
|
|
30
|
+
readonly field = input.required<TextField>();
|
|
31
|
+
readonly tree = input.required<RootTree>();
|
|
32
|
+
|
|
33
|
+
protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
|
|
34
|
+
protected readonly state = computed(() => this.node()());
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del campo `textarea` (R3.3). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'field',
|
|
6
|
+
type: 'textarea',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./textarea').then((module) => module.FieldTextarea),
|
|
9
|
+
// Nace como cadena: los textos, fechas, urls, selects y relacionales.
|
|
10
|
+
seed: 'string',
|
|
11
|
+
surfaces: ['form', 'display'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Component, EnvironmentInjector, signal } from '@angular/core';
|
|
2
|
+
import { TestBed } from '@angular/core/testing';
|
|
3
|
+
import { form } from '@angular/forms/signals';
|
|
4
|
+
|
|
5
|
+
import type { TextareaField } from '../../core/models/schema.model';
|
|
6
|
+
import { type RootTree } from '../field-node';
|
|
7
|
+
import { FieldTextarea } from './textarea';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* El anfitrión NO lleva `[formField]`: `FieldTextarea` delega en el
|
|
11
|
+
* `<textarea>` nativo de su propio template y ahí es donde vive la directiva
|
|
12
|
+
* `FormField`. Pasarla en el host revienta con NG01914 («host must be a
|
|
13
|
+
* native form control»). El host sólo monta el campo y le da el `tree`
|
|
14
|
+
* (mismo criterio que `multiselect.spec.ts`, salvo que ese campo sí es
|
|
15
|
+
* control propio con `model()`).
|
|
16
|
+
*/
|
|
17
|
+
@Component({
|
|
18
|
+
template: `<app-field-textarea [field]="field" [tree]="tree" />`,
|
|
19
|
+
imports: [FieldTextarea],
|
|
20
|
+
})
|
|
21
|
+
class Host {
|
|
22
|
+
field!: TextareaField;
|
|
23
|
+
tree!: RootTree;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('Campo textarea', () => {
|
|
27
|
+
beforeEach(async () => {
|
|
28
|
+
await TestBed.configureTestingModule({ imports: [Host] }).compileComponents();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function create(field: TextareaField, initial = '') {
|
|
32
|
+
const fixture = TestBed.createComponent(Host);
|
|
33
|
+
const model = signal<Record<string, unknown>>({ [field.key]: initial });
|
|
34
|
+
const tree = form(model, () => {}, {
|
|
35
|
+
injector: TestBed.inject(EnvironmentInjector),
|
|
36
|
+
}) as unknown as RootTree;
|
|
37
|
+
fixture.componentInstance.field = field;
|
|
38
|
+
fixture.componentInstance.tree = tree;
|
|
39
|
+
fixture.detectChanges();
|
|
40
|
+
return { fixture, model };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const area = (fixture: { nativeElement: HTMLElement }) =>
|
|
44
|
+
fixture.nativeElement.querySelector('textarea') as HTMLTextAreaElement;
|
|
45
|
+
|
|
46
|
+
const chips = (fixture: { nativeElement: HTMLElement }): HTMLButtonElement[] =>
|
|
47
|
+
Array.from(fixture.nativeElement.querySelectorAll('[data-testid="textarea-tokens"] button'));
|
|
48
|
+
|
|
49
|
+
const PLANTILLA: TextareaField = {
|
|
50
|
+
type: 'textarea',
|
|
51
|
+
key: 'share_message_template',
|
|
52
|
+
label: 'Plantilla',
|
|
53
|
+
tokens: [
|
|
54
|
+
{ key: 'ref', label: 'Código del pedido' },
|
|
55
|
+
{ key: 'pin', label: 'PIN' },
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
it('sin tokens no pinta la fila de chips', async () => {
|
|
60
|
+
const { fixture } = await create({ type: 'textarea', key: 'msg', label: 'Msg' });
|
|
61
|
+
expect(chips(fixture)).toHaveLength(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('con tokens pinta un chip por token, con el rótulo humano', async () => {
|
|
65
|
+
const { fixture } = await create(PLANTILLA);
|
|
66
|
+
const labels = chips(fixture).map((b) => b.textContent?.trim());
|
|
67
|
+
expect(labels).toEqual(['Código del pedido', 'PIN']);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('inserta {key} al final cuando el textarea está vacío', async () => {
|
|
71
|
+
const { fixture, model } = await create(PLANTILLA);
|
|
72
|
+
chips(fixture)[0].click();
|
|
73
|
+
fixture.detectChanges();
|
|
74
|
+
expect(model()[PLANTILLA.key]).toBe('{ref}');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('inserta el token en la posición del cursor, no al final', async () => {
|
|
78
|
+
const { fixture, model } = await create(PLANTILLA, 'Hola ');
|
|
79
|
+
const a = area(fixture);
|
|
80
|
+
// Cursor justo después de "Hola ".
|
|
81
|
+
a.setSelectionRange(6, 6);
|
|
82
|
+
chips(fixture)[1].click();
|
|
83
|
+
fixture.detectChanges();
|
|
84
|
+
expect(model()[PLANTILLA.key]).toBe('Hola {pin}');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('reemplaza la selección con el token', async () => {
|
|
88
|
+
const { fixture, model } = await create(PLANTILLA, 'borrar-esto');
|
|
89
|
+
const a = area(fixture);
|
|
90
|
+
a.setSelectionRange(0, 11);
|
|
91
|
+
chips(fixture)[0].click();
|
|
92
|
+
fixture.detectChanges();
|
|
93
|
+
expect(model()[PLANTILLA.key]).toBe('{ref}');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('devuelve el foco al textarea tras insertar', async () => {
|
|
97
|
+
const { fixture } = await create(PLANTILLA);
|
|
98
|
+
chips(fixture)[0].click();
|
|
99
|
+
fixture.detectChanges();
|
|
100
|
+
expect(fixture.nativeElement.ownerDocument.activeElement).toBe(area(fixture));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('acumula varios tokens escribiendo entre clics', async () => {
|
|
104
|
+
const { fixture, model } = await create({ ...PLANTILLA, key: 'm' });
|
|
105
|
+
chips(fixture)[0].click();
|
|
106
|
+
fixture.detectChanges();
|
|
107
|
+
// Escribir " con PIN " después del primer token.
|
|
108
|
+
const a = area(fixture);
|
|
109
|
+
a.setSelectionRange((model()['m'] as string).length, (model()['m'] as string).length);
|
|
110
|
+
a.value = `${model()['m']} con PIN `;
|
|
111
|
+
a.dispatchEvent(new Event('input'));
|
|
112
|
+
fixture.detectChanges();
|
|
113
|
+
chips(fixture)[1].click();
|
|
114
|
+
fixture.detectChanges();
|
|
115
|
+
expect(model()['m']).toBe('{ref} con PIN {pin}');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { Component, computed, ElementRef, input, viewChild } from '@angular/core';
|
|
2
|
+
import { FormField } from '@angular/forms/signals';
|
|
3
|
+
|
|
4
|
+
import type { TextareaField } from '../../core/models/schema.model';
|
|
5
|
+
import { FieldAria } from '../field-aria';
|
|
6
|
+
import { childTree, type RootTree } from '../field-node';
|
|
7
|
+
import { Button } from '../../shared/components/button/button';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Área multilínea con contador de caracteres/palabras (base.md §5.1).
|
|
11
|
+
* Los límites salen de `validators.*`, con `minWords`/`maxWords` del tipo
|
|
12
|
+
* como respaldo, y coinciden con las reglas de `form-view/form-schema.ts`.
|
|
13
|
+
*
|
|
14
|
+
* Cuando el campo declara `tokens`, se pinta una fila de chips debajo: cada
|
|
15
|
+
* chip escribe `{key}` **en la posición del cursor** (no al final) y devuelve
|
|
16
|
+
* el foco, así el operador escribe → apreta → sigue escribiendo. Es la UI de
|
|
17
|
+
* la plantilla de compartir; el motor back no se entera.
|
|
18
|
+
*/
|
|
19
|
+
@Component({
|
|
20
|
+
selector: 'app-field-textarea',
|
|
21
|
+
imports: [FieldAria, FormField, Button],
|
|
22
|
+
template: `
|
|
23
|
+
<div>
|
|
24
|
+
<textarea
|
|
25
|
+
class="field-input"
|
|
26
|
+
[id]="state().name()"
|
|
27
|
+
[formField]="node()"
|
|
28
|
+
[placeholder]="field().placeholder ?? ''"
|
|
29
|
+
[attr.rows]="field().rows ?? 4"
|
|
30
|
+
#area
|
|
31
|
+
></textarea>
|
|
32
|
+
@if (field().tokens?.length) {
|
|
33
|
+
<div class="mt-1.5 flex flex-wrap gap-2 lg:gap-1.5" data-testid="textarea-tokens">
|
|
34
|
+
@for (token of field().tokens; track token.key) {
|
|
35
|
+
<button
|
|
36
|
+
app-button
|
|
37
|
+
type="button"
|
|
38
|
+
variant="outline"
|
|
39
|
+
size="sm"
|
|
40
|
+
[attr.title]="'Inserta {' + token.key + '} en el texto'"
|
|
41
|
+
(click)="insertToken(token.key)"
|
|
42
|
+
>
|
|
43
|
+
{{ token.label }}
|
|
44
|
+
</button>
|
|
45
|
+
}
|
|
46
|
+
</div>
|
|
47
|
+
}
|
|
48
|
+
@if (counter(); as counter) {
|
|
49
|
+
<div class="mt-1 flex justify-right text-xs text-neutral" data-testid="textarea-counter">
|
|
50
|
+
{{ counter }}
|
|
51
|
+
</div>
|
|
52
|
+
}
|
|
53
|
+
</div>
|
|
54
|
+
`,
|
|
55
|
+
})
|
|
56
|
+
export class FieldTextarea {
|
|
57
|
+
readonly field = input.required<TextareaField>();
|
|
58
|
+
readonly tree = input.required<RootTree>();
|
|
59
|
+
|
|
60
|
+
private readonly area = viewChild<ElementRef<HTMLTextAreaElement>>('area');
|
|
61
|
+
|
|
62
|
+
protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
|
|
63
|
+
protected readonly state = computed(() => this.node()());
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Escribe `{key}` donde está el cursor (o reemplaza la selección) y
|
|
67
|
+
* reposiciona el cursor DESPUÉS del token insertado, para seguir escribiendo.
|
|
68
|
+
*
|
|
69
|
+
* Si el textarea aún no está montado (o no hay cursor válido) cae al
|
|
70
|
+
* final del texto: nunca revienta por un click temprano.
|
|
71
|
+
*/
|
|
72
|
+
protected insertToken(key: string): void {
|
|
73
|
+
const area = this.area()?.nativeElement;
|
|
74
|
+
const token = `{${key}}`;
|
|
75
|
+
const current =
|
|
76
|
+
typeof this.state().value() === 'string' ? (this.state().value() as string) : '';
|
|
77
|
+
|
|
78
|
+
let next: string;
|
|
79
|
+
let caret: number;
|
|
80
|
+
if (area) {
|
|
81
|
+
const start = area.selectionStart ?? current.length;
|
|
82
|
+
const end = area.selectionEnd ?? start;
|
|
83
|
+
next = current.slice(0, start) + token + current.slice(end);
|
|
84
|
+
caret = start + token.length;
|
|
85
|
+
} else {
|
|
86
|
+
next = current + token;
|
|
87
|
+
caret = next.length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.state().value.set(next);
|
|
91
|
+
|
|
92
|
+
if (area) {
|
|
93
|
+
area.focus();
|
|
94
|
+
area.setSelectionRange(caret, caret);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Contador `n/m …`; `null` cuando el campo no declara límites. */
|
|
99
|
+
protected readonly counter = computed<string | null>(() => {
|
|
100
|
+
const field = this.field();
|
|
101
|
+
const value = this.state().value();
|
|
102
|
+
const text = typeof value === 'string' ? value : '';
|
|
103
|
+
const chars = text.length;
|
|
104
|
+
const words = text.trim() === '' ? 0 : text.trim().split(/\s+/).length;
|
|
105
|
+
|
|
106
|
+
const parts: string[] = [];
|
|
107
|
+
const maxChars = field.validators?.maxLength;
|
|
108
|
+
if (maxChars !== undefined) {
|
|
109
|
+
parts.push(`${chars}/${maxChars} caracteres`);
|
|
110
|
+
}
|
|
111
|
+
// `validators` tiene prioridad sobre las opciones propias del tipo
|
|
112
|
+
// (mismo criterio que las reglas de `form-schema.ts`).
|
|
113
|
+
const maxWords = field.validators?.maxWords ?? field.maxWords;
|
|
114
|
+
const minWords = field.validators?.minWords ?? field.minWords;
|
|
115
|
+
if (maxWords !== undefined) {
|
|
116
|
+
parts.push(
|
|
117
|
+
minWords !== undefined
|
|
118
|
+
? `${words}/${maxWords} palabras (mín. ${minWords})`
|
|
119
|
+
: `${words}/${maxWords} palabras`,
|
|
120
|
+
);
|
|
121
|
+
} else if (minWords !== undefined) {
|
|
122
|
+
parts.push(`${words} palabras (mín. ${minWords})`);
|
|
123
|
+
}
|
|
124
|
+
return parts.length > 0 ? parts.join(' · ') : null;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del campo `time` (R3.3). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'field',
|
|
6
|
+
type: 'time',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./time').then((module) => module.FieldTime),
|
|
9
|
+
// Nace como cadena: los textos, fechas, urls, selects y relacionales.
|
|
10
|
+
seed: 'string',
|
|
11
|
+
surfaces: ['form', 'display'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
import { FormField } from '@angular/forms/signals';
|
|
3
|
+
|
|
4
|
+
import type { TimeField } from '../../core/models/schema.model';
|
|
5
|
+
import { FieldAria } from '../field-aria';
|
|
6
|
+
import { childTree, type RootTree } from '../field-node';
|
|
7
|
+
|
|
8
|
+
/** Selector de hora `HH:mm` (base.md §5.1). */
|
|
9
|
+
@Component({
|
|
10
|
+
selector: 'app-field-time',
|
|
11
|
+
imports: [FieldAria, FormField],
|
|
12
|
+
template: `
|
|
13
|
+
<input
|
|
14
|
+
type="time"
|
|
15
|
+
class="field-input"
|
|
16
|
+
[id]="state().name()"
|
|
17
|
+
[formField]="node()"
|
|
18
|
+
[placeholder]="field().placeholder ?? ''"
|
|
19
|
+
/>
|
|
20
|
+
`,
|
|
21
|
+
})
|
|
22
|
+
export class FieldTime {
|
|
23
|
+
readonly field = input.required<TimeField>();
|
|
24
|
+
readonly tree = input.required<RootTree>();
|
|
25
|
+
|
|
26
|
+
protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
|
|
27
|
+
protected readonly state = computed(() => this.node()());
|
|
28
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Component, input } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { LucideInfo } from '@lucide/angular';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Degradación controlada de un tipo de campo sin renderizador (R1.10).
|
|
7
|
+
*
|
|
8
|
+
* El `@switch` de `field-host` NO tiene `@default`: un `type` fuera de
|
|
9
|
+
* `SUPPORTED_FIELD_TYPE_LIST` no revienta el template, simplemente no pinta
|
|
10
|
+
* nada. Eso cumple «nunca pantalla rota», pero deja el formulario con un
|
|
11
|
+
* hueco mudo donde el operador no sabe si le falta un campo o si lo escribió
|
|
12
|
+
* mal.
|
|
13
|
+
*
|
|
14
|
+
* Este componente es el aviso de **desarrollo**: se renderiza sólo con
|
|
15
|
+
* `isDevMode()`, nombra el tipo y el campo, y deja claro que el VALOR existe
|
|
16
|
+
* en el schema y se conserva — no se pierde, sólo no se pinta. En producción
|
|
17
|
+
* `field-host` lo omite y deja un `console.warn`.
|
|
18
|
+
*
|
|
19
|
+
* No dibuja control ni etiqueta asociada: no hay `id` al que apuntar un
|
|
20
|
+
* `<label for>` (mismo criterio que el bloque de `composite()` en el host).
|
|
21
|
+
*/
|
|
22
|
+
@Component({
|
|
23
|
+
selector: 'app-unknown-render',
|
|
24
|
+
imports: [LucideInfo],
|
|
25
|
+
template: `
|
|
26
|
+
<div
|
|
27
|
+
class="mb-4 rounded-lg border border-amber-400/50 bg-amber-500/10 px-3 py-2"
|
|
28
|
+
data-field-unknown
|
|
29
|
+
>
|
|
30
|
+
<p class="flex items-center gap-1.5 text-xs font-medium text-amber-600">
|
|
31
|
+
<svg lucideInfo size="14" class="shrink-0" aria-hidden="true" />
|
|
32
|
+
Campo sin renderizador en esta versión
|
|
33
|
+
</p>
|
|
34
|
+
<p class="mt-1 text-help text-neutral">
|
|
35
|
+
Tipo <code class="rounded bg-amber-500/20 px-1 font-mono">{{ type() }}</code>
|
|
36
|
+
@if (label()) {
|
|
37
|
+
del campo «{{ label() }}»
|
|
38
|
+
}
|
|
39
|
+
— el valor existe en el schema y se conserva; sólo no se dibuja.
|
|
40
|
+
</p>
|
|
41
|
+
</div>
|
|
42
|
+
`,
|
|
43
|
+
})
|
|
44
|
+
export class UnknownRender {
|
|
45
|
+
/** `type` declarado en el schema y fuera de `SUPPORTED_FIELD_TYPE_LIST`. */
|
|
46
|
+
readonly type = input.required<string>();
|
|
47
|
+
/** Rótulo del campo, para que el aviso sea ubicable en el formulario. */
|
|
48
|
+
readonly label = input('');
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RenderDescriptor } from '../../core/registry/render-registry';
|
|
2
|
+
|
|
3
|
+
/** Descriptor del campo `url` (R3.3). */
|
|
4
|
+
export const descriptor: RenderDescriptor = {
|
|
5
|
+
kind: 'field',
|
|
6
|
+
type: 'url',
|
|
7
|
+
since: '1.0',
|
|
8
|
+
load: () => import('./url').then((module) => module.FieldUrl),
|
|
9
|
+
// Nace como cadena: los textos, fechas, urls, selects y relacionales.
|
|
10
|
+
seed: 'string',
|
|
11
|
+
surfaces: ['form', 'display'],
|
|
12
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Component, computed, input } from '@angular/core';
|
|
2
|
+
import { FormField } from '@angular/forms/signals';
|
|
3
|
+
|
|
4
|
+
import type { UrlField } from '../../core/models/schema.model';
|
|
5
|
+
import { FieldAria } from '../field-aria';
|
|
6
|
+
import { childTree, type RootTree } from '../field-node';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Input con validación de URL y botón «abrir» (base.md §5.1). La validación
|
|
10
|
+
* de formato la aporta Fase 10 (`pattern`/`validate`); acá solo se enlaza el
|
|
11
|
+
* valor cuando es http(s).
|
|
12
|
+
*/
|
|
13
|
+
@Component({
|
|
14
|
+
selector: 'app-field-url',
|
|
15
|
+
imports: [FieldAria, FormField],
|
|
16
|
+
template: `
|
|
17
|
+
<div class="flex items-start gap-2">
|
|
18
|
+
<input
|
|
19
|
+
type="url"
|
|
20
|
+
class="field-input"
|
|
21
|
+
[id]="state().name()"
|
|
22
|
+
[formField]="node()"
|
|
23
|
+
[placeholder]="field().placeholder ?? 'https://…'"
|
|
24
|
+
/>
|
|
25
|
+
@if (href(); as href) {
|
|
26
|
+
<!-- Mismo lenguaje que la variante outline de app-button (el
|
|
27
|
+
selector del componente es button[app-button], un ancla no
|
|
28
|
+
puede usarlo): borde /40, hover con fondo, 44 px en mobile. -->
|
|
29
|
+
<a
|
|
30
|
+
class="min-h-11 shrink-0 rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral hover:bg-neutral/10 lg:min-h-0"
|
|
31
|
+
[href]="href"
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
>
|
|
35
|
+
Abrir
|
|
36
|
+
</a>
|
|
37
|
+
}
|
|
38
|
+
</div>
|
|
39
|
+
`,
|
|
40
|
+
})
|
|
41
|
+
export class FieldUrl {
|
|
42
|
+
readonly field = input.required<UrlField>();
|
|
43
|
+
readonly tree = input.required<RootTree>();
|
|
44
|
+
|
|
45
|
+
protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
|
|
46
|
+
protected readonly state = computed(() => this.node()());
|
|
47
|
+
|
|
48
|
+
/** Solo http(s): evita enlazar `javascript:` u otros esquemas. */
|
|
49
|
+
protected readonly href = computed(() => {
|
|
50
|
+
const value = this.state().value();
|
|
51
|
+
return typeof value === 'string' && /^https?:\/\//i.test(value) ? value : null;
|
|
52
|
+
});
|
|
53
|
+
}
|