@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,882 @@
1
+ import { CdkDrag, CdkDragDrop, CdkDragHandle, CdkDropList } from '@angular/cdk/drag-drop';
2
+ import { Component, computed, effect, inject, input, output, signal } from '@angular/core';
3
+ import { Router } from '@angular/router';
4
+ import { firstValueFrom } from 'rxjs';
5
+
6
+ import {
7
+ LucideChevronDown,
8
+ LucideChevronRight,
9
+ LucideChevronUp,
10
+ LucideDynamicIcon,
11
+ LucideEye,
12
+ LucideGripVertical,
13
+ LucidePencil,
14
+ LucidePlus,
15
+ LucideRotateCw,
16
+ LucideTrash2,
17
+ LucideZap,
18
+ } from '@lucide/angular';
19
+ import type { LucideIcon } from '@lucide/angular';
20
+ import { actionType, getAction } from '../../actions/action-registry';
21
+ import { ActionService } from '../../actions/action-service';
22
+ import { ApiError } from '../../core/models/api.model';
23
+ import type {
24
+ FieldSchema,
25
+ ListColumn,
26
+ ListColumnFormat,
27
+ ListFilter,
28
+ ResourceAction,
29
+ ResourceSchema,
30
+ SelectOption,
31
+ } from '../../core/models/schema.model';
32
+ import { ApiService } from '../../core/services/api.service';
33
+ import { AuthService } from '../../core/services/auth.service';
34
+ import { NotificationService } from '../../core/services/notification.service';
35
+ import { DrawerForm } from '../drawer-form/drawer-form';
36
+ import { RenderHost } from '../../core/registry/render-host';
37
+ import { getSchema } from '../../core/registry/schema-catalog';
38
+ import { Button } from '../../shared/components/button/button';
39
+ import { EmptyState } from '../../shared/components/empty-state/empty-state';
40
+ import { Skeleton } from '../../shared/components/skeleton/skeleton';
41
+ import { getValue } from '../../shared/utils/field-path';
42
+ import { formatoDeColumna } from '../../columns/column-defaults';
43
+ import type { ColumnCell } from '../../columns/column-cell';
44
+
45
+ type Row = Record<string, unknown>;
46
+ type Status = 'loading' | 'ready' | 'error';
47
+
48
+ const BOOLEAN_FILTER_OPTIONS: SelectOption[] = [
49
+ { value: '', label: 'Todos' },
50
+ { value: 'yes', label: 'Sí' },
51
+ { value: 'no', label: 'No' },
52
+ ];
53
+
54
+ function errorMessage(err: unknown, fallback: string): string {
55
+ return err instanceof ApiError ? err.message : fallback;
56
+ }
57
+
58
+ /**
59
+ * Listado genérico de una colección (base.md §10 «Listado»): tabla desde
60
+ * `listColumns` con los 7 formatos **resueltos por el registro** (R4.3),
61
+ * búsqueda/filtros **en el cliente** (el
62
+ * contrato de api.md no define parámetros de consulta: el endpoint admin
63
+ * devuelve todos los registros), interruptor `active`, acciones de fila y
64
+ * estados universales (skeleton, vacío, error con reintento).
65
+ */
66
+ @Component({
67
+ selector: 'app-list-view',
68
+ imports: [
69
+ Button,
70
+ CdkDrag,
71
+ CdkDragHandle,
72
+ CdkDropList,
73
+ DrawerForm,
74
+ EmptyState,
75
+ LucideChevronDown,
76
+ LucideChevronRight,
77
+ LucideChevronUp,
78
+ LucideEye,
79
+ LucideGripVertical,
80
+ LucidePencil,
81
+ LucidePlus,
82
+ LucideRotateCw,
83
+ LucideDynamicIcon,
84
+ LucideTrash2,
85
+ LucideZap,
86
+ RenderHost,
87
+ Skeleton,
88
+ ],
89
+ template: `
90
+ <section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
91
+ <div class="flex flex-wrap items-end justify-between gap-3">
92
+ <div class="flex flex-wrap items-end gap-3">
93
+ @if (schema().search?.length) {
94
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
95
+ Buscar
96
+ <input
97
+ type="search"
98
+ class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
99
+ placeholder="Buscar…"
100
+ [value]="query()"
101
+ (input)="setQuery(inputValue($event))"
102
+ />
103
+ </label>
104
+ }
105
+ @for (filter of schema().filters ?? []; track filter.key) {
106
+ <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
107
+ {{ filter.label }}
108
+ @if (filter.type === 'text') {
109
+ <input
110
+ type="search"
111
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
112
+ [value]="filterValues()[filter.key] ?? ''"
113
+ (input)="setFilter(filter.key, inputValue($event))"
114
+ />
115
+ } @else {
116
+ <select
117
+ class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
118
+ [value]="filterValues()[filter.key] ?? ''"
119
+ (change)="setFilter(filter.key, selectValue($event))"
120
+ >
121
+ @for (option of filterOptions(filter); track option.value) {
122
+ <option [value]="option.value">{{ option.label }}</option>
123
+ }
124
+ </select>
125
+ }
126
+ </label>
127
+ }
128
+ </div>
129
+ @if (canCreate()) {
130
+ <button app-button type="button" (click)="goCreate()">
131
+ <svg lucidePlus size="16" />
132
+ Crear {{ schema().label }}
133
+ </button>
134
+ }
135
+ </div>
136
+
137
+ @switch (status()) {
138
+ @case ('loading') {
139
+ <app-skeleton [lines]="6" [height]="44" />
140
+ }
141
+ @case ('error') {
142
+ <app-empty-state
143
+ title="No se pudo cargar el listado"
144
+ message="Revisa tu conexión e intenta de nuevo."
145
+ >
146
+ <button app-button variant="outline" type="button" (click)="retry()">
147
+ <svg lucideRotateCw size="16" />
148
+ Reintentar
149
+ </button>
150
+ </app-empty-state>
151
+ }
152
+ @case ('ready') {
153
+ @if (visibleRows().length === 0) {
154
+ <app-empty-state
155
+ [title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
156
+ [message]="
157
+ rows().length === 0
158
+ ? 'Todavía no hay registros en este recurso.'
159
+ : 'Ningún registro coincide con la búsqueda o los filtros.'
160
+ "
161
+ >
162
+ @if (rows().length > 0) {
163
+ <button app-button variant="outline" type="button" (click)="clearFilters()">
164
+ Limpiar filtros
165
+ </button>
166
+ } @else if (canCreate()) {
167
+ <button app-button type="button" (click)="goCreate()">
168
+ <svg lucidePlus size="16" />
169
+ Crear el primero
170
+ </button>
171
+ }
172
+ </app-empty-state>
173
+ } @else {
174
+ <!-- La tabla es la vista de escritorio: se oculta bajo lg para
175
+ que la lista de tarjetas de abajo sea la ÚNICA vista en
176
+ móvil (con las dos visibles, el listado se duplicaba). -->
177
+ <div
178
+ class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
179
+ >
180
+ <table class="w-full text-sm">
181
+ <thead>
182
+ <tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
183
+ @if (canReorder()) {
184
+ <th scope="col" class="w-16 px-2 py-3 font-medium">
185
+ <span class="sr-only">Orden</span>
186
+ </th>
187
+ }
188
+ @for (column of schema().listColumns; track column.key) {
189
+ <th scope="col" class="px-4 py-3 font-medium">
190
+ {{ column.label }}
191
+ </th>
192
+ }
193
+ @if (hasRowActions()) {
194
+ <th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
195
+ }
196
+ </tr>
197
+ </thead>
198
+ <tbody
199
+ cdkDropList
200
+ [cdkDropListData]="visibleRows()"
201
+ [cdkDropListDisabled]="dropDisabled()"
202
+ (cdkDropListDropped)="onDrop($event)"
203
+ >
204
+ @for (row of visibleRows(); track keyOf(row); let i = $index) {
205
+ <tr
206
+ class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
207
+ [class.cursor-pointer]="isRowClickable()"
208
+ cdkDrag
209
+ [cdkDragDisabled]="dropDisabled()"
210
+ (click)="onRowClick(row)"
211
+ >
212
+ @if (canReorder()) {
213
+ <td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
214
+ <div class="flex items-center gap-1">
215
+ <button
216
+ type="button"
217
+ class="cursor-grab text-neutral hover:text-primary"
218
+ cdkDragHandle
219
+ [attr.aria-label]="'Reordenar ' + titleOf(row)"
220
+ title="Arrastrar para reordenar"
221
+ >
222
+ <svg lucideGripVertical size="16" />
223
+ </button>
224
+ <span class="flex flex-col leading-none">
225
+ <button
226
+ type="button"
227
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
228
+ [disabled]="i === 0 || reordering()"
229
+ [attr.aria-label]="'Subir ' + titleOf(row)"
230
+ (click)="moveBy(keyOf(row), -1)"
231
+ >
232
+ <svg lucideChevronUp size="16" />
233
+ </button>
234
+ <button
235
+ type="button"
236
+ class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
237
+ [disabled]="i === visibleRows().length - 1 || reordering()"
238
+ [attr.aria-label]="'Bajar ' + titleOf(row)"
239
+ (click)="moveBy(keyOf(row), 1)"
240
+ >
241
+ <svg lucideChevronDown size="16" />
242
+ </button>
243
+ </span>
244
+ </div>
245
+ </td>
246
+ }
247
+ @for (column of schema().listColumns; track column.key) {
248
+ <td class="px-4 py-2.5 align-middle">
249
+ <!-- Los 7 formatos salen del registro (R4.3):
250
+ la vista sólo elige cuál y entrega la celda. -->
251
+ <app-render-host
252
+ kind="column-format"
253
+ [type]="formatOf(column)"
254
+ [config]="column"
255
+ [data]="cellOf(row, column)"
256
+ />
257
+ </td>
258
+ }
259
+ @if (hasRowActions()) {
260
+ <td class="px-4 py-2.5" (click)="$event.stopPropagation()">
261
+ <div class="flex items-center justify-end gap-1">
262
+ @if (schema().detail?.enabled) {
263
+ <button
264
+ app-button
265
+ variant="ghost"
266
+ type="button"
267
+ (click)="goView(row)"
268
+ title="Ver ficha de detalle"
269
+ class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
270
+ >
271
+ <svg lucideEye size="15" />
272
+ <span>Ver</span>
273
+ </button>
274
+ }
275
+ @for (action of schema().actions ?? []; track action.id) {
276
+ <button
277
+ app-button
278
+ variant="ghost"
279
+ type="button"
280
+ [title]="action.label"
281
+ [attr.aria-label]="action.label"
282
+ (click)="runAction(action, row)"
283
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
284
+ >
285
+ @let actionIcon = iconOf(action);
286
+ @if (actionIcon) {
287
+ <svg [lucideIcon]="actionIcon" size="15" />
288
+ } @else {
289
+ {{ action.label }}
290
+ }
291
+ </button>
292
+ }
293
+ @if (canQuickEdit()) {
294
+ <button
295
+ app-button
296
+ variant="ghost"
297
+ type="button"
298
+ (click)="quickEditRow.set(row)"
299
+ title="Edición rápida"
300
+ class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
301
+ >
302
+ <svg lucideZap size="15" />
303
+ <span>Edición rápida</span>
304
+ </button>
305
+ }
306
+ @if (canUpdate()) {
307
+ <button
308
+ app-button
309
+ variant="ghost"
310
+ type="button"
311
+ (click)="goEdit(row)"
312
+ title="Editar registro"
313
+ class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
314
+ >
315
+ <svg lucidePencil size="15" />
316
+ <span>Editar</span>
317
+ </button>
318
+ }
319
+ @if (canRemove()) {
320
+ <button
321
+ app-button
322
+ variant="ghost"
323
+ type="button"
324
+ (click)="removeRow(row)"
325
+ title="Eliminar registro"
326
+ class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
327
+ >
328
+ <svg lucideTrash2 size="15" />
329
+ <span>Eliminar</span>
330
+ </button>
331
+ }
332
+ </div>
333
+ </td>
334
+ }
335
+ </tr>
336
+ }
337
+ </tbody>
338
+ </table>
339
+ </div>
340
+
341
+ <!-- Vista de tarjetas — mobile-first (guía mobile §6).
342
+ La tabla de arriba necesita scroll horizontal con 5–7 columnas
343
+ (el anti-patrón #2): en pantallas chicas se convierte en una
344
+ lista de tarjetas de dos niveles (principal + secundario),
345
+ donde cada fila completa es un objetivo táctil. lg:hidden la
346
+ retira cuando la tabla vuelve a ser usable. -->
347
+ <ul class="space-y-2 lg:hidden">
348
+ @for (row of visibleRows(); track keyOf(row)) {
349
+ <li
350
+ class="rounded-xl border border-neutral/20 bg-surface p-3"
351
+ [class.cursor-pointer]="isRowClickable()"
352
+ (click)="onRowClick(row)"
353
+ >
354
+ <!-- Nivel principal: el título del registro, siempre visible
355
+ (guía §6.2: el identificador nunca se esconde). -->
356
+ <div class="flex items-start justify-between gap-2">
357
+ <span class="text-sm font-medium">
358
+ {{ titleOf(row) }}
359
+ </span>
360
+ @if (schema().detail?.enabled) {
361
+ <svg
362
+ lucideChevronRight
363
+ size="16"
364
+ class="mt-0.5 shrink-0 text-neutral"
365
+ aria-hidden="true"
366
+ />
367
+ }
368
+ </div>
369
+ <!-- Nivel secundario: el resto de las columnas como pares
370
+ etiqueta/valor. Sin hover: todo lo accionable va en el
371
+ pie de la tarjeta, al alcance del pulgar. -->
372
+ <dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
373
+ @for (column of schema().listColumns; track column.key) {
374
+ <dt class="text-neutral">{{ column.label }}</dt>
375
+ <dd>
376
+ <app-render-host
377
+ kind="column-format"
378
+ [type]="formatOf(column)"
379
+ [config]="column"
380
+ [data]="cellOf(row, column)"
381
+ />
382
+ </dd>
383
+ }
384
+ </dl>
385
+ @if (hasRowActions()) {
386
+ <!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
387
+ <div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
388
+ @if (schema().detail?.enabled) {
389
+ <button
390
+ app-button
391
+ variant="outline"
392
+ type="button"
393
+ size="sm"
394
+ class="flex-1"
395
+ (click)="$event.stopPropagation(); goView(row)"
396
+ >
397
+ <svg lucideEye size="15" aria-hidden="true" />
398
+ <span>Ver</span>
399
+ </button>
400
+ }
401
+ @for (action of schema().actions ?? []; track action.id) {
402
+ <button
403
+ app-button
404
+ variant="outline"
405
+ type="button"
406
+ size="sm"
407
+ class="flex-1"
408
+ (click)="$event.stopPropagation(); runAction(action, row)"
409
+ >
410
+ @let actionIcon = iconOf(action);
411
+ @if (actionIcon) {
412
+ <svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
413
+ }
414
+ <span>{{ action.label }}</span>
415
+ </button>
416
+ }
417
+ <!-- Editar es O una vía u otra, como en la tabla: si el
418
+ schema declara quickEdit va el atajo; si no, la
419
+ pantalla completa. Apilados los dos sería ruido. -->
420
+ @if (canQuickEdit()) {
421
+ <button
422
+ app-button
423
+ variant="outline"
424
+ type="button"
425
+ size="sm"
426
+ class="flex-1"
427
+ (click)="$event.stopPropagation(); quickEditRow.set(row)"
428
+ >
429
+ <svg lucideZap size="15" aria-hidden="true" />
430
+ <span>Editar</span>
431
+ </button>
432
+ } @else if (canUpdate()) {
433
+ <button
434
+ app-button
435
+ variant="outline"
436
+ type="button"
437
+ size="sm"
438
+ class="flex-1"
439
+ (click)="$event.stopPropagation(); goEdit(row)"
440
+ >
441
+ <svg lucidePencil size="15" aria-hidden="true" />
442
+ <span>Editar</span>
443
+ </button>
444
+ }
445
+ @if (canRemove()) {
446
+ <button
447
+ app-button
448
+ variant="danger"
449
+ type="button"
450
+ size="sm"
451
+ class="flex-1"
452
+ (click)="$event.stopPropagation(); removeRow(row)"
453
+ >
454
+ <svg lucideTrash2 size="15" aria-hidden="true" />
455
+ <span>Eliminar</span>
456
+ </button>
457
+ }
458
+ </div>
459
+ }
460
+ </li>
461
+ }
462
+ </ul>
463
+ }
464
+ }
465
+ }
466
+
467
+ @if (quickEditRow(); as row) {
468
+ <app-drawer-form
469
+ [open]="true"
470
+ [schema]="schema()"
471
+ mode="edit"
472
+ [recordKey]="keyOf(row)"
473
+ [fields]="quickEditFields()"
474
+ title="Edición rápida"
475
+ size="sm"
476
+ (close)="quickEditRow.set(null)"
477
+ (saved)="quickEditSaved()"
478
+ />
479
+ }
480
+ </section>
481
+ `,
482
+ })
483
+ export class ListView {
484
+ readonly schema = input.required<ResourceSchema>();
485
+
486
+ /** Emitido al pulsar «Editar» cuando `schema.detail.enabled === true`. */
487
+ readonly viewRecord = output<Row>();
488
+
489
+ private readonly api = inject(ApiService);
490
+ private readonly auth = inject(AuthService);
491
+ private readonly router = inject(Router);
492
+ readonly notifications = inject(NotificationService);
493
+ private readonly actionService = inject(ActionService);
494
+
495
+ readonly status = signal<Status>('loading');
496
+ readonly rows = signal<Row[]>([]);
497
+ readonly query = signal('');
498
+ readonly filterValues = signal<Record<string, string>>({});
499
+ /** Clave del registro cuyo interruptor `active` está guardando. */
500
+ readonly savingKey = signal<string | null>(null);
501
+ /** Fila que se está editando en el drawer (plan 6.5); `null` = cerrado. */
502
+ readonly quickEditRow = signal<Row | null>(null);
503
+
504
+ /** Evita que una respuesta lenta pise el recurso ya cambiado. */
505
+ private loadEpoch = 0;
506
+
507
+ constructor() {
508
+ effect(() => {
509
+ const schema = this.schema();
510
+ this.query.set('');
511
+ this.filterValues.set({});
512
+ void this.load(schema);
513
+ });
514
+ }
515
+
516
+ /** Búsqueda + filtros + orden `positionField` ascendente (cliente). */
517
+ readonly visibleRows = computed(() => {
518
+ const schema = this.schema();
519
+ let rows = this.rows();
520
+
521
+ const query = this.query().trim().toLowerCase();
522
+ if (query !== '' && schema.search?.length) {
523
+ rows = rows.filter((row) =>
524
+ schema.search!.some((key) =>
525
+ String(getValue(row, key) ?? '')
526
+ .toLowerCase()
527
+ .includes(query),
528
+ ),
529
+ );
530
+ }
531
+
532
+ for (const filter of schema.filters ?? []) {
533
+ const value = this.filterValues()[filter.key] ?? '';
534
+ if (value === '') {
535
+ continue;
536
+ }
537
+ rows = rows.filter((row) => {
538
+ const cell = getValue(row, filter.key);
539
+ if (filter.type === 'boolean') {
540
+ return (cell === true || cell === 'true') === (value === 'yes');
541
+ }
542
+ if (filter.type === 'select') {
543
+ return String(cell ?? '') === value;
544
+ }
545
+ return String(cell ?? '')
546
+ .toLowerCase()
547
+ .includes(value.toLowerCase());
548
+ });
549
+ }
550
+
551
+ const positionField = schema.positionField;
552
+ if (positionField) {
553
+ const positionOf = (row: Row): number => {
554
+ const position = Number(getValue(row, positionField));
555
+ return Number.isFinite(position) ? position : Number.POSITIVE_INFINITY;
556
+ };
557
+ rows = [...rows].sort((a, b) => positionOf(a) - positionOf(b));
558
+ }
559
+
560
+ return rows;
561
+ });
562
+
563
+ readonly currencyCode = computed(() => {
564
+ // Moneda del singleton `site` (campo `currency`); Fase 12 podrá usar el
565
+ // valor real cargado del backend.
566
+ const field = getSchema('site')?.fields.find((f) => f.key === 'currency');
567
+ return field?.type === 'text' && field.default ? field.default : 'USD';
568
+ });
569
+
570
+ private readonly permissions = computed(() => this.schema().permissions);
571
+ readonly canCreate = computed(() => this.auth.canDoOperation(this.schema(), 'create'));
572
+ readonly canUpdate = computed(() => this.auth.canDoOperation(this.schema(), 'update'));
573
+ readonly canRemove = computed(() => this.auth.canDoOperation(this.schema(), 'remove'));
574
+
575
+ /** El atajo existe sólo si el schema lo declara y hay permiso de editar. */
576
+ readonly canQuickEdit = computed(
577
+ () => this.canUpdate() && (this.schema().quickEdit?.length ?? 0) > 0,
578
+ );
579
+
580
+ /** Subconjunto de campos del drawer, en el orden declarado en el schema. */
581
+ readonly quickEditFields = computed<readonly FieldSchema[]>(() => {
582
+ const keys = this.schema().quickEdit ?? [];
583
+ const byKey = new Map(this.schema().fields.map((field) => [field.key, field]));
584
+ return keys
585
+ .map((key) => byKey.get(key))
586
+ .filter((field): field is FieldSchema => field !== undefined);
587
+ });
588
+
589
+ readonly hasRowActions = computed(
590
+ () => this.canUpdate() || this.canRemove() || (this.schema().actions?.length ?? 0) > 0,
591
+ );
592
+
593
+ // ── Reordenamiento (base.md §8) ──
594
+
595
+ /** Mientras persiste un movimiento: bloquea drag y botones. */
596
+ readonly reordering = signal(false);
597
+
598
+ /** Sin búsqueda ni filtros aplicados (base.md §10). */
599
+ private readonly hasActiveFilters = computed(() => {
600
+ if (this.query().trim() !== '') {
601
+ return true;
602
+ }
603
+ return Object.values(this.filterValues()).some((value) => value !== '');
604
+ });
605
+
606
+ /**
607
+ * Reordenable sólo si el schema lo declara, el permiso lo permite y no hay
608
+ * filtros activos: con filtros filtrados, el índice visual no corresponde a
609
+ * la posición real.
610
+ */
611
+ readonly canReorder = computed(() => {
612
+ const schema = this.schema();
613
+ return (
614
+ schema.sortable === true &&
615
+ schema.positionField !== undefined &&
616
+ schema.permissions?.reorder !== false && // reorder está en permissions, no en policies aún
617
+ !this.hasActiveFilters()
618
+ );
619
+ });
620
+
621
+ readonly dropDisabled = computed(() => !this.canReorder() || this.reordering());
622
+
623
+ // ── Acciones de datos ──
624
+
625
+ setQuery(value: string): void {
626
+ this.query.set(value);
627
+ }
628
+
629
+ setFilter(key: string, value: string): void {
630
+ this.filterValues.update((values) => ({ ...values, [key]: value }));
631
+ }
632
+
633
+ clearFilters(): void {
634
+ this.query.set('');
635
+ this.filterValues.set({});
636
+ }
637
+
638
+ retry(): void {
639
+ void this.load(this.schema());
640
+ }
641
+
642
+ isRowClickable(): boolean {
643
+ return Boolean(this.schema().detail?.enabled || this.canUpdate());
644
+ }
645
+
646
+ goView(row: Row): void {
647
+ this.viewRecord.emit(row);
648
+ }
649
+
650
+ onRowClick(row: Row): void {
651
+ if (this.schema().detail?.enabled) {
652
+ this.goView(row);
653
+ } else if (this.canUpdate()) {
654
+ this.goEdit(row);
655
+ }
656
+ }
657
+
658
+ goCreate(): void {
659
+ void this.router.navigate(['/', this.schema().id, 'new']);
660
+ }
661
+
662
+ goEdit(row: Row): void {
663
+ if (this.schema().detail?.enabled === true) {
664
+ this.viewRecord.emit(row);
665
+ return;
666
+ }
667
+ void this.router.navigate(['/', this.schema().id, this.keyOf(row), 'edit']);
668
+ }
669
+
670
+ /** Cerró con guardado: se recarga la fila (la respuesta puede venir achatada). */
671
+ quickEditSaved(): void {
672
+ this.quickEditRow.set(null);
673
+ void this.load(this.schema());
674
+ }
675
+
676
+ async toggleActive(row: Row, checked: boolean): Promise<void> {
677
+ const schema = this.schema();
678
+ const key = this.keyOf(row);
679
+ this.savingKey.set(key);
680
+ try {
681
+ await firstValueFrom(this.api.update(schema.endpoint, key, { ...row, active: checked }));
682
+ this.rows.update((rows) =>
683
+ rows.map((candidate) =>
684
+ this.keyOf(candidate) === key ? { ...candidate, active: checked } : candidate,
685
+ ),
686
+ );
687
+ } catch (err) {
688
+ // El cambio no se guardó: la re-renderización revierte el interruptor.
689
+ this.notifications.error(errorMessage(err, 'No se pudo actualizar el registro.'));
690
+ } finally {
691
+ this.savingKey.set(null);
692
+ }
693
+ }
694
+
695
+ /** Drop del CDK: `currentIndex` ya es el índice en la lista sin el elemento. */
696
+ onDrop(event: CdkDragDrop<Row[]>): void {
697
+ if (event.previousContainer !== event.container || event.previousIndex === event.currentIndex) {
698
+ return;
699
+ }
700
+ const row = this.visibleRows()[event.previousIndex];
701
+ if (row !== undefined) {
702
+ void this.moveRow(this.keyOf(row), event.currentIndex);
703
+ }
704
+ }
705
+
706
+ /** Botones subir/bajar (accesibilidad y móvil, base.md §13.10). */
707
+ moveBy(key: string, delta: -1 | 1): void {
708
+ const index = this.visibleRows().findIndex((row) => this.keyOf(row) === key);
709
+ if (index < 0) {
710
+ return;
711
+ }
712
+ void this.moveRow(key, index + delta);
713
+ }
714
+
715
+ /**
716
+ * Un movimiento → UNA intención al backend (baseapi §2.3 y §9):
717
+ * `{ reorder: { key, after | toStart | toEnd } }`, nunca posiciones
718
+ * calculadas. El índice objetivo es la posición FINAL del registro (el
719
+ * `currentIndex` del CDK ya es el índice de inserción en la lista sin el
720
+ * elemento, idéntico al índice final; subir/bajar suma/resta 1). El
721
+ * backend re-balancea, recalcula `position` y devuelve el listado fresco:
722
+ * sin escrituras optimistas — si falla, no cambió nada. `gap-sorting`
723
+ * quedó sólo para los campos con listas locales.
724
+ */
725
+ private async moveRow(key: string, targetIndex: number): Promise<void> {
726
+ const schema = this.schema();
727
+ const path = schema.endpoint.list;
728
+ if (!path || !schema.positionField || this.reordering()) {
729
+ return;
730
+ }
731
+ const rows = this.visibleRows();
732
+ const from = rows.findIndex((row) => this.keyOf(row) === key);
733
+ if (from < 0) {
734
+ return;
735
+ }
736
+
737
+ // Índice final deseado, acotado a la lista SIN el elemento movido.
738
+ const without = rows.filter((row) => this.keyOf(row) !== key);
739
+ const target = Math.min(Math.max(targetIndex, 0), without.length);
740
+ if (target === from) {
741
+ return;
742
+ }
743
+
744
+ const reorder: Record<string, unknown> =
745
+ target === 0
746
+ ? { key, toStart: true }
747
+ : target >= without.length
748
+ ? { key, toEnd: true }
749
+ : { key, after: this.keyOf(without[target - 1]) };
750
+
751
+ this.reordering.set(true);
752
+ try {
753
+ const fresh = await firstValueFrom(this.api.request<Row[]>('PUT', path, { reorder }));
754
+ if (Array.isArray(fresh)) {
755
+ this.rows.set(fresh);
756
+ }
757
+ this.notifications.success('Orden actualizado.');
758
+ } catch (err) {
759
+ this.notifications.error(errorMessage(err, 'No se pudo guardar el orden.'));
760
+ } finally {
761
+ this.reordering.set(false);
762
+ }
763
+ }
764
+
765
+ async removeRow(row: Row): Promise<void> {
766
+ const schema = this.schema();
767
+ const key = this.keyOf(row);
768
+ const accepted = await this.notifications.confirm({
769
+ title: `Eliminar «${this.titleOf(row)}»`,
770
+ message: 'Esta acción no se puede deshacer.',
771
+ confirmLabel: 'Eliminar',
772
+ cancelLabel: 'Cancelar',
773
+ });
774
+ if (!accepted) {
775
+ return;
776
+ }
777
+ try {
778
+ await firstValueFrom(this.api.remove(schema.endpoint, key));
779
+ this.notifications.success('Registro eliminado.');
780
+ this.rows.update((rows) => rows.filter((candidate) => this.keyOf(candidate) !== key));
781
+ } catch (err) {
782
+ this.notifications.error(errorMessage(err, 'No se pudo eliminar el registro.'));
783
+ }
784
+ }
785
+
786
+ /**
787
+ * Ejecuta una acción del schema. El comportamiento vive en el registro de
788
+ * acciones (R3.9–R3.11): la vista sólo aporta el registro y los puertos.
789
+ */
790
+ async runAction(action: ResourceAction, row: Row): Promise<void> {
791
+ await this.actionService.run(action, {
792
+ record: row,
793
+ resource: this.schema().id,
794
+ api: this.api,
795
+ router: this.router,
796
+ toast: this.notifications,
797
+ user: this.auth.user(),
798
+ keyField: this.schema().keyField,
799
+ });
800
+ }
801
+
802
+ /** Icono que el descriptor declara para la acción; sin él, el rótulo. */
803
+ protected iconOf(action: ResourceAction): LucideIcon | undefined {
804
+ return getAction(actionType(action))?.icon;
805
+ }
806
+
807
+ // ── Helpers de celda (uso en template) ──
808
+
809
+ keyOf(row: Row): string {
810
+ return String(row[this.schema().keyField] ?? '');
811
+ }
812
+
813
+ titleOf(row: Row): string {
814
+ return String(row[this.schema().titleField] ?? '');
815
+ }
816
+
817
+ /**
818
+ * Formato efectivo de la columna (R4.4 · «declarada, no implícita»).
819
+ *
820
+ * `column.format` sólo sobreescribe; si no, manda la tabla de
821
+ * `column-defaults` según el `type` del campo — nunca el `?? 'text'`
822
+ * implícito que había.
823
+ */
824
+ formatOf(column: ListColumn): ListColumnFormat {
825
+ return formatoDeColumna(column, this.schema().fields);
826
+ }
827
+
828
+ /**
829
+ * Contexto de la celda que recibe el formato (R4.3 · `extras.data`).
830
+ *
831
+ * Concentra aquí lo que sólo la vista sabe — permisos, moneda, guardado y
832
+ * la escritura de la fila — para que «añadir un formato» no la toque.
833
+ */
834
+ cellOf(row: Row, column: ListColumn): ColumnCell {
835
+ return {
836
+ row,
837
+ column,
838
+ fields: this.schema().fields,
839
+ title: this.titleOf(row),
840
+ canUpdate: this.canUpdate(),
841
+ saving: this.savingKey() === this.keyOf(row),
842
+ currency: this.currencyCode(),
843
+ toggle: (checked: boolean) => void this.toggleActive(row, checked),
844
+ };
845
+ }
846
+
847
+ filterOptions(filter: ListFilter): SelectOption[] {
848
+ if (filter.type === 'boolean') {
849
+ return BOOLEAN_FILTER_OPTIONS;
850
+ }
851
+ if (filter.type === 'select') {
852
+ return [{ value: '', label: 'Todos' }, ...(filter.options ?? [])];
853
+ }
854
+ return [];
855
+ }
856
+
857
+ inputValue(event: Event): string {
858
+ return (event.target as HTMLInputElement).value;
859
+ }
860
+
861
+ selectValue(event: Event): string {
862
+ return (event.target as HTMLSelectElement).value;
863
+ }
864
+
865
+ private async load(schema: ResourceSchema): Promise<void> {
866
+ const epoch = ++this.loadEpoch;
867
+ this.status.set('loading');
868
+ try {
869
+ const rows = await firstValueFrom(this.api.list<Row>(schema.endpoint));
870
+ if (epoch !== this.loadEpoch) {
871
+ return;
872
+ }
873
+ this.rows.set(Array.isArray(rows) ? rows : []);
874
+ this.status.set('ready');
875
+ } catch {
876
+ if (epoch !== this.loadEpoch) {
877
+ return;
878
+ }
879
+ this.status.set('error');
880
+ }
881
+ }
882
+ }