@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,40 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+ import { FormField } from '@angular/forms/signals';
3
+
4
+ import type { NumberField } from '../../core/models/schema.model';
5
+ import { FieldAria } from '../field-aria';
6
+ import { childTree, type RootTree } from '../field-node';
7
+
8
+ /**
9
+ * Input numérico (base.md §5.1). `step` sale del tipo o de `decimals`;
10
+ * `min`/`max` los gestiona `FormField` desde las reglas (Fase 10).
11
+ */
12
+ @Component({
13
+ selector: 'app-field-number',
14
+ imports: [FieldAria, FormField],
15
+ template: `
16
+ <input
17
+ type="number"
18
+ class="field-input"
19
+ [id]="state().name()"
20
+ [formField]="node()"
21
+ [step]="stepAttr()"
22
+ [placeholder]="field().placeholder ?? ''"
23
+ />
24
+ `,
25
+ })
26
+ export class FieldNumber {
27
+ readonly field = input.required<NumberField>();
28
+ readonly tree = input.required<RootTree>();
29
+
30
+ protected readonly node = computed(() => childTree<number>(this.tree(), this.field().key));
31
+ protected readonly state = computed(() => this.node()());
32
+
33
+ protected readonly stepAttr = computed<number | null>(() => {
34
+ const field = this.field();
35
+ if (field.step !== undefined) {
36
+ return field.step;
37
+ }
38
+ return field.decimals !== undefined ? Math.pow(10, -field.decimals) : null;
39
+ });
40
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `phone` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'phone',
7
+ since: '1.0',
8
+ load: () => import('./phone').then((module) => module.FieldPhone),
9
+ // Nace como cadena: los textos, fechas, urls, selects y relacionales.
10
+ seed: 'string',
11
+ surfaces: ['form', 'display'],
12
+ };
@@ -0,0 +1,46 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+ import { FormField } from '@angular/forms/signals';
3
+
4
+ import type { PhoneField } from '../../core/models/schema.model';
5
+ import { FieldAria } from '../field-aria';
6
+ import { childTree, type RootTree } from '../field-node';
7
+
8
+ /**
9
+ * Input telefónico (base.md §5.1). El valor se normaliza al perder el foco:
10
+ * `e164` → `+` y dígitos (máx. 15); `digits` → solo dígitos. Mientras se
11
+ * escribe se conserva lo tipeado.
12
+ */
13
+ @Component({
14
+ selector: 'app-field-phone',
15
+ imports: [FieldAria, FormField],
16
+ template: `
17
+ <input
18
+ type="tel"
19
+ class="field-input"
20
+ [id]="state().name()"
21
+ [formField]="node()"
22
+ [placeholder]="field().placeholder ?? ''"
23
+ (blur)="normalize()"
24
+ />
25
+ `,
26
+ })
27
+ export class FieldPhone {
28
+ readonly field = input.required<PhoneField>();
29
+ readonly tree = input.required<RootTree>();
30
+
31
+ protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
32
+ protected readonly state = computed(() => this.node()());
33
+
34
+ protected normalize(): void {
35
+ const current = this.state().value();
36
+ if (typeof current !== 'string' || current === '') {
37
+ return;
38
+ }
39
+ const digits = current.replace(/\D/g, '');
40
+ const normalized =
41
+ this.field().format === 'digits' ? digits : digits !== '' ? `+${digits.slice(0, 15)}` : '';
42
+ if (normalized !== current) {
43
+ this.state().value.set(normalized);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `readonly-text` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'readonly-text',
7
+ since: '1.0',
8
+ load: () => import('./readonly-text').then((module) => module.FieldReadonlyText),
9
+ // Nace como cadena: los textos, fechas, urls, selects y relacionales.
10
+ seed: 'string',
11
+ surfaces: ['form', 'display'],
12
+ };
@@ -0,0 +1,58 @@
1
+ import { Component, computed, input } from '@angular/core';
2
+
3
+ import type { ReadonlyTextField } from '../../core/models/schema.model';
4
+ import { FieldAria } from '../field-aria';
5
+ import { childTree, type RootTree } from '../field-node';
6
+
7
+ /**
8
+ * Dato de solo lectura (base.md §5.1, ej. `updated_at`). No edita: lee el
9
+ * valor del árbol y lo presenta según `format` (`date`, `datetime`,
10
+ * `number`; por defecto, texto). Renderiza `<output>` para que el `<label>`
11
+ * del host pueda asociarse por `for`.
12
+ */
13
+ @Component({
14
+ selector: 'app-field-readonly-text',
15
+ imports: [FieldAria],
16
+ template: `
17
+ <output
18
+ [id]="state().name()"
19
+ class="block py-2 text-sm text-neutral"
20
+ data-testid="readonly-text"
21
+ >
22
+ {{ display() }}
23
+ </output>
24
+ `,
25
+ })
26
+ export class FieldReadonlyText {
27
+ readonly field = input.required<ReadonlyTextField>();
28
+ readonly tree = input.required<RootTree>();
29
+
30
+ private readonly node = computed(() => childTree<unknown>(this.tree(), this.field().key));
31
+ protected readonly state = computed(() => this.node()());
32
+
33
+ protected readonly display = computed(() => {
34
+ const value = this.state().value();
35
+ if (value === null || value === undefined || value === '') {
36
+ return '—';
37
+ }
38
+ const format = this.field().format;
39
+ if (format === 'date' || format === 'datetime') {
40
+ const date = new Date(String(value));
41
+ if (Number.isNaN(date.getTime())) {
42
+ return String(value);
43
+ }
44
+ return new Intl.DateTimeFormat('es-PE', {
45
+ timeZone: 'UTC',
46
+ day: '2-digit',
47
+ month: '2-digit',
48
+ year: 'numeric',
49
+ ...(format === 'datetime' ? { hour: '2-digit', minute: '2-digit' } : {}),
50
+ }).format(date);
51
+ }
52
+ if (format === 'number') {
53
+ const number = Number(value);
54
+ return Number.isNaN(number) ? String(value) : new Intl.NumberFormat('es-PE').format(number);
55
+ }
56
+ return typeof value === 'string' ? value : String(value);
57
+ });
58
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `relation` (R3.3). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'relation',
7
+ since: '1.0',
8
+ load: () => import('./relation').then((module) => module.FieldRelation),
9
+ // Nace como cadena: los textos, fechas, urls, selects y relacionales.
10
+ seed: 'string',
11
+ surfaces: ['form', 'display'],
12
+ };
@@ -0,0 +1,193 @@
1
+ import { setSchemaCatalog } from '../../core/registry/schema-catalog';
2
+ import { testCatalog } from '../../core/testing/test-catalog';
3
+
4
+ setSchemaCatalog(testCatalog);
5
+
6
+ import { TestBed } from '@angular/core/testing';
7
+ import { EnvironmentInjector, signal } from '@angular/core';
8
+ import { form } from '@angular/forms/signals';
9
+ import { of } from 'rxjs';
10
+ import { vi } from 'vitest';
11
+
12
+ import type { RelationField } from '../../core/models/schema.model';
13
+ import { ApiService } from '../../core/services/api.service';
14
+ import { DataSourceService } from '../../core/services/data-source.service';
15
+ import { RelationDrawerService } from '../../core/services/relation-drawer.service';
16
+ import { type RootTree } from '../field-node';
17
+ import { FieldRelation } from './relation';
18
+
19
+ /** Fila mínima de `categories` (keyField `slug`, titleField `name`). */
20
+ function rows(count: number): Record<string, unknown>[] {
21
+ return Array.from({ length: count }, (_, index) => ({
22
+ slug: `cat-${index}`,
23
+ name: `Categoría ${index}`,
24
+ active: index !== 3,
25
+ description: `Descripción ${index}`,
26
+ }));
27
+ }
28
+
29
+ describe('Campo relación', () => {
30
+ const listSpy = vi.fn((...args: unknown[]) => of(rows(9)));
31
+
32
+ beforeEach(async () => {
33
+ listSpy.mockClear();
34
+ listSpy.mockImplementation(() => of(rows(9)));
35
+ await TestBed.configureTestingModule({
36
+ imports: [FieldRelation],
37
+ providers: [{ provide: ApiService, useValue: { list: listSpy } }],
38
+ }).compileComponents();
39
+ });
40
+
41
+ async function create(field: RelationField, value = '', extra: Record<string, unknown> = {}) {
42
+ const fixture = TestBed.createComponent(FieldRelation);
43
+ // Un objeto literal no sirve: `childTree` espera un FieldTree real
44
+ // (el nodo es invocable y devuelve el estado del campo).
45
+ const model = signal<Record<string, unknown>>({ category_slug: value, ...extra });
46
+ // `form()` inyecta en el arranque: fuera de un contexto hay que pasarle
47
+ // el injector, igual que hace form-view.
48
+ const tree = form(model, () => {}, {
49
+ injector: TestBed.inject(EnvironmentInjector),
50
+ }) as unknown as RootTree;
51
+ fixture.componentRef.setInput('field', field);
52
+ fixture.componentRef.setInput('tree', tree);
53
+ fixture.detectChanges();
54
+ return { fixture, tree, model };
55
+ }
56
+
57
+ const categoryField = (): RelationField => ({
58
+ key: 'category_slug',
59
+ label: 'Categoría',
60
+ type: 'relation',
61
+ source: { resource: 'categories' },
62
+ });
63
+
64
+ const findButton = (fixture: { nativeElement: HTMLElement }, text: string): HTMLButtonElement => {
65
+ const found = Array.from(fixture.nativeElement.querySelectorAll('button')).find((b) =>
66
+ b.textContent?.includes(text),
67
+ );
68
+ if (found === undefined) throw new Error(`No se encontró el botón «${text}»`);
69
+ return found;
70
+ };
71
+
72
+ const options = (fixture: { nativeElement: HTMLElement }): HTMLOptionElement[] =>
73
+ Array.from(fixture.nativeElement.querySelectorAll('option'));
74
+
75
+ it('mientras cargan las opciones muestra el skeleton en un nodo labelable', async () => {
76
+ const { fixture } = await create(categoryField());
77
+
78
+ const loading = fixture.nativeElement.querySelector('output[aria-busy="true"]');
79
+ expect(loading).not.toBeNull();
80
+ expect(fixture.nativeElement.querySelector('app-skeleton')).not.toBeNull();
81
+ expect(loading!.textContent).toContain('Cargando opciones…');
82
+ });
83
+
84
+ it('tras cargar, el select trae las opciones del recurso destino', async () => {
85
+ const { fixture } = await create(categoryField());
86
+ await fixture.whenStable();
87
+ fixture.detectChanges();
88
+
89
+ // 9 filas + la opción vacía (el campo no es required)
90
+ expect(options(fixture)).toHaveLength(10);
91
+ expect(listSpy).toHaveBeenCalledTimes(1);
92
+ expect(options(fixture)[1].textContent?.trim()).toBe('Categoría 0');
93
+ expect(fixture.nativeElement.querySelector('output[aria-busy]')).toBeNull();
94
+ });
95
+
96
+ it('secondaryField agrega el dato al lado de la etiqueta', async () => {
97
+ const { fixture } = await create({ ...categoryField(), secondaryField: 'description' });
98
+ await fixture.whenStable();
99
+ fixture.detectChanges();
100
+
101
+ expect(options(fixture)[1].textContent?.trim()).toBe('Categoría 0 · Descripción 0');
102
+ });
103
+
104
+ it('source.params se resuelven del formulario y la fuente recarga al cambiar', async () => {
105
+ // F8 · R6.5: `{lang}` se interpola contra el registro y el resultado
106
+ // entra en la clave de caché — mismo recurso, otra petición.
107
+ const field: RelationField = {
108
+ ...categoryField(),
109
+ source: { resource: 'categories', params: { orden: '{lang}' } },
110
+ };
111
+ const { fixture, model } = await create(field, '', { lang: 'es' });
112
+ await fixture.whenStable();
113
+ fixture.detectChanges();
114
+
115
+ expect(listSpy).toHaveBeenCalledTimes(1);
116
+ expect(listSpy.mock.calls[0][1]).toEqual({ params: { orden: 'es' } });
117
+
118
+ model.set({ category_slug: '', lang: 'en' });
119
+ fixture.detectChanges();
120
+ await fixture.whenStable();
121
+ fixture.detectChanges();
122
+
123
+ expect(listSpy).toHaveBeenCalledTimes(2);
124
+ expect(listSpy.mock.calls[1][1]).toEqual({ params: { orden: 'en' } });
125
+ expect(options(fixture)).toHaveLength(10); // sigue habiendo opciones tras la recarga
126
+ });
127
+
128
+ it('con más de 7 opciones aparece el buscador y con menos no', async () => {
129
+ const { fixture: withSearch } = await create(categoryField());
130
+ await withSearch.whenStable();
131
+ withSearch.detectChanges();
132
+ expect(withSearch.nativeElement.querySelector('input[type="search"]')).not.toBeNull();
133
+
134
+ // El servicio cachea por clave de fuente (30 s): sin invalidar, el segundo
135
+ // formulario heredaría las 9 filas del primero y el umbral no se vería.
136
+ TestBed.inject(DataSourceService).invalidate();
137
+ listSpy.mockImplementation(() => of(rows(4)));
138
+ const { fixture: withoutSearch } = await create(categoryField());
139
+ await withoutSearch.whenStable();
140
+ withoutSearch.detectChanges();
141
+ expect(withoutSearch.nativeElement.querySelector('input[type="search"]')).toBeNull();
142
+ });
143
+
144
+ it('filtra y anuncia «Sin resultados» sin borrar la opción elegida', async () => {
145
+ const { fixture } = await create({ ...categoryField(), required: true }, 'cat-1');
146
+ await fixture.whenStable();
147
+ fixture.detectChanges();
148
+
149
+ const search = fixture.nativeElement.querySelector('input[type="search"]') as HTMLInputElement;
150
+ expect(search).not.toBeNull();
151
+
152
+ search.value = 'zzz-no-existe';
153
+ search.dispatchEvent(new Event('input', { bubbles: true }));
154
+ fixture.detectChanges();
155
+
156
+ const status = fixture.nativeElement.querySelector('[role="status"]');
157
+ expect(status?.textContent).toContain('Sin resultados para «zzz-no-existe»');
158
+ // Sigue visible aunque la etiqueta no coincida con el término
159
+ expect(options(fixture).map((option) => option.value)).toEqual(['cat-1']);
160
+ });
161
+
162
+ describe('botón «Crear nuevo» (plan 6.4)', () => {
163
+ it('pide el panel con el recurso de la relación', async () => {
164
+ const { fixture } = await create(categoryField());
165
+ await fixture.whenStable();
166
+ fixture.detectChanges();
167
+ const drawer = TestBed.inject(RelationDrawerService);
168
+
169
+ expect(drawer.request()).toBeNull();
170
+
171
+ findButton(fixture, 'Crear nuevo').click();
172
+
173
+ expect(drawer.request()?.resource).toBe('categories');
174
+ });
175
+
176
+ it('al confirmarse la creación, preselecciona la clave nueva', async () => {
177
+ const { fixture, tree } = await create(categoryField());
178
+ await fixture.whenStable();
179
+ fixture.detectChanges();
180
+ const drawer = TestBed.inject(RelationDrawerService);
181
+
182
+ findButton(fixture, 'Crear nuevo').click();
183
+ const requestsBefore = listSpy.mock.calls.length;
184
+ drawer.request()!.onCreated('cat-nueva');
185
+
186
+ expect(tree().value()).toEqual({ category_slug: 'cat-nueva' });
187
+ // pide recargar las opciones para que la clave tenga dónde engancharse
188
+ // (`reload()` de la resource encola la petición, no corre en el tick)
189
+ await fixture.whenStable();
190
+ expect(listSpy.mock.calls.length).toBeGreaterThan(requestsBefore);
191
+ });
192
+ });
193
+ });
@@ -0,0 +1,224 @@
1
+ import { Component, computed, inject, input, resource, signal } from '@angular/core';
2
+ import { FormField } from '@angular/forms/signals';
3
+
4
+ import { LucidePlus } from '@lucide/angular';
5
+
6
+ import type { RelationField, SelectOption } from '../../core/models/schema.model';
7
+ import { DataSourceService, dataSourceKey } from '../../core/services/data-source.service';
8
+ import { RelationDrawerService } from '../../core/services/relation-drawer.service';
9
+ import { optionsFromRows } from '../../core/utils/source-options';
10
+ import { Button } from '../../shared/components/button/button';
11
+ import { getSchema } from '../../core/registry/schema-catalog';
12
+ import { Skeleton } from '../../shared/components/skeleton/skeleton';
13
+ import { FieldAria } from '../field-aria';
14
+ import { childTree, type RootTree } from '../field-node';
15
+
16
+ /**
17
+ * Referencia a otro recurso (base.md §5.2): `<select>` con opciones del
18
+ * listado del recurso destino, cargadas desde `field.source` (F8 · R6.5).
19
+ *
20
+ * La vuelta no la arma este componente: pasa por `DataSourceService`, que
21
+ * centraliza la caché por `resource+view+params`. El `resource()` de Angular
22
+ * de acá sólo aporta el estado (loading/error/recarga), y su `params` es la
23
+ * clave de la fuente ya interpolada — así una relación con `params`
24
+ * dependientes del formulario se recarga cuando cambia el dato, y una sin
25
+ * `params` no se entera de las teclas.
26
+ *
27
+ * Más de 7 opciones → búsqueda encima del select (plan 6.1, guía §1). Se
28
+ * filtra un `<select>` nativo y no se construye un combobox propio: conserva
29
+ * la navegación con teclado del navegador, el valor seleccionado visible y
30
+ * la lista de opciones accesible sin gestionar `aria-activedescendant`.
31
+ */
32
+ @Component({
33
+ selector: 'app-field-relation',
34
+ imports: [Button, FieldAria, FormField, LucidePlus, Skeleton],
35
+ template: `
36
+ @switch (optionsResource.status()) {
37
+ @case ('loading') {
38
+ <!-- El output es labelable: el for del label del host sigue teniendo
39
+ ancla, y como ya es live region anuncia la carga. El skeleton
40
+ reemplaza al texto con animate-pulse (plan 6.7). -->
41
+ <output class="block" [id]="state().name()" aria-busy="true">
42
+ <app-skeleton [lines]="1" [height]="40" />
43
+ <span class="sr-only">Cargando opciones…</span>
44
+ </output>
45
+ }
46
+ @case ('error') {
47
+ <!-- Output en vez de un párrafo: es labelable (el label del host
48
+ tiene ancla) y ya es live region, así que con role=alert el
49
+ fallo de carga se anuncia sin repetir el texto en otro nodo. -->
50
+ <output class="field-input block text-danger" [id]="state().name()" role="alert">
51
+ No se pudieron cargar las opciones.
52
+ </output>
53
+ }
54
+ @default {
55
+ @if (useSearch()) {
56
+ <!-- Guía §1: más de 7 opciones → dropdown con búsqueda. El filtro
57
+ no reemplaza al select, lo acota; así el teclado y el valor
58
+ elegido siguen funcionando igual. -->
59
+ <input
60
+ type="search"
61
+ class="mb-1 w-full rounded-lg border border-neutral/30 bg-surface px-2 py-1 text-sm focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary"
62
+ placeholder="Buscar…"
63
+ aria-label="Buscar entre las opciones"
64
+ [attr.aria-controls]="state().name()"
65
+ [value]="query()"
66
+ (input)="onQuery($event)"
67
+ />
68
+ }
69
+ <select class="field-input" [id]="state().name()" [formField]="node()">
70
+ @if (!field().required) {
71
+ <option value="">{{ field().placeholder ?? '— Seleccionar —' }}</option>
72
+ }
73
+ @for (option of visibleOptions(); track option.value) {
74
+ <option [value]="option.value">{{ option.label }}</option>
75
+ }
76
+ </select>
77
+ @if (noMatches()) {
78
+ <p class="mt-1 text-xs text-neutral" role="status">
79
+ Sin resultados para «{{ query() }}».
80
+ </p>
81
+ } @else if (options().length === 0) {
82
+ <p class="mt-1 text-xs text-neutral">Sin opciones disponibles.</p>
83
+ }
84
+ @if (targetSchema() !== undefined && canCreate()) {
85
+ <!-- Un <option> no puede contener un botón: la acción va justo
86
+ debajo del control, pegada al dropdown. -->
87
+ <button
88
+ app-button
89
+ variant="ghost"
90
+ type="button"
91
+ class="mt-1 -ml-2"
92
+ (click)="requestCreate()"
93
+ >
94
+ <svg lucidePlus size="16" />
95
+ Crear nuevo
96
+ </button>
97
+ }
98
+ }
99
+ }
100
+ `,
101
+ })
102
+ export class FieldRelation {
103
+ readonly field = input.required<RelationField>();
104
+ readonly tree = input.required<RootTree>();
105
+
106
+ private readonly ds = inject(DataSourceService);
107
+ private readonly drawer = inject(RelationDrawerService);
108
+
109
+ protected readonly node = computed(() => childTree<string>(this.tree(), this.field().key));
110
+ protected readonly state = computed(() => this.node()());
111
+
112
+ /** Texto escrito en el buscador (sólo se usa si supera el umbral). */
113
+ protected readonly query = signal('');
114
+
115
+ protected readonly targetSchema = computed(() => getSchema(this.field().source.resource));
116
+
117
+ protected readonly canCreate = computed(() => this.targetSchema()?.permissions?.create !== false);
118
+
119
+ /** Umbral de la guía §1: 5–7 opciones dropdown simple, más de 7 con búsqueda. */
120
+ protected readonly useSearch = computed(() => this.options().length > 7);
121
+
122
+ protected onQuery(event: Event): void {
123
+ this.query.set((event.target as HTMLInputElement).value);
124
+ }
125
+
126
+ /**
127
+ * Opciones que coinciden con el término de búsqueda, SIN conservar la
128
+ * elegida. Es la base del mensaje «Sin resultados».
129
+ */
130
+ protected readonly matches = computed<SelectOption[]>(() => {
131
+ const all = this.options();
132
+ const term = this.query().trim().toLowerCase();
133
+ if (term === '') {
134
+ return all;
135
+ }
136
+ return all.filter((option) => option.label.toLowerCase().includes(term));
137
+ });
138
+
139
+ /** El buscador está activo y no encontró nada nuevo (el elegido no cuenta). */
140
+ protected readonly noMatches = computed(
141
+ () => this.useSearch() && this.query().trim() !== '' && this.matches().length === 0,
142
+ );
143
+
144
+ /**
145
+ * Opciones que se dibujan. Con búsqueda activa se acotan por etiqueta,
146
+ * PERO la opción elegida se conserva siempre: si desapareciera del DOM el
147
+ * select dejaría de mostrar el valor actual (guía §1: «valor seleccionado
148
+ * siempre visible»). Por eso no se dibuja a partir de `matches`.
149
+ */
150
+ protected readonly visibleOptions = computed<SelectOption[]>(() => {
151
+ const filtered = this.matches();
152
+ if (this.query().trim() === '') {
153
+ return filtered;
154
+ }
155
+ const selected = String(this.state().value() ?? '');
156
+ if (filtered.some((option) => option.value === selected)) {
157
+ return filtered;
158
+ }
159
+ const kept = this.options().find((option) => option.value === selected);
160
+ return kept === undefined ? filtered : [...filtered, kept];
161
+ });
162
+
163
+ /**
164
+ * Pide el panel de «Crear nuevo». El servicio guarda el callback que, al
165
+ * confirmarse la creación, recarga las opciones y preselecciona la clave.
166
+ */
167
+ protected requestCreate(): void {
168
+ const resource = this.field().source.resource;
169
+ this.drawer.open(resource, (key) => {
170
+ // La fila nueva todavía no está en la caché compartida: sin este
171
+ // `invalidate` la recarga devolvería la lista vieja (F8 · R6.5).
172
+ this.ds.invalidate(resource);
173
+ this.optionsResource.reload();
174
+ // Se setea DESPUÉS de pedir la recarga: si la opción todavía no está
175
+ // en el DOM el select mostraría vacío, y al llegar se reengancha sola
176
+ // (visibleOptions conserva la elegida aunque el filtro la oculte).
177
+ this.node()().value.set(key);
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Registro del formulario, sólo para resolver `{campo}` en `source.params`.
183
+ *
184
+ * Si la fuente no declara parámetros el registro NO se lee: la relación
185
+ * no depende del formulario y no se recalcula con cada tecla.
186
+ */
187
+ private readonly registro = computed<Record<string, unknown> | undefined>(() =>
188
+ this.field().source.params === undefined ? undefined : this.tree()().value(),
189
+ );
190
+
191
+ /** Clave de la fuente ya interpolada: es lo que `resource` compara. */
192
+ private readonly claveFuente = computed(() =>
193
+ dataSourceKey(this.field().source, this.registro()),
194
+ );
195
+
196
+ /**
197
+ * Carga de las opciones. `resource()` sólo aporta estado (loading/error/
198
+ * recarga): la vuelta y la caché son del `DataSourceService`, que así
199
+ * comparte filas con el resto de consumidores de la misma fuente.
200
+ */
201
+ private readonly optionsResource = resource({
202
+ params: () => this.claveFuente(),
203
+ loader: async () => this.ds.rows(this.field().source, this.registro()),
204
+ });
205
+
206
+ /**
207
+ * Opciones (F8): filas de la fuente declarada → `SelectOption`.
208
+ *
209
+ * El `@switch` del template sólo entra en `default` cuando no hubo fallo,
210
+ * pero el estado se mira acá también: `value()` del resource TIRA en
211
+ * estado error (contract de Angular) y un computed que se evalúe por otra
212
+ * vía tiraría el render entero.
213
+ */
214
+ protected readonly options = computed<SelectOption[]>(() =>
215
+ this.optionsResource.status() === 'error'
216
+ ? []
217
+ : optionsFromRows(this.optionsResource.value() ?? [], {
218
+ source: this.field().source,
219
+ target: this.targetSchema(),
220
+ onlyActive: this.field().onlyActive,
221
+ secondaryField: this.field().secondaryField,
222
+ }),
223
+ );
224
+ }
@@ -0,0 +1,12 @@
1
+ import type { RenderDescriptor } from '../../core/registry/render-registry';
2
+
3
+ /** Descriptor del campo `rich-select` (selector visual enriquecido). */
4
+ export const descriptor: RenderDescriptor = {
5
+ kind: 'field',
6
+ type: 'rich-select',
7
+ since: '1.0',
8
+ load: () => import('./rich-select').then((module) => module.FieldRichSelect),
9
+ // Nace como cadena: el valor seleccionado es un string.
10
+ seed: 'string',
11
+ surfaces: ['form', 'display'],
12
+ };