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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/README.md +222 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +3 -15
  4. package/src/lib/actions/action-registry.spec.ts +52 -0
  5. package/src/lib/actions/action-registry.ts +126 -0
  6. package/src/lib/actions/action-service.spec.ts +215 -0
  7. package/src/lib/actions/action-service.ts +87 -0
  8. package/src/lib/actions/descriptors.spec.ts +155 -0
  9. package/src/lib/actions/descriptors.ts +154 -0
  10. package/src/lib/columns/column-cell.ts +33 -0
  11. package/src/lib/columns/column-defaults.spec.ts +99 -0
  12. package/src/lib/columns/column-defaults.ts +84 -0
  13. package/src/lib/columns/column-format.spec.ts +274 -0
  14. package/src/lib/columns/descriptors.ts +113 -0
  15. package/src/lib/columns/format-badge.ts +51 -0
  16. package/src/lib/columns/format-boolean.ts +58 -0
  17. package/src/lib/columns/format-currency.ts +30 -0
  18. package/src/lib/columns/format-date.ts +35 -0
  19. package/src/lib/columns/format-number.ts +32 -0
  20. package/src/lib/columns/format-text.ts +23 -0
  21. package/src/lib/columns/format-thumbnail.ts +63 -0
  22. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  23. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  24. package/src/lib/core/guards/auth.guard.ts +48 -0
  25. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  27. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  28. package/src/lib/core/models/api.model.ts +104 -0
  29. package/src/lib/core/models/schema-descriptions.ts +118 -0
  30. package/src/lib/core/models/schema.model.ts +1097 -0
  31. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  32. package/src/lib/core/registry/composition-validator.ts +270 -0
  33. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  34. package/src/lib/core/registry/field-validators.ts +440 -0
  35. package/src/lib/core/registry/render-host.spec.ts +208 -0
  36. package/src/lib/core/registry/render-host.ts +646 -0
  37. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  38. package/src/lib/core/registry/render-registry.ts +312 -0
  39. package/src/lib/core/registry/schema-catalog.ts +156 -0
  40. package/src/lib/core/services/api.service.spec.ts +1142 -0
  41. package/src/lib/core/services/api.service.ts +371 -0
  42. package/src/lib/core/services/auth.service.spec.ts +653 -0
  43. package/src/lib/core/services/auth.service.ts +503 -0
  44. package/src/lib/core/services/config.service.spec.ts +121 -0
  45. package/src/lib/core/services/config.service.ts +119 -0
  46. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  47. package/src/lib/core/services/data-source.service.ts +142 -0
  48. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  49. package/src/lib/core/services/error-mapper.service.ts +49 -0
  50. package/src/lib/core/services/notification.service.ts +73 -0
  51. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  52. package/src/lib/core/services/theme.service.spec.ts +106 -0
  53. package/src/lib/core/services/theme.service.ts +86 -0
  54. package/src/lib/core/services/transport.spec.ts +198 -0
  55. package/src/lib/core/services/transport.ts +164 -0
  56. package/src/lib/core/services/upload.service.ts +81 -0
  57. package/src/lib/core/testing/categories.schema.ts +125 -0
  58. package/src/lib/core/testing/orders.schema.ts +372 -0
  59. package/src/lib/core/testing/products.schema.ts +258 -0
  60. package/src/lib/core/testing/site.schema.ts +195 -0
  61. package/src/lib/core/testing/test-catalog.ts +21 -0
  62. package/src/lib/core/tokens.ts +112 -0
  63. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  64. package/src/lib/core/utils/clipboard.ts +90 -0
  65. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  66. package/src/lib/core/utils/context-resolver.ts +101 -0
  67. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  68. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  69. package/src/lib/core/utils/source-options.spec.ts +96 -0
  70. package/src/lib/core/utils/source-options.ts +54 -0
  71. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  72. package/src/lib/core/utils/template-interpolator.ts +56 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  74. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  75. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  76. package/src/lib/fields/boolean/boolean.ts +40 -0
  77. package/src/lib/fields/color/color.descriptor.ts +12 -0
  78. package/src/lib/fields/color/color.ts +34 -0
  79. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  80. package/src/lib/fields/currency/currency.ts +67 -0
  81. package/src/lib/fields/date/date.descriptor.ts +12 -0
  82. package/src/lib/fields/date/date.ts +36 -0
  83. package/src/lib/fields/descriptors.ts +122 -0
  84. package/src/lib/fields/display/display-badge.ts +65 -0
  85. package/src/lib/fields/display/display-date.ts +39 -0
  86. package/src/lib/fields/display/display-defaults.ts +84 -0
  87. package/src/lib/fields/display/display-file.ts +41 -0
  88. package/src/lib/fields/display/display-image.ts +54 -0
  89. package/src/lib/fields/display/display-list.ts +107 -0
  90. package/src/lib/fields/display/display-number.ts +41 -0
  91. package/src/lib/fields/display/display-text.ts +42 -0
  92. package/src/lib/fields/display/display.spec.ts +242 -0
  93. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  94. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  95. package/src/lib/fields/email/email.descriptor.ts +12 -0
  96. package/src/lib/fields/email/email.ts +28 -0
  97. package/src/lib/fields/field-aria.ts +60 -0
  98. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  99. package/src/lib/fields/field-host/field-host.ts +47 -0
  100. package/src/lib/fields/field-node.ts +88 -0
  101. package/src/lib/fields/file/file.descriptor.ts +14 -0
  102. package/src/lib/fields/file/file.spec.ts +134 -0
  103. package/src/lib/fields/file/file.ts +281 -0
  104. package/src/lib/fields/group/group.descriptor.ts +21 -0
  105. package/src/lib/fields/group/group.ts +110 -0
  106. package/src/lib/fields/image/image.descriptor.ts +12 -0
  107. package/src/lib/fields/image/image.spec.ts +117 -0
  108. package/src/lib/fields/image/image.ts +281 -0
  109. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  110. package/src/lib/fields/key-value/key-value.ts +283 -0
  111. package/src/lib/fields/list/list.descriptor.ts +21 -0
  112. package/src/lib/fields/list/list.ts +376 -0
  113. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  114. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  115. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  116. package/src/lib/fields/number/number.descriptor.ts +12 -0
  117. package/src/lib/fields/number/number.ts +40 -0
  118. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  119. package/src/lib/fields/phone/phone.ts +46 -0
  120. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  121. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  122. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  123. package/src/lib/fields/relation/relation.spec.ts +193 -0
  124. package/src/lib/fields/relation/relation.ts +224 -0
  125. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  126. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  127. package/src/lib/fields/select/radio-mode.ts +25 -0
  128. package/src/lib/fields/select/select.descriptor.ts +12 -0
  129. package/src/lib/fields/select/select.spec.ts +162 -0
  130. package/src/lib/fields/select/select.ts +210 -0
  131. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  132. package/src/lib/fields/slug/slug.ts +67 -0
  133. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  134. package/src/lib/fields/string-list/string-list.ts +222 -0
  135. package/src/lib/fields/text/text.descriptor.ts +12 -0
  136. package/src/lib/fields/text/text.ts +35 -0
  137. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  138. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  139. package/src/lib/fields/textarea/textarea.ts +126 -0
  140. package/src/lib/fields/time/time.descriptor.ts +12 -0
  141. package/src/lib/fields/time/time.ts +28 -0
  142. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  143. package/src/lib/fields/url/url.descriptor.ts +12 -0
  144. package/src/lib/fields/url/url.ts +53 -0
  145. package/src/lib/gas-admin-ui.spec.ts +21 -0
  146. package/src/lib/gas-admin-ui.ts +9 -0
  147. package/src/lib/layouts/descriptors.ts +9 -0
  148. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  149. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  150. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  151. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  152. package/src/lib/pages/form-view/form-model.ts +88 -0
  153. package/src/lib/pages/form-view/form-schema.ts +255 -0
  154. package/src/lib/pages/form-view/form-view.ts +1622 -0
  155. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  156. package/src/lib/pages/list-view/list-view.ts +882 -0
  157. package/src/lib/pages/login/login.spec.ts +109 -0
  158. package/src/lib/pages/login/login.ts +227 -0
  159. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  160. package/src/lib/shared/components/badge/badge.ts +35 -0
  161. package/src/lib/shared/components/button/button.ts +65 -0
  162. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  163. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  164. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  165. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  166. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  167. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  168. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  169. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  170. package/src/lib/shared/components/modal/modal.ts +145 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  172. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  173. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  174. package/src/lib/shared/components/toast/toast.ts +58 -0
  175. package/src/lib/shared/utils/col-span.ts +25 -0
  176. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  177. package/src/lib/shared/utils/depends-on.ts +48 -0
  178. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  179. package/src/lib/shared/utils/field-path.ts +49 -0
  180. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  181. package/src/lib/shared/utils/seed.spec.ts +64 -0
  182. package/src/lib/shared/utils/seed.ts +149 -0
  183. package/src/lib/shared/utils/slugify.ts +13 -0
  184. package/src/lib/shared/utils/validators.ts +41 -0
  185. package/src/lib/shell/layout/layout.ts +63 -0
  186. package/src/lib/shell/shell.routes.spec.ts +59 -0
  187. package/src/lib/shell/shell.routes.ts +122 -0
  188. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  189. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  190. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  191. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  192. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  193. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  194. package/src/lib/views/composer/composer-host.ts +248 -0
  195. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  196. package/src/lib/views/composer/view-context.spec.ts +117 -0
  197. package/src/lib/views/composer/view-context.ts +109 -0
  198. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  199. package/src/lib/views/composer/view-slot.ts +178 -0
  200. package/src/lib/views/descriptors.ts +46 -0
  201. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  202. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  203. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  206. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  207. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  208. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  209. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  210. package/src/lib/widgets/descriptors.ts +29 -0
  211. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  212. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  213. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  216. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  217. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  218. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  219. package/src/lib/widgets/record-list/record-list.ts +115 -0
  220. package/src/lib/widgets/series.spec.ts +42 -0
  221. package/src/lib/widgets/series.ts +56 -0
  222. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  223. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  224. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  225. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  226. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  227. package/src/public-api.ts +104 -0
  228. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  229. package/tsconfig.lib.json +13 -0
  230. package/tsconfig.lib.prod.json +11 -0
  231. package/tsconfig.spec.json +10 -0
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  233. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  235. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  237. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  239. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  241. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  243. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  245. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  247. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  249. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  251. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  253. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  255. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  257. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  259. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  261. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  263. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  265. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  267. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  269. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  271. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  273. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  275. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  277. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  279. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  281. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  283. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  285. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  287. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  289. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  291. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  293. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  295. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  297. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  299. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  301. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  303. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  305. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  307. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  309. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  311. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  313. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  315. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  317. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  319. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  321. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  323. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  325. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  327. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  329. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  333. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  335. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  337. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  338. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  340. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  342. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  344. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  346. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  348. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  350. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  352. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  354. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  356. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  358. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  360. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  362. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  364. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  366. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  368. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  369. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -1,4599 +0,0 @@
1
- import { t as __exportAll } from "./tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs";
2
- import { _ as DEFAULT_RESPONSE_UNWRAP, a as provideFirebaseAdapter, b as ApiError, c as ConfigService, d as AuthService, f as LOGIN_PATH, g as API_URL, h as API_DRIVER, i as getSchema, l as ApiService, m as SESSION_USER_KEY, n as allSchemas, o as provideGasAdmin, p as SESSION_TOKEN_KEY, r as getAuth, s as setSchemaCatalog, t as GAS_SCHEMAS, u as apiErrorInterceptor, v as FIREBASE_AUTH_ADAPTER, x as renewedTokenOf, y as VALUE_SOURCES } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
3
- import { a as register, c as unregister, d as SUPPORTED_FIELD_TYPE_LIST, f as fieldDescriptors, i as provideRenderDescriptors, l as viewDescriptors, m as columnFormatDescriptors, n as get, o as renderKinds, p as COLUMN_FORMAT_TYPES, r as isSupported, u as layoutDescriptors } from "./tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs";
4
- import { n as ViewNavigationService, t as evaluateCondition } from "./tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs";
5
- import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
6
- import { t as NotificationService } from "./tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs";
7
- import { t as interpolateTemplate } from "./tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs";
8
- import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs";
9
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs";
10
- import { t as UploadService } from "./tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs";
11
- import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
12
- import { a as stringItemTree, i as listTree, n as groupTree, r as itemTree, t as childTree } from "./tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs";
13
- import { _ as resolveShare, a as DrawerForm, c as buildFormSchema, d as actionTypes, f as actionsFor, g as isTriggerAction, h as isShareAction, i as CopyShare, l as FieldHost, m as registerAction, o as buildPayload, p as getAction, r as Modal, s as seedModel, t as FormView, u as actionType, v as RenderHost } from "./tinya-code-gas-admin-ui-form-view-C709rc57.mjs";
14
- import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
15
- import { t as getValue } from "./tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs";
16
- import * as i0 from "@angular/core";
17
- import { ChangeDetectionStrategy, Component, EnvironmentInjector, Injector, Service, ViewContainerRef, computed, effect, inject, input, model, output, resource, signal, viewChild } from "@angular/core";
18
- import { catchError, combineLatest, distinctUntilChanged, firstValueFrom, map, of, switchMap } from "rxjs";
19
- import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet, convertToParamMap } from "@angular/router";
20
- import { LucideArrowLeft, LucideChevronDown, LucideChevronRight, LucideChevronUp, LucideCircleCheck, LucideCircleX, LucideDynamicIcon, LucideExternalLink, LucideEye, LucideGripVertical, LucideInfo, LucideLogOut, LucideMonitor, LucideMoon, LucidePencil, LucidePlus, LucideRotateCw, LucideSun, LucideTrash2, LucideX, LucideZap } from "@lucide/angular";
21
- import { takeUntilDestroyed, toObservable, toSignal } from "@angular/core/rxjs-interop";
22
- import { CdkDrag, CdkDragHandle, CdkDropList } from "@angular/cdk/drag-drop";
23
- const authGuard = () => {
24
- if (inject(AuthService).isAuthenticated()) return true;
25
- return inject(Router).createUrlTree([LOGIN_PATH]);
26
- };
27
- const roleGuard = (route) => {
28
- const auth = inject(AuthService);
29
- const router = inject(Router);
30
- if (!auth.isAuthenticated()) return router.createUrlTree([LOGIN_PATH]);
31
- const id = route.data?.["resourceId"] ?? route.paramMap.get("id");
32
- if (id === null) return true;
33
- const schema = getSchema(String(id));
34
- if (schema === void 0) return true;
35
- return auth.canAccess(schema.access);
36
- };
37
- const MAX_DEPTH = 3;
38
- var SchemaCompositionError = class extends Error {
39
- constructor(message) {
40
- super(message);
41
- this.name = "SchemaCompositionError";
42
- }
43
- };
44
- function configDe(vista) {
45
- return "config" in vista ? vista.config : void 0;
46
- }
47
- function hijosDe(vista) {
48
- const hijos = configDe(vista)?.children;
49
- return Array.isArray(hijos) ? hijos : void 0;
50
- }
51
- function validateComposition(schema) {
52
- const estado = {
53
- esquema: schema,
54
- nodos: 0,
55
- contados: /* @__PURE__ */ new Set(),
56
- camino: /* @__PURE__ */ new Set()
57
- };
58
- for (const [nombre, vista] of Object.entries(schema.uiViews ?? {})) {
59
- estado.camino.add(nombre);
60
- validarContenido(nombre, vista, 1, estado);
61
- estado.camino.delete(nombre);
62
- }
63
- }
64
- function validarContenido(nombre, vista, profundidadHijos, estado) {
65
- const donde = `${estado.esquema.id}.${nombre}`;
66
- validarSelect(donde, vista.select);
67
- validarSource(donde, configDe(vista)?.source);
68
- const hijos = hijosDe(vista);
69
- if (!hijos) return;
70
- validarHijos(nombre, hijos, profundidadHijos, estado);
71
- }
72
- function validarHijos(contenedor, hijos, profundidad, estado) {
73
- const ids = /* @__PURE__ */ new Set();
74
- for (const hijo of hijos) {
75
- const id = hijo.id;
76
- if (typeof id !== "string" || id === "") throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: hijo sin id`);
77
- if (ids.has(id)) throw new SchemaCompositionError(`${estado.esquema.id}.${contenedor}: id duplicado '${id}'`);
78
- ids.add(id);
79
- validarHijo(hijo, contenedor, id, profundidad, estado);
80
- }
81
- }
82
- function validarHijo(hijo, contenedor, id, profundidad, estado) {
83
- const donde = `${estado.esquema.id}.${contenedor} → child '${id}'`;
84
- if (!estado.contados.has(hijo)) {
85
- estado.contados.add(hijo);
86
- estado.nodos += 1;
87
- if (estado.nodos > 50) throw new SchemaCompositionError(`${estado.esquema.id}: la composición supera el límite de 50 nodos (observados ${estado.nodos})`);
88
- }
89
- if (profundidad > 3) throw new SchemaCompositionError(`${donde}: profundidad ${profundidad} supera el máximo 3`);
90
- if ("ref" in hijo) {
91
- validarRef(donde, hijo.ref, profundidad, estado);
92
- return;
93
- }
94
- const tipo = hijo.type;
95
- if (!get("view", tipo)) throw new SchemaCompositionError(`${donde}: tipo desconocido '${tipo}'`);
96
- validarSelect(donde, hijo.select);
97
- validarSource(donde, configDe(hijo)?.source);
98
- const internos = hijosDe(hijo);
99
- if (internos !== void 0) validarHijos(id, internos, profundidad + 1, estado);
100
- }
101
- function validarRef(donde, target, profundidad, estado) {
102
- if (estado.camino.has(target)) throw new SchemaCompositionError(`${donde}: ciclo detectado — '${target}' ya está en el camino`);
103
- const vista = estado.esquema.uiViews?.[target];
104
- if (!vista) throw new SchemaCompositionError(`${donde}: ref '${target}' no existe en ${estado.esquema.id}.uiViews`);
105
- estado.camino.add(target);
106
- validarContenido(target, vista, profundidad + 1, estado);
107
- estado.camino.delete(target);
108
- }
109
- function validarSelect(donde, select) {
110
- if (select === void 0 || select.mode !== "route") return;
111
- const target = select.target;
112
- if (target === void 0 || target === null) throw new SchemaCompositionError(`${donde}: select mode 'route' sin target`);
113
- if (typeof target === "string") throw new SchemaCompositionError(`${donde}: destino con URL cruda '${target}' — declará resource + view`);
114
- validarTarget(donde, target);
115
- }
116
- function validarTarget(donde, target) {
117
- const { resource, view, params } = target;
118
- if (typeof resource !== "string" || resource === "" || typeof view !== "string" || view === "") throw new SchemaCompositionError(`${donde}: destino sin resource + view`);
119
- const destino = getSchema(resource);
120
- if (!destino) throw new SchemaCompositionError(`${donde}: destino '${resource}' no existe`);
121
- const vista = destino.uiViews?.[view];
122
- if (!vista) throw new SchemaCompositionError(`${donde}: destino ${resource}.${view} no tiene uiView declarada`);
123
- const paramsRuta = (vista.path ?? "").split("/").filter((segmento) => segmento.startsWith(":")).map((segmento) => segmento.slice(1));
124
- const declarados = params ?? [];
125
- for (const param of declarados) if (!paramsRuta.includes(param)) throw new SchemaCompositionError(`${donde}: param '${param}' no existe en la ruta ${resource}.${view}`);
126
- for (const param of paramsRuta) if (!declarados.includes(param)) throw new SchemaCompositionError(`${donde}: falta declarar el param '${param}' de la ruta ${resource}.${view}`);
127
- }
128
- function validarSource(donde, source) {
129
- if (source === void 0) return;
130
- if (typeof source.resource !== "string" || source.resource === "") throw new SchemaCompositionError(`${donde}: source sin resource`);
131
- const origen = getSchema(source.resource);
132
- if (!origen) throw new SchemaCompositionError(`${donde}: source '${source.resource}' no existe`);
133
- if (source.view !== void 0 && origen.views?.[source.view] === void 0) throw new SchemaCompositionError(`${donde}: la vista '${source.view}' no existe en ${source.resource}.views`);
134
- }
135
- const unsavedChangesGuard = (component) => {
136
- return component?.canDeactivate?.() ?? true;
137
- };
138
- function instanceIdFor(parentId, childId) {
139
- return `${parentId}/${childId}`;
140
- }
141
- function virtualSchemaFor(schema, instanceId, view) {
142
- return {
143
- ...schema,
144
- uiViews: {
145
- ...schema.uiViews,
146
- [instanceId]: view
147
- }
148
- };
149
- }
150
- function syntheticRoute(data) {
151
- return {
152
- data: of(data),
153
- paramMap: of(convertToParamMap({}))
154
- };
155
- }
156
- function createViewContext(base, emit) {
157
- return emit === void 0 ? { ...base } : {
158
- ...base,
159
- emit
160
- };
161
- }
162
- const STORAGE_KEY = "admin-theme";
163
- const QUERY = "(prefers-color-scheme: dark)";
164
- const MODES = [
165
- "system",
166
- "light",
167
- "dark"
168
- ];
169
- function storedMode() {
170
- try {
171
- const raw = localStorage.getItem(STORAGE_KEY);
172
- return raw !== null && MODES.includes(raw) ? raw : "system";
173
- } catch {
174
- return "system";
175
- }
176
- }
177
- function prefersDark() {
178
- return typeof window.matchMedia === "function" && window.matchMedia(QUERY).matches;
179
- }
180
- function resolve(mode) {
181
- return mode === "system" ? prefersDark() ? "dark" : "light" : mode;
182
- }
183
- var ThemeService = class ThemeService {
184
- mode = signal(storedMode(), ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
185
- resolved = signal(resolve(this.mode()), ...ngDevMode ? [{ debugName: "resolved" }] : /* istanbul ignore next */ []);
186
- constructor() {
187
- this.apply();
188
- (typeof window.matchMedia === "function" ? window.matchMedia(QUERY) : null)?.addEventListener("change", () => {
189
- if (this.mode() === "system") this.apply();
190
- });
191
- }
192
- set(mode) {
193
- this.mode.set(mode);
194
- try {
195
- localStorage.setItem(STORAGE_KEY, mode);
196
- } catch {}
197
- this.apply();
198
- }
199
- apply() {
200
- const resolved = resolve(this.mode());
201
- this.resolved.set(resolved);
202
- document.documentElement.dataset["theme"] = resolved;
203
- document.documentElement.dataset["themeMode"] = this.mode();
204
- }
205
- static ɵfac = i0.ɵɵngDeclareFactory({
206
- minVersion: "12.0.0",
207
- version: "22.2.3",
208
- ngImport: i0,
209
- type: ThemeService,
210
- deps: [],
211
- target: i0.ɵɵFactoryTarget.Service
212
- });
213
- static ɵprov = i0.ɵɵngDeclareService({
214
- minVersion: "22.0.0",
215
- version: "22.2.3",
216
- ngImport: i0,
217
- type: ThemeService
218
- });
219
- };
220
- i0.ɵɵngDeclareClassMetadata({
221
- minVersion: "12.0.0",
222
- version: "22.2.3",
223
- ngImport: i0,
224
- type: ThemeService,
225
- decorators: [{ type: Service }],
226
- ctorParameters: () => []
227
- });
228
- const BASE = "rounded-md p-1.5 transition-colors min-h-11 min-w-11 lg:min-h-0 lg:min-w-0 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
229
- const ACTIVE = "bg-primary/10 text-primary";
230
- const IDLE = "text-neutral hover:bg-neutral/10";
231
- var ThemeToggle = class ThemeToggle {
232
- theme = inject(ThemeService);
233
- classes(mode) {
234
- return `${BASE} ${this.theme.mode() === mode ? ACTIVE : IDLE}`;
235
- }
236
- static ɵfac = i0.ɵɵngDeclareFactory({
237
- minVersion: "12.0.0",
238
- version: "22.2.3",
239
- ngImport: i0,
240
- type: ThemeToggle,
241
- deps: [],
242
- target: i0.ɵɵFactoryTarget.Component
243
- });
244
- static ɵcmp = i0.ɵɵngDeclareComponent({
245
- minVersion: "14.0.0",
246
- version: "22.2.3",
247
- type: ThemeToggle,
248
- isStandalone: true,
249
- selector: "app-theme-toggle",
250
- ngImport: i0,
251
- template: `
252
- <div
253
- role="group"
254
- aria-label="Tema del panel"
255
- class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
256
- >
257
- <button
258
- type="button"
259
- [class]="classes('system')"
260
- [attr.aria-pressed]="theme.mode() === 'system'"
261
- aria-label="Tema: seguir al sistema"
262
- title="Seguir al sistema"
263
- (click)="theme.set('system')"
264
- >
265
- <svg lucideMonitor size="16" />
266
- </button>
267
- <button
268
- type="button"
269
- [class]="classes('light')"
270
- [attr.aria-pressed]="theme.mode() === 'light'"
271
- aria-label="Tema: claro"
272
- title="Tema claro"
273
- (click)="theme.set('light')"
274
- >
275
- <svg lucideSun size="16" />
276
- </button>
277
- <button
278
- type="button"
279
- [class]="classes('dark')"
280
- [attr.aria-pressed]="theme.mode() === 'dark'"
281
- aria-label="Tema: oscuro"
282
- title="Tema oscuro"
283
- (click)="theme.set('dark')"
284
- >
285
- <svg lucideMoon size="16" />
286
- </button>
287
- </div>
288
- `,
289
- isInline: true,
290
- dependencies: [
291
- {
292
- kind: "component",
293
- type: LucideMonitor,
294
- selector: "svg[lucideMonitor]"
295
- },
296
- {
297
- kind: "component",
298
- type: LucideSun,
299
- selector: "svg[lucideSun]"
300
- },
301
- {
302
- kind: "component",
303
- type: LucideMoon,
304
- selector: "svg[lucideMoon]"
305
- }
306
- ]
307
- });
308
- };
309
- i0.ɵɵngDeclareClassMetadata({
310
- minVersion: "12.0.0",
311
- version: "22.2.3",
312
- ngImport: i0,
313
- type: ThemeToggle,
314
- decorators: [{
315
- type: Component,
316
- args: [{
317
- selector: "app-theme-toggle",
318
- imports: [
319
- LucideMonitor,
320
- LucideSun,
321
- LucideMoon
322
- ],
323
- template: `
324
- <div
325
- role="group"
326
- aria-label="Tema del panel"
327
- class="inline-flex items-center gap-0.5 rounded-lg border border-neutral/20 bg-surface p-0.5"
328
- >
329
- <button
330
- type="button"
331
- [class]="classes('system')"
332
- [attr.aria-pressed]="theme.mode() === 'system'"
333
- aria-label="Tema: seguir al sistema"
334
- title="Seguir al sistema"
335
- (click)="theme.set('system')"
336
- >
337
- <svg lucideMonitor size="16" />
338
- </button>
339
- <button
340
- type="button"
341
- [class]="classes('light')"
342
- [attr.aria-pressed]="theme.mode() === 'light'"
343
- aria-label="Tema: claro"
344
- title="Tema claro"
345
- (click)="theme.set('light')"
346
- >
347
- <svg lucideSun size="16" />
348
- </button>
349
- <button
350
- type="button"
351
- [class]="classes('dark')"
352
- [attr.aria-pressed]="theme.mode() === 'dark'"
353
- aria-label="Tema: oscuro"
354
- title="Tema oscuro"
355
- (click)="theme.set('dark')"
356
- >
357
- <svg lucideMoon size="16" />
358
- </button>
359
- </div>
360
- `
361
- }]
362
- }]
363
- });
364
- var Sidebar = class Sidebar {
365
- open = model(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
366
- auth = inject(AuthService);
367
- notifications = inject(NotificationService);
368
- isAuthenticated = this.auth.isAuthenticated;
369
- loggingOut = signal(false, ...ngDevMode ? [{ debugName: "loggingOut" }] : /* istanbul ignore next */ []);
370
- menuItems = computed(() => allSchemas().filter((schema) => isVisible(schema) && this.auth.canAccess(schema.access)).map((schema) => ({
371
- id: schema.id,
372
- label: schema.labelPlural
373
- })), ...ngDevMode ? [{ debugName: "menuItems" }] : /* istanbul ignore next */ []);
374
- async closeSession() {
375
- if (!await this.notifications.confirm({
376
- title: "Cerrar sesión",
377
- message: "¿Seguro que querés cerrar tu sesión?",
378
- confirmLabel: "Cerrar sesión"
379
- })) return;
380
- this.loggingOut.set(true);
381
- this.auth.logout().subscribe({ complete: () => this.loggingOut.set(false) });
382
- }
383
- static ɵfac = i0.ɵɵngDeclareFactory({
384
- minVersion: "12.0.0",
385
- version: "22.2.3",
386
- ngImport: i0,
387
- type: Sidebar,
388
- deps: [],
389
- target: i0.ɵɵFactoryTarget.Component
390
- });
391
- static ɵcmp = i0.ɵɵngDeclareComponent({
392
- minVersion: "17.0.0",
393
- version: "22.2.3",
394
- type: Sidebar,
395
- isStandalone: true,
396
- selector: "app-sidebar",
397
- inputs: { open: {
398
- classPropertyName: "open",
399
- publicName: "open",
400
- isSignal: true,
401
- isRequired: false,
402
- transformFunction: null
403
- } },
404
- outputs: { open: "openChange" },
405
- ngImport: i0,
406
- template: `
407
- <!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
408
- y siempre visible en pantallas grandes. -->
409
- <aside
410
- class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
411
- [class.-translate-x-full]="!open()"
412
- >
413
- <div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
414
- <!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
415
- trunca en vez de desbordar la sidebar de 240 px. -->
416
- <span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
417
- <app-theme-toggle />
418
- </div>
419
- <nav aria-label="Principal" class="p-3">
420
- <ul class="space-y-2 lg:space-y-1">
421
- @for (item of menuItems(); track item.id) {
422
- <li>
423
- <a
424
- [routerLink]="['/', item.id]"
425
- routerLinkActive="bg-primary/10 text-primary"
426
- class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
427
- (click)="open.set(false)"
428
- >
429
- {{ item.label }}
430
- </a>
431
- </li>
432
- }
433
- </ul>
434
- </nav>
435
- @if (isAuthenticated()) {
436
- <div class="border-t border-neutral/20 p-3">
437
- <button
438
- app-button
439
- type="button"
440
- variant="danger"
441
- [loading]="loggingOut()"
442
- [disabled]="loggingOut()"
443
- (click)="closeSession()"
444
- class="w-full"
445
- >
446
- <svg lucideLogOut size="16" />
447
- Cerrar sesión
448
- </button>
449
- </div>
450
- }
451
- </aside>
452
- `,
453
- isInline: true,
454
- dependencies: [
455
- {
456
- kind: "directive",
457
- type: RouterLink,
458
- selector: "[routerLink]",
459
- inputs: [
460
- "target",
461
- "queryParams",
462
- "fragment",
463
- "queryParamsHandling",
464
- "state",
465
- "info",
466
- "relativeTo",
467
- "preserveFragment",
468
- "skipLocationChange",
469
- "replaceUrl",
470
- "browserUrl",
471
- "routerLink"
472
- ]
473
- },
474
- {
475
- kind: "directive",
476
- type: RouterLinkActive,
477
- selector: "[routerLinkActive]",
478
- inputs: [
479
- "routerLinkActiveOptions",
480
- "ariaCurrentWhenActive",
481
- "routerLinkActive"
482
- ],
483
- outputs: ["isActiveChange"],
484
- exportAs: ["routerLinkActive"]
485
- },
486
- {
487
- kind: "component",
488
- type: ThemeToggle,
489
- selector: "app-theme-toggle"
490
- },
491
- {
492
- kind: "component",
493
- type: Button,
494
- selector: "button[app-button]",
495
- inputs: [
496
- "variant",
497
- "size",
498
- "type",
499
- "disabled",
500
- "loading"
501
- ]
502
- },
503
- {
504
- kind: "component",
505
- type: LucideLogOut,
506
- selector: "svg[lucideLogOut]"
507
- }
508
- ]
509
- });
510
- };
511
- i0.ɵɵngDeclareClassMetadata({
512
- minVersion: "12.0.0",
513
- version: "22.2.3",
514
- ngImport: i0,
515
- type: Sidebar,
516
- decorators: [{
517
- type: Component,
518
- args: [{
519
- selector: "app-sidebar",
520
- imports: [
521
- RouterLink,
522
- RouterLinkActive,
523
- ThemeToggle,
524
- Button,
525
- LucideLogOut
526
- ],
527
- template: `
528
- <!-- Offcanvas en mobile (transform); las clases lg: la dejan estática
529
- y siempre visible en pantallas grandes. -->
530
- <aside
531
- class="fixed inset-y-0 left-0 z-40 w-60 shrink-0 h-[100svh] min-h-[80svh] lg:sticky border-r border-neutral/20 bg-surface-raised transition-transform duration-200 lg:static lg:z-auto lg:translate-x-0"
532
- [class.-translate-x-full]="!open()"
533
- >
534
- <div class="flex items-center justify-between gap-2 border-b border-neutral/20 px-4 py-4">
535
- <!-- min-w-0+truncate: si el toggle exige su ancho, el rótulo se
536
- trunca en vez de desbordar la sidebar de 240 px. -->
537
- <span class="min-w-0 truncate font-display text-base font-semibold">Admin</span>
538
- <app-theme-toggle />
539
- </div>
540
- <nav aria-label="Principal" class="p-3">
541
- <ul class="space-y-2 lg:space-y-1">
542
- @for (item of menuItems(); track item.id) {
543
- <li>
544
- <a
545
- [routerLink]="['/', item.id]"
546
- routerLinkActive="bg-primary/10 text-primary"
547
- class="flex min-h-11 items-center rounded-lg px-3 py-2 text-sm font-medium text-neutral hover:bg-neutral/10"
548
- (click)="open.set(false)"
549
- >
550
- {{ item.label }}
551
- </a>
552
- </li>
553
- }
554
- </ul>
555
- </nav>
556
- @if (isAuthenticated()) {
557
- <div class="border-t border-neutral/20 p-3">
558
- <button
559
- app-button
560
- type="button"
561
- variant="danger"
562
- [loading]="loggingOut()"
563
- [disabled]="loggingOut()"
564
- (click)="closeSession()"
565
- class="w-full"
566
- >
567
- <svg lucideLogOut size="16" />
568
- Cerrar sesión
569
- </button>
570
- </div>
571
- }
572
- </aside>
573
- `
574
- }]
575
- }],
576
- propDecorators: { open: [{
577
- type: i0.Input,
578
- args: [{
579
- isSignal: true,
580
- alias: "open",
581
- required: false
582
- }]
583
- }, {
584
- type: i0.Output,
585
- args: ["openChange"]
586
- }] }
587
- });
588
- function isVisible(schema) {
589
- if (schema.policies?.access) return true;
590
- const permissions = schema.permissions;
591
- if (!permissions) return true;
592
- const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
593
- const canWrite = permissions.create !== false || permissions.update !== false || permissions.remove !== false;
594
- return hasReadEndpoint || canWrite;
595
- }
596
- var Layout = class Layout {
597
- sidebarOpen = signal(false, ...ngDevMode ? [{ debugName: "sidebarOpen" }] : /* istanbul ignore next */ []);
598
- static ɵfac = i0.ɵɵngDeclareFactory({
599
- minVersion: "12.0.0",
600
- version: "22.2.3",
601
- ngImport: i0,
602
- type: Layout,
603
- deps: [],
604
- target: i0.ɵɵFactoryTarget.Component
605
- });
606
- static ɵcmp = i0.ɵɵngDeclareComponent({
607
- minVersion: "17.0.0",
608
- version: "22.2.3",
609
- type: Layout,
610
- isStandalone: true,
611
- selector: "app-layout",
612
- ngImport: i0,
613
- template: `
614
- <div class="flex min-h-screen bg-neutral/5">
615
- <button
616
- app-button
617
- type="button"
618
- variant="outline"
619
- size="icon"
620
- class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
621
- [attr.aria-expanded]="sidebarOpen()"
622
- aria-controls="main-sidebar"
623
- aria-label="Abrir menú"
624
- (click)="sidebarOpen.set(true)"
625
- >
626
- <svg
627
- aria-hidden="true"
628
- class="size-5"
629
- fill="none"
630
- stroke="currentColor"
631
- stroke-width="2"
632
- viewBox="0 0 24 24"
633
- >
634
- <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
635
- </svg>
636
- </button>
637
- @if (sidebarOpen()) {
638
- <div
639
- class="fixed inset-0 z-30 bg-black/30 lg:hidden"
640
- aria-hidden="true"
641
- (click)="sidebarOpen.set(false)"
642
- ></div>
643
- }
644
- <app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
645
- <!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
646
- cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
647
- base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
648
- <main
649
- class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
650
- >
651
- <router-outlet />
652
- </main>
653
- </div>
654
- `,
655
- isInline: true,
656
- dependencies: [
657
- {
658
- kind: "directive",
659
- type: RouterOutlet,
660
- selector: "router-outlet",
661
- inputs: ["name", "routerOutletData"],
662
- outputs: [
663
- "activate",
664
- "deactivate",
665
- "attach",
666
- "detach"
667
- ],
668
- exportAs: ["outlet"]
669
- },
670
- {
671
- kind: "component",
672
- type: Sidebar,
673
- selector: "app-sidebar",
674
- inputs: ["open"],
675
- outputs: ["openChange"]
676
- },
677
- {
678
- kind: "component",
679
- type: Button,
680
- selector: "button[app-button]",
681
- inputs: [
682
- "variant",
683
- "size",
684
- "type",
685
- "disabled",
686
- "loading"
687
- ]
688
- }
689
- ]
690
- });
691
- };
692
- i0.ɵɵngDeclareClassMetadata({
693
- minVersion: "12.0.0",
694
- version: "22.2.3",
695
- ngImport: i0,
696
- type: Layout,
697
- decorators: [{
698
- type: Component,
699
- args: [{
700
- selector: "app-layout",
701
- imports: [
702
- RouterOutlet,
703
- Sidebar,
704
- Button
705
- ],
706
- template: `
707
- <div class="flex min-h-screen bg-neutral/5">
708
- <button
709
- app-button
710
- type="button"
711
- variant="outline"
712
- size="icon"
713
- class="fixed top-[calc(env(safe-area-inset-top)+0.75rem)] left-3 z-40 shadow-md lg:hidden"
714
- [attr.aria-expanded]="sidebarOpen()"
715
- aria-controls="main-sidebar"
716
- aria-label="Abrir menú"
717
- (click)="sidebarOpen.set(true)"
718
- >
719
- <svg
720
- aria-hidden="true"
721
- class="size-5"
722
- fill="none"
723
- stroke="currentColor"
724
- stroke-width="2"
725
- viewBox="0 0 24 24"
726
- >
727
- <path stroke-linecap="round" d="M4 6h16M4 12h16M4 18h16" />
728
- </svg>
729
- </button>
730
- @if (sidebarOpen()) {
731
- <div
732
- class="fixed inset-0 z-30 bg-black/30 lg:hidden"
733
- aria-hidden="true"
734
- (click)="sidebarOpen.set(false)"
735
- ></div>
736
- }
737
- <app-sidebar id="main-sidebar" [(open)]="sidebarOpen" />
738
- <!-- pt reafirmado en sm: sin esto la variante sm:p-6 (va después en la
739
- cascada) pisa el pt-16 base y el contenido choca con la hamburguesa.
740
- base/sm dejan pasar el safe-area del notch; en lg no hay hamburguesa. -->
741
- <main
742
- class="min-w-0 flex-1 p-4 pt-[calc(4rem+env(safe-area-inset-top))] sm:p-6 sm:pt-[calc(4rem+env(safe-area-inset-top))] lg:p-8"
743
- >
744
- <router-outlet />
745
- </main>
746
- </div>
747
- `
748
- }]
749
- }]
750
- });
751
- function pathRank(path) {
752
- const segments = path.split("/");
753
- return [segments.length, segments.filter((segment) => !segment.startsWith(":")).length];
754
- }
755
- function compareRoutes(a, b) {
756
- const aRank = pathRank(a.path ?? "");
757
- const bRank = pathRank(b.path ?? "");
758
- return bRank[0] - aRank[0] || bRank[1] - aRank[1];
759
- }
760
- function resourceRoutes(catalog = allSchemas()) {
761
- return catalog.flatMap((schema) => {
762
- if (!schema.uiViews || Object.keys(schema.uiViews).length === 0) throw new Error(`El recurso '${schema.id}' no declara uiViews`);
763
- validateComposition(schema);
764
- return Object.entries(schema.uiViews).map(([name, view]) => {
765
- const descriptor = get("view", view.type);
766
- if (!descriptor) throw new Error(`No existe descriptor kind:view type '${view.type}' para ${schema.id}.${name}`);
767
- const relativePath = view.path ?? "";
768
- if (relativePath.startsWith("/") || relativePath.endsWith("/") || relativePath !== "" && relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) throw new Error(`Ruta uiViews inválida en ${schema.id}.${name}: '${relativePath}'`);
769
- const route = {
770
- path: [schema.id, relativePath].filter(Boolean).join("/"),
771
- loadComponent: descriptor.load,
772
- canActivate: [roleGuard],
773
- data: {
774
- resourceId: schema.id,
775
- uiView: name,
776
- viewType: view.type
777
- },
778
- resolve: { viewContext: () => {
779
- const base = {
780
- resourceId: schema.id,
781
- uiView: name,
782
- viewType: view.type,
783
- ...view.select !== void 0 ? { select: view.select } : {}
784
- };
785
- const select = view.select;
786
- if (select?.mode !== "route") return createViewContext(base);
787
- const nav = inject(ViewNavigationService);
788
- return createViewContext(base, (event) => {
789
- nav.dispatch(select, event);
790
- });
791
- } }
792
- };
793
- if (view.type === "form") route.canDeactivate = [unsavedChangesGuard];
794
- return route;
795
- });
796
- }).sort(compareRoutes);
797
- }
798
- const shellRoutes = [{
799
- path: "",
800
- loadChildren: () => [{
801
- path: "",
802
- component: Layout,
803
- children: resourceRoutes()
804
- }]
805
- }];
806
- const LABELS = {
807
- email: "Correo electrónico",
808
- password: "Contraseña"
809
- };
810
- const FIELD_BASE = "mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary";
811
- var Login = class Login {
812
- auth = inject(AuthService);
813
- router = inject(Router);
814
- schema = getAuth();
815
- providers = this.schema.providers;
816
- error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
817
- loading = signal(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
818
- providerLoading = signal(null, ...ngDevMode ? [{ debugName: "providerLoading" }] : /* istanbul ignore next */ []);
819
- values = signal({}, ...ngDevMode ? [{ debugName: "values" }] : /* istanbul ignore next */ []);
820
- label(key) {
821
- return LABELS[key] ?? key;
822
- }
823
- value(key) {
824
- return this.values()[key] ?? "";
825
- }
826
- setValue(key, next) {
827
- this.values.update((current) => ({
828
- ...current,
829
- [key]: next
830
- }));
831
- }
832
- loginWithGoogle() {
833
- if (this.providerLoading()) return;
834
- this.providerLoading.set("google");
835
- this.error.set(null);
836
- this.auth.loginWithGoogle().subscribe({
837
- next: () => {
838
- this.providerLoading.set(null);
839
- this.goAfterLogin();
840
- },
841
- error: (err) => {
842
- this.providerLoading.set(null);
843
- this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión con Google.");
844
- }
845
- });
846
- }
847
- submit(event) {
848
- event.preventDefault();
849
- if (this.loading()) return;
850
- const email = this.value("email").trim();
851
- const password = this.value("password");
852
- if (email === "" || password === "") {
853
- this.error.set("Completa el correo y la contraseña.");
854
- return;
855
- }
856
- if (this.schema.firebase && this.schema.providers.includes("password")) {
857
- this.loading.set(true);
858
- this.error.set(null);
859
- this.auth.loginWithFirebasePassword(email, password).subscribe({
860
- next: () => {
861
- this.loading.set(false);
862
- this.goAfterLogin();
863
- },
864
- error: (err) => {
865
- this.loading.set(false);
866
- this.error.set(err instanceof ApiError && err.message !== "" ? err.message : err instanceof Error && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
867
- }
868
- });
869
- return;
870
- }
871
- this.loading.set(true);
872
- this.error.set(null);
873
- this.auth.login(email, password).subscribe({
874
- next: () => {
875
- this.loading.set(false);
876
- this.goAfterLogin();
877
- },
878
- error: (err) => {
879
- this.loading.set(false);
880
- this.error.set(err instanceof ApiError && err.message !== "" ? err.message : "No se pudo iniciar sesión.");
881
- }
882
- });
883
- }
884
- goAfterLogin() {
885
- const previous = this.auth.consumeReturnUrl();
886
- const target = previous !== null && previous !== "" && !previous.startsWith("/login") ? previous : this.schema.redirectAfterLogin;
887
- this.router.navigateByUrl(target).catch(() => void 0);
888
- }
889
- static ɵfac = i0.ɵɵngDeclareFactory({
890
- minVersion: "12.0.0",
891
- version: "22.2.3",
892
- ngImport: i0,
893
- type: Login,
894
- deps: [],
895
- target: i0.ɵɵFactoryTarget.Component
896
- });
897
- static ɵcmp = i0.ɵɵngDeclareComponent({
898
- minVersion: "17.0.0",
899
- version: "22.2.3",
900
- type: Login,
901
- isStandalone: true,
902
- selector: "app-login",
903
- ngImport: i0,
904
- template: "\n <main class=\"flex min-h-[100svh] items-center justify-center bg-surface px-4\">\n <div\n class=\"w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm\"\n >\n <h1 class=\"font-display text-lg font-semibold\">Admin</h1>\n <p class=\"mt-1 text-sm text-neutral\">Inicia sesión para continuar</p>\n\n @if (providers.includes('google')) {\n <button\n type=\"button\"\n id=\"btn-login-google\"\n app-button\n variant=\"outline\"\n [loading]=\"providerLoading() === 'google'\"\n (click)=\"loginWithGoogle()\"\n class=\"mt-6 w-full\"\n >\n Continuar con Google\n </button>\n <div class=\"my-4 flex items-center gap-3\">\n <hr class=\"flex-1 border-neutral/20\" />\n <span class=\"text-xs text-neutral/50\">o</span>\n <hr class=\"flex-1 border-neutral/20\" />\n </div>\n }\n\n <form\n [class]=\"providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'\"\n (submit)=\"submit($event)\"\n novalidate\n >\n @for (field of schema.loginFields; track field.key) {\n <div>\n <label [attr.for]=\"field.key\" class=\"block text-sm font-medium text-neutral\">\n {{ label(field.key) }}\n </label>\n\n @switch (field.type) {\n @case ('email') {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"email\"\n autocomplete=\"email\"\n inputmode=\"email\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n @default {\n <input\n [attr.id]=\"field.key\"\n [attr.name]=\"field.key\"\n type=\"password\"\n autocomplete=\"current-password\"\n [attr.required]=\"field.required ? '' : null\"\n [value]=\"value(field.key)\"\n (input)=\"setValue(field.key, $any($event.target).value)\"\n class=\"mt-1 w-full rounded-lg border border-neutral/40 bg-surface px-3 py-2 text-sm text-neutral placeholder:text-neutral/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary\"\n />\n }\n }\n </div>\n }\n\n @if (error(); as message) {\n <p role=\"alert\" class=\"rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger\">\n {{ message }}\n </p>\n }\n\n <button app-button type=\"submit\" [loading]=\"loading()\" class=\"w-full\">\n Iniciar sesión\n </button>\n </form>\n </div>\n </main>\n ",
905
- isInline: true,
906
- dependencies: [{
907
- kind: "component",
908
- type: Button,
909
- selector: "button[app-button]",
910
- inputs: [
911
- "variant",
912
- "size",
913
- "type",
914
- "disabled",
915
- "loading"
916
- ]
917
- }]
918
- });
919
- };
920
- i0.ɵɵngDeclareClassMetadata({
921
- minVersion: "12.0.0",
922
- version: "22.2.3",
923
- ngImport: i0,
924
- type: Login,
925
- decorators: [{
926
- type: Component,
927
- args: [{
928
- selector: "app-login",
929
- imports: [Button],
930
- template: `
931
- <main class="flex min-h-[100svh] items-center justify-center bg-surface px-4">
932
- <div
933
- class="w-full max-w-sm rounded-xl border border-neutral/20 bg-surface-raised p-8 shadow-sm"
934
- >
935
- <h1 class="font-display text-lg font-semibold">Admin</h1>
936
- <p class="mt-1 text-sm text-neutral">Inicia sesión para continuar</p>
937
-
938
- @if (providers.includes('google')) {
939
- <button
940
- type="button"
941
- id="btn-login-google"
942
- app-button
943
- variant="outline"
944
- [loading]="providerLoading() === 'google'"
945
- (click)="loginWithGoogle()"
946
- class="mt-6 w-full"
947
- >
948
- Continuar con Google
949
- </button>
950
- <div class="my-4 flex items-center gap-3">
951
- <hr class="flex-1 border-neutral/20" />
952
- <span class="text-xs text-neutral/50">o</span>
953
- <hr class="flex-1 border-neutral/20" />
954
- </div>
955
- }
956
-
957
- <form
958
- [class]="providers.includes('google') ? 'space-y-4' : 'mt-6 space-y-4'"
959
- (submit)="submit($event)"
960
- novalidate
961
- >
962
- @for (field of schema.loginFields; track field.key) {
963
- <div>
964
- <label [attr.for]="field.key" class="block text-sm font-medium text-neutral">
965
- {{ label(field.key) }}
966
- </label>
967
-
968
- @switch (field.type) {
969
- @case ('email') {
970
- <input
971
- [attr.id]="field.key"
972
- [attr.name]="field.key"
973
- type="email"
974
- autocomplete="email"
975
- inputmode="email"
976
- [attr.required]="field.required ? '' : null"
977
- [value]="value(field.key)"
978
- (input)="setValue(field.key, $any($event.target).value)"
979
- class="${FIELD_BASE}"
980
- />
981
- }
982
- @default {
983
- <input
984
- [attr.id]="field.key"
985
- [attr.name]="field.key"
986
- type="password"
987
- autocomplete="current-password"
988
- [attr.required]="field.required ? '' : null"
989
- [value]="value(field.key)"
990
- (input)="setValue(field.key, $any($event.target).value)"
991
- class="${FIELD_BASE}"
992
- />
993
- }
994
- }
995
- </div>
996
- }
997
-
998
- @if (error(); as message) {
999
- <p role="alert" class="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">
1000
- {{ message }}
1001
- </p>
1002
- }
1003
-
1004
- <button app-button type="submit" [loading]="loading()" class="w-full">
1005
- Iniciar sesión
1006
- </button>
1007
- </form>
1008
- </div>
1009
- </main>
1010
- `
1011
- }]
1012
- }]
1013
- });
1014
- const MOBILE_SPANS = {
1015
- 1: "col-span-1",
1016
- 2: "col-span-2",
1017
- 3: "col-span-3",
1018
- 4: "col-span-4",
1019
- 5: "col-span-5",
1020
- 6: "col-span-6",
1021
- 7: "col-span-7",
1022
- 8: "col-span-8",
1023
- 9: "col-span-9",
1024
- 10: "col-span-10",
1025
- 11: "col-span-11",
1026
- 12: "col-span-12"
1027
- };
1028
- const RESPONSIVE_SPANS = {
1029
- 12: "",
1030
- 11: "lg:col-span-11",
1031
- 10: "lg:col-span-10",
1032
- 9: "lg:col-span-9",
1033
- 8: "lg:col-span-8",
1034
- 7: "sm:col-span-6 lg:col-span-7",
1035
- 6: "sm:col-span-6",
1036
- 5: "sm:col-span-6 lg:col-span-5",
1037
- 4: "sm:col-span-6 lg:col-span-4",
1038
- 3: "sm:col-span-6 lg:col-span-3",
1039
- 2: "sm:col-span-6 lg:col-span-2",
1040
- 1: "sm:col-span-6 lg:col-span-1"
1041
- };
1042
- var WidgetHost = class WidgetHost {
1043
- widget = input.required(...ngDevMode ? [{ debugName: "widget" }] : /* istanbul ignore next */ []);
1044
- data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
1045
- loading = input(false, ...ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []);
1046
- gridSpanClass = computed(() => {
1047
- const w = this.widget().width ?? 4;
1048
- const mw = this.widget().mobileWidth ?? 12;
1049
- return [
1050
- MOBILE_SPANS[mw] ?? "col-span-12",
1051
- RESPONSIVE_SPANS[w] ?? "sm:col-span-6 lg:col-span-4",
1052
- "block"
1053
- ].filter(Boolean).join(" ");
1054
- }, ...ngDevMode ? [{ debugName: "gridSpanClass" }] : /* istanbul ignore next */ []);
1055
- static ɵfac = i0.ɵɵngDeclareFactory({
1056
- minVersion: "12.0.0",
1057
- version: "22.2.3",
1058
- ngImport: i0,
1059
- type: WidgetHost,
1060
- deps: [],
1061
- target: i0.ɵɵFactoryTarget.Component
1062
- });
1063
- static ɵcmp = i0.ɵɵngDeclareComponent({
1064
- minVersion: "17.0.0",
1065
- version: "22.2.3",
1066
- type: WidgetHost,
1067
- isStandalone: true,
1068
- selector: "app-widget-host",
1069
- inputs: {
1070
- widget: {
1071
- classPropertyName: "widget",
1072
- publicName: "widget",
1073
- isSignal: true,
1074
- isRequired: true,
1075
- transformFunction: null
1076
- },
1077
- data: {
1078
- classPropertyName: "data",
1079
- publicName: "data",
1080
- isSignal: true,
1081
- isRequired: false,
1082
- transformFunction: null
1083
- },
1084
- loading: {
1085
- classPropertyName: "loading",
1086
- publicName: "loading",
1087
- isSignal: true,
1088
- isRequired: false,
1089
- transformFunction: null
1090
- }
1091
- },
1092
- host: { properties: { "class": "gridSpanClass()" } },
1093
- ngImport: i0,
1094
- template: `
1095
- @if (loading()) {
1096
- <!-- Reserva el espacio del widget mientras su dato viaja (la red
1097
- Apps Script tarda ~1s): sin esto el panel se veía "completo"
1098
- con valores vacíos que después aparecían solos (cargar ≠ vacío,
1099
- doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
1100
- span porque la clase la pone el host. -->
1101
- <div class="rounded-lg border border-neutral/20 bg-surface p-4">
1102
- <app-skeleton [lines]="1" [height]="14" width="40%" />
1103
- <div class="mt-3">
1104
- <app-skeleton [lines]="2" [height]="32" />
1105
- </div>
1106
- </div>
1107
- } @else {
1108
- <app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
1109
- }
1110
- `,
1111
- isInline: true,
1112
- dependencies: [{
1113
- kind: "component",
1114
- type: RenderHost,
1115
- selector: "app-render-host",
1116
- inputs: [
1117
- "kind",
1118
- "type",
1119
- "config",
1120
- "ctx",
1121
- "data",
1122
- "mode",
1123
- "value",
1124
- "chip"
1125
- ]
1126
- }, {
1127
- kind: "component",
1128
- type: Skeleton,
1129
- selector: "app-skeleton",
1130
- inputs: [
1131
- "lines",
1132
- "height",
1133
- "width"
1134
- ]
1135
- }]
1136
- });
1137
- };
1138
- i0.ɵɵngDeclareClassMetadata({
1139
- minVersion: "12.0.0",
1140
- version: "22.2.3",
1141
- ngImport: i0,
1142
- type: WidgetHost,
1143
- decorators: [{
1144
- type: Component,
1145
- args: [{
1146
- selector: "app-widget-host",
1147
- imports: [RenderHost, Skeleton],
1148
- host: { "[class]": "gridSpanClass()" },
1149
- template: `
1150
- @if (loading()) {
1151
- <!-- Reserva el espacio del widget mientras su dato viaja (la red
1152
- Apps Script tarda ~1s): sin esto el panel se veía "completo"
1153
- con valores vacíos que después aparecían solos (cargar ≠ vacío,
1154
- doc/ux-ui-mobile.md §estados de UI). La grilla mantiene su
1155
- span porque la clase la pone el host. -->
1156
- <div class="rounded-lg border border-neutral/20 bg-surface p-4">
1157
- <app-skeleton [lines]="1" [height]="14" width="40%" />
1158
- <div class="mt-3">
1159
- <app-skeleton [lines]="2" [height]="32" />
1160
- </div>
1161
- </div>
1162
- } @else {
1163
- <app-render-host kind="widget" [type]="widget().type" [config]="widget()" [data]="data()" />
1164
- }
1165
- `
1166
- }]
1167
- }],
1168
- propDecorators: {
1169
- widget: [{
1170
- type: i0.Input,
1171
- args: [{
1172
- isSignal: true,
1173
- alias: "widget",
1174
- required: true
1175
- }]
1176
- }],
1177
- data: [{
1178
- type: i0.Input,
1179
- args: [{
1180
- isSignal: true,
1181
- alias: "data",
1182
- required: false
1183
- }]
1184
- }],
1185
- loading: [{
1186
- type: i0.Input,
1187
- args: [{
1188
- isSignal: true,
1189
- alias: "loading",
1190
- required: false
1191
- }]
1192
- }]
1193
- }
1194
- });
1195
- var Dashboard = class Dashboard {
1196
- api = inject(ApiService, { optional: true });
1197
- ds = inject(DataSourceService);
1198
- schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
1199
- data = input(null, ...ngDevMode ? [{ debugName: "data" }] : /* istanbul ignore next */ []);
1200
- fetchedData = toSignal(toObservable(this.schema).pipe(switchMap((schema) => {
1201
- const getEndpoint = schema.endpoint?.get;
1202
- if (!getEndpoint || !this.api) return of({});
1203
- return this.api.request("GET", getEndpoint).pipe(catchError(() => of({})));
1204
- })), { initialValue: null });
1205
- effectiveData = computed(() => {
1206
- const override = this.data();
1207
- if (override !== null && override !== void 0) return override;
1208
- return this.fetchedData() ?? {};
1209
- }, ...ngDevMode ? [{ debugName: "effectiveData" }] : /* istanbul ignore next */ []);
1210
- widgets = computed(() => this.schema().widgets ?? [], ...ngDevMode ? [{ debugName: "widgets" }] : /* istanbul ignore next */ []);
1211
- clavesFuente = computed(() => this.widgets().map((widget) => esFuente(widget.source) ? widget.key : "").filter((key) => key !== "").join("|"), ...ngDevMode ? [{ debugName: "clavesFuente" }] : /* istanbul ignore next */ []);
1212
- fuentes = resource({
1213
- ...ngDevMode ? { debugName: "fuentes" } : /* istanbul ignore next */ {},
1214
- params: () => this.clavesFuente() === "" ? void 0 : this.clavesFuente(),
1215
- loader: async () => {
1216
- const filas = {};
1217
- for (const widget of this.widgets()) {
1218
- if (!esFuente(widget.source)) continue;
1219
- filas[widget.key] = await this.ds.rows(widget.source);
1220
- }
1221
- return filas;
1222
- }
1223
- });
1224
- falloFuentes = computed(() => this.fuentes.status() === "error", ...ngDevMode ? [{ debugName: "falloFuentes" }] : /* istanbul ignore next */ []);
1225
- cargando = computed(() => {
1226
- const payloadViaja = this.data() === null && this.fetchedData() === null;
1227
- const fuentesViajan = this.fuentes.status() === "loading";
1228
- return payloadViaja || fuentesViajan;
1229
- }, ...ngDevMode ? [{ debugName: "cargando" }] : /* istanbul ignore next */ []);
1230
- datum(widget) {
1231
- if (esFuente(widget.source)) return this.fuentes.status() === "error" ? null : this.fuentes.value()?.[widget.key] ?? null;
1232
- const allData = this.effectiveData();
1233
- return widget.source?.kind === "payload" ? allData : allData[widget.key] ?? null;
1234
- }
1235
- static ɵfac = i0.ɵɵngDeclareFactory({
1236
- minVersion: "12.0.0",
1237
- version: "22.2.3",
1238
- ngImport: i0,
1239
- type: Dashboard,
1240
- deps: [],
1241
- target: i0.ɵɵFactoryTarget.Component
1242
- });
1243
- static ɵcmp = i0.ɵɵngDeclareComponent({
1244
- minVersion: "17.0.0",
1245
- version: "22.2.3",
1246
- type: Dashboard,
1247
- isStandalone: true,
1248
- selector: "app-dashboard",
1249
- inputs: {
1250
- schema: {
1251
- classPropertyName: "schema",
1252
- publicName: "schema",
1253
- isSignal: true,
1254
- isRequired: true,
1255
- transformFunction: null
1256
- },
1257
- data: {
1258
- classPropertyName: "data",
1259
- publicName: "data",
1260
- isSignal: true,
1261
- isRequired: false,
1262
- transformFunction: null
1263
- }
1264
- },
1265
- ngImport: i0,
1266
- template: `
1267
- <header class="mb-6">
1268
- <h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
1269
- <p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
1270
- </header>
1271
-
1272
- @if (falloFuentes()) {
1273
- <!-- Degradación visible (F9): sin este aviso el fallo de una fuente
1274
- se confundiría con «el panel no tiene datos». -->
1275
- <p class="mb-4 text-sm text-danger" role="alert">
1276
- No se pudieron cargar los datos de algunos widgets.
1277
- </p>
1278
- }
1279
-
1280
- @if (widgets().length === 0) {
1281
- <app-empty-state
1282
- title="Sin widgets declarados"
1283
- message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
1284
- />
1285
- } @else {
1286
- <div class="grid grid-cols-12 gap-4">
1287
- @for (widget of widgets(); track widget.key) {
1288
- <app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
1289
- }
1290
- </div>
1291
- }
1292
- `,
1293
- isInline: true,
1294
- dependencies: [{
1295
- kind: "component",
1296
- type: EmptyState,
1297
- selector: "app-empty-state",
1298
- inputs: ["title", "message"]
1299
- }, {
1300
- kind: "component",
1301
- type: WidgetHost,
1302
- selector: "app-widget-host",
1303
- inputs: [
1304
- "widget",
1305
- "data",
1306
- "loading"
1307
- ]
1308
- }]
1309
- });
1310
- };
1311
- i0.ɵɵngDeclareClassMetadata({
1312
- minVersion: "12.0.0",
1313
- version: "22.2.3",
1314
- ngImport: i0,
1315
- type: Dashboard,
1316
- decorators: [{
1317
- type: Component,
1318
- args: [{
1319
- selector: "app-dashboard",
1320
- imports: [EmptyState, WidgetHost],
1321
- template: `
1322
- <header class="mb-6">
1323
- <h1 class="font-display text-2xl font-semibold">{{ schema().labelPlural }}</h1>
1324
- <p class="text-sm text-neutral">Dashboard · {{ widgets().length }} widgets</p>
1325
- </header>
1326
-
1327
- @if (falloFuentes()) {
1328
- <!-- Degradación visible (F9): sin este aviso el fallo de una fuente
1329
- se confundiría con «el panel no tiene datos». -->
1330
- <p class="mb-4 text-sm text-danger" role="alert">
1331
- No se pudieron cargar los datos de algunos widgets.
1332
- </p>
1333
- }
1334
-
1335
- @if (widgets().length === 0) {
1336
- <app-empty-state
1337
- title="Sin widgets declarados"
1338
- message="Los widgets se declaran en la propiedad widgets del schema del dashboard."
1339
- />
1340
- } @else {
1341
- <div class="grid grid-cols-12 gap-4">
1342
- @for (widget of widgets(); track widget.key) {
1343
- <app-widget-host [widget]="widget" [data]="datum(widget)" [loading]="cargando()" />
1344
- }
1345
- </div>
1346
- }
1347
- `
1348
- }]
1349
- }],
1350
- propDecorators: {
1351
- schema: [{
1352
- type: i0.Input,
1353
- args: [{
1354
- isSignal: true,
1355
- alias: "schema",
1356
- required: true
1357
- }]
1358
- }],
1359
- data: [{
1360
- type: i0.Input,
1361
- args: [{
1362
- isSignal: true,
1363
- alias: "data",
1364
- required: false
1365
- }]
1366
- }]
1367
- }
1368
- });
1369
- function esFuente(source) {
1370
- return source !== void 0 && source !== null && "resource" in source;
1371
- }
1372
- var ActionService = class ActionService {
1373
- config = inject(ConfigService);
1374
- errores = inject(ErrorMapperService);
1375
- async run(action, request) {
1376
- const type = actionType(action);
1377
- const descriptor = getAction(type);
1378
- if (!descriptor) throw new Error(`[actions] «${action.label}» no tiene descriptor registrado (type "${type}").`);
1379
- if (descriptor.confirm) {
1380
- if (!await request.toast.confirm({
1381
- title: action.label,
1382
- message: confirmMessage(action),
1383
- confirmLabel: "Confirmar",
1384
- cancelLabel: "Cancelar"
1385
- })) return { ok: false };
1386
- }
1387
- if (descriptor.type === "copy-share") await this.config.ensureLoaded();
1388
- const ctx = {
1389
- ...request,
1390
- vars: this.vars(request.record)
1391
- };
1392
- try {
1393
- return await descriptor.run(ctx, action);
1394
- } catch (err) {
1395
- request.toast.error(this.errores.messageOf(err));
1396
- return { ok: false };
1397
- }
1398
- }
1399
- vars(record) {
1400
- return record ? this.config.buildInterpolationContext(record) : {};
1401
- }
1402
- static ɵfac = i0.ɵɵngDeclareFactory({
1403
- minVersion: "12.0.0",
1404
- version: "22.2.3",
1405
- ngImport: i0,
1406
- type: ActionService,
1407
- deps: [],
1408
- target: i0.ɵɵFactoryTarget.Service
1409
- });
1410
- static ɵprov = i0.ɵɵngDeclareService({
1411
- minVersion: "22.0.0",
1412
- version: "22.2.3",
1413
- ngImport: i0,
1414
- type: ActionService
1415
- });
1416
- };
1417
- i0.ɵɵngDeclareClassMetadata({
1418
- minVersion: "12.0.0",
1419
- version: "22.2.3",
1420
- ngImport: i0,
1421
- type: ActionService,
1422
- decorators: [{ type: Service }]
1423
- });
1424
- function confirmMessage(action) {
1425
- return "confirmMessage" in action && action.confirmMessage ? action.confirmMessage : action.label;
1426
- }
1427
- const FORMATO_POR_DEFECTO = {
1428
- text: "text",
1429
- textarea: "text",
1430
- slug: "text",
1431
- url: "text",
1432
- email: "text",
1433
- phone: "text",
1434
- "readonly-text": "text",
1435
- relation: "text",
1436
- group: "text",
1437
- "key-value": "text",
1438
- "string-list": "text",
1439
- multiselect: "text",
1440
- time: "text",
1441
- list: "text",
1442
- date: "date",
1443
- number: "number",
1444
- currency: "currency",
1445
- boolean: "boolean",
1446
- select: "badge",
1447
- image: "thumbnail",
1448
- file: "text",
1449
- color: "text",
1450
- "rich-select": "badge",
1451
- dropdown: "badge"
1452
- };
1453
- function formatoPorTipo(type) {
1454
- return Object.prototype.hasOwnProperty.call(FORMATO_POR_DEFECTO, type) ? FORMATO_POR_DEFECTO[type] : void 0;
1455
- }
1456
- function formatoDeColumna(column, fields) {
1457
- if (column.format) return column.format;
1458
- const field = fields.find((candidate) => candidate.key === column.key);
1459
- return (field ? formatoPorTipo(field.type) : void 0) ?? "text";
1460
- }
1461
- const BOOLEAN_FILTER_OPTIONS = [
1462
- {
1463
- value: "",
1464
- label: "Todos"
1465
- },
1466
- {
1467
- value: "yes",
1468
- label: "Sí"
1469
- },
1470
- {
1471
- value: "no",
1472
- label: "No"
1473
- }
1474
- ];
1475
- function errorMessage(err, fallback) {
1476
- return err instanceof ApiError ? err.message : fallback;
1477
- }
1478
- var ListView = class ListView {
1479
- schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
1480
- viewRecord = output();
1481
- api = inject(ApiService);
1482
- auth = inject(AuthService);
1483
- router = inject(Router);
1484
- notifications = inject(NotificationService);
1485
- actionService = inject(ActionService);
1486
- status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
1487
- rows = signal([], ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
1488
- query = signal("", ...ngDevMode ? [{ debugName: "query" }] : /* istanbul ignore next */ []);
1489
- filterValues = signal({}, ...ngDevMode ? [{ debugName: "filterValues" }] : /* istanbul ignore next */ []);
1490
- savingKey = signal(null, ...ngDevMode ? [{ debugName: "savingKey" }] : /* istanbul ignore next */ []);
1491
- quickEditRow = signal(null, ...ngDevMode ? [{ debugName: "quickEditRow" }] : /* istanbul ignore next */ []);
1492
- loadEpoch = 0;
1493
- constructor() {
1494
- effect(() => {
1495
- const schema = this.schema();
1496
- this.query.set("");
1497
- this.filterValues.set({});
1498
- this.load(schema);
1499
- });
1500
- }
1501
- visibleRows = computed(() => {
1502
- const schema = this.schema();
1503
- let rows = this.rows();
1504
- const query = this.query().trim().toLowerCase();
1505
- if (query !== "" && schema.search?.length) rows = rows.filter((row) => schema.search.some((key) => String(getValue(row, key) ?? "").toLowerCase().includes(query)));
1506
- for (const filter of schema.filters ?? []) {
1507
- const value = this.filterValues()[filter.key] ?? "";
1508
- if (value === "") continue;
1509
- rows = rows.filter((row) => {
1510
- const cell = getValue(row, filter.key);
1511
- if (filter.type === "boolean") return (cell === true || cell === "true") === (value === "yes");
1512
- if (filter.type === "select") return String(cell ?? "") === value;
1513
- return String(cell ?? "").toLowerCase().includes(value.toLowerCase());
1514
- });
1515
- }
1516
- const positionField = schema.positionField;
1517
- if (positionField) {
1518
- const positionOf = (row) => {
1519
- const position = Number(getValue(row, positionField));
1520
- return Number.isFinite(position) ? position : Number.POSITIVE_INFINITY;
1521
- };
1522
- rows = [...rows].sort((a, b) => positionOf(a) - positionOf(b));
1523
- }
1524
- return rows;
1525
- }, ...ngDevMode ? [{ debugName: "visibleRows" }] : /* istanbul ignore next */ []);
1526
- currencyCode = computed(() => {
1527
- const field = getSchema("site")?.fields.find((f) => f.key === "currency");
1528
- return field?.type === "text" && field.default ? field.default : "USD";
1529
- }, ...ngDevMode ? [{ debugName: "currencyCode" }] : /* istanbul ignore next */ []);
1530
- permissions = computed(() => this.schema().permissions, ...ngDevMode ? [{ debugName: "permissions" }] : /* istanbul ignore next */ []);
1531
- canCreate = computed(() => this.auth.canDoOperation(this.schema(), "create"), ...ngDevMode ? [{ debugName: "canCreate" }] : /* istanbul ignore next */ []);
1532
- canUpdate = computed(() => this.auth.canDoOperation(this.schema(), "update"), ...ngDevMode ? [{ debugName: "canUpdate" }] : /* istanbul ignore next */ []);
1533
- canRemove = computed(() => this.auth.canDoOperation(this.schema(), "remove"), ...ngDevMode ? [{ debugName: "canRemove" }] : /* istanbul ignore next */ []);
1534
- canQuickEdit = computed(() => this.canUpdate() && (this.schema().quickEdit?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "canQuickEdit" }] : /* istanbul ignore next */ []);
1535
- quickEditFields = computed(() => {
1536
- const keys = this.schema().quickEdit ?? [];
1537
- const byKey = new Map(this.schema().fields.map((field) => [field.key, field]));
1538
- return keys.map((key) => byKey.get(key)).filter((field) => field !== void 0);
1539
- }, ...ngDevMode ? [{ debugName: "quickEditFields" }] : /* istanbul ignore next */ []);
1540
- hasRowActions = computed(() => this.canUpdate() || this.canRemove() || (this.schema().actions?.length ?? 0) > 0, ...ngDevMode ? [{ debugName: "hasRowActions" }] : /* istanbul ignore next */ []);
1541
- reordering = signal(false, ...ngDevMode ? [{ debugName: "reordering" }] : /* istanbul ignore next */ []);
1542
- hasActiveFilters = computed(() => {
1543
- if (this.query().trim() !== "") return true;
1544
- return Object.values(this.filterValues()).some((value) => value !== "");
1545
- }, ...ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []);
1546
- canReorder = computed(() => {
1547
- const schema = this.schema();
1548
- return schema.sortable === true && schema.positionField !== void 0 && schema.permissions?.reorder !== false && !this.hasActiveFilters();
1549
- }, ...ngDevMode ? [{ debugName: "canReorder" }] : /* istanbul ignore next */ []);
1550
- dropDisabled = computed(() => !this.canReorder() || this.reordering(), ...ngDevMode ? [{ debugName: "dropDisabled" }] : /* istanbul ignore next */ []);
1551
- setQuery(value) {
1552
- this.query.set(value);
1553
- }
1554
- setFilter(key, value) {
1555
- this.filterValues.update((values) => ({
1556
- ...values,
1557
- [key]: value
1558
- }));
1559
- }
1560
- clearFilters() {
1561
- this.query.set("");
1562
- this.filterValues.set({});
1563
- }
1564
- retry() {
1565
- this.load(this.schema());
1566
- }
1567
- isRowClickable() {
1568
- return Boolean(this.schema().detail?.enabled || this.canUpdate());
1569
- }
1570
- goView(row) {
1571
- this.viewRecord.emit(row);
1572
- }
1573
- onRowClick(row) {
1574
- if (this.schema().detail?.enabled) this.goView(row);
1575
- else if (this.canUpdate()) this.goEdit(row);
1576
- }
1577
- goCreate() {
1578
- this.router.navigate([
1579
- "/",
1580
- this.schema().id,
1581
- "new"
1582
- ]);
1583
- }
1584
- goEdit(row) {
1585
- if (this.schema().detail?.enabled === true) {
1586
- this.viewRecord.emit(row);
1587
- return;
1588
- }
1589
- this.router.navigate([
1590
- "/",
1591
- this.schema().id,
1592
- this.keyOf(row),
1593
- "edit"
1594
- ]);
1595
- }
1596
- quickEditSaved() {
1597
- this.quickEditRow.set(null);
1598
- this.load(this.schema());
1599
- }
1600
- async toggleActive(row, checked) {
1601
- const schema = this.schema();
1602
- const key = this.keyOf(row);
1603
- this.savingKey.set(key);
1604
- try {
1605
- await firstValueFrom(this.api.update(schema.endpoint, key, {
1606
- ...row,
1607
- active: checked
1608
- }));
1609
- this.rows.update((rows) => rows.map((candidate) => this.keyOf(candidate) === key ? {
1610
- ...candidate,
1611
- active: checked
1612
- } : candidate));
1613
- } catch (err) {
1614
- this.notifications.error(errorMessage(err, "No se pudo actualizar el registro."));
1615
- } finally {
1616
- this.savingKey.set(null);
1617
- }
1618
- }
1619
- onDrop(event) {
1620
- if (event.previousContainer !== event.container || event.previousIndex === event.currentIndex) return;
1621
- const row = this.visibleRows()[event.previousIndex];
1622
- if (row !== void 0) this.moveRow(this.keyOf(row), event.currentIndex);
1623
- }
1624
- moveBy(key, delta) {
1625
- const index = this.visibleRows().findIndex((row) => this.keyOf(row) === key);
1626
- if (index < 0) return;
1627
- this.moveRow(key, index + delta);
1628
- }
1629
- async moveRow(key, targetIndex) {
1630
- const schema = this.schema();
1631
- const path = schema.endpoint.list;
1632
- if (!path || !schema.positionField || this.reordering()) return;
1633
- const rows = this.visibleRows();
1634
- const from = rows.findIndex((row) => this.keyOf(row) === key);
1635
- if (from < 0) return;
1636
- const without = rows.filter((row) => this.keyOf(row) !== key);
1637
- const target = Math.min(Math.max(targetIndex, 0), without.length);
1638
- if (target === from) return;
1639
- const reorder = target === 0 ? {
1640
- key,
1641
- toStart: true
1642
- } : target >= without.length ? {
1643
- key,
1644
- toEnd: true
1645
- } : {
1646
- key,
1647
- after: this.keyOf(without[target - 1])
1648
- };
1649
- this.reordering.set(true);
1650
- try {
1651
- const fresh = await firstValueFrom(this.api.request("PUT", path, { reorder }));
1652
- if (Array.isArray(fresh)) this.rows.set(fresh);
1653
- this.notifications.success("Orden actualizado.");
1654
- } catch (err) {
1655
- this.notifications.error(errorMessage(err, "No se pudo guardar el orden."));
1656
- } finally {
1657
- this.reordering.set(false);
1658
- }
1659
- }
1660
- async removeRow(row) {
1661
- const schema = this.schema();
1662
- const key = this.keyOf(row);
1663
- if (!await this.notifications.confirm({
1664
- title: `Eliminar «${this.titleOf(row)}»`,
1665
- message: "Esta acción no se puede deshacer.",
1666
- confirmLabel: "Eliminar",
1667
- cancelLabel: "Cancelar"
1668
- })) return;
1669
- try {
1670
- await firstValueFrom(this.api.remove(schema.endpoint, key));
1671
- this.notifications.success("Registro eliminado.");
1672
- this.rows.update((rows) => rows.filter((candidate) => this.keyOf(candidate) !== key));
1673
- } catch (err) {
1674
- this.notifications.error(errorMessage(err, "No se pudo eliminar el registro."));
1675
- }
1676
- }
1677
- async runAction(action, row) {
1678
- await this.actionService.run(action, {
1679
- record: row,
1680
- resource: this.schema().id,
1681
- api: this.api,
1682
- router: this.router,
1683
- toast: this.notifications,
1684
- user: this.auth.user(),
1685
- keyField: this.schema().keyField
1686
- });
1687
- }
1688
- iconOf(action) {
1689
- return getAction(actionType(action))?.icon;
1690
- }
1691
- keyOf(row) {
1692
- return String(row[this.schema().keyField] ?? "");
1693
- }
1694
- titleOf(row) {
1695
- return String(row[this.schema().titleField] ?? "");
1696
- }
1697
- formatOf(column) {
1698
- return formatoDeColumna(column, this.schema().fields);
1699
- }
1700
- cellOf(row, column) {
1701
- return {
1702
- row,
1703
- column,
1704
- fields: this.schema().fields,
1705
- title: this.titleOf(row),
1706
- canUpdate: this.canUpdate(),
1707
- saving: this.savingKey() === this.keyOf(row),
1708
- currency: this.currencyCode(),
1709
- toggle: (checked) => void this.toggleActive(row, checked)
1710
- };
1711
- }
1712
- filterOptions(filter) {
1713
- if (filter.type === "boolean") return BOOLEAN_FILTER_OPTIONS;
1714
- if (filter.type === "select") return [{
1715
- value: "",
1716
- label: "Todos"
1717
- }, ...filter.options ?? []];
1718
- return [];
1719
- }
1720
- inputValue(event) {
1721
- return event.target.value;
1722
- }
1723
- selectValue(event) {
1724
- return event.target.value;
1725
- }
1726
- async load(schema) {
1727
- const epoch = ++this.loadEpoch;
1728
- this.status.set("loading");
1729
- try {
1730
- const rows = await firstValueFrom(this.api.list(schema.endpoint));
1731
- if (epoch !== this.loadEpoch) return;
1732
- this.rows.set(Array.isArray(rows) ? rows : []);
1733
- this.status.set("ready");
1734
- } catch {
1735
- if (epoch !== this.loadEpoch) return;
1736
- this.status.set("error");
1737
- }
1738
- }
1739
- static ɵfac = i0.ɵɵngDeclareFactory({
1740
- minVersion: "12.0.0",
1741
- version: "22.2.3",
1742
- ngImport: i0,
1743
- type: ListView,
1744
- deps: [],
1745
- target: i0.ɵɵFactoryTarget.Component
1746
- });
1747
- static ɵcmp = i0.ɵɵngDeclareComponent({
1748
- minVersion: "17.0.0",
1749
- version: "22.2.3",
1750
- type: ListView,
1751
- isStandalone: true,
1752
- selector: "app-list-view",
1753
- inputs: { schema: {
1754
- classPropertyName: "schema",
1755
- publicName: "schema",
1756
- isSignal: true,
1757
- isRequired: true,
1758
- transformFunction: null
1759
- } },
1760
- outputs: { viewRecord: "viewRecord" },
1761
- ngImport: i0,
1762
- template: `
1763
- <section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
1764
- <div class="flex flex-wrap items-end justify-between gap-3">
1765
- <div class="flex flex-wrap items-end gap-3">
1766
- @if (schema().search?.length) {
1767
- <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
1768
- Buscar
1769
- <input
1770
- type="search"
1771
- class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1772
- placeholder="Buscar…"
1773
- [value]="query()"
1774
- (input)="setQuery(inputValue($event))"
1775
- />
1776
- </label>
1777
- }
1778
- @for (filter of schema().filters ?? []; track filter.key) {
1779
- <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
1780
- {{ filter.label }}
1781
- @if (filter.type === 'text') {
1782
- <input
1783
- type="search"
1784
- class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1785
- [value]="filterValues()[filter.key] ?? ''"
1786
- (input)="setFilter(filter.key, inputValue($event))"
1787
- />
1788
- } @else {
1789
- <select
1790
- class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
1791
- [value]="filterValues()[filter.key] ?? ''"
1792
- (change)="setFilter(filter.key, selectValue($event))"
1793
- >
1794
- @for (option of filterOptions(filter); track option.value) {
1795
- <option [value]="option.value">{{ option.label }}</option>
1796
- }
1797
- </select>
1798
- }
1799
- </label>
1800
- }
1801
- </div>
1802
- @if (canCreate()) {
1803
- <button app-button type="button" (click)="goCreate()">
1804
- <svg lucidePlus size="16" />
1805
- Crear {{ schema().label }}
1806
- </button>
1807
- }
1808
- </div>
1809
-
1810
- @switch (status()) {
1811
- @case ('loading') {
1812
- <app-skeleton [lines]="6" [height]="44" />
1813
- }
1814
- @case ('error') {
1815
- <app-empty-state
1816
- title="No se pudo cargar el listado"
1817
- message="Revisa tu conexión e intenta de nuevo."
1818
- >
1819
- <button app-button variant="outline" type="button" (click)="retry()">
1820
- <svg lucideRotateCw size="16" />
1821
- Reintentar
1822
- </button>
1823
- </app-empty-state>
1824
- }
1825
- @case ('ready') {
1826
- @if (visibleRows().length === 0) {
1827
- <app-empty-state
1828
- [title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
1829
- [message]="
1830
- rows().length === 0
1831
- ? 'Todavía no hay registros en este recurso.'
1832
- : 'Ningún registro coincide con la búsqueda o los filtros.'
1833
- "
1834
- >
1835
- @if (rows().length > 0) {
1836
- <button app-button variant="outline" type="button" (click)="clearFilters()">
1837
- Limpiar filtros
1838
- </button>
1839
- } @else if (canCreate()) {
1840
- <button app-button type="button" (click)="goCreate()">
1841
- <svg lucidePlus size="16" />
1842
- Crear el primero
1843
- </button>
1844
- }
1845
- </app-empty-state>
1846
- } @else {
1847
- <!-- La tabla es la vista de escritorio: se oculta bajo lg para
1848
- que la lista de tarjetas de abajo sea la ÚNICA vista en
1849
- móvil (con las dos visibles, el listado se duplicaba). -->
1850
- <div
1851
- class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
1852
- >
1853
- <table class="w-full text-sm">
1854
- <thead>
1855
- <tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
1856
- @if (canReorder()) {
1857
- <th scope="col" class="w-16 px-2 py-3 font-medium">
1858
- <span class="sr-only">Orden</span>
1859
- </th>
1860
- }
1861
- @for (column of schema().listColumns; track column.key) {
1862
- <th scope="col" class="px-4 py-3 font-medium">
1863
- {{ column.label }}
1864
- </th>
1865
- }
1866
- @if (hasRowActions()) {
1867
- <th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
1868
- }
1869
- </tr>
1870
- </thead>
1871
- <tbody
1872
- cdkDropList
1873
- [cdkDropListData]="visibleRows()"
1874
- [cdkDropListDisabled]="dropDisabled()"
1875
- (cdkDropListDropped)="onDrop($event)"
1876
- >
1877
- @for (row of visibleRows(); track keyOf(row); let i = $index) {
1878
- <tr
1879
- class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
1880
- [class.cursor-pointer]="isRowClickable()"
1881
- cdkDrag
1882
- [cdkDragDisabled]="dropDisabled()"
1883
- (click)="onRowClick(row)"
1884
- >
1885
- @if (canReorder()) {
1886
- <td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
1887
- <div class="flex items-center gap-1">
1888
- <button
1889
- type="button"
1890
- class="cursor-grab text-neutral hover:text-primary"
1891
- cdkDragHandle
1892
- [attr.aria-label]="'Reordenar ' + titleOf(row)"
1893
- title="Arrastrar para reordenar"
1894
- >
1895
- <svg lucideGripVertical size="16" />
1896
- </button>
1897
- <span class="flex flex-col leading-none">
1898
- <button
1899
- type="button"
1900
- class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
1901
- [disabled]="i === 0 || reordering()"
1902
- [attr.aria-label]="'Subir ' + titleOf(row)"
1903
- (click)="moveBy(keyOf(row), -1)"
1904
- >
1905
- <svg lucideChevronUp size="16" />
1906
- </button>
1907
- <button
1908
- type="button"
1909
- class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
1910
- [disabled]="i === visibleRows().length - 1 || reordering()"
1911
- [attr.aria-label]="'Bajar ' + titleOf(row)"
1912
- (click)="moveBy(keyOf(row), 1)"
1913
- >
1914
- <svg lucideChevronDown size="16" />
1915
- </button>
1916
- </span>
1917
- </div>
1918
- </td>
1919
- }
1920
- @for (column of schema().listColumns; track column.key) {
1921
- <td class="px-4 py-2.5 align-middle">
1922
- <!-- Los 7 formatos salen del registro (R4.3):
1923
- la vista sólo elige cuál y entrega la celda. -->
1924
- <app-render-host
1925
- kind="column-format"
1926
- [type]="formatOf(column)"
1927
- [config]="column"
1928
- [data]="cellOf(row, column)"
1929
- />
1930
- </td>
1931
- }
1932
- @if (hasRowActions()) {
1933
- <td class="px-4 py-2.5" (click)="$event.stopPropagation()">
1934
- <div class="flex items-center justify-end gap-1">
1935
- @if (schema().detail?.enabled) {
1936
- <button
1937
- app-button
1938
- variant="ghost"
1939
- type="button"
1940
- (click)="goView(row)"
1941
- title="Ver ficha de detalle"
1942
- class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
1943
- >
1944
- <svg lucideEye size="15" />
1945
- <span>Ver</span>
1946
- </button>
1947
- }
1948
- @for (action of schema().actions ?? []; track action.id) {
1949
- <button
1950
- app-button
1951
- variant="ghost"
1952
- type="button"
1953
- [title]="action.label"
1954
- [attr.aria-label]="action.label"
1955
- (click)="runAction(action, row)"
1956
- class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
1957
- >
1958
- @let actionIcon = iconOf(action);
1959
- @if (actionIcon) {
1960
- <svg [lucideIcon]="actionIcon" size="15" />
1961
- } @else {
1962
- {{ action.label }}
1963
- }
1964
- </button>
1965
- }
1966
- @if (canQuickEdit()) {
1967
- <button
1968
- app-button
1969
- variant="ghost"
1970
- type="button"
1971
- (click)="quickEditRow.set(row)"
1972
- title="Edición rápida"
1973
- class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
1974
- >
1975
- <svg lucideZap size="15" />
1976
- <span>Edición rápida</span>
1977
- </button>
1978
- }
1979
- @if (canUpdate()) {
1980
- <button
1981
- app-button
1982
- variant="ghost"
1983
- type="button"
1984
- (click)="goEdit(row)"
1985
- title="Editar registro"
1986
- class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
1987
- >
1988
- <svg lucidePencil size="15" />
1989
- <span>Editar</span>
1990
- </button>
1991
- }
1992
- @if (canRemove()) {
1993
- <button
1994
- app-button
1995
- variant="ghost"
1996
- type="button"
1997
- (click)="removeRow(row)"
1998
- title="Eliminar registro"
1999
- class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
2000
- >
2001
- <svg lucideTrash2 size="15" />
2002
- <span>Eliminar</span>
2003
- </button>
2004
- }
2005
- </div>
2006
- </td>
2007
- }
2008
- </tr>
2009
- }
2010
- </tbody>
2011
- </table>
2012
- </div>
2013
-
2014
- <!-- Vista de tarjetas — mobile-first (guía mobile §6).
2015
- La tabla de arriba necesita scroll horizontal con 5–7 columnas
2016
- (el anti-patrón #2): en pantallas chicas se convierte en una
2017
- lista de tarjetas de dos niveles (principal + secundario),
2018
- donde cada fila completa es un objetivo táctil. lg:hidden la
2019
- retira cuando la tabla vuelve a ser usable. -->
2020
- <ul class="space-y-2 lg:hidden">
2021
- @for (row of visibleRows(); track keyOf(row)) {
2022
- <li
2023
- class="rounded-xl border border-neutral/20 bg-surface p-3"
2024
- [class.cursor-pointer]="isRowClickable()"
2025
- (click)="onRowClick(row)"
2026
- >
2027
- <!-- Nivel principal: el título del registro, siempre visible
2028
- (guía §6.2: el identificador nunca se esconde). -->
2029
- <div class="flex items-start justify-between gap-2">
2030
- <span class="text-sm font-medium">
2031
- {{ titleOf(row) }}
2032
- </span>
2033
- @if (schema().detail?.enabled) {
2034
- <svg
2035
- lucideChevronRight
2036
- size="16"
2037
- class="mt-0.5 shrink-0 text-neutral"
2038
- aria-hidden="true"
2039
- />
2040
- }
2041
- </div>
2042
- <!-- Nivel secundario: el resto de las columnas como pares
2043
- etiqueta/valor. Sin hover: todo lo accionable va en el
2044
- pie de la tarjeta, al alcance del pulgar. -->
2045
- <dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
2046
- @for (column of schema().listColumns; track column.key) {
2047
- <dt class="text-neutral">{{ column.label }}</dt>
2048
- <dd>
2049
- <app-render-host
2050
- kind="column-format"
2051
- [type]="formatOf(column)"
2052
- [config]="column"
2053
- [data]="cellOf(row, column)"
2054
- />
2055
- </dd>
2056
- }
2057
- </dl>
2058
- @if (hasRowActions()) {
2059
- <!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
2060
- <div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
2061
- @if (schema().detail?.enabled) {
2062
- <button
2063
- app-button
2064
- variant="outline"
2065
- type="button"
2066
- size="sm"
2067
- class="flex-1"
2068
- (click)="$event.stopPropagation(); goView(row)"
2069
- >
2070
- <svg lucideEye size="15" aria-hidden="true" />
2071
- <span>Ver</span>
2072
- </button>
2073
- }
2074
- @for (action of schema().actions ?? []; track action.id) {
2075
- <button
2076
- app-button
2077
- variant="outline"
2078
- type="button"
2079
- size="sm"
2080
- class="flex-1"
2081
- (click)="$event.stopPropagation(); runAction(action, row)"
2082
- >
2083
- @let actionIcon = iconOf(action);
2084
- @if (actionIcon) {
2085
- <svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
2086
- }
2087
- <span>{{ action.label }}</span>
2088
- </button>
2089
- }
2090
- <!-- Editar es O una vía u otra, como en la tabla: si el
2091
- schema declara quickEdit va el atajo; si no, la
2092
- pantalla completa. Apilados los dos sería ruido. -->
2093
- @if (canQuickEdit()) {
2094
- <button
2095
- app-button
2096
- variant="outline"
2097
- type="button"
2098
- size="sm"
2099
- class="flex-1"
2100
- (click)="$event.stopPropagation(); quickEditRow.set(row)"
2101
- >
2102
- <svg lucideZap size="15" aria-hidden="true" />
2103
- <span>Editar</span>
2104
- </button>
2105
- } @else if (canUpdate()) {
2106
- <button
2107
- app-button
2108
- variant="outline"
2109
- type="button"
2110
- size="sm"
2111
- class="flex-1"
2112
- (click)="$event.stopPropagation(); goEdit(row)"
2113
- >
2114
- <svg lucidePencil size="15" aria-hidden="true" />
2115
- <span>Editar</span>
2116
- </button>
2117
- }
2118
- @if (canRemove()) {
2119
- <button
2120
- app-button
2121
- variant="danger"
2122
- type="button"
2123
- size="sm"
2124
- class="flex-1"
2125
- (click)="$event.stopPropagation(); removeRow(row)"
2126
- >
2127
- <svg lucideTrash2 size="15" aria-hidden="true" />
2128
- <span>Eliminar</span>
2129
- </button>
2130
- }
2131
- </div>
2132
- }
2133
- </li>
2134
- }
2135
- </ul>
2136
- }
2137
- }
2138
- }
2139
-
2140
- @if (quickEditRow(); as row) {
2141
- <app-drawer-form
2142
- [open]="true"
2143
- [schema]="schema()"
2144
- mode="edit"
2145
- [recordKey]="keyOf(row)"
2146
- [fields]="quickEditFields()"
2147
- title="Edición rápida"
2148
- size="sm"
2149
- (close)="quickEditRow.set(null)"
2150
- (saved)="quickEditSaved()"
2151
- />
2152
- }
2153
- </section>
2154
- `,
2155
- isInline: true,
2156
- dependencies: [
2157
- {
2158
- kind: "component",
2159
- type: Button,
2160
- selector: "button[app-button]",
2161
- inputs: [
2162
- "variant",
2163
- "size",
2164
- "type",
2165
- "disabled",
2166
- "loading"
2167
- ]
2168
- },
2169
- {
2170
- kind: "directive",
2171
- type: CdkDrag,
2172
- selector: "[cdkDrag]",
2173
- inputs: [
2174
- "cdkDragData",
2175
- "cdkDragLockAxis",
2176
- "cdkDragRootElement",
2177
- "cdkDragBoundary",
2178
- "cdkDragStartDelay",
2179
- "cdkDragFreeDragPosition",
2180
- "cdkDragDisabled",
2181
- "cdkDragConstrainPosition",
2182
- "cdkDragPreviewClass",
2183
- "cdkDragPreviewContainer",
2184
- "cdkDragScale"
2185
- ],
2186
- outputs: [
2187
- "cdkDragStarted",
2188
- "cdkDragReleased",
2189
- "cdkDragEnded",
2190
- "cdkDragEntered",
2191
- "cdkDragExited",
2192
- "cdkDragDropped",
2193
- "cdkDragMoved"
2194
- ],
2195
- exportAs: ["cdkDrag"]
2196
- },
2197
- {
2198
- kind: "directive",
2199
- type: CdkDragHandle,
2200
- selector: "[cdkDragHandle]",
2201
- inputs: ["cdkDragHandleDisabled"]
2202
- },
2203
- {
2204
- kind: "directive",
2205
- type: CdkDropList,
2206
- selector: "[cdkDropList], cdk-drop-list",
2207
- inputs: [
2208
- "cdkDropListConnectedTo",
2209
- "cdkDropListData",
2210
- "cdkDropListOrientation",
2211
- "id",
2212
- "cdkDropListLockAxis",
2213
- "cdkDropListDisabled",
2214
- "cdkDropListSortingDisabled",
2215
- "cdkDropListEnterPredicate",
2216
- "cdkDropListSortPredicate",
2217
- "cdkDropListAutoScrollDisabled",
2218
- "cdkDropListAutoScrollStep",
2219
- "cdkDropListElementContainer",
2220
- "cdkDropListHasAnchor"
2221
- ],
2222
- outputs: [
2223
- "cdkDropListDropped",
2224
- "cdkDropListEntered",
2225
- "cdkDropListExited",
2226
- "cdkDropListSorted"
2227
- ],
2228
- exportAs: ["cdkDropList"]
2229
- },
2230
- {
2231
- kind: "component",
2232
- type: DrawerForm,
2233
- selector: "app-drawer-form",
2234
- inputs: [
2235
- "open",
2236
- "schema",
2237
- "fields",
2238
- "mode",
2239
- "recordKey",
2240
- "title",
2241
- "size"
2242
- ],
2243
- outputs: ["saved", "close"]
2244
- },
2245
- {
2246
- kind: "component",
2247
- type: EmptyState,
2248
- selector: "app-empty-state",
2249
- inputs: ["title", "message"]
2250
- },
2251
- {
2252
- kind: "component",
2253
- type: LucideChevronDown,
2254
- selector: "svg[lucideChevronDown]"
2255
- },
2256
- {
2257
- kind: "component",
2258
- type: LucideChevronRight,
2259
- selector: "svg[lucideChevronRight]"
2260
- },
2261
- {
2262
- kind: "component",
2263
- type: LucideChevronUp,
2264
- selector: "svg[lucideChevronUp]"
2265
- },
2266
- {
2267
- kind: "component",
2268
- type: LucideEye,
2269
- selector: "svg[lucideEye]"
2270
- },
2271
- {
2272
- kind: "component",
2273
- type: LucideGripVertical,
2274
- selector: "svg[lucideGripVertical]"
2275
- },
2276
- {
2277
- kind: "component",
2278
- type: LucidePencil,
2279
- selector: "svg[lucidePencil]"
2280
- },
2281
- {
2282
- kind: "component",
2283
- type: LucidePlus,
2284
- selector: "svg[lucidePlus]"
2285
- },
2286
- {
2287
- kind: "component",
2288
- type: LucideRotateCw,
2289
- selector: "svg[lucideRotateCw]"
2290
- },
2291
- {
2292
- kind: "component",
2293
- type: LucideDynamicIcon,
2294
- selector: "svg[lucideIcon]",
2295
- inputs: ["lucideIcon"]
2296
- },
2297
- {
2298
- kind: "component",
2299
- type: LucideTrash2,
2300
- selector: "svg[lucideTrash], svg[lucideTrash2]"
2301
- },
2302
- {
2303
- kind: "component",
2304
- type: LucideZap,
2305
- selector: "svg[lucideZap]"
2306
- },
2307
- {
2308
- kind: "component",
2309
- type: RenderHost,
2310
- selector: "app-render-host",
2311
- inputs: [
2312
- "kind",
2313
- "type",
2314
- "config",
2315
- "ctx",
2316
- "data",
2317
- "mode",
2318
- "value",
2319
- "chip"
2320
- ]
2321
- },
2322
- {
2323
- kind: "component",
2324
- type: Skeleton,
2325
- selector: "app-skeleton",
2326
- inputs: [
2327
- "lines",
2328
- "height",
2329
- "width"
2330
- ]
2331
- }
2332
- ]
2333
- });
2334
- };
2335
- i0.ɵɵngDeclareClassMetadata({
2336
- minVersion: "12.0.0",
2337
- version: "22.2.3",
2338
- ngImport: i0,
2339
- type: ListView,
2340
- decorators: [{
2341
- type: Component,
2342
- args: [{
2343
- selector: "app-list-view",
2344
- imports: [
2345
- Button,
2346
- CdkDrag,
2347
- CdkDragHandle,
2348
- CdkDropList,
2349
- DrawerForm,
2350
- EmptyState,
2351
- LucideChevronDown,
2352
- LucideChevronRight,
2353
- LucideChevronUp,
2354
- LucideEye,
2355
- LucideGripVertical,
2356
- LucidePencil,
2357
- LucidePlus,
2358
- LucideRotateCw,
2359
- LucideDynamicIcon,
2360
- LucideTrash2,
2361
- LucideZap,
2362
- RenderHost,
2363
- Skeleton
2364
- ],
2365
- template: `
2366
- <section class="space-y-4" aria-label="Listado" [attr.aria-busy]="status() === 'loading'">
2367
- <div class="flex flex-wrap items-end justify-between gap-3">
2368
- <div class="flex flex-wrap items-end gap-3">
2369
- @if (schema().search?.length) {
2370
- <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
2371
- Buscar
2372
- <input
2373
- type="search"
2374
- class="w-56 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2375
- placeholder="Buscar…"
2376
- [value]="query()"
2377
- (input)="setQuery(inputValue($event))"
2378
- />
2379
- </label>
2380
- }
2381
- @for (filter of schema().filters ?? []; track filter.key) {
2382
- <label class="flex flex-col gap-1 text-xs font-medium text-neutral">
2383
- {{ filter.label }}
2384
- @if (filter.type === 'text') {
2385
- <input
2386
- type="search"
2387
- class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2388
- [value]="filterValues()[filter.key] ?? ''"
2389
- (input)="setFilter(filter.key, inputValue($event))"
2390
- />
2391
- } @else {
2392
- <select
2393
- class="w-44 rounded-lg border border-neutral/30 bg-surface px-3 py-2 text-sm"
2394
- [value]="filterValues()[filter.key] ?? ''"
2395
- (change)="setFilter(filter.key, selectValue($event))"
2396
- >
2397
- @for (option of filterOptions(filter); track option.value) {
2398
- <option [value]="option.value">{{ option.label }}</option>
2399
- }
2400
- </select>
2401
- }
2402
- </label>
2403
- }
2404
- </div>
2405
- @if (canCreate()) {
2406
- <button app-button type="button" (click)="goCreate()">
2407
- <svg lucidePlus size="16" />
2408
- Crear {{ schema().label }}
2409
- </button>
2410
- }
2411
- </div>
2412
-
2413
- @switch (status()) {
2414
- @case ('loading') {
2415
- <app-skeleton [lines]="6" [height]="44" />
2416
- }
2417
- @case ('error') {
2418
- <app-empty-state
2419
- title="No se pudo cargar el listado"
2420
- message="Revisa tu conexión e intenta de nuevo."
2421
- >
2422
- <button app-button variant="outline" type="button" (click)="retry()">
2423
- <svg lucideRotateCw size="16" />
2424
- Reintentar
2425
- </button>
2426
- </app-empty-state>
2427
- }
2428
- @case ('ready') {
2429
- @if (visibleRows().length === 0) {
2430
- <app-empty-state
2431
- [title]="rows().length === 0 ? 'Sin registros' : 'Sin resultados'"
2432
- [message]="
2433
- rows().length === 0
2434
- ? 'Todavía no hay registros en este recurso.'
2435
- : 'Ningún registro coincide con la búsqueda o los filtros.'
2436
- "
2437
- >
2438
- @if (rows().length > 0) {
2439
- <button app-button variant="outline" type="button" (click)="clearFilters()">
2440
- Limpiar filtros
2441
- </button>
2442
- } @else if (canCreate()) {
2443
- <button app-button type="button" (click)="goCreate()">
2444
- <svg lucidePlus size="16" />
2445
- Crear el primero
2446
- </button>
2447
- }
2448
- </app-empty-state>
2449
- } @else {
2450
- <!-- La tabla es la vista de escritorio: se oculta bajo lg para
2451
- que la lista de tarjetas de abajo sea la ÚNICA vista en
2452
- móvil (con las dos visibles, el listado se duplicaba). -->
2453
- <div
2454
- class="hidden overflow-x-auto rounded-xl border border-neutral/20 bg-surface lg:block"
2455
- >
2456
- <table class="w-full text-sm">
2457
- <thead>
2458
- <tr class="border-b border-neutral/20 text-left text-xs uppercase text-neutral">
2459
- @if (canReorder()) {
2460
- <th scope="col" class="w-16 px-2 py-3 font-medium">
2461
- <span class="sr-only">Orden</span>
2462
- </th>
2463
- }
2464
- @for (column of schema().listColumns; track column.key) {
2465
- <th scope="col" class="px-4 py-3 font-medium">
2466
- {{ column.label }}
2467
- </th>
2468
- }
2469
- @if (hasRowActions()) {
2470
- <th scope="col" class="px-4 py-3 text-right font-medium">Acciones</th>
2471
- }
2472
- </tr>
2473
- </thead>
2474
- <tbody
2475
- cdkDropList
2476
- [cdkDropListData]="visibleRows()"
2477
- [cdkDropListDisabled]="dropDisabled()"
2478
- (cdkDropListDropped)="onDrop($event)"
2479
- >
2480
- @for (row of visibleRows(); track keyOf(row); let i = $index) {
2481
- <tr
2482
- class="border-b border-neutral/10 last:border-0 hover:bg-neutral/5 transition-colors"
2483
- [class.cursor-pointer]="isRowClickable()"
2484
- cdkDrag
2485
- [cdkDragDisabled]="dropDisabled()"
2486
- (click)="onRowClick(row)"
2487
- >
2488
- @if (canReorder()) {
2489
- <td class="px-2 py-2.5 align-middle" (click)="$event.stopPropagation()">
2490
- <div class="flex items-center gap-1">
2491
- <button
2492
- type="button"
2493
- class="cursor-grab text-neutral hover:text-primary"
2494
- cdkDragHandle
2495
- [attr.aria-label]="'Reordenar ' + titleOf(row)"
2496
- title="Arrastrar para reordenar"
2497
- >
2498
- <svg lucideGripVertical size="16" />
2499
- </button>
2500
- <span class="flex flex-col leading-none">
2501
- <button
2502
- type="button"
2503
- class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
2504
- [disabled]="i === 0 || reordering()"
2505
- [attr.aria-label]="'Subir ' + titleOf(row)"
2506
- (click)="moveBy(keyOf(row), -1)"
2507
- >
2508
- <svg lucideChevronUp size="16" />
2509
- </button>
2510
- <button
2511
- type="button"
2512
- class="px-1 text-xs text-neutral hover:text-primary disabled:opacity-30"
2513
- [disabled]="i === visibleRows().length - 1 || reordering()"
2514
- [attr.aria-label]="'Bajar ' + titleOf(row)"
2515
- (click)="moveBy(keyOf(row), 1)"
2516
- >
2517
- <svg lucideChevronDown size="16" />
2518
- </button>
2519
- </span>
2520
- </div>
2521
- </td>
2522
- }
2523
- @for (column of schema().listColumns; track column.key) {
2524
- <td class="px-4 py-2.5 align-middle">
2525
- <!-- Los 7 formatos salen del registro (R4.3):
2526
- la vista sólo elige cuál y entrega la celda. -->
2527
- <app-render-host
2528
- kind="column-format"
2529
- [type]="formatOf(column)"
2530
- [config]="column"
2531
- [data]="cellOf(row, column)"
2532
- />
2533
- </td>
2534
- }
2535
- @if (hasRowActions()) {
2536
- <td class="px-4 py-2.5" (click)="$event.stopPropagation()">
2537
- <div class="flex items-center justify-end gap-1">
2538
- @if (schema().detail?.enabled) {
2539
- <button
2540
- app-button
2541
- variant="ghost"
2542
- type="button"
2543
- (click)="goView(row)"
2544
- title="Ver ficha de detalle"
2545
- class="h-8 px-2.5 text-xs text-neutral-light hover:text-primary hover:bg-primary/10"
2546
- >
2547
- <svg lucideEye size="15" />
2548
- <span>Ver</span>
2549
- </button>
2550
- }
2551
- @for (action of schema().actions ?? []; track action.id) {
2552
- <button
2553
- app-button
2554
- variant="ghost"
2555
- type="button"
2556
- [title]="action.label"
2557
- [attr.aria-label]="action.label"
2558
- (click)="runAction(action, row)"
2559
- class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-primary/10"
2560
- >
2561
- @let actionIcon = iconOf(action);
2562
- @if (actionIcon) {
2563
- <svg [lucideIcon]="actionIcon" size="15" />
2564
- } @else {
2565
- {{ action.label }}
2566
- }
2567
- </button>
2568
- }
2569
- @if (canQuickEdit()) {
2570
- <button
2571
- app-button
2572
- variant="ghost"
2573
- type="button"
2574
- (click)="quickEditRow.set(row)"
2575
- title="Edición rápida"
2576
- class="h-8 px-2 text-xs text-neutral hover:text-primary hover:bg-neutral/10"
2577
- >
2578
- <svg lucideZap size="15" />
2579
- <span>Edición rápida</span>
2580
- </button>
2581
- }
2582
- @if (canUpdate()) {
2583
- <button
2584
- app-button
2585
- variant="ghost"
2586
- type="button"
2587
- (click)="goEdit(row)"
2588
- title="Editar registro"
2589
- class="h-8 px-2.5 text-xs text-primary hover:bg-primary/10"
2590
- >
2591
- <svg lucidePencil size="15" />
2592
- <span>Editar</span>
2593
- </button>
2594
- }
2595
- @if (canRemove()) {
2596
- <button
2597
- app-button
2598
- variant="ghost"
2599
- type="button"
2600
- (click)="removeRow(row)"
2601
- title="Eliminar registro"
2602
- class="h-8 px-2 text-xs text-danger hover:bg-danger/10"
2603
- >
2604
- <svg lucideTrash2 size="15" />
2605
- <span>Eliminar</span>
2606
- </button>
2607
- }
2608
- </div>
2609
- </td>
2610
- }
2611
- </tr>
2612
- }
2613
- </tbody>
2614
- </table>
2615
- </div>
2616
-
2617
- <!-- Vista de tarjetas — mobile-first (guía mobile §6).
2618
- La tabla de arriba necesita scroll horizontal con 5–7 columnas
2619
- (el anti-patrón #2): en pantallas chicas se convierte en una
2620
- lista de tarjetas de dos niveles (principal + secundario),
2621
- donde cada fila completa es un objetivo táctil. lg:hidden la
2622
- retira cuando la tabla vuelve a ser usable. -->
2623
- <ul class="space-y-2 lg:hidden">
2624
- @for (row of visibleRows(); track keyOf(row)) {
2625
- <li
2626
- class="rounded-xl border border-neutral/20 bg-surface p-3"
2627
- [class.cursor-pointer]="isRowClickable()"
2628
- (click)="onRowClick(row)"
2629
- >
2630
- <!-- Nivel principal: el título del registro, siempre visible
2631
- (guía §6.2: el identificador nunca se esconde). -->
2632
- <div class="flex items-start justify-between gap-2">
2633
- <span class="text-sm font-medium">
2634
- {{ titleOf(row) }}
2635
- </span>
2636
- @if (schema().detail?.enabled) {
2637
- <svg
2638
- lucideChevronRight
2639
- size="16"
2640
- class="mt-0.5 shrink-0 text-neutral"
2641
- aria-hidden="true"
2642
- />
2643
- }
2644
- </div>
2645
- <!-- Nivel secundario: el resto de las columnas como pares
2646
- etiqueta/valor. Sin hover: todo lo accionable va en el
2647
- pie de la tarjeta, al alcance del pulgar. -->
2648
- <dl class="mt-2 grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
2649
- @for (column of schema().listColumns; track column.key) {
2650
- <dt class="text-neutral">{{ column.label }}</dt>
2651
- <dd>
2652
- <app-render-host
2653
- kind="column-format"
2654
- [type]="formatOf(column)"
2655
- [config]="column"
2656
- [data]="cellOf(row, column)"
2657
- />
2658
- </dd>
2659
- }
2660
- </dl>
2661
- @if (hasRowActions()) {
2662
- <!-- Pie táctil: objetivos ≥44 px (guía §8.2), sin hover. -->
2663
- <div class="mt-3 flex flex-wrap gap-2 border-t border-neutral/10 pt-2">
2664
- @if (schema().detail?.enabled) {
2665
- <button
2666
- app-button
2667
- variant="outline"
2668
- type="button"
2669
- size="sm"
2670
- class="flex-1"
2671
- (click)="$event.stopPropagation(); goView(row)"
2672
- >
2673
- <svg lucideEye size="15" aria-hidden="true" />
2674
- <span>Ver</span>
2675
- </button>
2676
- }
2677
- @for (action of schema().actions ?? []; track action.id) {
2678
- <button
2679
- app-button
2680
- variant="outline"
2681
- type="button"
2682
- size="sm"
2683
- class="flex-1"
2684
- (click)="$event.stopPropagation(); runAction(action, row)"
2685
- >
2686
- @let actionIcon = iconOf(action);
2687
- @if (actionIcon) {
2688
- <svg [lucideIcon]="actionIcon" size="15" aria-hidden="true" />
2689
- }
2690
- <span>{{ action.label }}</span>
2691
- </button>
2692
- }
2693
- <!-- Editar es O una vía u otra, como en la tabla: si el
2694
- schema declara quickEdit va el atajo; si no, la
2695
- pantalla completa. Apilados los dos sería ruido. -->
2696
- @if (canQuickEdit()) {
2697
- <button
2698
- app-button
2699
- variant="outline"
2700
- type="button"
2701
- size="sm"
2702
- class="flex-1"
2703
- (click)="$event.stopPropagation(); quickEditRow.set(row)"
2704
- >
2705
- <svg lucideZap size="15" aria-hidden="true" />
2706
- <span>Editar</span>
2707
- </button>
2708
- } @else if (canUpdate()) {
2709
- <button
2710
- app-button
2711
- variant="outline"
2712
- type="button"
2713
- size="sm"
2714
- class="flex-1"
2715
- (click)="$event.stopPropagation(); goEdit(row)"
2716
- >
2717
- <svg lucidePencil size="15" aria-hidden="true" />
2718
- <span>Editar</span>
2719
- </button>
2720
- }
2721
- @if (canRemove()) {
2722
- <button
2723
- app-button
2724
- variant="danger"
2725
- type="button"
2726
- size="sm"
2727
- class="flex-1"
2728
- (click)="$event.stopPropagation(); removeRow(row)"
2729
- >
2730
- <svg lucideTrash2 size="15" aria-hidden="true" />
2731
- <span>Eliminar</span>
2732
- </button>
2733
- }
2734
- </div>
2735
- }
2736
- </li>
2737
- }
2738
- </ul>
2739
- }
2740
- }
2741
- }
2742
-
2743
- @if (quickEditRow(); as row) {
2744
- <app-drawer-form
2745
- [open]="true"
2746
- [schema]="schema()"
2747
- mode="edit"
2748
- [recordKey]="keyOf(row)"
2749
- [fields]="quickEditFields()"
2750
- title="Edición rápida"
2751
- size="sm"
2752
- (close)="quickEditRow.set(null)"
2753
- (saved)="quickEditSaved()"
2754
- />
2755
- }
2756
- </section>
2757
- `
2758
- }]
2759
- }],
2760
- ctorParameters: () => [],
2761
- propDecorators: {
2762
- schema: [{
2763
- type: i0.Input,
2764
- args: [{
2765
- isSignal: true,
2766
- alias: "schema",
2767
- required: true
2768
- }]
2769
- }],
2770
- viewRecord: [{
2771
- type: i0.Output,
2772
- args: ["viewRecord"]
2773
- }]
2774
- }
2775
- });
2776
- var DetailView = class DetailView {
2777
- api = inject(ApiService, { optional: true });
2778
- actionService = inject(ActionService);
2779
- auth = inject(AuthService);
2780
- router = inject(Router);
2781
- nav = inject(ViewNavigationService);
2782
- notifications = inject(NotificationService);
2783
- configService = inject(ConfigService);
2784
- constructor() {
2785
- this.configService.ensureLoaded();
2786
- }
2787
- schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
2788
- record = input.required(...ngDevMode ? [{ debugName: "record" }] : /* istanbul ignore next */ []);
2789
- edit = output();
2790
- back = output();
2791
- delete = output();
2792
- refreshTick = signal(0, ...ngDevMode ? [{ debugName: "refreshTick" }] : /* istanbul ignore next */ []);
2793
- fetched = toSignal(combineLatest([toObservable(this.record), toObservable(this.refreshTick)]).pipe(switchMap(([rec]) => {
2794
- const schema = this.schema();
2795
- const key = rec ? String(rec[schema.keyField] ?? "") : "";
2796
- const getEndpoint = schema.endpoint?.get;
2797
- if (!key || !getEndpoint || typeof this.api?.get !== "function") return of({
2798
- phase: "done",
2799
- data: null
2800
- });
2801
- return this.api.get(schema.endpoint, key).pipe(map((data) => ({
2802
- phase: "done",
2803
- data
2804
- })), catchError((error) => of({
2805
- phase: "error",
2806
- error
2807
- })));
2808
- })), { initialValue: { phase: "pending" } });
2809
- status = computed(() => {
2810
- const fetched = this.fetched();
2811
- if (fetched.phase === "pending") return "loading";
2812
- if (fetched.phase === "error") return "error";
2813
- return "ready";
2814
- }, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
2815
- retry() {
2816
- this.refreshTick.update((tick) => tick + 1);
2817
- }
2818
- errorDeCarga() {
2819
- const fetched = this.fetched();
2820
- return fetched.phase === "error" ? fetched.error : void 0;
2821
- }
2822
- volverDeError() {
2823
- this.back.emit();
2824
- this.nav.back();
2825
- }
2826
- effectiveRecord = computed(() => {
2827
- const initial = this.record();
2828
- const fetched = this.fetched();
2829
- if (fetched.phase === "done" && fetched.data && typeof fetched.data === "object" && Object.keys(fetched.data).length > 0) return {
2830
- ...initial,
2831
- ...fetched.data
2832
- };
2833
- return initial;
2834
- }, ...ngDevMode ? [{ debugName: "effectiveRecord" }] : /* istanbul ignore next */ []);
2835
- detail = computed(() => this.schema().detail ?? { enabled: true }, ...ngDevMode ? [{ debugName: "detail" }] : /* istanbul ignore next */ []);
2836
- title = computed(() => {
2837
- const rec = this.effectiveRecord();
2838
- const schema = this.schema();
2839
- const raw = rec[schema.titleField];
2840
- return raw !== void 0 && raw !== null && raw !== "" ? String(raw) : schema.label;
2841
- }, ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
2842
- recordKey = computed(() => {
2843
- const rec = this.effectiveRecord();
2844
- const schema = this.schema();
2845
- return String(rec[schema.keyField] ?? "—");
2846
- }, ...ngDevMode ? [{ debugName: "recordKey" }] : /* istanbul ignore next */ []);
2847
- headerFields = computed(() => {
2848
- return (this.detail().headerFields ?? []).map((key) => this.resolveFieldValue(key)).filter((f) => f !== null);
2849
- }, ...ngDevMode ? [{ debugName: "headerFields" }] : /* istanbul ignore next */ []);
2850
- sections = computed(() => {
2851
- const schema = this.schema();
2852
- const detailConfig = this.detail();
2853
- if (detailConfig.sections && detailConfig.sections.length > 0) return detailConfig.sections.map((sec) => ({
2854
- id: sec.id,
2855
- label: sec.label,
2856
- fields: sec.fields.map((key) => this.resolveFieldValue(key)).filter((f) => f !== null)
2857
- })).filter((sec) => sec.fields.length > 0);
2858
- const layoutSections = schema.layout?.sections ?? [];
2859
- const sectionMap = /* @__PURE__ */ new Map();
2860
- for (const layoutSection of layoutSections) sectionMap.set(layoutSection.id, {
2861
- id: layoutSection.id,
2862
- label: layoutSection.label,
2863
- fields: []
2864
- });
2865
- const unsectioned = {
2866
- id: "_unsectioned",
2867
- label: schema.label,
2868
- fields: []
2869
- };
2870
- for (const field of schema.fields) {
2871
- const value = this.resolveFieldValue(field.key);
2872
- if (value === null) continue;
2873
- if (field.section && sectionMap.has(field.section)) sectionMap.get(field.section).fields.push(value);
2874
- else unsectioned.fields.push(value);
2875
- }
2876
- const result = [];
2877
- if (unsectioned.fields.length > 0) result.push(unsectioned);
2878
- for (const sec of sectionMap.values()) if (sec.fields.length > 0) result.push(sec);
2879
- return result;
2880
- }, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
2881
- copyShareActions = computed(() => {
2882
- return (this.schema().actions ?? this.detail().actions ?? []).filter(isShareAction);
2883
- }, ...ngDevMode ? [{ debugName: "copyShareActions" }] : /* istanbul ignore next */ []);
2884
- triggerActions = computed(() => {
2885
- return (this.schema().actions ?? this.detail().actions ?? []).filter(isTriggerAction);
2886
- }, ...ngDevMode ? [{ debugName: "triggerActions" }] : /* istanbul ignore next */ []);
2887
- async runTrigger(action) {
2888
- if (!this.api) return;
2889
- const result = await this.actionService.run(action, {
2890
- record: this.effectiveRecord(),
2891
- resource: this.schema().id,
2892
- api: this.api,
2893
- router: this.router,
2894
- toast: this.notifications,
2895
- user: this.auth.user(),
2896
- keyField: this.schema().keyField
2897
- });
2898
- if (result.ok && result.reload) this.refreshTick.update((tick) => tick + 1);
2899
- }
2900
- resolveFieldValue(key) {
2901
- const schema = this.schema();
2902
- const rec = this.effectiveRecord();
2903
- const field = schema.fields.find((f) => f.key === key);
2904
- const raw = getValue(rec, key);
2905
- const declarado = field ?? {
2906
- key,
2907
- label: key.replace(/_/g, " "),
2908
- type: "text"
2909
- };
2910
- if (!this.auth.canReadField(declarado)) return null;
2911
- return {
2912
- key,
2913
- label: field?.label ?? declarado.label,
2914
- field: declarado,
2915
- raw
2916
- };
2917
- }
2918
- vacio(valor) {
2919
- return valor === null || valor === void 0 || valor === "";
2920
- }
2921
- esHistorial(campo) {
2922
- return campo.field.type === "list" && !this.vacio(campo.raw);
2923
- }
2924
- regularFields(section) {
2925
- return section.fields.filter((f) => !this.esHistorial(f));
2926
- }
2927
- listFields(section) {
2928
- return section.fields.filter((f) => this.esHistorial(f));
2929
- }
2930
- hasLargeContent(section) {
2931
- return section.fields.some((f) => this.esHistorial(f) || f.key === "description");
2932
- }
2933
- shareOf(action) {
2934
- return resolveShare(action, this.actionService.vars(this.effectiveRecord()));
2935
- }
2936
- static ɵfac = i0.ɵɵngDeclareFactory({
2937
- minVersion: "12.0.0",
2938
- version: "22.2.3",
2939
- ngImport: i0,
2940
- type: DetailView,
2941
- deps: [],
2942
- target: i0.ɵɵFactoryTarget.Component
2943
- });
2944
- static ɵcmp = i0.ɵɵngDeclareComponent({
2945
- minVersion: "17.0.0",
2946
- version: "22.2.3",
2947
- type: DetailView,
2948
- isStandalone: true,
2949
- selector: "app-detail-view",
2950
- inputs: {
2951
- schema: {
2952
- classPropertyName: "schema",
2953
- publicName: "schema",
2954
- isSignal: true,
2955
- isRequired: true,
2956
- transformFunction: null
2957
- },
2958
- record: {
2959
- classPropertyName: "record",
2960
- publicName: "record",
2961
- isSignal: true,
2962
- isRequired: true,
2963
- transformFunction: null
2964
- }
2965
- },
2966
- outputs: {
2967
- edit: "edit",
2968
- back: "back",
2969
- delete: "delete"
2970
- },
2971
- ngImport: i0,
2972
- template: `
2973
- <div class="space-y-6">
2974
- <!-- ── Cabecera Principal ── -->
2975
- <header
2976
- class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
2977
- >
2978
- <div class="space-y-2">
2979
- <div class="flex flex-wrap items-center gap-2.5">
2980
- <h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
2981
- {{ title() }}
2982
- </h1>
2983
- @for (campo of headerFields(); track campo.key) {
2984
- @if (!vacio(campo.raw)) {
2985
- <!-- La cabecera siempre mostró los headerFields como chips:
2986
- el matiz viaja al host, no un @switch por tipo acá. -->
2987
- <app-render-host
2988
- mode="display"
2989
- [chip]="true"
2990
- [type]="campo.field.type"
2991
- [config]="campo.field"
2992
- [value]="campo.raw"
2993
- />
2994
- }
2995
- }
2996
- </div>
2997
- <p class="text-sm text-neutral">
2998
- {{ schema().label }} · Identificador:
2999
- <span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
3000
- </p>
3001
- </div>
3002
-
3003
- <div class="flex flex-wrap items-center gap-2.5">
3004
- <button app-button variant="outline" type="button" (click)="back.emit()">
3005
- <svg lucideArrowLeft size="16" />
3006
- Volver
3007
- </button>
3008
- @if (detail().allowEdit !== false && status() !== 'error') {
3009
- <button app-button variant="primary" type="button" (click)="edit.emit(record())">
3010
- <svg lucidePencil size="16" />
3011
- Editar
3012
- </button>
3013
- }
3014
- @if (detail().allowDelete !== false && status() !== 'error') {
3015
- <button app-button variant="danger" type="button" (click)="delete.emit(record())">
3016
- <svg lucideTrash2 size="16" />
3017
- Eliminar
3018
- </button>
3019
- }
3020
- </div>
3021
- </header>
3022
-
3023
- <!-- ── Acciones Destacadas del Registro (si existen) ── -->
3024
- @if (copyShareActions().length > 0 || triggerActions().length > 0) {
3025
- <section
3026
- class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
3027
- >
3028
- <div class="mb-3 flex items-center gap-2">
3029
- <svg lucideExternalLink size="16" class="text-primary" />
3030
- <h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
3031
- </div>
3032
- <div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
3033
- @for (action of copyShareActions(); track action.id) {
3034
- <div class="flex-1 min-w-[280px]">
3035
- @let share = shareOf(action);
3036
- <app-copy-share
3037
- [url]="share.url"
3038
- [shareText]="share.shareText"
3039
- [label]="action.label"
3040
- />
3041
- </div>
3042
- }
3043
- @for (action of triggerActions(); track action.id) {
3044
- <button app-button variant="outline" type="button" (click)="runTrigger(action)">
3045
- {{ action.label }}
3046
- </button>
3047
- }
3048
- </div>
3049
- </section>
3050
- }
3051
-
3052
- <!-- ── Secciones de Datos ── -->
3053
- @switch (status()) {
3054
- @case ('loading') {
3055
- <!-- Skeleton con la forma de las tarjetas de sección: reserva el
3056
- espacio (sin layout jump) y distingue «cargando» de «vacío».
3057
- La cabecera y las acciones de arriba sí se pintan ya: son la
3058
- fila de la lista, datos reales que no están en disputa. -->
3059
- <div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3060
- @for (placeholder of [0, 1]; track placeholder) {
3061
- <section
3062
- class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
3063
- >
3064
- <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3065
- <app-skeleton [lines]="1" [height]="14" width="30%" />
3066
- </header>
3067
- <div class="p-5">
3068
- <app-skeleton [lines]="6" [height]="44" />
3069
- </div>
3070
- </section>
3071
- }
3072
- </div>
3073
- }
3074
- @case ('error') {
3075
- <app-empty-state
3076
- title="No se pudo cargar el registro"
3077
- message="Revisa tu conexión e intenta de nuevo."
3078
- >
3079
- <button app-button variant="outline" type="button" (click)="retry()">
3080
- <svg lucideRotateCw size="16" />
3081
- Reintentar
3082
- </button>
3083
- <button app-button variant="outline" type="button" (click)="volverDeError()">
3084
- <svg lucideArrowLeft size="16" />
3085
- Volver
3086
- </button>
3087
- </app-empty-state>
3088
- }
3089
- @case ('ready') {
3090
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3091
- @for (section of sections(); track section.id) {
3092
- <section
3093
- class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
3094
- [class.lg:col-span-2]="hasLargeContent(section)"
3095
- >
3096
- <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3097
- <h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
3098
- {{ section.label }}
3099
- </h2>
3100
- </header>
3101
-
3102
- <div class="p-5 flex-1 space-y-4">
3103
- <!-- Grid para campos regulares -->
3104
- @if (regularFields(section).length > 0) {
3105
- <dl class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
3106
- @for (campo of regularFields(section); track campo.key) {
3107
- <div class="space-y-1">
3108
- <dt class="text-xs font-medium text-neutral">{{ campo.label }}</dt>
3109
- <dd class="text-sm font-medium text-neutral-light break-words">
3110
- <app-render-host
3111
- mode="display"
3112
- [type]="campo.field.type"
3113
- [config]="campo.field"
3114
- [value]="campo.raw"
3115
- />
3116
- </dd>
3117
- </div>
3118
- }
3119
- </dl>
3120
- }
3121
-
3122
- <!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
3123
- @for (campo of listFields(section); track campo.key) {
3124
- <div class="pt-2">
3125
- <h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
3126
- {{ campo.label }}
3127
- </h3>
3128
- <app-render-host
3129
- mode="display"
3130
- [type]="campo.field.type"
3131
- [config]="campo.field"
3132
- [value]="campo.raw"
3133
- />
3134
- </div>
3135
- }
3136
- </div>
3137
- </section>
3138
- }
3139
- </div>
3140
- }
3141
- }
3142
- </div>
3143
- `,
3144
- isInline: true,
3145
- dependencies: [
3146
- {
3147
- kind: "component",
3148
- type: Button,
3149
- selector: "button[app-button]",
3150
- inputs: [
3151
- "variant",
3152
- "size",
3153
- "type",
3154
- "disabled",
3155
- "loading"
3156
- ]
3157
- },
3158
- {
3159
- kind: "component",
3160
- type: CopyShare,
3161
- selector: "app-copy-share",
3162
- inputs: [
3163
- "url",
3164
- "label",
3165
- "shareText"
3166
- ]
3167
- },
3168
- {
3169
- kind: "component",
3170
- type: EmptyState,
3171
- selector: "app-empty-state",
3172
- inputs: ["title", "message"]
3173
- },
3174
- {
3175
- kind: "component",
3176
- type: LucideArrowLeft,
3177
- selector: "svg[lucideArrowLeft]"
3178
- },
3179
- {
3180
- kind: "component",
3181
- type: LucideExternalLink,
3182
- selector: "svg[lucideExternalLink]"
3183
- },
3184
- {
3185
- kind: "component",
3186
- type: LucidePencil,
3187
- selector: "svg[lucidePencil]"
3188
- },
3189
- {
3190
- kind: "component",
3191
- type: LucideRotateCw,
3192
- selector: "svg[lucideRotateCw]"
3193
- },
3194
- {
3195
- kind: "component",
3196
- type: LucideTrash2,
3197
- selector: "svg[lucideTrash], svg[lucideTrash2]"
3198
- },
3199
- {
3200
- kind: "component",
3201
- type: RenderHost,
3202
- selector: "app-render-host",
3203
- inputs: [
3204
- "kind",
3205
- "type",
3206
- "config",
3207
- "ctx",
3208
- "data",
3209
- "mode",
3210
- "value",
3211
- "chip"
3212
- ]
3213
- },
3214
- {
3215
- kind: "component",
3216
- type: Skeleton,
3217
- selector: "app-skeleton",
3218
- inputs: [
3219
- "lines",
3220
- "height",
3221
- "width"
3222
- ]
3223
- }
3224
- ]
3225
- });
3226
- };
3227
- i0.ɵɵngDeclareClassMetadata({
3228
- minVersion: "12.0.0",
3229
- version: "22.2.3",
3230
- ngImport: i0,
3231
- type: DetailView,
3232
- decorators: [{
3233
- type: Component,
3234
- args: [{
3235
- selector: "app-detail-view",
3236
- imports: [
3237
- Button,
3238
- CopyShare,
3239
- EmptyState,
3240
- LucideArrowLeft,
3241
- LucideExternalLink,
3242
- LucidePencil,
3243
- LucideRotateCw,
3244
- LucideTrash2,
3245
- RenderHost,
3246
- Skeleton
3247
- ],
3248
- template: `
3249
- <div class="space-y-6">
3250
- <!-- ── Cabecera Principal ── -->
3251
- <header
3252
- class="flex flex-wrap items-start justify-between gap-4 rounded-2xl border border-neutral/15 bg-surface p-6 shadow-xs"
3253
- >
3254
- <div class="space-y-2">
3255
- <div class="flex flex-wrap items-center gap-2.5">
3256
- <h1 class="font-display text-2xl font-bold tracking-tight text-neutral-light">
3257
- {{ title() }}
3258
- </h1>
3259
- @for (campo of headerFields(); track campo.key) {
3260
- @if (!vacio(campo.raw)) {
3261
- <!-- La cabecera siempre mostró los headerFields como chips:
3262
- el matiz viaja al host, no un @switch por tipo acá. -->
3263
- <app-render-host
3264
- mode="display"
3265
- [chip]="true"
3266
- [type]="campo.field.type"
3267
- [config]="campo.field"
3268
- [value]="campo.raw"
3269
- />
3270
- }
3271
- }
3272
- </div>
3273
- <p class="text-sm text-neutral">
3274
- {{ schema().label }} · Identificador:
3275
- <span class="font-mono font-medium text-neutral-light">{{ recordKey() }}</span>
3276
- </p>
3277
- </div>
3278
-
3279
- <div class="flex flex-wrap items-center gap-2.5">
3280
- <button app-button variant="outline" type="button" (click)="back.emit()">
3281
- <svg lucideArrowLeft size="16" />
3282
- Volver
3283
- </button>
3284
- @if (detail().allowEdit !== false && status() !== 'error') {
3285
- <button app-button variant="primary" type="button" (click)="edit.emit(record())">
3286
- <svg lucidePencil size="16" />
3287
- Editar
3288
- </button>
3289
- }
3290
- @if (detail().allowDelete !== false && status() !== 'error') {
3291
- <button app-button variant="danger" type="button" (click)="delete.emit(record())">
3292
- <svg lucideTrash2 size="16" />
3293
- Eliminar
3294
- </button>
3295
- }
3296
- </div>
3297
- </header>
3298
-
3299
- <!-- ── Acciones Destacadas del Registro (si existen) ── -->
3300
- @if (copyShareActions().length > 0 || triggerActions().length > 0) {
3301
- <section
3302
- class="rounded-2xl border border-primary/25 bg-gradient-to-r from-primary/10 via-primary/5 to-transparent p-5 shadow-xs"
3303
- >
3304
- <div class="mb-3 flex items-center gap-2">
3305
- <svg lucideExternalLink size="16" class="text-primary" />
3306
- <h2 class="text-sm font-semibold text-primary">Acciones del Registro</h2>
3307
- </div>
3308
- <div class="flex flex-col gap-3 sm:flex-row sm:flex-wrap">
3309
- @for (action of copyShareActions(); track action.id) {
3310
- <div class="flex-1 min-w-[280px]">
3311
- @let share = shareOf(action);
3312
- <app-copy-share
3313
- [url]="share.url"
3314
- [shareText]="share.shareText"
3315
- [label]="action.label"
3316
- />
3317
- </div>
3318
- }
3319
- @for (action of triggerActions(); track action.id) {
3320
- <button app-button variant="outline" type="button" (click)="runTrigger(action)">
3321
- {{ action.label }}
3322
- </button>
3323
- }
3324
- </div>
3325
- </section>
3326
- }
3327
-
3328
- <!-- ── Secciones de Datos ── -->
3329
- @switch (status()) {
3330
- @case ('loading') {
3331
- <!-- Skeleton con la forma de las tarjetas de sección: reserva el
3332
- espacio (sin layout jump) y distingue «cargando» de «vacío».
3333
- La cabecera y las acciones de arriba sí se pintan ya: son la
3334
- fila de la lista, datos reales que no están en disputa. -->
3335
- <div aria-busy="true" class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3336
- @for (placeholder of [0, 1]; track placeholder) {
3337
- <section
3338
- class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden"
3339
- >
3340
- <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3341
- <app-skeleton [lines]="1" [height]="14" width="30%" />
3342
- </header>
3343
- <div class="p-5">
3344
- <app-skeleton [lines]="6" [height]="44" />
3345
- </div>
3346
- </section>
3347
- }
3348
- </div>
3349
- }
3350
- @case ('error') {
3351
- <app-empty-state
3352
- title="No se pudo cargar el registro"
3353
- message="Revisa tu conexión e intenta de nuevo."
3354
- >
3355
- <button app-button variant="outline" type="button" (click)="retry()">
3356
- <svg lucideRotateCw size="16" />
3357
- Reintentar
3358
- </button>
3359
- <button app-button variant="outline" type="button" (click)="volverDeError()">
3360
- <svg lucideArrowLeft size="16" />
3361
- Volver
3362
- </button>
3363
- </app-empty-state>
3364
- }
3365
- @case ('ready') {
3366
- <div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
3367
- @for (section of sections(); track section.id) {
3368
- <section
3369
- class="rounded-2xl border border-neutral/15 bg-surface shadow-xs overflow-hidden flex flex-col"
3370
- [class.lg:col-span-2]="hasLargeContent(section)"
3371
- >
3372
- <header class="border-b border-neutral/10 bg-neutral/5 px-5 py-3.5">
3373
- <h2 class="text-sm font-semibold tracking-wide text-neutral-light uppercase">
3374
- {{ section.label }}
3375
- </h2>
3376
- </header>
3377
-
3378
- <div class="p-5 flex-1 space-y-4">
3379
- <!-- Grid para campos regulares -->
3380
- @if (regularFields(section).length > 0) {
3381
- <dl class="grid grid-cols-1 gap-x-6 gap-y-4 sm:grid-cols-2">
3382
- @for (campo of regularFields(section); track campo.key) {
3383
- <div class="space-y-1">
3384
- <dt class="text-xs font-medium text-neutral">{{ campo.label }}</dt>
3385
- <dd class="text-sm font-medium text-neutral-light break-words">
3386
- <app-render-host
3387
- mode="display"
3388
- [type]="campo.field.type"
3389
- [config]="campo.field"
3390
- [value]="campo.raw"
3391
- />
3392
- </dd>
3393
- </div>
3394
- }
3395
- </dl>
3396
- }
3397
-
3398
- <!-- Renderizado de listas anidadas (Timeline de eventos / items) -->
3399
- @for (campo of listFields(section); track campo.key) {
3400
- <div class="pt-2">
3401
- <h3 class="mb-3 text-xs font-semibold text-neutral uppercase">
3402
- {{ campo.label }}
3403
- </h3>
3404
- <app-render-host
3405
- mode="display"
3406
- [type]="campo.field.type"
3407
- [config]="campo.field"
3408
- [value]="campo.raw"
3409
- />
3410
- </div>
3411
- }
3412
- </div>
3413
- </section>
3414
- }
3415
- </div>
3416
- }
3417
- }
3418
- </div>
3419
- `
3420
- }]
3421
- }],
3422
- ctorParameters: () => [],
3423
- propDecorators: {
3424
- schema: [{
3425
- type: i0.Input,
3426
- args: [{
3427
- isSignal: true,
3428
- alias: "schema",
3429
- required: true
3430
- }]
3431
- }],
3432
- record: [{
3433
- type: i0.Input,
3434
- args: [{
3435
- isSignal: true,
3436
- alias: "record",
3437
- required: true
3438
- }]
3439
- }],
3440
- edit: [{
3441
- type: i0.Output,
3442
- args: ["edit"]
3443
- }],
3444
- back: [{
3445
- type: i0.Output,
3446
- args: ["back"]
3447
- }],
3448
- delete: [{
3449
- type: i0.Output,
3450
- args: ["delete"]
3451
- }]
3452
- }
3453
- });
3454
- var resource_page_exports = /* @__PURE__ */ __exportAll({ ResourcePage: () => ResourcePage });
3455
- var ResourcePage = class ResourcePage {
3456
- route = inject(ActivatedRoute);
3457
- router = inject(Router);
3458
- auth = inject(AuthService);
3459
- params = toSignal(this.route.paramMap, { requireSync: true });
3460
- routeData = toSignal(this.route.data ?? of({}), { requireSync: true });
3461
- schema = computed(() => {
3462
- const resourceId = this.routeData()["resourceId"] ?? this.params().get("id");
3463
- return getSchema(typeof resourceId === "string" ? resourceId : "");
3464
- }, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3465
- currentView = signal("list", ...ngDevMode ? [{ debugName: "currentView" }] : /* istanbul ignore next */ []);
3466
- selectedRecord = signal(null, ...ngDevMode ? [{ debugName: "selectedRecord" }] : /* istanbul ignore next */ []);
3467
- constructor() {
3468
- this.route.paramMap.pipe(map((params) => params.get("key")), distinctUntilChanged(), takeUntilDestroyed()).subscribe((key) => this.openFromRoute(key));
3469
- this.route.data?.pipe(takeUntilDestroyed()).subscribe(() => this.openFromRoute(this.params().get("key")));
3470
- }
3471
- openFromRoute(key) {
3472
- const schema = this.schema();
3473
- if (!schema || schema.kind !== "collection") return;
3474
- if ((this.routeData()["viewType"] === "detail" || key) && key && schema.detail?.enabled) {
3475
- const current = this.selectedRecord();
3476
- if (!current || String(current[schema.keyField]) !== key) this.selectedRecord.set({ [schema.keyField]: key });
3477
- this.currentView.set("detail");
3478
- } else if (this.currentView() === "detail") {
3479
- this.selectedRecord.set(null);
3480
- this.currentView.set("list");
3481
- }
3482
- }
3483
- switchToDetail(record) {
3484
- const schema = this.schema();
3485
- const data = this.routeData();
3486
- const binding = data["select"];
3487
- if (binding !== void 0) {
3488
- const key = schema && record[schema.keyField] !== void 0 && record[schema.keyField] !== null ? String(record[schema.keyField]) : "";
3489
- if (binding.mode === "route") {
3490
- const emit = data["emit"];
3491
- emit?.({
3492
- resource: schema?.id ?? String(data["resourceId"] ?? ""),
3493
- key,
3494
- action: binding.on
3495
- });
3496
- return;
3497
- }
3498
- if (binding.mode === "none") return;
3499
- this.selectedRecord.set(record);
3500
- this.currentView.set("detail");
3501
- return;
3502
- }
3503
- this.selectedRecord.set(record);
3504
- this.currentView.set("detail");
3505
- const k = schema ? record[schema.keyField] : void 0;
3506
- if (schema && k !== void 0 && k !== null && String(k) !== "") this.router.navigate([
3507
- "/",
3508
- schema.id,
3509
- String(k)
3510
- ]);
3511
- }
3512
- switchToList() {
3513
- this.selectedRecord.set(null);
3514
- this.currentView.set("list");
3515
- const schema = this.schema();
3516
- if (schema && this.params().get("key")) this.router.navigate(["/", schema.id]);
3517
- }
3518
- goEdit(record) {
3519
- const schema = this.schema();
3520
- if (!schema) return;
3521
- const key = record[schema.keyField];
3522
- if (key) this.router.navigate([
3523
- "/",
3524
- schema.id,
3525
- key,
3526
- "edit"
3527
- ]);
3528
- }
3529
- hasNoAccess(schema) {
3530
- if (schema.policies?.access) {
3531
- if (schema.kind === "singleton") return !this.auth.canDoOperation(schema, "update");
3532
- return !this.auth.canDoOperation(schema, "list") && !this.auth.canDoOperation(schema, "create") && !this.auth.canDoOperation(schema, "update") && !this.auth.canDoOperation(schema, "remove");
3533
- }
3534
- const permissions = schema.permissions;
3535
- if (!permissions || schema.kind === "dashboard") return false;
3536
- if (schema.kind === "singleton") return !!!schema.endpoint?.get && permissions.update === false;
3537
- const hasReadEndpoint = !!schema.endpoint?.list || !!schema.endpoint?.get;
3538
- const canWrite = permissions.create !== false || permissions.update !== false || permissions.remove !== false;
3539
- return !hasReadEndpoint && !canWrite;
3540
- }
3541
- static ɵfac = i0.ɵɵngDeclareFactory({
3542
- minVersion: "12.0.0",
3543
- version: "22.2.3",
3544
- ngImport: i0,
3545
- type: ResourcePage,
3546
- deps: [],
3547
- target: i0.ɵɵFactoryTarget.Component
3548
- });
3549
- static ɵcmp = i0.ɵɵngDeclareComponent({
3550
- minVersion: "17.0.0",
3551
- version: "22.2.3",
3552
- type: ResourcePage,
3553
- isStandalone: true,
3554
- selector: "app-resource-page",
3555
- ngImport: i0,
3556
- template: `
3557
- @if (schema(); as schema) {
3558
- @if (hasNoAccess(schema)) {
3559
- <app-empty-state
3560
- title="Sin permisos"
3561
- message="No tiene permisos para gestionar este recurso."
3562
- />
3563
- } @else {
3564
- @switch (schema.kind) {
3565
- @case ('collection') {
3566
- @switch (currentView()) {
3567
- @case ('detail') {
3568
- @if (selectedRecord(); as rec) {
3569
- <app-detail-view
3570
- [schema]="schema"
3571
- [record]="rec"
3572
- (back)="switchToList()"
3573
- (edit)="goEdit($event)"
3574
- (delete)="switchToList()"
3575
- />
3576
- }
3577
- }
3578
- @default {
3579
- <header class="mb-6">
3580
- <h1 class="font-display text-2xl font-semibold">
3581
- {{ schema.labelPlural }}
3582
- </h1>
3583
- <p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
3584
- </header>
3585
- <app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
3586
- }
3587
- }
3588
- }
3589
- @case ('dashboard') {
3590
- <app-dashboard [schema]="schema" />
3591
- }
3592
- @default {
3593
- <!-- singleton: formulario directo -->
3594
- <app-form-view />
3595
- }
3596
- }
3597
- }
3598
- } @else {
3599
- <app-empty-state
3600
- title="Recurso no encontrado"
3601
- message="No existe un schema con ese id en el registry."
3602
- />
3603
- }
3604
- `,
3605
- isInline: true,
3606
- dependencies: [
3607
- {
3608
- kind: "component",
3609
- type: Dashboard,
3610
- selector: "app-dashboard",
3611
- inputs: ["schema", "data"]
3612
- },
3613
- {
3614
- kind: "component",
3615
- type: DetailView,
3616
- selector: "app-detail-view",
3617
- inputs: ["schema", "record"],
3618
- outputs: [
3619
- "edit",
3620
- "back",
3621
- "delete"
3622
- ]
3623
- },
3624
- {
3625
- kind: "component",
3626
- type: EmptyState,
3627
- selector: "app-empty-state",
3628
- inputs: ["title", "message"]
3629
- },
3630
- {
3631
- kind: "component",
3632
- type: FormView,
3633
- selector: "app-form-view"
3634
- },
3635
- {
3636
- kind: "component",
3637
- type: ListView,
3638
- selector: "app-list-view",
3639
- inputs: ["schema"],
3640
- outputs: ["viewRecord"]
3641
- }
3642
- ]
3643
- });
3644
- };
3645
- i0.ɵɵngDeclareClassMetadata({
3646
- minVersion: "12.0.0",
3647
- version: "22.2.3",
3648
- ngImport: i0,
3649
- type: ResourcePage,
3650
- decorators: [{
3651
- type: Component,
3652
- args: [{
3653
- selector: "app-resource-page",
3654
- imports: [
3655
- Dashboard,
3656
- DetailView,
3657
- EmptyState,
3658
- FormView,
3659
- ListView
3660
- ],
3661
- template: `
3662
- @if (schema(); as schema) {
3663
- @if (hasNoAccess(schema)) {
3664
- <app-empty-state
3665
- title="Sin permisos"
3666
- message="No tiene permisos para gestionar este recurso."
3667
- />
3668
- } @else {
3669
- @switch (schema.kind) {
3670
- @case ('collection') {
3671
- @switch (currentView()) {
3672
- @case ('detail') {
3673
- @if (selectedRecord(); as rec) {
3674
- <app-detail-view
3675
- [schema]="schema"
3676
- [record]="rec"
3677
- (back)="switchToList()"
3678
- (edit)="goEdit($event)"
3679
- (delete)="switchToList()"
3680
- />
3681
- }
3682
- }
3683
- @default {
3684
- <header class="mb-6">
3685
- <h1 class="font-display text-2xl font-semibold">
3686
- {{ schema.labelPlural }}
3687
- </h1>
3688
- <p class="text-sm text-neutral">Colección · {{ schema.fields.length }} campos</p>
3689
- </header>
3690
- <app-list-view [schema]="schema" (viewRecord)="switchToDetail($event)" />
3691
- }
3692
- }
3693
- }
3694
- @case ('dashboard') {
3695
- <app-dashboard [schema]="schema" />
3696
- }
3697
- @default {
3698
- <!-- singleton: formulario directo -->
3699
- <app-form-view />
3700
- }
3701
- }
3702
- }
3703
- } @else {
3704
- <app-empty-state
3705
- title="Recurso no encontrado"
3706
- message="No existe un schema con ese id en el registry."
3707
- />
3708
- }
3709
- `
3710
- }]
3711
- }],
3712
- ctorParameters: () => []
3713
- });
3714
- var ViewSlot = class ViewSlot {
3715
- schema = input.required(...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3716
- node = input.required(...ngDevMode ? [{ debugName: "node" }] : /* istanbul ignore next */ []);
3717
- parentInstanceId = input.required(...ngDevMode ? [{ debugName: "parentInstanceId" }] : /* istanbul ignore next */ []);
3718
- depth = input(0, ...ngDevMode ? [{ debugName: "depth" }] : /* istanbul ignore next */ []);
3719
- status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
3720
- error = signal(null, ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3721
- payload = signal(null, ...ngDevMode ? [{ debugName: "payload" }] : /* istanbul ignore next */ []);
3722
- nav = inject(ViewNavigationService);
3723
- envInjector = inject(EnvironmentInjector);
3724
- ancla = viewChild("host", {
3725
- ...ngDevMode ? { debugName: "ancla" } : /* istanbul ignore next */ {},
3726
- read: ViewContainerRef
3727
- });
3728
- epoch = 0;
3729
- pendiente = null;
3730
- constructor() {
3731
- effect(() => this.monar());
3732
- effect(() => {
3733
- if (this.status() !== "ready") return;
3734
- const pend = this.pendiente;
3735
- let ancla;
3736
- try {
3737
- ancla = this.ancla();
3738
- } catch {
3739
- ancla = void 0;
3740
- }
3741
- if (!pend || !ancla) return;
3742
- this.pendiente = null;
3743
- ancla.clear();
3744
- ancla.createComponent(pend.tipo, { injector: pend.injector });
3745
- });
3746
- }
3747
- async monar() {
3748
- const esquema = this.schema();
3749
- const nodo = this.node();
3750
- if (!esquema || !nodo) return;
3751
- const turno = ++this.epoch;
3752
- this.pendiente = null;
3753
- if (this.depth() > 3) {
3754
- this.error.set(/* @__PURE__ */ new Error(`Límite de profundidad de composición excedido (3) en ${esquema.id}.${nodo.id}`));
3755
- this.status.set("error");
3756
- return;
3757
- }
3758
- const resuelto = "ref" in nodo ? esquema.uiViews?.[nodo.ref] : nodo;
3759
- if (resuelto === void 0) {
3760
- const ref = "ref" in nodo ? nodo.ref : nodo.id;
3761
- this.error.set(/* @__PURE__ */ new Error(`Referencia '${ref}' inexistente en ${esquema.id}`));
3762
- this.status.set("error");
3763
- return;
3764
- }
3765
- const descriptor = get("view", resuelto.type);
3766
- if (!descriptor) {
3767
- this.error.set(/* @__PURE__ */ new Error(`No existe descriptor kind:view type '${resuelto.type}' en ${esquema.id}`));
3768
- this.status.set("error");
3769
- return;
3770
- }
3771
- const instanceId = instanceIdFor(this.parentInstanceId(), nodo.id);
3772
- const select = "ref" in nodo ? resuelto.select : nodo.select;
3773
- const virtual = virtualSchemaFor(esquema, instanceId, resuelto);
3774
- const emit = select?.mode === "route" ? (event) => {
3775
- this.nav.dispatch(select, event);
3776
- } : void 0;
3777
- this.payload.set(createViewContext({
3778
- schema: virtual,
3779
- resourceId: esquema.id,
3780
- uiView: instanceId,
3781
- viewType: resuelto.type,
3782
- depth: this.depth(),
3783
- ...select !== void 0 ? { select } : {}
3784
- }, emit));
3785
- this.error.set(null);
3786
- this.status.set("loading");
3787
- const injector = Injector.create({
3788
- providers: [{
3789
- provide: ActivatedRoute,
3790
- useValue: syntheticRoute(this.payload())
3791
- }],
3792
- parent: this.envInjector
3793
- });
3794
- try {
3795
- const tipo = await descriptor.load();
3796
- if (turno !== this.epoch) return;
3797
- this.pendiente = {
3798
- tipo,
3799
- injector
3800
- };
3801
- this.status.set("ready");
3802
- } catch (e) {
3803
- if (turno !== this.epoch) return;
3804
- this.error.set(e);
3805
- this.status.set("error");
3806
- }
3807
- }
3808
- mensaje() {
3809
- const e = this.error();
3810
- return e instanceof Error ? e.message : String(e);
3811
- }
3812
- static ɵfac = i0.ɵɵngDeclareFactory({
3813
- minVersion: "12.0.0",
3814
- version: "22.2.3",
3815
- ngImport: i0,
3816
- type: ViewSlot,
3817
- deps: [],
3818
- target: i0.ɵɵFactoryTarget.Component
3819
- });
3820
- static ɵcmp = i0.ɵɵngDeclareComponent({
3821
- minVersion: "17.0.0",
3822
- version: "22.2.3",
3823
- type: ViewSlot,
3824
- isStandalone: true,
3825
- selector: "app-view-slot",
3826
- inputs: {
3827
- schema: {
3828
- classPropertyName: "schema",
3829
- publicName: "schema",
3830
- isSignal: true,
3831
- isRequired: true,
3832
- transformFunction: null
3833
- },
3834
- node: {
3835
- classPropertyName: "node",
3836
- publicName: "node",
3837
- isSignal: true,
3838
- isRequired: true,
3839
- transformFunction: null
3840
- },
3841
- parentInstanceId: {
3842
- classPropertyName: "parentInstanceId",
3843
- publicName: "parentInstanceId",
3844
- isSignal: true,
3845
- isRequired: true,
3846
- transformFunction: null
3847
- },
3848
- depth: {
3849
- classPropertyName: "depth",
3850
- publicName: "depth",
3851
- isSignal: true,
3852
- isRequired: false,
3853
- transformFunction: null
3854
- }
3855
- },
3856
- viewQueries: [{
3857
- propertyName: "ancla",
3858
- first: true,
3859
- predicate: ["host"],
3860
- descendants: true,
3861
- read: ViewContainerRef,
3862
- isSignal: true
3863
- }],
3864
- ngImport: i0,
3865
- template: `
3866
- @if (status() === 'loading') {
3867
- <div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
3868
- } @else if (status() === 'error') {
3869
- <app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
3870
- } @else {
3871
- <div #host></div>
3872
- }
3873
- `,
3874
- isInline: true,
3875
- dependencies: [{
3876
- kind: "component",
3877
- type: EmptyState,
3878
- selector: "app-empty-state",
3879
- inputs: ["title", "message"]
3880
- }],
3881
- changeDetection: i0.ChangeDetectionStrategy.OnPush
3882
- });
3883
- };
3884
- i0.ɵɵngDeclareClassMetadata({
3885
- minVersion: "12.0.0",
3886
- version: "22.2.3",
3887
- ngImport: i0,
3888
- type: ViewSlot,
3889
- decorators: [{
3890
- type: Component,
3891
- args: [{
3892
- selector: "app-view-slot",
3893
- standalone: true,
3894
- imports: [EmptyState],
3895
- changeDetection: ChangeDetectionStrategy.OnPush,
3896
- template: `
3897
- @if (status() === 'loading') {
3898
- <div role="status" class="p-3 text-sm text-neutral">Cargando…</div>
3899
- } @else if (status() === 'error') {
3900
- <app-empty-state title="No se pudo mostrar esta vista" [message]="mensaje()" />
3901
- } @else {
3902
- <div #host></div>
3903
- }
3904
- `
3905
- }]
3906
- }],
3907
- ctorParameters: () => [],
3908
- propDecorators: {
3909
- schema: [{
3910
- type: i0.Input,
3911
- args: [{
3912
- isSignal: true,
3913
- alias: "schema",
3914
- required: true
3915
- }]
3916
- }],
3917
- node: [{
3918
- type: i0.Input,
3919
- args: [{
3920
- isSignal: true,
3921
- alias: "node",
3922
- required: true
3923
- }]
3924
- }],
3925
- parentInstanceId: [{
3926
- type: i0.Input,
3927
- args: [{
3928
- isSignal: true,
3929
- alias: "parentInstanceId",
3930
- required: true
3931
- }]
3932
- }],
3933
- depth: [{
3934
- type: i0.Input,
3935
- args: [{
3936
- isSignal: true,
3937
- alias: "depth",
3938
- required: false
3939
- }]
3940
- }],
3941
- ancla: [{
3942
- type: i0.ViewChild,
3943
- args: ["host", {
3944
- read: ViewContainerRef,
3945
- isSignal: true
3946
- }]
3947
- }]
3948
- }
3949
- });
3950
- var composer_host_exports = /* @__PURE__ */ __exportAll({ ComposerHost: () => ComposerHost });
3951
- var ComposerHost = class ComposerHost {
3952
- route = inject(ActivatedRoute);
3953
- routeData = toSignal(this.route.data, { requireSync: true });
3954
- schema = computed(() => {
3955
- const data = this.routeData();
3956
- const explicit = data["schema"];
3957
- if (explicit !== void 0) return explicit;
3958
- const resourceId = data["resourceId"];
3959
- return typeof resourceId === "string" ? getSchema(resourceId) : void 0;
3960
- }, ...ngDevMode ? [{ debugName: "schema" }] : /* istanbul ignore next */ []);
3961
- uiViewName = computed(() => {
3962
- const v = this.routeData()["uiView"];
3963
- return typeof v === "string" ? v : "";
3964
- }, ...ngDevMode ? [{ debugName: "uiViewName" }] : /* istanbul ignore next */ []);
3965
- profundidad = computed(() => {
3966
- const d = this.routeData()["depth"];
3967
- return typeof d === "number" ? d : 0;
3968
- }, ...ngDevMode ? [{ debugName: "profundidad" }] : /* istanbul ignore next */ []);
3969
- vista = computed(() => {
3970
- const schema = this.schema();
3971
- const nombre = this.uiViewName();
3972
- if (schema === void 0 || nombre === "") return void 0;
3973
- const vista = schema.uiViews?.[nombre];
3974
- return vista !== void 0 && "config" in vista && vista.type === "tabs" ? vista : void 0;
3975
- }, ...ngDevMode ? [{ debugName: "vista" }] : /* istanbul ignore next */ []);
3976
- errorConfig = computed(() => {
3977
- const data = this.routeData();
3978
- const schema = this.schema();
3979
- const nombre = this.uiViewName();
3980
- if (schema === void 0) return `Schema no encontrado para '${String(data["resourceId"] ?? "")}'`;
3981
- if (nombre === "") return "Ruta de composición sin uiView declarada";
3982
- const vista = schema.uiViews?.[nombre];
3983
- if (vista === void 0) return `Vista '${nombre}' no declarada en ${schema.id}`;
3984
- if (vista.type !== "tabs") return `Configuración inválida: ${schema.id}.${nombre} declara '${vista.type}', no es una vista tabs`;
3985
- return null;
3986
- }, ...ngDevMode ? [{ debugName: "errorConfig" }] : /* istanbul ignore next */ []);
3987
- errorValidacion = signal(null, ...ngDevMode ? [{ debugName: "errorValidacion" }] : /* istanbul ignore next */ []);
3988
- error = computed(() => this.errorValidacion() ?? this.errorConfig(), ...ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []);
3989
- hijos = computed(() => this.vista()?.config.children ?? [], ...ngDevMode ? [{ debugName: "hijos" }] : /* istanbul ignore next */ []);
3990
- visibles = computed(() => this.hijos().filter((h) => h.visibleWhen === void 0 || h.visibleWhen.every((cond) => evaluateCondition(cond, {}, {}))), ...ngDevMode ? [{ debugName: "visibles" }] : /* istanbul ignore next */ []);
3991
- seleccion = signal("", ...ngDevMode ? [{ debugName: "seleccion" }] : /* istanbul ignore next */ []);
3992
- activo = computed(() => {
3993
- const vis = this.visibles();
3994
- if (vis.length === 0) return "";
3995
- const sel = this.seleccion();
3996
- if (vis.some((h) => h.id === sel)) return sel;
3997
- const inicial = this.vista()?.config.initial;
3998
- return inicial !== void 0 && vis.some((h) => h.id === inicial) ? inicial : vis[0].id;
3999
- }, ...ngDevMode ? [{ debugName: "activo" }] : /* istanbul ignore next */ []);
4000
- visitados = signal(/* @__PURE__ */ new Set(), ...ngDevMode ? [{ debugName: "visitados" }] : /* istanbul ignore next */ []);
4001
- montados = computed(() => {
4002
- const visibles = new Set(this.visibles().map((h) => h.id));
4003
- const activo = this.activo();
4004
- const visitados = this.visitados();
4005
- return this.hijos().filter((h) => visibles.has(h.id) && (h.id === activo || h.retain === true && visitados.has(h.id)));
4006
- }, ...ngDevMode ? [{ debugName: "montados" }] : /* istanbul ignore next */ []);
4007
- constructor() {
4008
- const schema = this.schema();
4009
- if (schema !== void 0) try {
4010
- validateComposition(schema);
4011
- } catch (e) {
4012
- this.errorValidacion.set(e instanceof Error ? e.message : String(e));
4013
- }
4014
- const inicial = this.activo();
4015
- if (inicial !== "") this.visitados.set(/* @__PURE__ */ new Set([inicial]));
4016
- }
4017
- seleccionar(id, conFoco) {
4018
- this.seleccion.set(id);
4019
- this.visitados.update((v) => new Set(v).add(id));
4020
- if (conFoco) setTimeout(() => document.getElementById(`tab-${id}`)?.focus(), 0);
4021
- }
4022
- onKeydown(event) {
4023
- const vis = this.visibles();
4024
- if (vis.length === 0) return;
4025
- const i = vis.findIndex((h) => h.id === this.activo());
4026
- let next = -1;
4027
- if (event.key === "ArrowRight") next = (i + 1) % vis.length;
4028
- else if (event.key === "ArrowLeft") next = (i - 1 + vis.length) % vis.length;
4029
- else if (event.key === "Home") next = 0;
4030
- else if (event.key === "End") next = vis.length - 1;
4031
- if (next < 0) return;
4032
- event.preventDefault();
4033
- this.seleccionar(vis[next].id, true);
4034
- }
4035
- montado(id) {
4036
- return this.montados().some((h) => h.id === id);
4037
- }
4038
- claseTab(id) {
4039
- const base = "rounded-t-lg px-3 py-2 text-sm transition-colors focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary";
4040
- return id === this.activo() ? `${base} bg-primary/10 font-medium text-primary` : `${base} text-neutral hover:text-primary`;
4041
- }
4042
- etiqueta() {
4043
- return this.schema()?.labelPlural ?? "Secciones";
4044
- }
4045
- static ɵfac = i0.ɵɵngDeclareFactory({
4046
- minVersion: "12.0.0",
4047
- version: "22.2.3",
4048
- ngImport: i0,
4049
- type: ComposerHost,
4050
- deps: [],
4051
- target: i0.ɵɵFactoryTarget.Component
4052
- });
4053
- static ɵcmp = i0.ɵɵngDeclareComponent({
4054
- minVersion: "17.0.0",
4055
- version: "22.2.3",
4056
- type: ComposerHost,
4057
- isStandalone: true,
4058
- selector: "app-composer-host",
4059
- ngImport: i0,
4060
- template: `
4061
- @if (error() !== null) {
4062
- <app-empty-state title="Composición no disponible" [message]="error()!" />
4063
- } @else if (visibles().length === 0) {
4064
- <app-empty-state
4065
- title="Sin secciones disponibles"
4066
- message="La vista no declara secciones visibles."
4067
- />
4068
- } @else {
4069
- <div
4070
- role="tablist"
4071
- class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
4072
- [attr.aria-label]="etiqueta()"
4073
- (keydown)="onKeydown($event)"
4074
- >
4075
- @for (h of visibles(); track h.id) {
4076
- <button
4077
- type="button"
4078
- role="tab"
4079
- [id]="'tab-' + h.id"
4080
- [attr.aria-selected]="h.id === activo()"
4081
- [attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
4082
- [attr.tabindex]="h.id === activo() ? 0 : -1"
4083
- [class]="claseTab(h.id)"
4084
- (click)="seleccionar(h.id, true)"
4085
- >
4086
- {{ h.label }}
4087
- </button>
4088
- }
4089
- </div>
4090
-
4091
- @for (h of montados(); track h.id) {
4092
- <div
4093
- role="tabpanel"
4094
- [id]="'panel-' + h.id"
4095
- [attr.aria-labelledby]="'tab-' + h.id"
4096
- [class.hidden]="h.id !== activo()"
4097
- class="py-3 focus:outline-none"
4098
- tabindex="0"
4099
- >
4100
- <app-view-slot
4101
- [schema]="schema()!"
4102
- [node]="h"
4103
- [parentInstanceId]="uiViewName()"
4104
- [depth]="profundidad() + 1"
4105
- />
4106
- </div>
4107
- }
4108
- }
4109
- `,
4110
- isInline: true,
4111
- dependencies: [{
4112
- kind: "component",
4113
- type: ViewSlot,
4114
- selector: "app-view-slot",
4115
- inputs: [
4116
- "schema",
4117
- "node",
4118
- "parentInstanceId",
4119
- "depth"
4120
- ]
4121
- }, {
4122
- kind: "component",
4123
- type: EmptyState,
4124
- selector: "app-empty-state",
4125
- inputs: ["title", "message"]
4126
- }],
4127
- changeDetection: i0.ChangeDetectionStrategy.OnPush
4128
- });
4129
- };
4130
- i0.ɵɵngDeclareClassMetadata({
4131
- minVersion: "12.0.0",
4132
- version: "22.2.3",
4133
- ngImport: i0,
4134
- type: ComposerHost,
4135
- decorators: [{
4136
- type: Component,
4137
- args: [{
4138
- selector: "app-composer-host",
4139
- standalone: true,
4140
- imports: [ViewSlot, EmptyState],
4141
- changeDetection: ChangeDetectionStrategy.OnPush,
4142
- template: `
4143
- @if (error() !== null) {
4144
- <app-empty-state title="Composición no disponible" [message]="error()!" />
4145
- } @else if (visibles().length === 0) {
4146
- <app-empty-state
4147
- title="Sin secciones disponibles"
4148
- message="La vista no declara secciones visibles."
4149
- />
4150
- } @else {
4151
- <div
4152
- role="tablist"
4153
- class="flex flex-wrap gap-1 border-b border-neutral/20 px-1"
4154
- [attr.aria-label]="etiqueta()"
4155
- (keydown)="onKeydown($event)"
4156
- >
4157
- @for (h of visibles(); track h.id) {
4158
- <button
4159
- type="button"
4160
- role="tab"
4161
- [id]="'tab-' + h.id"
4162
- [attr.aria-selected]="h.id === activo()"
4163
- [attr.aria-controls]="montado(h.id) ? 'panel-' + h.id : null"
4164
- [attr.tabindex]="h.id === activo() ? 0 : -1"
4165
- [class]="claseTab(h.id)"
4166
- (click)="seleccionar(h.id, true)"
4167
- >
4168
- {{ h.label }}
4169
- </button>
4170
- }
4171
- </div>
4172
-
4173
- @for (h of montados(); track h.id) {
4174
- <div
4175
- role="tabpanel"
4176
- [id]="'panel-' + h.id"
4177
- [attr.aria-labelledby]="'tab-' + h.id"
4178
- [class.hidden]="h.id !== activo()"
4179
- class="py-3 focus:outline-none"
4180
- tabindex="0"
4181
- >
4182
- <app-view-slot
4183
- [schema]="schema()!"
4184
- [node]="h"
4185
- [parentInstanceId]="uiViewName()"
4186
- [depth]="profundidad() + 1"
4187
- />
4188
- </div>
4189
- }
4190
- }
4191
- `
4192
- }]
4193
- }],
4194
- ctorParameters: () => []
4195
- });
4196
- var ConfirmDialog = class ConfirmDialog {
4197
- options = input(null, ...ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []);
4198
- danger = input(false, ...ngDevMode ? [{ debugName: "danger" }] : /* istanbul ignore next */ []);
4199
- confirmed = output();
4200
- cancelled = output();
4201
- static ɵfac = i0.ɵɵngDeclareFactory({
4202
- minVersion: "12.0.0",
4203
- version: "22.2.3",
4204
- ngImport: i0,
4205
- type: ConfirmDialog,
4206
- deps: [],
4207
- target: i0.ɵɵFactoryTarget.Component
4208
- });
4209
- static ɵcmp = i0.ɵɵngDeclareComponent({
4210
- minVersion: "17.0.0",
4211
- version: "22.2.3",
4212
- type: ConfirmDialog,
4213
- isStandalone: true,
4214
- selector: "app-confirm-dialog",
4215
- inputs: {
4216
- options: {
4217
- classPropertyName: "options",
4218
- publicName: "options",
4219
- isSignal: true,
4220
- isRequired: false,
4221
- transformFunction: null
4222
- },
4223
- danger: {
4224
- classPropertyName: "danger",
4225
- publicName: "danger",
4226
- isSignal: true,
4227
- isRequired: false,
4228
- transformFunction: null
4229
- }
4230
- },
4231
- outputs: {
4232
- confirmed: "confirmed",
4233
- cancelled: "cancelled"
4234
- },
4235
- ngImport: i0,
4236
- template: `
4237
- @if (options(); as opts) {
4238
- <app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
4239
- <p class="text-sm text-neutral">{{ opts.message }}</p>
4240
- <div class="mt-6 flex justify-end gap-3">
4241
- <button app-button variant="outline" type="button" (click)="cancelled.emit()">
4242
- {{ opts.cancelLabel ?? 'Cancel' }}
4243
- </button>
4244
- <button
4245
- app-button
4246
- type="button"
4247
- [variant]="danger() ? 'danger' : 'primary'"
4248
- (click)="confirmed.emit()"
4249
- >
4250
- {{ opts.confirmLabel ?? 'Confirm' }}
4251
- </button>
4252
- </div>
4253
- </app-modal>
4254
- }
4255
- `,
4256
- isInline: true,
4257
- dependencies: [{
4258
- kind: "component",
4259
- type: Modal,
4260
- selector: "app-modal",
4261
- inputs: [
4262
- "open",
4263
- "title",
4264
- "closeLabel"
4265
- ],
4266
- outputs: ["close"]
4267
- }, {
4268
- kind: "component",
4269
- type: Button,
4270
- selector: "button[app-button]",
4271
- inputs: [
4272
- "variant",
4273
- "size",
4274
- "type",
4275
- "disabled",
4276
- "loading"
4277
- ]
4278
- }]
4279
- });
4280
- };
4281
- i0.ɵɵngDeclareClassMetadata({
4282
- minVersion: "12.0.0",
4283
- version: "22.2.3",
4284
- ngImport: i0,
4285
- type: ConfirmDialog,
4286
- decorators: [{
4287
- type: Component,
4288
- args: [{
4289
- selector: "app-confirm-dialog",
4290
- imports: [Modal, Button],
4291
- template: `
4292
- @if (options(); as opts) {
4293
- <app-modal [open]="true" [title]="opts.title" (close)="cancelled.emit()">
4294
- <p class="text-sm text-neutral">{{ opts.message }}</p>
4295
- <div class="mt-6 flex justify-end gap-3">
4296
- <button app-button variant="outline" type="button" (click)="cancelled.emit()">
4297
- {{ opts.cancelLabel ?? 'Cancel' }}
4298
- </button>
4299
- <button
4300
- app-button
4301
- type="button"
4302
- [variant]="danger() ? 'danger' : 'primary'"
4303
- (click)="confirmed.emit()"
4304
- >
4305
- {{ opts.confirmLabel ?? 'Confirm' }}
4306
- </button>
4307
- </div>
4308
- </app-modal>
4309
- }
4310
- `
4311
- }]
4312
- }],
4313
- propDecorators: {
4314
- options: [{
4315
- type: i0.Input,
4316
- args: [{
4317
- isSignal: true,
4318
- alias: "options",
4319
- required: false
4320
- }]
4321
- }],
4322
- danger: [{
4323
- type: i0.Input,
4324
- args: [{
4325
- isSignal: true,
4326
- alias: "danger",
4327
- required: false
4328
- }]
4329
- }],
4330
- confirmed: [{
4331
- type: i0.Output,
4332
- args: ["confirmed"]
4333
- }],
4334
- cancelled: [{
4335
- type: i0.Output,
4336
- args: ["cancelled"]
4337
- }]
4338
- }
4339
- });
4340
- var Toast = class Toast {
4341
- toasts = input([], ...ngDevMode ? [{ debugName: "toasts" }] : /* istanbul ignore next */ []);
4342
- closeLabel = input("Cerrar", ...ngDevMode ? [{ debugName: "closeLabel" }] : /* istanbul ignore next */ []);
4343
- dismissed = output();
4344
- static ɵfac = i0.ɵɵngDeclareFactory({
4345
- minVersion: "12.0.0",
4346
- version: "22.2.3",
4347
- ngImport: i0,
4348
- type: Toast,
4349
- deps: [],
4350
- target: i0.ɵɵFactoryTarget.Component
4351
- });
4352
- static ɵcmp = i0.ɵɵngDeclareComponent({
4353
- minVersion: "17.0.0",
4354
- version: "22.2.3",
4355
- type: Toast,
4356
- isStandalone: true,
4357
- selector: "app-toast",
4358
- inputs: {
4359
- toasts: {
4360
- classPropertyName: "toasts",
4361
- publicName: "toasts",
4362
- isSignal: true,
4363
- isRequired: false,
4364
- transformFunction: null
4365
- },
4366
- closeLabel: {
4367
- classPropertyName: "closeLabel",
4368
- publicName: "closeLabel",
4369
- isSignal: true,
4370
- isRequired: false,
4371
- transformFunction: null
4372
- }
4373
- },
4374
- outputs: { dismissed: "dismissed" },
4375
- host: {
4376
- attributes: {
4377
- "role": "status",
4378
- "aria-live": "polite"
4379
- },
4380
- classAttribute: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2"
4381
- },
4382
- ngImport: i0,
4383
- template: `
4384
- @for (toast of toasts(); track toast.id) {
4385
- <div
4386
- class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
4387
- [class.border-l-success]="toast.kind === 'success'"
4388
- [class.border-l-danger]="toast.kind === 'error'"
4389
- [class.border-l-primary]="toast.kind === 'info'"
4390
- >
4391
- @if (toast.kind === 'success') {
4392
- <svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
4393
- } @else if (toast.kind === 'error') {
4394
- <svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
4395
- } @else {
4396
- <svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
4397
- }
4398
- <p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
4399
- <button
4400
- type="button"
4401
- class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
4402
- [attr.aria-label]="closeLabel()"
4403
- (click)="dismissed.emit(toast.id)"
4404
- >
4405
- <svg lucideX size="18" />
4406
- </button>
4407
- </div>
4408
- }
4409
- `,
4410
- isInline: true,
4411
- dependencies: [
4412
- {
4413
- kind: "component",
4414
- type: LucideCircleCheck,
4415
- selector: "svg[lucideCircleCheck], svg[lucideCheckCircle2]"
4416
- },
4417
- {
4418
- kind: "component",
4419
- type: LucideCircleX,
4420
- selector: "svg[lucideCircleX], svg[lucideXCircle]"
4421
- },
4422
- {
4423
- kind: "component",
4424
- type: LucideInfo,
4425
- selector: "svg[lucideInfo]"
4426
- },
4427
- {
4428
- kind: "component",
4429
- type: LucideX,
4430
- selector: "svg[lucideX]"
4431
- }
4432
- ]
4433
- });
4434
- };
4435
- i0.ɵɵngDeclareClassMetadata({
4436
- minVersion: "12.0.0",
4437
- version: "22.2.3",
4438
- ngImport: i0,
4439
- type: Toast,
4440
- decorators: [{
4441
- type: Component,
4442
- args: [{
4443
- selector: "app-toast",
4444
- imports: [
4445
- LucideCircleCheck,
4446
- LucideCircleX,
4447
- LucideInfo,
4448
- LucideX
4449
- ],
4450
- host: {
4451
- class: "pointer-events-none fixed right-4 top-4 z-50 flex w-full max-w-sm flex-col gap-2",
4452
- role: "status",
4453
- "aria-live": "polite"
4454
- },
4455
- template: `
4456
- @for (toast of toasts(); track toast.id) {
4457
- <div
4458
- class="pointer-events-auto flex items-start gap-3 border-l-4 bg-surface-raised p-4 shadow-lg"
4459
- [class.border-l-success]="toast.kind === 'success'"
4460
- [class.border-l-danger]="toast.kind === 'error'"
4461
- [class.border-l-primary]="toast.kind === 'info'"
4462
- >
4463
- @if (toast.kind === 'success') {
4464
- <svg lucideCircleCheck size="18" class="mt-0.5 shrink-0 text-success" />
4465
- } @else if (toast.kind === 'error') {
4466
- <svg lucideCircleX size="18" class="mt-0.5 shrink-0 text-danger" />
4467
- } @else {
4468
- <svg lucideInfo size="18" class="mt-0.5 shrink-0 text-primary" />
4469
- }
4470
- <p class="flex-1 text-sm font-medium">{{ toast.message }}</p>
4471
- <button
4472
- type="button"
4473
- class="flex min-h-11 min-w-11 items-center justify-center rounded text-neutral hover:bg-neutral/10 focus-visible:outline-2 focus-visible:outline-primary lg:min-h-0 lg:min-w-0"
4474
- [attr.aria-label]="closeLabel()"
4475
- (click)="dismissed.emit(toast.id)"
4476
- >
4477
- <svg lucideX size="18" />
4478
- </button>
4479
- </div>
4480
- }
4481
- `
4482
- }]
4483
- }],
4484
- propDecorators: {
4485
- toasts: [{
4486
- type: i0.Input,
4487
- args: [{
4488
- isSignal: true,
4489
- alias: "toasts",
4490
- required: false
4491
- }]
4492
- }],
4493
- closeLabel: [{
4494
- type: i0.Input,
4495
- args: [{
4496
- isSignal: true,
4497
- alias: "closeLabel",
4498
- required: false
4499
- }]
4500
- }],
4501
- dismissed: [{
4502
- type: i0.Output,
4503
- args: ["dismissed"]
4504
- }]
4505
- }
4506
- });
4507
- const schemaDescriptions = {
4508
- key: "Nombre de la propiedad en el JSON que envía/recibe el back.",
4509
- label: "Rótulo visible para el usuario en el formulario y la ficha.",
4510
- help: "Texto corto de ayuda que se muestra debajo del campo.",
4511
- placeholder: "Texto sugerente que se muestra dentro del input cuando está vacío.",
4512
- required: "Si es obligatorio. El back también lo valida (R7.4).",
4513
- default: "Valor por defecto si el registro no trae este campo (ej. `default: \"none\"` en un `select`/`dropdown`).",
4514
- readonly: "Si el campo es solo lectura (no editable en el formulario).",
4515
- readonlyOn: "Bloquea el campo solo en creación, solo en edición o siempre.",
4516
- readonlyWhen: "Condición bajo la cual el campo es solo lectura (expresión sobre otros campos).",
4517
- visibleWhen: "Condición bajo la cual el campo se muestra u oculta.",
4518
- hiddenOn: "Oculta el campo en creación, en edición o siempre.",
4519
- dependsOn: "Selección dependiente (guía §1: país → ciudad). Clave del campo padre del mismo nivel.",
4520
- validators: "Reglas de validación (mínimo, máximo, patrón, unicidad, etc.).",
4521
- width: "Ancho en la grilla del formulario (1–12), igual que un widget del dashboard.",
4522
- section: "Id de la sección/pestaña del layout a la que pertenece (base.md §4).",
4523
- read: "Código de nivel de acceso requerido para leer el campo.",
4524
- write: "Código de nivel de acceso requerido para escribir el campo.",
4525
- description: "Descripción corta de para qué sirve este campo. Se muestra como tooltip al pasar el cursor sobre el rótulo.",
4526
- docLink: "URL de documentación externa para este campo (se abre al hacer clic en el tooltip).",
4527
- value: "Valor que se envía al back cuando se elige esta opción.",
4528
- labelOption: "Texto visible de la opción.",
4529
- disabled: "Si la opción está deshabilitada (no se puede elegir).",
4530
- type: "Tipo de campo: `text`, `color`, `number`, `currency`, `boolean`, `select`, `dropdown`, `rich-select`, `multiselect`, `url`, `email`, `phone`, `date`, `time`, `readonly-text`, `slug`, `textarea`, `relation`, `image`, `file`, `group`, `list`, `string-list`, `key-value`.",
4531
- maxLength: "Longitud máxima de caracteres permitida.",
4532
- pattern: "Expresión regular que debe cumplir el valor.",
4533
- rows: "Número de filas visibles del textarea.",
4534
- minWords: "Cantidad mínima de palabras.",
4535
- maxWords: "Cantidad máxima de palabras.",
4536
- tokens: "Marcadores insertables como chips bajo el área (plantillas de mensaje).",
4537
- from: "Campo del registro del que se autogenera el slug.",
4538
- lockAfterCreate: "Bloquea el campo una vez creado el registro (por defecto: sí).",
4539
- min: "Valor mínimo permitido.",
4540
- max: "Valor máximo permitido.",
4541
- step: "Incremento entre valores permitidos.",
4542
- decimals: "Número de decimales mostrados.",
4543
- currencyFrom: "Campo del registro del que se toma la moneda (ej. `currency`).",
4544
- trueLabel: "Texto que se muestra cuando el valor es verdadero.",
4545
- falseLabel: "Texto que se muestra cuando el valor es falso.",
4546
- source: "De dónde salen las opciones: `{ resource, view?, params?, … }` (F8 · R6.5).",
4547
- resource: "Recurso declarado (`resource.id`): su listado es el de las opciones.",
4548
- view: "Vista declarada en `resource.views` (opcional): filtra en el back.",
4549
- params: "Valores interpolados para la vista (`{campo}` ← contexto).",
4550
- labelField: "Etiqueta por defecto de las filas (si no, `titleField` del recurso).",
4551
- valueField: "Valor por defecto de las filas (si no, `keyField` del recurso).",
4552
- secondaryField: "Segundo dato a mostrar junto a la etiqueta (ej. \"Juan Pérez · DNI 123\").",
4553
- onlyActive: "Cargar solo registros activos (lo exige además la FK en el back).",
4554
- accept: "Tipos aceptados (ej. `image/*`, `image/png,image/jpeg` o `pdf,docx`).",
4555
- maxSizeMB: "Tamaño máximo del archivo en megabytes.",
4556
- aspectRatio: "Relación de aspecto de la vista previa (ej. `16/9`).",
4557
- previewSize: "Tamaño de la vista previa.",
4558
- formatCurrency: "Campo del registro del que se toma la moneda (ej. `currency`).",
4559
- display: "Presentación del grupo: `card`, `collapsible` o `inline`.",
4560
- fields: "Campos de cada item (cualquier tipo, incluso `group` o `list`).",
4561
- minItems: "Cantidad mínima de items.",
4562
- maxItems: "Cantidad máxima de items.",
4563
- sortable: "Si se puede reordenar (por defecto: sí).",
4564
- positionField: "Campo donde se guarda el orden dentro de cada item (`position`).",
4565
- addLabel: "Etiqueta del botón para agregar un item nuevo.",
4566
- emptyText: "Texto que se muestra cuando no hay items.",
4567
- itemDisplay: "Cómo se muestra cada item: `card`, `row` o `accordion`.",
4568
- itemFields: "Campos de cada item (cualquier tipo, incluso `group` o `list`).",
4569
- itemTitle: "Campo del item usado como título cuando está colapsado.",
4570
- primaryFirst: "El primer item tiene significado especial (ej. imagen principal).",
4571
- itemType: "Tipo permitido de cada item (base.md §9: solo `text`, `url` o `email`).",
4572
- sort: "Si la lista se ordena alfabéticamente o por posición.",
4573
- filter: "Si el listado permite filtrado por texto.",
4574
- search: "Campos buscables en el listado.",
4575
- filters: "Filtros declarados para el listado.",
4576
- listColumns: "Columnas que se muestran en el listado.",
4577
- searchable: "Si el campo es buscable.",
4578
- filterable: "Si el campo es filtrable.",
4579
- sortableItems: "Si los items de la lista se pueden reordenar.",
4580
- groupBy: "Campo por el que agrupar los items.",
4581
- cardWidth: "Ancho de cada tarjeta de opción en la grilla (1–12). Por defecto `4` (3 tarjetas por fila en escritorio).",
4582
- cardCols: "Cuántas tarjetas mostrar por fila en el punto de ruptura `lg`. Sobrescribe el cálculo automático basado en `cardWidth`.",
4583
- hintMetric: "Texto secundario bajo la cifra (estático, va en el schema).",
4584
- unitChart: "Sufijo de las cifras del eje («unidades», «S/»).",
4585
- segments: "Segmentos de la barra de distribución porcentual.",
4586
- actions: "Acciones de navegación por fila.",
4587
- labelAction: "Etiqueta de la acción.",
4588
- icon: "Icono de la acción.",
4589
- navigateTo: "Ruta de destino de la acción.",
4590
- variant: "Variante visual: `primary`, `secondary` u `outline`.",
4591
- widgets: "Widgets del panel. Se declara en `ResourceSchema.widgets` — no en `fields`, porque un KPI no es un campo.",
4592
- mobileWidth: "Ancho en la grilla en mobile (1–12). Por defecto `12` (ancho completo, apilado).",
4593
- sourceWidget: "De dónde saca sus datos: `DataSource` o `{ kind: \"payload\" }`.",
4594
- hintWidget: "Texto secundario bajo la cifra (estático, va en el schema).",
4595
- unitWidget: "Sufijo de las cifras del eje."
4596
- };
4597
- export { API_DRIVER, API_URL, ApiError, ApiService, AuthService, COLUMN_FORMAT_TYPES, ComposerHost, ConfirmDialog, DEFAULT_RESPONSE_UNWRAP, Dashboard, DataSourceService, ErrorMapperService, FIREBASE_AUTH_ADAPTER, FieldHost, FormView, GAS_SCHEMAS, ListView, Login, MAX_DEPTH, NotificationService, ResourcePage, SESSION_TOKEN_KEY, SESSION_USER_KEY, SUPPORTED_FIELD_TYPE_LIST, SchemaCompositionError, Sidebar, Toast, UploadService, VALUE_SOURCES, actionType, actionTypes, actionsFor, allSchemas, apiErrorInterceptor, authGuard, buildFormSchema, buildPayload, childTree, columnFormatDescriptors, fieldDescriptors, getAction, getAuth, get as getRenderDescriptor, getSchema, groupTree, interpolateTemplate, isSupported as isRenderSupported, isSupported, isShareAction, isTriggerAction, itemTree, layoutDescriptors, listTree, resource_page_exports as n, provideFirebaseAdapter, provideGasAdmin, provideRenderDescriptors, registerAction, register as registerRenderDescriptor, renderKinds, renewedTokenOf, resolveShare, resourceRoutes, schemaDescriptions, seedModel, setSchemaCatalog, shellRoutes, stringItemTree, composer_host_exports as t, unregister as unregisterRenderDescriptor, validateComposition, viewDescriptors };
4598
-
4599
- //# sourceMappingURL=tinya-code-gas-admin-ui.mjs.map