@tinya-code/gas-admin-ui 0.2.0-alpha.13 → 0.2.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (368) hide show
  1. package/ng-package.json +7 -0
  2. package/package.json +3 -15
  3. package/src/lib/actions/action-registry.spec.ts +52 -0
  4. package/src/lib/actions/action-registry.ts +126 -0
  5. package/src/lib/actions/action-service.spec.ts +215 -0
  6. package/src/lib/actions/action-service.ts +87 -0
  7. package/src/lib/actions/descriptors.spec.ts +155 -0
  8. package/src/lib/actions/descriptors.ts +154 -0
  9. package/src/lib/columns/column-cell.ts +33 -0
  10. package/src/lib/columns/column-defaults.spec.ts +99 -0
  11. package/src/lib/columns/column-defaults.ts +84 -0
  12. package/src/lib/columns/column-format.spec.ts +274 -0
  13. package/src/lib/columns/descriptors.ts +113 -0
  14. package/src/lib/columns/format-badge.ts +51 -0
  15. package/src/lib/columns/format-boolean.ts +58 -0
  16. package/src/lib/columns/format-currency.ts +30 -0
  17. package/src/lib/columns/format-date.ts +35 -0
  18. package/src/lib/columns/format-number.ts +32 -0
  19. package/src/lib/columns/format-text.ts +23 -0
  20. package/src/lib/columns/format-thumbnail.ts +63 -0
  21. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  22. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  23. package/src/lib/core/guards/auth.guard.ts +48 -0
  24. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  25. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  27. package/src/lib/core/models/api.model.ts +104 -0
  28. package/src/lib/core/models/schema-descriptions.ts +118 -0
  29. package/src/lib/core/models/schema.model.ts +1097 -0
  30. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  31. package/src/lib/core/registry/composition-validator.ts +270 -0
  32. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  33. package/src/lib/core/registry/field-validators.ts +440 -0
  34. package/src/lib/core/registry/render-host.spec.ts +208 -0
  35. package/src/lib/core/registry/render-host.ts +646 -0
  36. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  37. package/src/lib/core/registry/render-registry.ts +312 -0
  38. package/src/lib/core/registry/schema-catalog.ts +156 -0
  39. package/src/lib/core/services/api.service.spec.ts +1142 -0
  40. package/src/lib/core/services/api.service.ts +371 -0
  41. package/src/lib/core/services/auth.service.spec.ts +653 -0
  42. package/src/lib/core/services/auth.service.ts +503 -0
  43. package/src/lib/core/services/config.service.spec.ts +121 -0
  44. package/src/lib/core/services/config.service.ts +119 -0
  45. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  46. package/src/lib/core/services/data-source.service.ts +142 -0
  47. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  48. package/src/lib/core/services/error-mapper.service.ts +49 -0
  49. package/src/lib/core/services/notification.service.ts +73 -0
  50. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  51. package/src/lib/core/services/theme.service.spec.ts +106 -0
  52. package/src/lib/core/services/theme.service.ts +86 -0
  53. package/src/lib/core/services/transport.spec.ts +198 -0
  54. package/src/lib/core/services/transport.ts +164 -0
  55. package/src/lib/core/services/upload.service.ts +81 -0
  56. package/src/lib/core/testing/categories.schema.ts +125 -0
  57. package/src/lib/core/testing/orders.schema.ts +372 -0
  58. package/src/lib/core/testing/products.schema.ts +258 -0
  59. package/src/lib/core/testing/site.schema.ts +195 -0
  60. package/src/lib/core/testing/test-catalog.ts +21 -0
  61. package/src/lib/core/tokens.ts +112 -0
  62. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  63. package/src/lib/core/utils/clipboard.ts +90 -0
  64. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  65. package/src/lib/core/utils/context-resolver.ts +101 -0
  66. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  67. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  68. package/src/lib/core/utils/source-options.spec.ts +96 -0
  69. package/src/lib/core/utils/source-options.ts +54 -0
  70. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  71. package/src/lib/core/utils/template-interpolator.ts +56 -0
  72. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  74. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  75. package/src/lib/fields/boolean/boolean.ts +40 -0
  76. package/src/lib/fields/color/color.descriptor.ts +12 -0
  77. package/src/lib/fields/color/color.ts +34 -0
  78. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  79. package/src/lib/fields/currency/currency.ts +67 -0
  80. package/src/lib/fields/date/date.descriptor.ts +12 -0
  81. package/src/lib/fields/date/date.ts +36 -0
  82. package/src/lib/fields/descriptors.ts +122 -0
  83. package/src/lib/fields/display/display-badge.ts +65 -0
  84. package/src/lib/fields/display/display-date.ts +39 -0
  85. package/src/lib/fields/display/display-defaults.ts +84 -0
  86. package/src/lib/fields/display/display-file.ts +41 -0
  87. package/src/lib/fields/display/display-image.ts +54 -0
  88. package/src/lib/fields/display/display-list.ts +107 -0
  89. package/src/lib/fields/display/display-number.ts +41 -0
  90. package/src/lib/fields/display/display-text.ts +42 -0
  91. package/src/lib/fields/display/display.spec.ts +242 -0
  92. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  93. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  94. package/src/lib/fields/email/email.descriptor.ts +12 -0
  95. package/src/lib/fields/email/email.ts +28 -0
  96. package/src/lib/fields/field-aria.ts +60 -0
  97. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  98. package/src/lib/fields/field-host/field-host.ts +47 -0
  99. package/src/lib/fields/field-node.ts +88 -0
  100. package/src/lib/fields/file/file.descriptor.ts +14 -0
  101. package/src/lib/fields/file/file.spec.ts +134 -0
  102. package/src/lib/fields/file/file.ts +281 -0
  103. package/src/lib/fields/group/group.descriptor.ts +21 -0
  104. package/src/lib/fields/group/group.ts +110 -0
  105. package/src/lib/fields/image/image.descriptor.ts +12 -0
  106. package/src/lib/fields/image/image.spec.ts +117 -0
  107. package/src/lib/fields/image/image.ts +281 -0
  108. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  109. package/src/lib/fields/key-value/key-value.ts +283 -0
  110. package/src/lib/fields/list/list.descriptor.ts +21 -0
  111. package/src/lib/fields/list/list.ts +376 -0
  112. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  113. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  114. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  115. package/src/lib/fields/number/number.descriptor.ts +12 -0
  116. package/src/lib/fields/number/number.ts +40 -0
  117. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  118. package/src/lib/fields/phone/phone.ts +46 -0
  119. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  120. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  121. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  122. package/src/lib/fields/relation/relation.spec.ts +193 -0
  123. package/src/lib/fields/relation/relation.ts +224 -0
  124. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  125. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  126. package/src/lib/fields/select/radio-mode.ts +25 -0
  127. package/src/lib/fields/select/select.descriptor.ts +12 -0
  128. package/src/lib/fields/select/select.spec.ts +162 -0
  129. package/src/lib/fields/select/select.ts +210 -0
  130. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  131. package/src/lib/fields/slug/slug.ts +67 -0
  132. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  133. package/src/lib/fields/string-list/string-list.ts +222 -0
  134. package/src/lib/fields/text/text.descriptor.ts +12 -0
  135. package/src/lib/fields/text/text.ts +35 -0
  136. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  137. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  138. package/src/lib/fields/textarea/textarea.ts +126 -0
  139. package/src/lib/fields/time/time.descriptor.ts +12 -0
  140. package/src/lib/fields/time/time.ts +28 -0
  141. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  142. package/src/lib/fields/url/url.descriptor.ts +12 -0
  143. package/src/lib/fields/url/url.ts +53 -0
  144. package/src/lib/gas-admin-ui.spec.ts +21 -0
  145. package/src/lib/gas-admin-ui.ts +9 -0
  146. package/src/lib/layouts/descriptors.ts +9 -0
  147. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  148. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  149. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  150. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  151. package/src/lib/pages/form-view/form-model.ts +88 -0
  152. package/src/lib/pages/form-view/form-schema.ts +255 -0
  153. package/src/lib/pages/form-view/form-view.ts +1622 -0
  154. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  155. package/src/lib/pages/list-view/list-view.ts +882 -0
  156. package/src/lib/pages/login/login.spec.ts +109 -0
  157. package/src/lib/pages/login/login.ts +227 -0
  158. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  159. package/src/lib/shared/components/badge/badge.ts +35 -0
  160. package/src/lib/shared/components/button/button.ts +65 -0
  161. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  162. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  163. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  164. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  165. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  166. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  167. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  168. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  169. package/src/lib/shared/components/modal/modal.ts +145 -0
  170. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  172. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  173. package/src/lib/shared/components/toast/toast.ts +58 -0
  174. package/src/lib/shared/utils/col-span.ts +25 -0
  175. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  176. package/src/lib/shared/utils/depends-on.ts +48 -0
  177. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  178. package/src/lib/shared/utils/field-path.ts +49 -0
  179. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  180. package/src/lib/shared/utils/seed.spec.ts +64 -0
  181. package/src/lib/shared/utils/seed.ts +149 -0
  182. package/src/lib/shared/utils/slugify.ts +13 -0
  183. package/src/lib/shared/utils/validators.ts +41 -0
  184. package/src/lib/shell/layout/layout.ts +63 -0
  185. package/src/lib/shell/shell.routes.spec.ts +59 -0
  186. package/src/lib/shell/shell.routes.ts +122 -0
  187. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  188. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  189. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  190. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  191. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  192. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  193. package/src/lib/views/composer/composer-host.ts +248 -0
  194. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  195. package/src/lib/views/composer/view-context.spec.ts +117 -0
  196. package/src/lib/views/composer/view-context.ts +109 -0
  197. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  198. package/src/lib/views/composer/view-slot.ts +178 -0
  199. package/src/lib/views/descriptors.ts +46 -0
  200. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  201. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  202. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  203. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  206. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  207. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  208. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  209. package/src/lib/widgets/descriptors.ts +29 -0
  210. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  211. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  212. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  213. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  216. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  217. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  218. package/src/lib/widgets/record-list/record-list.ts +115 -0
  219. package/src/lib/widgets/series.spec.ts +42 -0
  220. package/src/lib/widgets/series.ts +56 -0
  221. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  222. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  223. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  224. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  225. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  226. package/src/public-api.ts +104 -0
  227. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  228. package/tsconfig.lib.json +13 -0
  229. package/tsconfig.lib.prod.json +11 -0
  230. package/tsconfig.spec.json +10 -0
  231. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  233. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  235. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  237. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  239. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  241. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  243. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  245. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  247. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  249. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  251. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  253. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  255. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  257. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  259. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  261. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  263. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  265. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  267. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  269. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  271. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  273. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  275. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  277. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  279. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  281. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  283. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  285. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  287. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  289. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  291. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  293. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  295. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  297. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  299. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  301. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  303. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  305. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  307. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  309. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  311. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  313. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  315. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  317. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  319. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  321. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  323. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  325. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  327. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  329. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  333. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  335. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  337. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  338. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  340. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  342. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  344. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  346. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  348. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  350. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  352. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  354. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  356. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  358. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  360. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  362. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  364. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  366. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  368. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,540 @@
