@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.
Files changed (369) hide show
  1. package/README.md +222 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +3 -15
  4. package/src/lib/actions/action-registry.spec.ts +52 -0
  5. package/src/lib/actions/action-registry.ts +126 -0
  6. package/src/lib/actions/action-service.spec.ts +215 -0
  7. package/src/lib/actions/action-service.ts +87 -0
  8. package/src/lib/actions/descriptors.spec.ts +155 -0
  9. package/src/lib/actions/descriptors.ts +154 -0
  10. package/src/lib/columns/column-cell.ts +33 -0
  11. package/src/lib/columns/column-defaults.spec.ts +99 -0
  12. package/src/lib/columns/column-defaults.ts +84 -0
  13. package/src/lib/columns/column-format.spec.ts +274 -0
  14. package/src/lib/columns/descriptors.ts +113 -0
  15. package/src/lib/columns/format-badge.ts +51 -0
  16. package/src/lib/columns/format-boolean.ts +58 -0
  17. package/src/lib/columns/format-currency.ts +30 -0
  18. package/src/lib/columns/format-date.ts +35 -0
  19. package/src/lib/columns/format-number.ts +32 -0
  20. package/src/lib/columns/format-text.ts +23 -0
  21. package/src/lib/columns/format-thumbnail.ts +63 -0
  22. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  23. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  24. package/src/lib/core/guards/auth.guard.ts +48 -0
  25. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  27. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  28. package/src/lib/core/models/api.model.ts +104 -0
  29. package/src/lib/core/models/schema-descriptions.ts +118 -0
  30. package/src/lib/core/models/schema.model.ts +1097 -0
  31. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  32. package/src/lib/core/registry/composition-validator.ts +270 -0
  33. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  34. package/src/lib/core/registry/field-validators.ts +440 -0
  35. package/src/lib/core/registry/render-host.spec.ts +208 -0
  36. package/src/lib/core/registry/render-host.ts +646 -0
  37. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  38. package/src/lib/core/registry/render-registry.ts +312 -0
  39. package/src/lib/core/registry/schema-catalog.ts +156 -0
  40. package/src/lib/core/services/api.service.spec.ts +1142 -0
  41. package/src/lib/core/services/api.service.ts +371 -0
  42. package/src/lib/core/services/auth.service.spec.ts +653 -0
  43. package/src/lib/core/services/auth.service.ts +503 -0
  44. package/src/lib/core/services/config.service.spec.ts +121 -0
  45. package/src/lib/core/services/config.service.ts +119 -0
  46. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  47. package/src/lib/core/services/data-source.service.ts +142 -0
  48. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  49. package/src/lib/core/services/error-mapper.service.ts +49 -0
  50. package/src/lib/core/services/notification.service.ts +73 -0
  51. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  52. package/src/lib/core/services/theme.service.spec.ts +106 -0
  53. package/src/lib/core/services/theme.service.ts +86 -0
  54. package/src/lib/core/services/transport.spec.ts +198 -0
  55. package/src/lib/core/services/transport.ts +164 -0
  56. package/src/lib/core/services/upload.service.ts +81 -0
  57. package/src/lib/core/testing/categories.schema.ts +125 -0
  58. package/src/lib/core/testing/orders.schema.ts +372 -0
  59. package/src/lib/core/testing/products.schema.ts +258 -0
  60. package/src/lib/core/testing/site.schema.ts +195 -0
  61. package/src/lib/core/testing/test-catalog.ts +21 -0
  62. package/src/lib/core/tokens.ts +112 -0
  63. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  64. package/src/lib/core/utils/clipboard.ts +90 -0
  65. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  66. package/src/lib/core/utils/context-resolver.ts +101 -0
  67. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  68. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  69. package/src/lib/core/utils/source-options.spec.ts +96 -0
  70. package/src/lib/core/utils/source-options.ts +54 -0
  71. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  72. package/src/lib/core/utils/template-interpolator.ts +56 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  74. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  75. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  76. package/src/lib/fields/boolean/boolean.ts +40 -0
  77. package/src/lib/fields/color/color.descriptor.ts +12 -0
  78. package/src/lib/fields/color/color.ts +34 -0
  79. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  80. package/src/lib/fields/currency/currency.ts +67 -0
  81. package/src/lib/fields/date/date.descriptor.ts +12 -0
  82. package/src/lib/fields/date/date.ts +36 -0
  83. package/src/lib/fields/descriptors.ts +122 -0
  84. package/src/lib/fields/display/display-badge.ts +65 -0
  85. package/src/lib/fields/display/display-date.ts +39 -0
  86. package/src/lib/fields/display/display-defaults.ts +84 -0
  87. package/src/lib/fields/display/display-file.ts +41 -0
  88. package/src/lib/fields/display/display-image.ts +54 -0
  89. package/src/lib/fields/display/display-list.ts +107 -0
  90. package/src/lib/fields/display/display-number.ts +41 -0
  91. package/src/lib/fields/display/display-text.ts +42 -0
  92. package/src/lib/fields/display/display.spec.ts +242 -0
  93. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  94. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  95. package/src/lib/fields/email/email.descriptor.ts +12 -0
  96. package/src/lib/fields/email/email.ts +28 -0
  97. package/src/lib/fields/field-aria.ts +60 -0
  98. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  99. package/src/lib/fields/field-host/field-host.ts +47 -0
  100. package/src/lib/fields/field-node.ts +88 -0
  101. package/src/lib/fields/file/file.descriptor.ts +14 -0
  102. package/src/lib/fields/file/file.spec.ts +134 -0
  103. package/src/lib/fields/file/file.ts +281 -0
  104. package/src/lib/fields/group/group.descriptor.ts +21 -0
  105. package/src/lib/fields/group/group.ts +110 -0
  106. package/src/lib/fields/image/image.descriptor.ts +12 -0
  107. package/src/lib/fields/image/image.spec.ts +117 -0
  108. package/src/lib/fields/image/image.ts +281 -0
  109. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  110. package/src/lib/fields/key-value/key-value.ts +283 -0
  111. package/src/lib/fields/list/list.descriptor.ts +21 -0
  112. package/src/lib/fields/list/list.ts +376 -0
  113. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  114. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  115. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  116. package/src/lib/fields/number/number.descriptor.ts +12 -0
  117. package/src/lib/fields/number/number.ts +40 -0
  118. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  119. package/src/lib/fields/phone/phone.ts +46 -0
  120. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  121. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  122. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  123. package/src/lib/fields/relation/relation.spec.ts +193 -0
  124. package/src/lib/fields/relation/relation.ts +224 -0
  125. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  126. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  127. package/src/lib/fields/select/radio-mode.ts +25 -0
  128. package/src/lib/fields/select/select.descriptor.ts +12 -0
  129. package/src/lib/fields/select/select.spec.ts +162 -0
  130. package/src/lib/fields/select/select.ts +210 -0
  131. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  132. package/src/lib/fields/slug/slug.ts +67 -0
  133. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  134. package/src/lib/fields/string-list/string-list.ts +222 -0
  135. package/src/lib/fields/text/text.descriptor.ts +12 -0
  136. package/src/lib/fields/text/text.ts +35 -0
  137. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  138. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  139. package/src/lib/fields/textarea/textarea.ts +126 -0
  140. package/src/lib/fields/time/time.descriptor.ts +12 -0
  141. package/src/lib/fields/time/time.ts +28 -0
  142. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  143. package/src/lib/fields/url/url.descriptor.ts +12 -0
  144. package/src/lib/fields/url/url.ts +53 -0
  145. package/src/lib/gas-admin-ui.spec.ts +21 -0
  146. package/src/lib/gas-admin-ui.ts +9 -0
  147. package/src/lib/layouts/descriptors.ts +9 -0
  148. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  149. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  150. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  151. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  152. package/src/lib/pages/form-view/form-model.ts +88 -0
  153. package/src/lib/pages/form-view/form-schema.ts +255 -0
  154. package/src/lib/pages/form-view/form-view.ts +1622 -0
  155. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  156. package/src/lib/pages/list-view/list-view.ts +882 -0
  157. package/src/lib/pages/login/login.spec.ts +109 -0
  158. package/src/lib/pages/login/login.ts +227 -0
  159. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  160. package/src/lib/shared/components/badge/badge.ts +35 -0
  161. package/src/lib/shared/components/button/button.ts +65 -0
  162. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  163. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  164. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  165. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  166. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  167. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  168. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  169. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  170. package/src/lib/shared/components/modal/modal.ts +145 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  172. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  173. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  174. package/src/lib/shared/components/toast/toast.ts +58 -0
  175. package/src/lib/shared/utils/col-span.ts +25 -0
  176. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  177. package/src/lib/shared/utils/depends-on.ts +48 -0
  178. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  179. package/src/lib/shared/utils/field-path.ts +49 -0
  180. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  181. package/src/lib/shared/utils/seed.spec.ts +64 -0
  182. package/src/lib/shared/utils/seed.ts +149 -0
  183. package/src/lib/shared/utils/slugify.ts +13 -0
  184. package/src/lib/shared/utils/validators.ts +41 -0
  185. package/src/lib/shell/layout/layout.ts +63 -0
  186. package/src/lib/shell/shell.routes.spec.ts +59 -0
  187. package/src/lib/shell/shell.routes.ts +122 -0
  188. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  189. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  190. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  191. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  192. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  193. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  194. package/src/lib/views/composer/composer-host.ts +248 -0
  195. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  196. package/src/lib/views/composer/view-context.spec.ts +117 -0
  197. package/src/lib/views/composer/view-context.ts +109 -0
  198. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  199. package/src/lib/views/composer/view-slot.ts +178 -0
  200. package/src/lib/views/descriptors.ts +46 -0
  201. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  202. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  203. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  206. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  207. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  208. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  209. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  210. package/src/lib/widgets/descriptors.ts +29 -0
  211. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  212. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  213. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  216. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  217. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  218. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  219. package/src/lib/widgets/record-list/record-list.ts +115 -0
  220. package/src/lib/widgets/series.spec.ts +42 -0
  221. package/src/lib/widgets/series.ts +56 -0
  222. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  223. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  224. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  225. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  226. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  227. package/src/public-api.ts +104 -0
  228. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  229. package/tsconfig.lib.json +13 -0
  230. package/tsconfig.lib.prod.json +11 -0
  231. package/tsconfig.spec.json +10 -0
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  233. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  235. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  237. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  239. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  241. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  243. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  245. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  247. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  249. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  251. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  253. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  255. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  257. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  259. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  261. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  263. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  265. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  267. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  269. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  271. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  273. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  275. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  277. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  279. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  281. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  283. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  285. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  287. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  289. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  291. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  293. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  295. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  297. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  299. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  301. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  303. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  305. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  307. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  309. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  311. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  313. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  315. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  317. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  319. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  321. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  323. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  325. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  327. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  329. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  333. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  335. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  337. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  338. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  340. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  342. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  344. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  346. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  348. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  350. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  352. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  354. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  356. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  358. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  360. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  362. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  364. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  366. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  368. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  369. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,376 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import { Component, computed, inject, input, signal, type TemplateRef } from '@angular/core';
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 { ListField } from '../../core/models/schema.model';
13
+ import { NotificationService } from '../../core/services/notification.service';
14
+ import { Button } from '../../shared/components/button/button';
15
+ import { colSpanClass } from '../../shared/utils/col-span';
16
+ import {
17
+ appendPosition,
18
+ computeMove,
19
+ GAP,
20
+ type PositionedItem,
21
+ } from '../../shared/utils/gap-sorting';
22
+ import { seedFields } from '../../shared/utils/seed';
23
+ import { itemTree, listTree, type ListChildContext, type RootTree } from '../field-node';
24
+
25
+ /**
26
+ * Lista de ítems (base.md §6): asa de arrastre, subir/bajar, eliminar con
27
+ * confirmación (sólo si el ítem tiene contenido) y agregar respetando
28
+ * `min`/`max`.
29
+ *
30
+ * **Los hijos los renderiza el despachador**: este componente recibe el
31
+ * `TemplateRef` (#childTpl, declarado en el `@case` de `field-host`) y lo
32
+ * instancia una vez por ítem con `ngTemplateOutlet`. No importa FieldHost —
33
+ * un import mutuo revienta en runtime (ciclo-check del repo).
34
+ *
35
+ * El orden de visualización ES el orden del arreglo modelo; `position`
36
+ * (gap-sorting, base.md §8) se calcula junto con cada mutación y viaja en el
37
+ * payload. Un hueco que deja un `remove` se autocura en el siguiente `move`.
38
+ */
39
+ @Component({
40
+ selector: 'app-field-list',
41
+ imports: [
42
+ Button,
43
+ CdkDrag,
44
+ CdkDragHandle,
45
+ CdkDropList,
46
+ LucideArrowDown,
47
+ LucideArrowUp,
48
+ LucideGripVertical,
49
+ LucideTrash2,
50
+ NgTemplateOutlet,
51
+ ],
52
+ template: `
53
+ @if (items().length === 0) {
54
+ <p
55
+ class="rounded-lg border border-dashed border-neutral/30 px-4 py-6 text-center text-sm text-neutral"
56
+ >
57
+ {{ field().emptyText ?? 'Sin elementos.' }}
58
+ </p>
59
+ } @else {
60
+ <div
61
+ cdkDropList
62
+ class="space-y-3"
63
+ [cdkDropListData]="items()"
64
+ [cdkDropListDisabled]="!sortable()"
65
+ (cdkDropListDropped)="onDrop($event)"
66
+ >
67
+ @for (item of items(); track $index; let i = $index) {
68
+ <div
69
+ class="rounded-xl border border-neutral/20 bg-surface p-3"
70
+ cdkDrag
71
+ [cdkDragDisabled]="!sortable()"
72
+ >
73
+ <div class="mb-2 flex items-center gap-1.5">
74
+ @if (sortable()) {
75
+ <button
76
+ type="button"
77
+ app-button
78
+ variant="ghost"
79
+ size="icon"
80
+ class="cursor-grab"
81
+ cdkDragHandle
82
+ [attr.aria-label]="'Reordenar elemento ' + (i + 1)"
83
+ title="Arrastrar para reordenar"
84
+ >
85
+ <svg lucideGripVertical size="16" />
86
+ </button>
87
+ }
88
+ @if (isAccordion()) {
89
+ <button
90
+ type="button"
91
+ class="flex min-w-0 items-center gap-1 text-sm font-medium"
92
+ [attr.aria-expanded]="openOf(i)"
93
+ (click)="toggle(i)"
94
+ >
95
+ <span aria-hidden="true">{{ openOf(i) ? '▾' : '▸' }}</span>
96
+ <span class="truncate">{{ titleOf(i) }}</span>
97
+ </button>
98
+ }
99
+ @if (isPrimary(i)) {
100
+ <span
101
+ class="rounded-full bg-primary/10 px-2 py-0.5 text-xs font-medium text-primary"
102
+ >
103
+ Principal
104
+ </span>
105
+ }
106
+ @if (hasProblem(i)) {
107
+ <span class="text-xs font-medium text-danger">Revisar</span>
108
+ }
109
+ <span class="ml-auto flex shrink-0 items-center gap-2 lg:gap-1">
110
+ <button
111
+ type="button"
112
+ app-button
113
+ variant="ghost"
114
+ size="icon"
115
+ [disabled]="i === 0"
116
+ [attr.aria-label]="'Subir elemento ' + (i + 1)"
117
+ (click)="move(i, -1)"
118
+ >
119
+ <svg lucideArrowUp size="16" />
120
+ </button>
121
+ <button
122
+ type="button"
123
+ app-button
124
+ variant="ghost"
125
+ size="icon"
126
+ [disabled]="i === items().length - 1"
127
+ [attr.aria-label]="'Bajar elemento ' + (i + 1)"
128
+ (click)="move(i, 1)"
129
+ >
130
+ <svg lucideArrowDown size="16" />
131
+ </button>
132
+ <button
133
+ type="button"
134
+ app-button
135
+ variant="ghost"
136
+ size="icon"
137
+ [disabled]="!canRemove()"
138
+ [attr.aria-label]="'Eliminar elemento ' + (i + 1)"
139
+ (click)="remove(i)"
140
+ >
141
+ <svg lucideTrash2 size="16" />
142
+ </button>
143
+ </span>
144
+ </div>
145
+ @if (!isAccordion() || openOf(i)) {
146
+ @if (isRow()) {
147
+ <div class="flex flex-wrap items-start gap-3">
148
+ <ng-container
149
+ [ngTemplateOutlet]="childTemplate()"
150
+ [ngTemplateOutletContext]="childContext(i)"
151
+ />
152
+ </div>
153
+ } @else {
154
+ <div class="grid grid-cols-1 gap-x-4 sm:grid-cols-12">
155
+ <ng-container
156
+ [ngTemplateOutlet]="childTemplate()"
157
+ [ngTemplateOutletContext]="childContext(i)"
158
+ />
159
+ </div>
160
+ }
161
+ }
162
+ </div>
163
+ }
164
+ </div>
165
+ }
166
+
167
+ <div class="mt-3">
168
+ <button app-button variant="outline" type="button" [disabled]="!canAdd()" (click)="add()">
169
+ + {{ field().addLabel ?? 'Agregar elemento' }}
170
+ </button>
171
+ </div>
172
+ `,
173
+ })
174
+ export class FieldList {
175
+ readonly field = input.required<ListField>();
176
+ readonly tree = input.required<RootTree>();
177
+ readonly serverErrors = input<Record<string, string>>({});
178
+ readonly pathPrefix = input('');
179
+ readonly exists = input(false);
180
+ /** Template de los hijos, declarado en el `@case` del despachador. */
181
+ readonly childTemplate = input.required<TemplateRef<ListChildContext>>();
182
+
183
+ private readonly notifications = inject(NotificationService);
184
+
185
+ /** Intento de apertura por ítem (`undefined` → sigue el auto-open). */
186
+ private readonly intent = signal<Record<number, boolean>>({});
187
+
188
+ private readonly itemsTree = computed(() =>
189
+ listTree<Record<string, unknown>>(this.tree(), this.field().key),
190
+ );
191
+ private readonly state = computed(() => this.itemsTree()());
192
+ protected readonly items = computed(() => this.state().value() ?? []);
193
+
194
+ protected readonly sortable = computed(() => this.field().sortable ?? true);
195
+ /** Clase fija de la barra de ícono (literal: Tailwind v4 es JIT). */
196
+
197
+ protected canAdd(): boolean {
198
+ const max = this.field().max;
199
+ return max === undefined || this.items().length < max;
200
+ }
201
+
202
+ protected canRemove(): boolean {
203
+ return this.items().length > (this.field().min ?? 0);
204
+ }
205
+
206
+ protected isAccordion(): boolean {
207
+ return this.field().itemDisplay === 'accordion';
208
+ }
209
+
210
+ protected isRow(): boolean {
211
+ return this.field().itemDisplay === 'row';
212
+ }
213
+
214
+ protected isPrimary(index: number): boolean {
215
+ return (this.field().primaryFirst ?? false) && index === 0;
216
+ }
217
+
218
+ protected titleOf(index: number): string {
219
+ const item = this.items()[index];
220
+ const key = this.field().itemTitle;
221
+ const value = key !== undefined && item !== undefined ? item[key] : undefined;
222
+ return typeof value === 'string' && value !== '' ? value : `#${index + 1}`;
223
+ }
224
+
225
+ // ── Ítem abierto (accordion) ──
226
+
227
+ protected openOf(index: number): boolean {
228
+ const forced = this.intent()[index];
229
+ if (forced !== undefined) {
230
+ return forced;
231
+ }
232
+ return this.hasProblem(index);
233
+ }
234
+
235
+ protected toggle(index: number): void {
236
+ const willOpen = !this.openOf(index);
237
+ this.intent.update((map) => ({ ...map, [index]: willOpen }));
238
+ }
239
+
240
+ /** Inválido+tocado (agrega hijos) o error del backend bajo `key[i].`. */
241
+ hasProblem(index: number): boolean {
242
+ const state = itemTree(this.itemsTree(), index)();
243
+ if (state.invalid() && state.touched()) {
244
+ return true;
245
+ }
246
+ const prefix = `${this.pathPrefix()}${this.field().key}[${index}].`;
247
+ return Object.keys(this.serverErrors()).some((path) => path.startsWith(prefix));
248
+ }
249
+
250
+ // ── Mutaciones (todas sobre `controlValue` → marcan `dirty` solas) ──
251
+
252
+ protected add(): void {
253
+ if (!this.canAdd()) {
254
+ return;
255
+ }
256
+ const item = seedFields(this.field().itemFields);
257
+ const positionKey = this.field().positionField;
258
+ if (positionKey !== undefined) {
259
+ item[positionKey] = appendPosition(this.positioned());
260
+ }
261
+ const newIndex = this.items().length;
262
+ this.updateItems((next) => {
263
+ next.push(item);
264
+ });
265
+ // El ítem nuevo arranca visible.
266
+ this.intent.update((map) => ({ ...map, [newIndex]: true }));
267
+ }
268
+
269
+ protected move(index: number, direction: -1 | 1): void {
270
+ const target = index + direction;
271
+ if (target < 0 || target >= this.items().length) {
272
+ return;
273
+ }
274
+ this.applyMove(index, target);
275
+ }
276
+
277
+ protected onDrop(event: CdkDragDrop<Record<string, unknown>[]>): void {
278
+ if (event.previousIndex === event.currentIndex) {
279
+ return;
280
+ }
281
+ this.applyMove(event.previousIndex, event.currentIndex);
282
+ }
283
+
284
+ protected async remove(index: number): Promise<void> {
285
+ if (!this.canRemove()) {
286
+ return;
287
+ }
288
+ if (this.hasContent(index)) {
289
+ const accepted = await this.notifications.confirm({
290
+ title: `Eliminar elemento ${index + 1}`,
291
+ message: 'Esta acción no se puede deshacer.',
292
+ confirmLabel: 'Eliminar',
293
+ cancelLabel: 'Cancelar',
294
+ });
295
+ if (!accepted) {
296
+ return;
297
+ }
298
+ }
299
+ this.updateItems((next) => {
300
+ next.splice(index, 1);
301
+ });
302
+ this.intent.set({});
303
+ }
304
+
305
+ /**
306
+ * Contexto fresco por ítem: los nodos se atan a la identidad (NG01904).
307
+ *
308
+ * `$implicit` lleva el objeto completo: en el `ng-template` del host,
309
+ * `let-child` (sin alias) lee `$implicit` — `NgTemplateOutlet` reparte el
310
+ * contexto como propiedades top-level, no existe un alias mágico.
311
+ */
312
+ protected childContext(index: number): ListChildContext & { $implicit: ListChildContext } {
313
+ const context: ListChildContext = {
314
+ fields: this.field().itemFields,
315
+ tree: itemTree(this.itemsTree(), index),
316
+ prefix: `${this.pathPrefix()}${this.field().key}[${index}].`,
317
+ errors: this.serverErrors(),
318
+ exists: this.exists(),
319
+ row: this.isRow(),
320
+ colSpan: colSpanClass,
321
+ };
322
+ return { ...context, $implicit: context };
323
+ }
324
+
325
+ /** Ítem con algo distinto de sus `default` → pide confirmación. */
326
+ private hasContent(index: number): boolean {
327
+ const item = this.items()[index];
328
+ if (item === undefined) {
329
+ return false;
330
+ }
331
+ return JSON.stringify(item) !== JSON.stringify(seedFields(this.field().itemFields));
332
+ }
333
+
334
+ /**
335
+ * Mueve el ítem y recalcula su `position` (base.md §8): los cambios de
336
+ * posición se aplican ANTES del splice porque las claves referencian los
337
+ * índices originales.
338
+ */
339
+ private applyMove(from: number, to: number): void {
340
+ const positionKey = this.field().positionField;
341
+ this.updateItems((next) => {
342
+ if (positionKey !== undefined) {
343
+ const result = computeMove(this.positioned(), String(from), to);
344
+ for (const change of result.changes) {
345
+ const item = next[Number(change.key)];
346
+ if (item !== undefined) {
347
+ item[positionKey] = change.position;
348
+ }
349
+ }
350
+ }
351
+ moveItemInArray(next, from, to);
352
+ });
353
+ this.intent.set({});
354
+ }
355
+
356
+ /** Posiciones actuales; sin `positionField` no se calculan. */
357
+ private positioned(): PositionedItem[] {
358
+ const positionKey = this.field().positionField;
359
+ return this.items().map((item, index) => {
360
+ const raw = positionKey !== undefined ? item[positionKey] : undefined;
361
+ // Fallback: un registro viejo sin `position` se numera como rebalanceo.
362
+ return {
363
+ key: String(index),
364
+ position: typeof raw === 'number' ? raw : (index + 1) * GAP,
365
+ };
366
+ });
367
+ }
368
+
369
+ private updateItems(mutator: (next: Record<string, unknown>[]) => void): void {
370
+ this.state().controlValue.update((current) => {
371
+ const next = [...current];
372
+ mutator(next);
373
+ return next;
374
+ });
375
+ }
376
+ }
@@ -0,0 +1,11 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `multiselect` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'multiselect',
7
+ since: '1.0',
8
+ load: () => import('./multiselect').then((module) => module.FieldMultiselect),
9
+ seed: 'array',
10
+ surfaces: ['form', 'display'],
11
+ };
@@ -0,0 +1,142 @@
1
+ import { setSchemaCatalog } from '../../core/registry/schema-catalog';
2
+ import { testCatalog } from '../../core/testing/test-catalog';
3
+
4
+ setSchemaCatalog(testCatalog);
5
+
6
+ import { Component, EnvironmentInjector, signal } from '@angular/core';
7
+ import { TestBed } from '@angular/core/testing';
8
+ import { type FieldTree, FormField, form } from '@angular/forms/signals';
9
+ import { of } from 'rxjs';
10
+ import { vi } from 'vitest';
11
+
12
+ import type { MultiselectField } from '../../core/models/schema.model';
13
+ import { ApiService } from '../../core/services/api.service';
14
+ import { childTree, type RootTree } from '../field-node';
15
+ import { FieldMultiselect } from './multiselect';
16
+
17
+ const FILAS = [
18
+ { slug: 'bebidas', name: 'Bebidas' },
19
+ { slug: 'pan', name: 'Pan' },
20
+ ];
21
+
22
+ /**
23
+ * El anfitrión escrito a mano es obligatorio (D27): `model()` de `value`
24
+ * sólo se inicializa si el binding `[formField]` está en el HTML de una
25
+ * plantilla compilada — igual que en `render-host`.
26
+ */
27
+ @Component({
28
+ template: `<app-field-multiselect [field]="field" [formField]="node" [tree]="tree" />`,
29
+ imports: [FieldMultiselect, FormField],
30
+ })
31
+ class Host {
32
+ field!: MultiselectField;
33
+ node!: FieldTree<string[]>;
34
+ tree!: RootTree;
35
+ }
36
+
37
+ describe('Campo multiselect', () => {
38
+ const listSpy = vi.fn((endpoint?: unknown, payload?: unknown) => of([...FILAS]));
39
+
40
+ beforeEach(async () => {
41
+ listSpy.mockReset();
42
+ listSpy.mockImplementation(() => of(FILAS.map((fila) => ({ ...fila }))));
43
+ await TestBed.configureTestingModule({
44
+ imports: [Host],
45
+ providers: [{ provide: ApiService, useValue: { list: listSpy } }],
46
+ }).compileComponents();
47
+ });
48
+
49
+ async function create(field: MultiselectField, extra: Record<string, unknown> = {}) {
50
+ const fixture = TestBed.createComponent(Host);
51
+ const model = signal<Record<string, unknown>>({ [field.key]: [], ...extra });
52
+ const tree = form(model, () => {}, {
53
+ injector: TestBed.inject(EnvironmentInjector),
54
+ }) as unknown as RootTree;
55
+ fixture.componentInstance.field = field;
56
+ fixture.componentInstance.tree = tree;
57
+ fixture.componentInstance.node = childTree<string[]>(tree, field.key);
58
+ fixture.detectChanges();
59
+ return { fixture, model };
60
+ }
61
+
62
+ const botones = (fixture: { nativeElement: HTMLElement }): string[] =>
63
+ Array.from(fixture.nativeElement.querySelectorAll('button')).map(
64
+ (button) => button.textContent?.trim() ?? '',
65
+ );
66
+
67
+ it('con source pide las filas y pinta una opción por fila', async () => {
68
+ const { fixture } = await create({
69
+ type: 'multiselect',
70
+ key: 'tags',
71
+ label: 'Etiquetas',
72
+ source: { resource: 'categories', valueField: 'slug', labelField: 'name' },
73
+ });
74
+ await fixture.whenStable();
75
+ fixture.detectChanges();
76
+
77
+ expect(listSpy).toHaveBeenCalledTimes(1);
78
+ expect(botones(fixture)).toEqual(['Bebidas', 'Pan']);
79
+ });
80
+
81
+ it('las opciones estáticas no piden nada a la red', async () => {
82
+ const { fixture } = await create({
83
+ type: 'multiselect',
84
+ key: 'tags',
85
+ label: 'Etiquetas',
86
+ options: [
87
+ { value: 'a', label: 'A' },
88
+ { value: 'b', label: 'B' },
89
+ ],
90
+ });
91
+ await fixture.whenStable();
92
+ fixture.detectChanges();
93
+
94
+ expect(listSpy).not.toHaveBeenCalled();
95
+ expect(botones(fixture)).toEqual(['A', 'B']);
96
+ });
97
+
98
+ it('source.params se resuelven con el registro del formulario (`tree`)', async () => {
99
+ // Sin `tree` el multiselect no podría resolver `{lang}` y mandaría la
100
+ // plantilla cruda: sería la única regla distinta del select (F8).
101
+ const { fixture, model } = await create(
102
+ {
103
+ type: 'multiselect',
104
+ key: 'tags',
105
+ label: 'Etiquetas',
106
+ source: { resource: 'categories', params: { orden: '{lang}' } },
107
+ },
108
+ { lang: 'es' },
109
+ );
110
+ await fixture.whenStable();
111
+ fixture.detectChanges();
112
+
113
+ expect(listSpy.mock.calls[0][1]).toEqual({ params: { orden: 'es' } });
114
+
115
+ model.set({ tags: [], lang: 'en' });
116
+ fixture.detectChanges();
117
+ await fixture.whenStable();
118
+ fixture.detectChanges();
119
+
120
+ expect(listSpy).toHaveBeenCalledTimes(2);
121
+ expect(listSpy.mock.calls[1][1]).toEqual({ params: { orden: 'en' } });
122
+ });
123
+
124
+ it('un fallo de carga se anuncia y no pinta opciones', async () => {
125
+ listSpy.mockImplementation(() => {
126
+ throw new Error('503');
127
+ });
128
+ const { fixture } = await create({
129
+ type: 'multiselect',
130
+ key: 'tags',
131
+ label: 'Etiquetas',
132
+ source: { resource: 'categories' },
133
+ });
134
+ await fixture.whenStable();
135
+ fixture.detectChanges();
136
+
137
+ const aviso = fixture.nativeElement.querySelector('[role="alert"]');
138
+ expect(aviso).not.toBeNull();
139
+ expect(aviso.textContent).toContain('No se pudieron cargar las opciones.');
140
+ expect(botones(fixture)).toEqual([]);
141
+ });
142
+ });
@@ -0,0 +1,152 @@
1
+ import { Component, computed, inject, input, model, resource } from '@angular/core';
2
+ import { LucideX } from '@lucide/angular';
3
+ import type { FormValueControl } from '@angular/forms/signals';
4
+
5
+ import type { MultiselectField, SelectOption } from '../../core/models/schema.model';
6
+ import { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';
7
+ import { optionsFromRows } from '../../core/utils/source-options';
8
+ import { getSchema } from '../../core/registry/schema-catalog';
9
+ import type { RootTree } from '../field-node';
10
+
11
+ /**
12
+ * Selección múltiple con chips (base.md §5.1). `<select multiple>` no está
13
+ * soportado por `[formField]`, así que es un control propio que cumple el
14
+ * contrato `FormValueControl<string[]>`: el nodo llega desde el host con
15
+ * `[formField]` y el valor se sincroniza por `model()`.
16
+ *
17
+ * Opciones: estáticas (`options`) o una fuente declarada (`source`), con la
18
+ * misma regla XOR que el `select` y el mismo fetcher (F8 · R6.5). `tree`
19
+ * existe para resolver `{campo}` en `source.params` con el registro del
20
+ * formulario — sin él un multiselect con parámetros quedaría con el mismo
21
+ * nombre de opción y otra regla que el select.
22
+ */
23
+ @Component({
24
+ selector: 'app-field-multiselect',
25
+ imports: [LucideX],
26
+ template: `
27
+ <div class="space-y-2">
28
+ @if (fallo()) {
29
+ <p class="text-xs text-danger" role="alert">No se pudieron cargar las opciones.</p>
30
+ } @else if (cargando()) {
31
+ <p class="text-xs text-neutral" role="status">Cargando opciones…</p>
32
+ }
33
+ @if (value().length > 0) {
34
+ <div class="flex flex-wrap gap-1.5" data-testid="multiselect-chips">
35
+ @for (item of value(); track item) {
36
+ <span
37
+ class="inline-flex items-center rounded-full bg-primary/10 px-2.5 py-1 text-xs text-primary"
38
+ >
39
+ {{ labelOf(item) }}
40
+ <button
41
+ type="button"
42
+ class="rounded-full px-0.5 hover:text-danger"
43
+ [attr.aria-label]="'Quitar ' + labelOf(item)"
44
+ [disabled]="disabled()"
45
+ (click)="remove(item)"
46
+ >
47
+ <svg lucideX size="14" />
48
+ </button>
49
+ </span>
50
+ }
51
+ </div>
52
+ }
53
+ <div class="flex flex-wrap gap-1.5">
54
+ @for (option of opciones(); track option.value) {
55
+ @if (!isSelected(option.value)) {
56
+ <button
57
+ type="button"
58
+ class="rounded-full border border-neutral/30 px-2.5 py-1 text-xs hover:border-primary disabled:cursor-not-allowed disabled:opacity-50"
59
+ [disabled]="disabled() || (option.disabled ?? false)"
60
+ (click)="toggle(option)"
61
+ >
62
+ {{ option.label }}
63
+ </button>
64
+ }
65
+ }
66
+ </div>
67
+ </div>
68
+ `,
69
+ })
70
+ export class FieldMultiselect implements FormValueControl<string[]> {
71
+ readonly field = input.required<MultiselectField>();
72
+
73
+ /** Registro del formulario, sólo para resolver `{campo}` en `source.params`. */
74
+ readonly tree = input<RootTree>();
75
+
76
+ /** Sincronizado por el directive `FormField` del host. */
77
+ readonly value = model.required<string[]>();
78
+ readonly disabled = input(false);
79
+
80
+ private readonly ds = inject(DataSourceService);
81
+
82
+ private readonly registro = computed<Record<string, unknown> | undefined>(() => {
83
+ const source = this.field().source;
84
+ const tree = this.tree();
85
+ if (source === undefined || source.params === undefined || tree === undefined) return undefined;
86
+ return tree().value();
87
+ });
88
+
89
+ /** `undefined` sin fuente ⇒ `resource` queda idle y jamás llama al loader. */
90
+ private readonly claveFuente = computed(() => {
91
+ const source = this.field().source;
92
+ return source === undefined ? undefined : dataSourceKey(source, this.registro());
93
+ });
94
+
95
+ private readonly fuenteResource = resource({
96
+ params: () => this.claveFuente(),
97
+ loader: async () => {
98
+ const source = this.field().source;
99
+ if (source === undefined) return [];
100
+ return this.ds.rows(source, this.registro());
101
+ },
102
+ });
103
+
104
+ /** Estáticas del schema o filas de la fuente declarada (F8 · R6.5). */
105
+ protected readonly opciones = computed<SelectOption[]>(() => {
106
+ const field = this.field();
107
+ if (field.source === undefined) return field.options ?? [];
108
+ // `value()` del resource TIRA en estado error (contract de Angular):
109
+ // se mira el estado antes de leerlo, o el render rompe en vez de
110
+ // pintar el aviso de fallo.
111
+ if (this.fuenteResource.status() === 'error') return [];
112
+ return optionsFromRows(this.fuenteResource.value() ?? [], {
113
+ source: field.source,
114
+ target: getSchema(field.source.resource),
115
+ });
116
+ });
117
+
118
+ /** Sólo aplica con `source`: una lista estática no tiene vuelta que esperar. */
119
+ protected readonly cargando = computed(
120
+ () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'loading',
121
+ );
122
+
123
+ protected readonly fallo = computed(
124
+ () => this.claveFuente() !== undefined && this.fuenteResource.status() === 'error',
125
+ );
126
+
127
+ protected toggle(option: SelectOption): void {
128
+ if (this.disabled()) {
129
+ return;
130
+ }
131
+ this.value.update((current) =>
132
+ current.includes(option.value)
133
+ ? current.filter((item) => item !== option.value)
134
+ : [...current, option.value],
135
+ );
136
+ }
137
+
138
+ protected remove(item: string): void {
139
+ if (this.disabled()) {
140
+ return;
141
+ }
142
+ this.value.update((current) => current.filter((value) => value !== item));
143
+ }
144
+
145
+ protected isSelected(item: string): boolean {
146
+ return this.value().includes(item);
147
+ }
148
+
149
+ protected labelOf(item: string): string {
150
+ return this.opciones().find((option) => option.value === item)?.label ?? item;
151
+ }
152
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `number` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'number',
7
+ since: '1.0',
8
+ load: () => import('./number').then((module) => module.FieldNumber),
9
+ // Nace como `null`: no puede ser `0`, que sería un valor ya cargado.
10
+ seed: 'number',
11
+ surfaces: ['form', 'display'],
12
+ };