1
+ import { setSchemaCatalog } from '../../core/registry/schema-catalog';
2
+ import { testCatalog } from '../../core/testing/test-catalog';
3
+
4
+ setSchemaCatalog(testCatalog);
5
+
6
+ import { ComponentFixture, TestBed } from '@angular/core/testing';
7
+ import { ActivatedRoute } from '@angular/router';
8
+ import { of } from 'rxjs';
9
+ import { describe, expect, it, vi } from 'vitest';
10
+
11
+ import { ApiError } from '../../core/models/api.model';
12
+ import type {
13
+ KanbanViewConfig,
14
+ ResourceSchema,
15
+ SelectionBinding,
16
+ } from '../../core/models/schema.model';
17
+ import { DataSourceService, type SourceRow } from '../../core/services/data-source.service';
18
+ import { CARD_NO_TITLE, cardSubtitle, cardTitle, mapKanbanRows } from './kanban-model';
19
+ import { KanbanView } from './kanban-view';
20
+
21
+ /**
22
+ * R9.8.4 — generic `KanbanView`.
23
+ *
24
+ * The pure model (grouping, card title/subtitle) is tested directly; the
25
+ * component is tested against a stubbed `DataSourceService` + a stubbed
26
+ * `ActivatedRoute.data` carrying `uiView` and the schema — the exact harness
27
+ * of `calendar-view.spec.ts`, because the shell mounts views via
28
+ * `loadComponent` with NO input bindings.
29
+ */
30
+
31
+ const BASE_CONFIG: KanbanViewConfig = {
32
+ groupField: 'status',
33
+ columns: [
34
+ { value: 'todo', label: 'Por hacer' },
35
+ { value: 'doing', label: 'En curso' },
36
+ { value: 'done', label: 'Hecho' },
37
+ ],
38
+ titleField: 'name',
39
+ subtitleField: 'owner',
40
+ };
41
+
42
+ function kanbanSchema(config: KanbanViewConfig | null = BASE_CONFIG): ResourceSchema {
43
+ return {
44
+ id: 'kanban_tasks',
45
+ label: 'Tarea',
46
+ labelPlural: 'Tareas',
47
+ kind: 'collection',
48
+ endpoint: { list: '/admin/kanban_tasks' },
49
+ keyField: 'slug',
50
+ titleField: 'name',
51
+ listColumns: [],
52
+ fields: [
53
+ { type: 'text', key: 'slug', label: 'Slug' },
54
+ { type: 'text', key: 'name', label: 'Nombre' },
55
+ { type: 'text', key: 'owner', label: 'Responsable' },
56
+ { type: 'select', key: 'status', label: 'Estado', options: [] },
57
+ ],
58
+ uiViews: config === null ? { board: { type: 'list' } } : { board: { type: 'kanban', config } },
59
+ };
60
+ }
61
+
62
+ type RowsBehavior = SourceRow[] | 'pending' | 'reject';
63
+
64
+ interface Harness {
65
+ fixture: ComponentFixture<KanbanView>;
66
+ component: KanbanView;
67
+ rowsMock: ReturnType<typeof vi.fn>;
68
+ el: HTMLElement;
69
+ }
70
+
71
+ async function create(options: {
72
+ rows?: RowsBehavior;
73
+ /** Custom fetcher — lets a test resolve two loads out of order. */
74
+ rowsImpl?: () => Promise<SourceRow[]>;
75
+ config?: KanbanViewConfig | null;
76
+ /** Route data override — the schema travels here, never as an input. */
77
+ data?: Record<string, unknown>;
78
+ }): Promise<Harness> {
79
+ const rowsBehavior = options.rows ?? [];
80
+ const rowsMock = options.rowsImpl
81
+ ? vi.fn(options.rowsImpl)
82
+ : vi.fn(() => {
83
+ if (rowsBehavior === 'pending') return new Promise<SourceRow[]>(() => {});
84
+ if (rowsBehavior === 'reject') {
85
+ return Promise.reject(new ApiError(500, 'Fallo del servidor'));
86
+ }
87
+ return Promise.resolve(rowsBehavior);
88
+ });
89
+
90
+ const data =
91
+ options.data ??
92
+ ({
93
+ // `uiView` names the view within `uiViews`; the schema travels by route
94
+ // data because the shell mounts views with `loadComponent` (no input
95
+ // bindings) and the schema registry is immutable.
96
+ uiView: 'board',
97
+ schema: kanbanSchema(options.config === null ? null : (options.config ?? BASE_CONFIG)),
98
+ } satisfies Record<string, unknown>);
99
+
100
+ await TestBed.configureTestingModule({
101
+ imports: [KanbanView],
102
+ providers: [
103
+ { provide: ActivatedRoute, useValue: { data: of(data) } },
104
+ { provide: DataSourceService, useValue: { rows: rowsMock } },
105
+ ],
106
+ }).compileComponents();
107
+
108
+ const fixture = TestBed.createComponent(KanbanView);
109
+ fixture.detectChanges();
110
+ await fixture.whenStable();
111
+ fixture.detectChanges();
112
+ return { fixture, component: fixture.componentInstance, rowsMock, el: fixture.nativeElement };
113
+ }
114
+
115
+ /** `value` of every painted column, in DOM order. */
116
+ function columnValues(el: HTMLElement): string[] {
117
+ return [...el.querySelectorAll('[data-column]')].map(
118
+ (node) => node.getAttribute('data-column') ?? '',
119
+ );
120
+ }
121
+
122
+ /** Title of each card of a column, in order (first `p` of every card). */
123
+ function cardTitles(el: HTMLElement, value: string): string[] {
124
+ return [...el.querySelectorAll(`[data-column="${value}"] li`)].map(
125
+ (card) => card.querySelector('p')?.textContent?.trim() ?? '',
126
+ );
127
+ }
128
+
129
+ // ─────────────────────────────────────────────────────────────
130
+ // Pure model — grouping
131
+ // ─────────────────────────────────────────────────────────────
132
+
133
+ describe('kanban-model — mapKanbanRows', () => {
134
+ it('agrupa por groupField respetando el orden declarado y las columnas vacías', () => {
135
+ const rows: SourceRow[] = [
136
+ { slug: 't1', name: 'Deploy', status: 'done' },
137
+ { slug: 't2', name: 'Diseño', status: 'todo' },
138
+ { slug: 't3', name: 'QA', status: 'todo' },
139
+ ];
140
+ const { columns, unknown } = mapKanbanRows(rows, BASE_CONFIG, {});
141
+ expect(unknown).toHaveLength(0);
142
+ expect(columns.map((column) => column.value)).toEqual(['todo', 'doing', 'done']);
143
+ expect(columns.map((column) => column.label)).toEqual(['Por hacer', 'En curso', 'Hecho']);
144
+ // la columna declarada sin filas SIGUE ahí (vacía, no ausente)
145
+ expect(columns[1].rows).toEqual([]);
146
+ expect(columns[0].rows.map((row) => row['slug'])).toEqual(['t2', 't3']);
147
+ expect(columns[2].rows.map((row) => row['slug'])).toEqual(['t1']);
148
+ });
149
+
150
+ it('un valor no declarado queda como columna propia, después de las declaradas', () => {
151
+ const rows: SourceRow[] = [
152
+ { slug: 't1', status: 'blocked' },
153
+ { slug: 't2', status: 'todo' },
154
+ { slug: 't3', status: 'blocked' },
155
+ ];
156
+ const { columns } = mapKanbanRows(rows, BASE_CONFIG, {});
157
+ expect(columns.map((column) => column.value)).toEqual(['todo', 'doing', 'done', 'blocked']);
158
+ // rótulo = el valor crudo: lo no declarado se VE, no se esconde
159
+ expect(columns[3].label).toBe('blocked');
160
+ expect(columns[3].rows.map((row) => row['slug'])).toEqual(['t1', 't3']);
161
+ expect(columns.reduce((total, column) => total + column.rows.length, 0)).toBe(3);
162
+ });
163
+
164
+ it('un valor vacío o ausente va a «unknown», nunca se pierde', () => {
165
+ const rows: SourceRow[] = [
166
+ { slug: 't1', name: 'Sin estado', status: '' },
167
+ { slug: 't2', name: 'Sin clave' },
168
+ { slug: 't3', name: 'Nulo', status: null },
169
+ { slug: 't4', name: 'Ok', status: 'todo' },
170
+ ];
171
+ const { columns, unknown } = mapKanbanRows(rows, BASE_CONFIG, {});
172
+ expect(unknown.map((row) => row['slug'])).toEqual(['t1', 't2', 't3']);
173
+ expect(unknown).toHaveLength(3);
174
+ // ninguna fila queda fuera del recuento total
175
+ const total = columns.reduce((sum, column) => sum + column.rows.length, 0) + unknown.length;
176
+ expect(total).toBe(rows.length);
177
+ expect(columns.map((column) => column.value)).toEqual(['todo', 'doing', 'done']);
178
+ });
179
+
180
+ it('es estable: el orden de inserción dentro de cada columna se conserva', () => {
181
+ const rows: SourceRow[] = [
182
+ { slug: 'a', status: 'todo' },
183
+ { slug: 'b', status: 'todo' },
184
+ { slug: 'c', status: 'todo' },
185
+ ];
186
+ const first = mapKanbanRows(rows, BASE_CONFIG, {});
187
+ const second = mapKanbanRows(rows, BASE_CONFIG, {});
188
+ expect(first.columns[0].rows.map((row) => row['slug'])).toEqual(['a', 'b', 'c']);
189
+ expect(second.columns[0].rows.map((row) => row['slug'])).toEqual(['a', 'b', 'c']);
190
+ // mismo input ⇒ misma salida (pura)
191
+ expect(second.columns.map((column) => column.value)).toEqual(
192
+ first.columns.map((column) => column.value),
193
+ );
194
+ });
195
+
196
+ it('labelFor sólo decora el rótulo de los valores no declarados', () => {
197
+ const rows: SourceRow[] = [
198
+ { slug: 't1', status: 'blocked' },
199
+ { slug: 't2', status: 'todo' },
200
+ ];
201
+ const { columns } = mapKanbanRows(rows, BASE_CONFIG, {
202
+ labelFor: (value) => `Sin columna: ${value}`,
203
+ });
204
+ expect(columns[0].label).toBe('Por hacer'); // declarada: intocable
205
+ expect(columns[3].label).toBe('Sin columna: blocked');
206
+ });
207
+
208
+ it('una celda objeto se reduce a su etiqueta legible, nunca a [object Object]', () => {
209
+ const rows: SourceRow[] = [{ slug: 't1', status: { label: 'En curso' } }];
210
+ const { columns, unknown } = mapKanbanRows(rows, BASE_CONFIG, {});
211
+ expect(unknown).toHaveLength(0);
212
+ expect(columns.map((column) => column.value)).toContain('En curso');
213
+ // un objeto sin escalar legible no puede nombrar una columna: va a unknown
214
+ const orphan = mapKanbanRows([{ slug: 't2', status: { weird: true } }], BASE_CONFIG, {});
215
+ expect(orphan.unknown.map((row) => row['slug'])).toEqual(['t2']);
216
+ expect(JSON.stringify(orphan)).not.toContain('[object Object]');
217
+ });
218
+ });
219
+
220
+ // ─────────────────────────────────────────────────────────────
221
+ // Pure model — card title / subtitle
222
+ // ─────────────────────────────────────────────────────────────
223
+
224
+ describe('kanban-model — cardTitle / cardSubtitle', () => {
225
+ const schema = kanbanSchema();
226
+
227
+ it('cardTitle: config.titleField → schema.titleField → keyField', () => {
228
+ const cfg: KanbanViewConfig = { ...BASE_CONFIG, titleField: 'owner' };
229
+ expect(cardTitle({ slug: 't1', name: 'Deploy', owner: 'Ana' }, cfg, schema)).toBe('Ana');
230
+ // sin titleField en la config ⇒ el titleField del schema
231
+ const withoutTitle: KanbanViewConfig = { groupField: 'status', columns: BASE_CONFIG.columns };
232
+ expect(cardTitle({ slug: 't1', name: 'Deploy' }, withoutTitle, schema)).toBe('Deploy');
233
+ // ni config ni fila con título legible ⇒ la clave del registro
234
+ expect(cardTitle({ slug: 't1' }, withoutTitle, schema)).toBe('t1');
235
+ });
236
+
237
+ it('cardTitle nunca pinta undefined ni [object Object]', () => {
238
+ const cfg: KanbanViewConfig = { ...BASE_CONFIG, titleField: 'name' };
239
+ const row: SourceRow = { slug: 't9', name: { weird: true } };
240
+ const title = cardTitle(row, cfg, schema);
241
+ expect(title).toBe('t9'); // cae a la clave
242
+ expect(title).not.toContain('[object Object]');
243
+ expect(cardTitle({ slug: 't9', name: { label: 'Legible' } }, cfg, schema)).toBe('Legible');
244
+ expect(cardTitle({ slug: 't9', name: '' }, cfg, schema)).toBe('t9');
245
+ });
246
+
247
+ it('cardTitle con ningún candidato legible ⇒ «Sin título» visible', () => {
248
+ const cfg: KanbanViewConfig = { groupField: 'status', columns: BASE_CONFIG.columns };
249
+ expect(cardTitle({}, cfg, schema)).toBe(CARD_NO_TITLE);
250
+ expect(CARD_NO_TITLE).toBe('Sin título');
251
+ });
252
+
253
+ it('cardSubtitle: valor de config.subtitleField si lo hay, si no vacío', () => {
254
+ expect(cardSubtitle({ slug: 't1', owner: 'Ana' }, BASE_CONFIG, schema)).toBe('Ana');
255
+ expect(cardSubtitle({ slug: 't1' }, BASE_CONFIG, schema)).toBe('');
256
+ const noSubtitle: KanbanViewConfig = {
257
+ groupField: 'status',
258
+ columns: BASE_CONFIG.columns,
259
+ titleField: 'name',
260
+ };
261
+ expect(cardSubtitle({ slug: 't1', owner: 'Ana' }, noSubtitle, schema)).toBe('');
262
+ });
263
+ });
264
+
265
+ // ─────────────────────────────────────────────────────────────
266
+ // Component
267
+ // ─────────────────────────────────────────────────────────────
268
+
269
+ describe('KanbanView', () => {
270
+ it('pinta las columnas declaradas en orden, con rótulo y conteo en texto', async () => {
271
+ const { el } = await create({
272
+ rows: [
273
+ { slug: 't1', name: 'Deploy', status: 'done' },
274
+ { slug: 't2', name: 'Diseño', owner: 'Ana', status: 'todo' },
275
+ { slug: 't3', name: 'QA', status: 'todo' },
276
+ ],
277
+ });
278
+
279
+ expect(columnValues(el)).toEqual(['todo', 'doing', 'done']);
280
+ expect(el.querySelector('[data-column="todo"] h3')?.textContent).toBe('Por hacer');
281
+ expect(cardTitles(el, 'todo')).toEqual(['Diseño', 'QA']);
282
+ expect(cardTitles(el, 'done')).toEqual(['Deploy']);
283
+
284
+ // el conteo es TEXTO, no color: va en el badge y en el aria-label
285
+ const badge = el.querySelector('[data-column="todo"] header span')?.textContent ?? '';
286
+ expect(badge).toContain('2');
287
+ expect(badge).toContain('tarjetas');
288
+ expect(el.querySelector('section[aria-label="Por hacer: 2 tarjetas"]')).not.toBeNull();
289
+ expect(el.querySelector('section[aria-label="Hecho: 1 tarjeta"]')).not.toBeNull();
290
+ });
291
+
292
+ it('un valor no declarado se ve como columna propia al final', async () => {
293
+ const { el } = await create({
294
+ rows: [
295
+ { slug: 't1', name: 'Bloqueada', status: 'blocked' },
296
+ { slug: 't2', name: 'Diseño', status: 'todo' },
297
+ ],
298
+ });
299
+ expect(columnValues(el)).toEqual(['todo', 'doing', 'done', 'blocked']);
300
+ expect(el.querySelector('[data-column="blocked"] h3')?.textContent).toBe('blocked');
301
+ expect(cardTitles(el, 'blocked')).toEqual(['Bloqueada']);
302
+ });
303
+
304
+ it('las filas sin grupo van a la columna «Sin valor» al final', async () => {
305
+ const { el } = await create({
306
+ rows: [
307
+ { slug: 't1', name: 'Sin estado', status: '' },
308
+ { slug: 't2', name: 'Sin clave' },
309
+ { slug: 't3', name: 'Diseño', status: 'todo' },
310
+ ],
311
+ });
312
+ expect(columnValues(el)).toEqual(['todo', 'doing', 'done', '__sin_valor__']);
313
+ expect(el.querySelector('[data-column="__sin_valor__"] h3')?.textContent).toBe('Sin valor');
314
+ expect(cardTitles(el, '__sin_valor__')).toEqual(['Sin estado', 'Sin clave']);
315
+ expect(el.querySelector('section[aria-label="Sin valor: 2 tarjetas"]')).not.toBeNull();
316
+ });
317
+
318
+ it('pinta título y subtítulo con sus fallbacks (clave, sin segundo renglón)', async () => {
319
+ const { el } = await create({
320
+ rows: [
321
+ { slug: 't1', name: 'Deploy', owner: 'Ana', status: 'todo' },
322
+ { slug: 't2', name: '', owner: '', status: 'todo' },
323
+ ],
324
+ });
325
+ const cards = [...el.querySelectorAll('[data-column="todo"] li')];
326
+ expect(cards).toHaveLength(2);
327
+ expect(cards[0].textContent).toContain('Deploy');
328
+ expect(cards[0].textContent).toContain('Ana');
329
+ // sin título legible ⇒ la clave; sin subtítulo ⇒ un solo renglón
330
+ expect(cards[1].querySelector('p')?.textContent?.trim()).toBe('t2');
331
+ expect(cards[1].querySelectorAll('p')).toHaveLength(1);
332
+ expect(cards[1].textContent).not.toContain('[object Object]');
333
+ });
334
+
335
+ it('muestra «Sin tarjetas» en la columna declarada vacía', async () => {
336
+ const { el } = await create({ rows: [{ slug: 't1', name: 'Diseño', status: 'todo' }] });
337
+ expect(el.querySelector('[data-column="doing"]')?.textContent).toContain('Sin tarjetas');
338
+ expect(el.querySelector('[data-column="todo"]')?.textContent).not.toContain('Sin tarjetas');
339
+ // sin filas desconocidas NO se pinta la columna «Sin valor»
340
+ expect(columnValues(el)).toEqual(['todo', 'doing', 'done']);
341
+ });
342
+
343
+ it('muestra el skeleton mientras la fuente no responde', async () => {
344
+ const { el } = await create({ rows: 'pending' });
345
+ expect(el.querySelector('app-skeleton')).not.toBeNull();
346
+ expect(el.querySelector('section')?.getAttribute('aria-busy')).toBe('true');
347
+ });
348
+
349
+ it('pinta el estado de error con el mensaje mapeado', async () => {
350
+ const { el } = await create({ rows: 'reject' });
351
+ expect(el.querySelector('app-empty-state')?.textContent).toContain(
352
+ 'No se pudo cargar el tablero',
353
+ );
354
+ expect(el.textContent).toContain('Fallo del servidor');
355
+ expect(columnValues(el)).toEqual([]);
356
+ });
357
+
358
+ it('pinta la config inválida como estado vacío, sin pedir filas', async () => {
359
+ const { el, rowsMock } = await create({ config: null });
360
+ expect(el.textContent).toContain('Configuración inválida');
361
+ expect(rowsMock).not.toHaveBeenCalled();
362
+ });
363
+
364
+ it('resuelve el schema desde route data, sin ningún input', async () => {
365
+ // The shell mounts views via `loadComponent` (no input bindings), so
366
+ // there is NO input to set: the schema must come from route data.
367
+ const { component, rowsMock } = await create({ rows: [] });
368
+ expect(component.schema()?.id).toBe('kanban_tasks');
369
+ expect(component.config()?.groupField).toBe('status');
370
+ expect(rowsMock).toHaveBeenCalledWith({ resource: 'kanban_tasks' }, undefined);
371
+ });
372
+
373
+ it('resuelve el schema por resourceId en route data (la ruta de producción)', async () => {
374
+ const { component, el, rowsMock } = await create({
375
+ data: { uiView: 'list', resourceId: 'products' },
376
+ });
377
+ expect(component.schema()?.id).toBe('products');
378
+ // products no declara una vista kanban usable ⇒ estado inválido, sin fetch
379
+ expect(component.config()).toBeUndefined();
380
+ expect(el.textContent).toContain('Configuración inválida');
381
+ expect(rowsMock).not.toHaveBeenCalled();
382
+ });
383
+
384
+ it('sin schema ni resourceId en route data pinta «Configuración inválida»', async () => {
385
+ // Regression the old spec missed: `input.required` never received a
386
+ // schema in production. With neither `schema` nor `resourceId` there is
387
+ // nothing to resolve, and the only honest render is the invalid-config
388
+ // empty state (no board, no fetch).
389
+ const rowsMock = vi.fn(() => Promise.resolve<SourceRow[]>([]));
390
+ await TestBed.configureTestingModule({
391
+ imports: [KanbanView],
392
+ providers: [
393
+ { provide: ActivatedRoute, useValue: { data: of({ uiView: 'board' }) } },
394
+ { provide: DataSourceService, useValue: { rows: rowsMock } },
395
+ ],
396
+ }).compileComponents();
397
+ const fixture = TestBed.createComponent(KanbanView);
398
+ fixture.detectChanges();
399
+ await fixture.whenStable();
400
+ fixture.detectChanges();
401
+ expect(fixture.nativeElement.textContent).toContain('Configuración inválida');
402
+ expect(rowsMock).not.toHaveBeenCalled();
403
+ });
404
+
405
+ it('pide la fuente declarada y, sin source, el listado del propio recurso', async () => {
406
+ const declared = await create({
407
+ rows: [],
408
+ config: { ...BASE_CONFIG, source: { resource: 'oth', view: 'por_estado' } },
409
+ });
410
+ expect(declared.rowsMock).toHaveBeenCalledWith(
411
+ { resource: 'oth', view: 'por_estado' },
412
+ undefined,
413
+ );
414
+ });
415
+ });
416
+
417
+ // ─────────────────────────────────────────────────────────────
418
+ // R9.9.4 · decision table — selection contract + load race
419
+ // ─────────────────────────────────────────────────────────────
420
+
421
+ describe('KanbanView — contrato de selección declarado (R9.9.4)', () => {
422
+ const ROUTE: SelectionBinding = {
423
+ on: 'open',
424
+ mode: 'route',
425
+ target: { resource: 'orders', view: 'detail', params: ['key'] },
426
+ };
427
+ const ROWS: SourceRow[] = [{ slug: 't-1', name: 'Tarea 1', status: 'todo' }];
428
+
429
+ function boardData(extra: Record<string, unknown>): Record<string, unknown> {
430
+ return { uiView: 'board', schema: kanbanSchema(), ...extra };
431
+ }
432
+
433
+ it("mode:'route' ⇒ la tarjeta emite con la acción declarada", async () => {
434
+ const emit = vi.fn();
435
+ const h = await create({ rows: ROWS, data: boardData({ select: ROUTE, emit }) });
436
+
437
+ (h.el.querySelector('[data-column="todo"] li') as HTMLElement).click();
438
+
439
+ expect(emit).toHaveBeenCalledTimes(1);
440
+ expect(emit).toHaveBeenCalledWith({
441
+ resource: 'kanban_tasks',
442
+ key: 't-1',
443
+ action: 'open',
444
+ });
445
+ expect(h.el.querySelector('[data-column="todo"] li')?.getAttribute('data-selectable')).toBe(
446
+ 'true',
447
+ );
448
+ });
449
+
450
+ it("mode:'none' ⇒ la tarjeta es inerte aunque tenga emit disponible", async () => {
451
+ const emit = vi.fn();
452
+ const h = await create({
453
+ rows: ROWS,
454
+ data: boardData({ select: { on: 'open', mode: 'none' }, emit }),
455
+ });
456
+
457
+ (h.el.querySelector('[data-column="todo"] li') as HTMLElement).click();
458
+
459
+ expect(emit).not.toHaveBeenCalled();
460
+ });
461
+
462
+ it('sin select declarado ⇒ el clic de tarjeta no emite nada (comportamiento actual)', async () => {
463
+ const emit = vi.fn();
464
+ const h = await create({ rows: ROWS, data: boardData({ emit }) });
465
+
466
+ (h.el.querySelector('[data-column="todo"] li') as HTMLElement).click();
467
+
468
+ expect(emit).not.toHaveBeenCalled();
469
+ expect(h.el.querySelector('[data-column="todo"] li')?.hasAttribute('data-selectable')).toBe(
470
+ false,
471
+ );
472
+ });
473
+
474
+ // ── R9.8.7 — operabilidad con teclado ──
475
+
476
+ it('la tarjeta seleccionable es focusable y Enter/Space activan la selección', async () => {
477
+ const emit = vi.fn();
478
+ const h = await create({ rows: ROWS, data: boardData({ select: ROUTE, emit }) });
479
+ const card = h.el.querySelector('[data-column="todo"] li') as HTMLElement;
480
+
481
+ expect(card.getAttribute('tabindex')).toBe('0');
482
+ expect(card.getAttribute('role')).toBe('button');
483
+
484
+ card.focus();
485
+ expect(document.activeElement).toBe(card);
486
+
487
+ card.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
488
+ expect(emit).toHaveBeenCalledTimes(1);
489
+ expect(emit).toHaveBeenCalledWith({ resource: 'kanban_tasks', key: 't-1', action: 'open' });
490
+
491
+ card.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
492
+ expect(emit).toHaveBeenCalledTimes(2);
493
+ });
494
+
495
+ it('sin binding declarado la tarjeta no es focusable (comportamiento local intacto)', async () => {
496
+ const emit = vi.fn();
497
+ const h = await create({ rows: ROWS, data: boardData({ emit }) });
498
+ const card = h.el.querySelector('[data-column="todo"] li') as HTMLElement;
499
+
500
+ expect(card.hasAttribute('tabindex')).toBe(false);
501
+ expect(card.hasAttribute('role')).toBe(false);
502
+
503
+ card.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
504
+ expect(emit).not.toHaveBeenCalled();
505
+ });
506
+
507
+ it('cargas fuera de orden ⇒ sólo la última respuesta se pinta (loadEpoch)', async () => {
508
+ let resolverPrimera!: (rows: SourceRow[]) => void;
509
+ let resolverSegunda!: (rows: SourceRow[]) => void;
510
+ const primera = new Promise<SourceRow[]>((r) => (resolverPrimera = r));
511
+ const segunda = new Promise<SourceRow[]>((r) => (resolverSegunda = r));
512
+ const pendientes = [primera, segunda];
513
+ let llamada = 0;
514
+
515
+ const h = await create({
516
+ rowsImpl: () => {
517
+ const p = pendientes[llamada++];
518
+ // Non-null: el harness sólo dispara dos cargas (load inicial + retry).
519
+ return p ?? Promise.resolve<SourceRow[]>([]);
520
+ },
521
+ });
522
+ expect(h.rowsMock).toHaveBeenCalledTimes(1);
523
+
524
+ h.component.retry(); // segunda carga, epoch 2
525
+ expect(h.rowsMock).toHaveBeenCalledTimes(2);
526
+
527
+ // Responder la SEGUNDA primero…
528
+ resolverSegunda([{ slug: 'nueva', name: 'Nueva', status: 'todo' }]);
529
+ await h.fixture.whenStable();
530
+ h.fixture.detectChanges();
531
+ expect(h.el.textContent ?? '').toContain('Nueva');
532
+
533
+ // …y la PRIMERA tarde: no debe pisar lo ya pintado.
534
+ resolverPrimera([{ slug: 'vieja', name: 'Vieja', status: 'todo' }]);
535
+ await h.fixture.whenStable();
536
+ h.fixture.detectChanges();
537
+ expect(h.el.textContent ?? '').toContain('Nueva');
538
+ expect(h.el.textContent ?? '').not.toContain('Vieja');
539
+ });
540
+ });