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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (369) hide show
  1. package/README.md +222 -0
  2. package/ng-package.json +7 -0
  3. package/package.json +3 -15
  4. package/src/lib/actions/action-registry.spec.ts +52 -0
  5. package/src/lib/actions/action-registry.ts +126 -0
  6. package/src/lib/actions/action-service.spec.ts +215 -0
  7. package/src/lib/actions/action-service.ts +87 -0
  8. package/src/lib/actions/descriptors.spec.ts +155 -0
  9. package/src/lib/actions/descriptors.ts +154 -0
  10. package/src/lib/columns/column-cell.ts +33 -0
  11. package/src/lib/columns/column-defaults.spec.ts +99 -0
  12. package/src/lib/columns/column-defaults.ts +84 -0
  13. package/src/lib/columns/column-format.spec.ts +274 -0
  14. package/src/lib/columns/descriptors.ts +113 -0
  15. package/src/lib/columns/format-badge.ts +51 -0
  16. package/src/lib/columns/format-boolean.ts +58 -0
  17. package/src/lib/columns/format-currency.ts +30 -0
  18. package/src/lib/columns/format-date.ts +35 -0
  19. package/src/lib/columns/format-number.ts +32 -0
  20. package/src/lib/columns/format-text.ts +23 -0
  21. package/src/lib/columns/format-thumbnail.ts +63 -0
  22. package/src/lib/core/adapters/firebase-auth.adapter.ts +6 -0
  23. package/src/lib/core/guards/auth.guard.spec.ts +88 -0
  24. package/src/lib/core/guards/auth.guard.ts +48 -0
  25. package/src/lib/core/guards/unsaved-changes.guard.ts +16 -0
  26. package/src/lib/core/interceptors/api-error.interceptor.spec.ts +182 -0
  27. package/src/lib/core/interceptors/api-error.interceptor.ts +87 -0
  28. package/src/lib/core/models/api.model.ts +104 -0
  29. package/src/lib/core/models/schema-descriptions.ts +118 -0
  30. package/src/lib/core/models/schema.model.ts +1097 -0
  31. package/src/lib/core/registry/composition-validator.spec.ts +358 -0
  32. package/src/lib/core/registry/composition-validator.ts +270 -0
  33. package/src/lib/core/registry/field-validators.spec.ts +199 -0
  34. package/src/lib/core/registry/field-validators.ts +440 -0
  35. package/src/lib/core/registry/render-host.spec.ts +208 -0
  36. package/src/lib/core/registry/render-host.ts +646 -0
  37. package/src/lib/core/registry/render-registry.spec.ts +96 -0
  38. package/src/lib/core/registry/render-registry.ts +312 -0
  39. package/src/lib/core/registry/schema-catalog.ts +156 -0
  40. package/src/lib/core/services/api.service.spec.ts +1142 -0
  41. package/src/lib/core/services/api.service.ts +371 -0
  42. package/src/lib/core/services/auth.service.spec.ts +653 -0
  43. package/src/lib/core/services/auth.service.ts +503 -0
  44. package/src/lib/core/services/config.service.spec.ts +121 -0
  45. package/src/lib/core/services/config.service.ts +119 -0
  46. package/src/lib/core/services/data-source.service.spec.ts +179 -0
  47. package/src/lib/core/services/data-source.service.ts +142 -0
  48. package/src/lib/core/services/error-mapper.service.spec.ts +50 -0
  49. package/src/lib/core/services/error-mapper.service.ts +49 -0
  50. package/src/lib/core/services/notification.service.ts +73 -0
  51. package/src/lib/core/services/relation-drawer.service.ts +41 -0
  52. package/src/lib/core/services/theme.service.spec.ts +106 -0
  53. package/src/lib/core/services/theme.service.ts +86 -0
  54. package/src/lib/core/services/transport.spec.ts +198 -0
  55. package/src/lib/core/services/transport.ts +164 -0
  56. package/src/lib/core/services/upload.service.ts +81 -0
  57. package/src/lib/core/testing/categories.schema.ts +125 -0
  58. package/src/lib/core/testing/orders.schema.ts +372 -0
  59. package/src/lib/core/testing/products.schema.ts +258 -0
  60. package/src/lib/core/testing/site.schema.ts +195 -0
  61. package/src/lib/core/testing/test-catalog.ts +21 -0
  62. package/src/lib/core/tokens.ts +112 -0
  63. package/src/lib/core/utils/clipboard.spec.ts +55 -0
  64. package/src/lib/core/utils/clipboard.ts +90 -0
  65. package/src/lib/core/utils/context-resolver.spec.ts +95 -0
  66. package/src/lib/core/utils/context-resolver.ts +101 -0
  67. package/src/lib/core/utils/response-unwrapper.spec.ts +70 -0
  68. package/src/lib/core/utils/response-unwrapper.ts +103 -0
  69. package/src/lib/core/utils/source-options.spec.ts +96 -0
  70. package/src/lib/core/utils/source-options.ts +54 -0
  71. package/src/lib/core/utils/template-interpolator.spec.ts +59 -0
  72. package/src/lib/core/utils/template-interpolator.ts +56 -0
  73. package/src/lib/core/view-navigation/view-navigation.service.spec.ts +207 -0
  74. package/src/lib/core/view-navigation/view-navigation.service.ts +132 -0
  75. package/src/lib/fields/boolean/boolean.descriptor.ts +11 -0
  76. package/src/lib/fields/boolean/boolean.ts +40 -0
  77. package/src/lib/fields/color/color.descriptor.ts +12 -0
  78. package/src/lib/fields/color/color.ts +34 -0
  79. package/src/lib/fields/currency/currency.descriptor.ts +12 -0
  80. package/src/lib/fields/currency/currency.ts +67 -0
  81. package/src/lib/fields/date/date.descriptor.ts +12 -0
  82. package/src/lib/fields/date/date.ts +36 -0
  83. package/src/lib/fields/descriptors.ts +122 -0
  84. package/src/lib/fields/display/display-badge.ts +65 -0
  85. package/src/lib/fields/display/display-date.ts +39 -0
  86. package/src/lib/fields/display/display-defaults.ts +84 -0
  87. package/src/lib/fields/display/display-file.ts +41 -0
  88. package/src/lib/fields/display/display-image.ts +54 -0
  89. package/src/lib/fields/display/display-list.ts +107 -0
  90. package/src/lib/fields/display/display-number.ts +41 -0
  91. package/src/lib/fields/display/display-text.ts +42 -0
  92. package/src/lib/fields/display/display.spec.ts +242 -0
  93. package/src/lib/fields/dropdown/dropdown.descriptor.ts +12 -0
  94. package/src/lib/fields/dropdown/dropdown.ts +45 -0
  95. package/src/lib/fields/email/email.descriptor.ts +12 -0
  96. package/src/lib/fields/email/email.ts +28 -0
  97. package/src/lib/fields/field-aria.ts +60 -0
  98. package/src/lib/fields/field-host/field-host.spec.ts +118 -0
  99. package/src/lib/fields/field-host/field-host.ts +47 -0
  100. package/src/lib/fields/field-node.ts +88 -0
  101. package/src/lib/fields/file/file.descriptor.ts +14 -0
  102. package/src/lib/fields/file/file.spec.ts +134 -0
  103. package/src/lib/fields/file/file.ts +281 -0
  104. package/src/lib/fields/group/group.descriptor.ts +21 -0
  105. package/src/lib/fields/group/group.ts +110 -0
  106. package/src/lib/fields/image/image.descriptor.ts +12 -0
  107. package/src/lib/fields/image/image.spec.ts +117 -0
  108. package/src/lib/fields/image/image.ts +281 -0
  109. package/src/lib/fields/key-value/key-value.descriptor.ts +11 -0
  110. package/src/lib/fields/key-value/key-value.ts +283 -0
  111. package/src/lib/fields/list/list.descriptor.ts +21 -0
  112. package/src/lib/fields/list/list.ts +376 -0
  113. package/src/lib/fields/multiselect/multiselect.descriptor.ts +11 -0
  114. package/src/lib/fields/multiselect/multiselect.spec.ts +142 -0
  115. package/src/lib/fields/multiselect/multiselect.ts +152 -0
  116. package/src/lib/fields/number/number.descriptor.ts +12 -0
  117. package/src/lib/fields/number/number.ts +40 -0
  118. package/src/lib/fields/phone/phone.descriptor.ts +12 -0
  119. package/src/lib/fields/phone/phone.ts +46 -0
  120. package/src/lib/fields/readonly-text/readonly-text.descriptor.ts +12 -0
  121. package/src/lib/fields/readonly-text/readonly-text.ts +58 -0
  122. package/src/lib/fields/relation/relation.descriptor.ts +12 -0
  123. package/src/lib/fields/relation/relation.spec.ts +193 -0
  124. package/src/lib/fields/relation/relation.ts +224 -0
  125. package/src/lib/fields/rich-select/rich-select.descriptor.ts +12 -0
  126. package/src/lib/fields/rich-select/rich-select.ts +175 -0
  127. package/src/lib/fields/select/radio-mode.ts +25 -0
  128. package/src/lib/fields/select/select.descriptor.ts +12 -0
  129. package/src/lib/fields/select/select.spec.ts +162 -0
  130. package/src/lib/fields/select/select.ts +210 -0
  131. package/src/lib/fields/slug/slug.descriptor.ts +19 -0
  132. package/src/lib/fields/slug/slug.ts +67 -0
  133. package/src/lib/fields/string-list/string-list.descriptor.ts +11 -0
  134. package/src/lib/fields/string-list/string-list.ts +222 -0
  135. package/src/lib/fields/text/text.descriptor.ts +12 -0
  136. package/src/lib/fields/text/text.ts +35 -0
  137. package/src/lib/fields/textarea/textarea.descriptor.ts +12 -0
  138. package/src/lib/fields/textarea/textarea.spec.ts +117 -0
  139. package/src/lib/fields/textarea/textarea.ts +126 -0
  140. package/src/lib/fields/time/time.descriptor.ts +12 -0
  141. package/src/lib/fields/time/time.ts +28 -0
  142. package/src/lib/fields/unknown-render/unknown-render.ts +49 -0
  143. package/src/lib/fields/url/url.descriptor.ts +12 -0
  144. package/src/lib/fields/url/url.ts +53 -0
  145. package/src/lib/gas-admin-ui.spec.ts +21 -0
  146. package/src/lib/gas-admin-ui.ts +9 -0
  147. package/src/lib/layouts/descriptors.ts +9 -0
  148. package/src/lib/pages/dashboard/dashboard.ts +167 -0
  149. package/src/lib/pages/detail-view/detail-view.spec.ts +333 -0
  150. package/src/lib/pages/detail-view/detail-view.ts +570 -0
  151. package/src/lib/pages/drawer-form/drawer-form.ts +228 -0
  152. package/src/lib/pages/form-view/form-model.ts +88 -0
  153. package/src/lib/pages/form-view/form-schema.ts +255 -0
  154. package/src/lib/pages/form-view/form-view.ts +1622 -0
  155. package/src/lib/pages/list-view/list-view.formats.spec.ts +214 -0
  156. package/src/lib/pages/list-view/list-view.ts +882 -0
  157. package/src/lib/pages/login/login.spec.ts +109 -0
  158. package/src/lib/pages/login/login.ts +227 -0
  159. package/src/lib/pages/resource-page/resource-page.ts +234 -0
  160. package/src/lib/shared/components/badge/badge.ts +35 -0
  161. package/src/lib/shared/components/button/button.ts +65 -0
  162. package/src/lib/shared/components/confirm-dialog/confirm-dialog.ts +48 -0
  163. package/src/lib/shared/components/copy-button/copy-button.ts +56 -0
  164. package/src/lib/shared/components/copy-share/copy-share.spec.ts +122 -0
  165. package/src/lib/shared/components/copy-share/copy-share.ts +107 -0
  166. package/src/lib/shared/components/drawer/drawer.spec.ts +127 -0
  167. package/src/lib/shared/components/drawer/drawer.ts +172 -0
  168. package/src/lib/shared/components/empty-state/empty-state.ts +30 -0
  169. package/src/lib/shared/components/modal/modal.spec.ts +149 -0
  170. package/src/lib/shared/components/modal/modal.ts +145 -0
  171. package/src/lib/shared/components/post-create-modal/post-create-modal.spec.ts +165 -0
  172. package/src/lib/shared/components/post-create-modal/post-create-modal.ts +159 -0
  173. package/src/lib/shared/components/skeleton/skeleton.ts +31 -0
  174. package/src/lib/shared/components/toast/toast.ts +58 -0
  175. package/src/lib/shared/utils/col-span.ts +25 -0
  176. package/src/lib/shared/utils/condition-evaluator.ts +53 -0
  177. package/src/lib/shared/utils/depends-on.ts +48 -0
  178. package/src/lib/shared/utils/dialog-focus.ts +28 -0
  179. package/src/lib/shared/utils/field-path.ts +49 -0
  180. package/src/lib/shared/utils/gap-sorting.ts +144 -0
  181. package/src/lib/shared/utils/seed.spec.ts +64 -0
  182. package/src/lib/shared/utils/seed.ts +149 -0
  183. package/src/lib/shared/utils/slugify.ts +13 -0
  184. package/src/lib/shared/utils/validators.ts +41 -0
  185. package/src/lib/shell/layout/layout.ts +63 -0
  186. package/src/lib/shell/shell.routes.spec.ts +59 -0
  187. package/src/lib/shell/shell.routes.ts +122 -0
  188. package/src/lib/shell/sidebar/sidebar.ts +139 -0
  189. package/src/lib/shell/theme-toggle/theme-toggle.ts +74 -0
  190. package/src/lib/views/calendar-view/calendar-model.ts +426 -0
  191. package/src/lib/views/calendar-view/calendar-view.spec.ts +557 -0
  192. package/src/lib/views/calendar-view/calendar-view.ts +568 -0
  193. package/src/lib/views/composer/composer-host.spec.ts +301 -0
  194. package/src/lib/views/composer/composer-host.ts +248 -0
  195. package/src/lib/views/composer/composer-imports.spec.ts +54 -0
  196. package/src/lib/views/composer/view-context.spec.ts +117 -0
  197. package/src/lib/views/composer/view-context.ts +109 -0
  198. package/src/lib/views/composer/view-slot.spec.ts +130 -0
  199. package/src/lib/views/composer/view-slot.ts +178 -0
  200. package/src/lib/views/descriptors.ts +46 -0
  201. package/src/lib/views/kanban-view/kanban-model.ts +173 -0
  202. package/src/lib/views/kanban-view/kanban-view.spec.ts +540 -0
  203. package/src/lib/views/kanban-view/kanban-view.ts +327 -0
  204. package/src/lib/widgets/bar-chart/bar-chart.descriptor.ts +17 -0
  205. package/src/lib/widgets/bar-chart/bar-chart.spec.ts +66 -0
  206. package/src/lib/widgets/bar-chart/bar-chart.ts +76 -0
  207. package/src/lib/widgets/chart-line/chart-line.descriptor.ts +17 -0
  208. package/src/lib/widgets/chart-line/chart-line.spec.ts +113 -0
  209. package/src/lib/widgets/chart-line/chart-line.ts +219 -0
  210. package/src/lib/widgets/descriptors.ts +29 -0
  211. package/src/lib/widgets/metric-card/metric-card.descriptor.ts +41 -0
  212. package/src/lib/widgets/metric-card/metric-card.spec.ts +52 -0
  213. package/src/lib/widgets/metric-card/metric-card.ts +48 -0
  214. package/src/lib/widgets/quick-actions/quick-actions.descriptor.ts +13 -0
  215. package/src/lib/widgets/quick-actions/quick-actions.spec.ts +61 -0
  216. package/src/lib/widgets/quick-actions/quick-actions.ts +100 -0
  217. package/src/lib/widgets/record-list/record-list.descriptor.ts +16 -0
  218. package/src/lib/widgets/record-list/record-list.spec.ts +79 -0
  219. package/src/lib/widgets/record-list/record-list.ts +115 -0
  220. package/src/lib/widgets/series.spec.ts +42 -0
  221. package/src/lib/widgets/series.ts +56 -0
  222. package/src/lib/widgets/status-progress/status-progress.descriptor.ts +19 -0
  223. package/src/lib/widgets/status-progress/status-progress.spec.ts +63 -0
  224. package/src/lib/widgets/status-progress/status-progress.ts +104 -0
  225. package/src/lib/widgets/widget-host/widget-host.spec.ts +179 -0
  226. package/src/lib/widgets/widget-host/widget-host.ts +99 -0
  227. package/src/public-api.ts +104 -0
  228. package/tinya-code-gas-admin-ui-0.2.0-alpha.15.tgz +0 -0
  229. package/tsconfig.lib.json +13 -0
  230. package/tsconfig.lib.prod.json +11 -0
  231. package/tsconfig.spec.json +10 -0
  232. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs +0 -84
  233. package/fesm2022/tinya-code-gas-admin-ui-badge-EeCA_l-o.mjs.map +0 -1
  234. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs +0 -140
  235. package/fesm2022/tinya-code-gas-admin-ui-bar-chart-G7sEMSBY.mjs.map +0 -1
  236. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs +0 -113
  237. package/fesm2022/tinya-code-gas-admin-ui-boolean-B2ASKL0E.mjs.map +0 -1
  238. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs +0 -163
  239. package/fesm2022/tinya-code-gas-admin-ui-button-BQjNW8KB.mjs.map +0 -1
  240. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs +0 -1027
  241. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +0 -1
  242. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs +0 -278
  243. package/fesm2022/tinya-code-gas-admin-ui-chart-line-BjdlKHoH.mjs.map +0 -1
  244. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +0 -107
  245. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +0 -1
  246. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs +0 -101
  247. package/fesm2022/tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map +0 -1
  248. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs +0 -141
  249. package/fesm2022/tinya-code-gas-admin-ui-currency-BvuF3Z63.mjs.map +0 -1
  250. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs +0 -88
  251. package/fesm2022/tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map +0 -1
  252. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs +0 -107
  253. package/fesm2022/tinya-code-gas-admin-ui-date-1SJGXZrd.mjs.map +0 -1
  254. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs +0 -140
  255. package/fesm2022/tinya-code-gas-admin-ui-display-badge-fwk6sDU7.mjs.map +0 -1
  256. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs +0 -119
  257. package/fesm2022/tinya-code-gas-admin-ui-display-date-D7zLiDiG.mjs.map +0 -1
  258. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs +0 -125
  259. package/fesm2022/tinya-code-gas-admin-ui-display-file-BiIhqvQC.mjs.map +0 -1
  260. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs +0 -152
  261. package/fesm2022/tinya-code-gas-admin-ui-display-image-C1dhnZi1.mjs.map +0 -1
  262. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs +0 -211
  263. package/fesm2022/tinya-code-gas-admin-ui-display-list-DOEuUbGe.mjs.map +0 -1
  264. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs +0 -116
  265. package/fesm2022/tinya-code-gas-admin-ui-display-number-DDMzr4Hn.mjs.map +0 -1
  266. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs +0 -111
  267. package/fesm2022/tinya-code-gas-admin-ui-display-text-B8CdVNpv.mjs.map +0 -1
  268. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +0 -122
  269. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +0 -1
  270. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs +0 -107
  271. package/fesm2022/tinya-code-gas-admin-ui-email-CT_O5ihA.mjs.map +0 -1
  272. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs +0 -109
  273. package/fesm2022/tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs.map +0 -1
  274. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs +0 -45
  275. package/fesm2022/tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map +0 -1
  276. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs +0 -50
  277. package/fesm2022/tinya-code-gas-admin-ui-field-aria-BD1Wiggc.mjs.map +0 -1
  278. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs +0 -24
  279. package/fesm2022/tinya-code-gas-admin-ui-field-node-CIK6KNN_.mjs.map +0 -1
  280. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs +0 -27
  281. package/fesm2022/tinya-code-gas-admin-ui-field-path-B4-FTBGZ.mjs.map +0 -1
  282. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs +0 -408
  283. package/fesm2022/tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map +0 -1
  284. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs +0 -4516
  285. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +0 -1
  286. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs +0 -82
  287. package/fesm2022/tinya-code-gas-admin-ui-format-badge-CK8ct5Wk.mjs.map +0 -1
  288. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs +0 -105
  289. package/fesm2022/tinya-code-gas-admin-ui-format-boolean-ClzezZz-.mjs.map +0 -1
  290. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs +0 -65
  291. package/fesm2022/tinya-code-gas-admin-ui-format-currency-CVmniKml.mjs.map +0 -1
  292. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs +0 -65
  293. package/fesm2022/tinya-code-gas-admin-ui-format-date-Dr1YsDd_.mjs.map +0 -1
  294. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs +0 -65
  295. package/fesm2022/tinya-code-gas-admin-ui-format-number-CWCjpI4n.mjs.map +0 -1
  296. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs +0 -59
  297. package/fesm2022/tinya-code-gas-admin-ui-format-text-DBOgJkH0.mjs.map +0 -1
  298. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs +0 -94
  299. package/fesm2022/tinya-code-gas-admin-ui-format-thumbnail-PCRvxWKt.mjs.map +0 -1
  300. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs +0 -72
  301. package/fesm2022/tinya-code-gas-admin-ui-gap-sorting-iTdy_DYG.mjs.map +0 -1
  302. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs +0 -238
  303. package/fesm2022/tinya-code-gas-admin-ui-group-h7-O8tt4.mjs.map +0 -1
  304. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs +0 -416
  305. package/fesm2022/tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map +0 -1
  306. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs +0 -439
  307. package/fesm2022/tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map +0 -1
  308. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs +0 -513
  309. package/fesm2022/tinya-code-gas-admin-ui-key-value-nVdgg5at.mjs.map +0 -1
  310. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs +0 -637
  311. package/fesm2022/tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map +0 -1
  312. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs +0 -127
  313. package/fesm2022/tinya-code-gas-admin-ui-metric-card-DnGMQL8l.mjs.map +0 -1
  314. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs +0 -251
  315. package/fesm2022/tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map +0 -1
  316. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs +0 -66
  317. package/fesm2022/tinya-code-gas-admin-ui-notification.service-LjSULER6.mjs.map +0 -1
  318. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs +0 -114
  319. package/fesm2022/tinya-code-gas-admin-ui-number-JNr7glc8.mjs.map +0 -1
  320. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs +0 -116
  321. package/fesm2022/tinya-code-gas-admin-ui-phone-jR417hPf.mjs.map +0 -1
  322. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs +0 -235
  323. package/fesm2022/tinya-code-gas-admin-ui-quick-actions-DCQ6emmF.mjs.map +0 -1
  324. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs +0 -8
  325. package/fesm2022/tinya-code-gas-admin-ui-radio-mode-DxvN07DY.mjs.map +0 -1
  326. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs +0 -124
  327. package/fesm2022/tinya-code-gas-admin-ui-readonly-text-BL7-4qie.mjs.map +0 -1
  328. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs +0 -249
  329. package/fesm2022/tinya-code-gas-admin-ui-record-list-BiZfnd_7.mjs.map +0 -1
  330. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs +0 -39
  331. package/fesm2022/tinya-code-gas-admin-ui-relation-drawer.service-B0kT8EiW.mjs.map +0 -1
  332. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs +0 -309
  333. package/fesm2022/tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map +0 -1
  334. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs +0 -576
  335. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +0 -1
  336. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +0 -161
  337. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +0 -1
  338. package/fesm2022/tinya-code-gas-admin-ui-rolldown-runtime-DpJrqANO.mjs +0 -11
  339. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs +0 -685
  340. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +0 -1
  341. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs +0 -80
  342. package/fesm2022/tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map +0 -1
  343. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs +0 -296
  344. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +0 -1
  345. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs +0 -119
  346. package/fesm2022/tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs.map +0 -1
  347. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs +0 -158
  348. package/fesm2022/tinya-code-gas-admin-ui-slug-DU58cbiY.mjs.map +0 -1
  349. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs +0 -26
  350. package/fesm2022/tinya-code-gas-admin-ui-source-options-B9R4mw8-.mjs.map +0 -1
  351. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs +0 -205
  352. package/fesm2022/tinya-code-gas-admin-ui-status-progress-DmeG8htE.mjs.map +0 -1
  353. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs +0 -432
  354. package/fesm2022/tinya-code-gas-admin-ui-string-list-gleasqzD.mjs.map +0 -1
  355. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs +0 -36
  356. package/fesm2022/tinya-code-gas-admin-ui-template-interpolator-ewv7CkNX.mjs.map +0 -1
  357. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs +0 -107
  358. package/fesm2022/tinya-code-gas-admin-ui-text-CUPmL7W_.mjs.map +0 -1
  359. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs +0 -223
  360. package/fesm2022/tinya-code-gas-admin-ui-textarea-CGLSIW13.mjs.map +0 -1
  361. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs +0 -107
  362. package/fesm2022/tinya-code-gas-admin-ui-time-BcHzsTWf.mjs.map +0 -1
  363. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs +0 -60
  364. package/fesm2022/tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map +0 -1
  365. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs +0 -141
  366. package/fesm2022/tinya-code-gas-admin-ui-url-GO4LZi__.mjs.map +0 -1
  367. package/fesm2022/tinya-code-gas-admin-ui.mjs +0 -4599
  368. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +0 -1
  369. package/types/tinya-code-gas-admin-ui.d.ts +0 -3632
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Re-exporta `FirebaseAuthAdapter` desde `tokens.ts` (única fuente de verdad).
3
+ * Mantiene este archivo como punto de importación estable para consumidores
4
+ * que ya referencian esta ruta, sin duplicar la definición de la interfaz.
5
+ */
6
+ export type { FirebaseAuthAdapter } from '../tokens';
@@ -0,0 +1,88 @@
1
+ import { provideHttpClient } from '@angular/common/http';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { ActivatedRouteSnapshot, Router, UrlTree, provideRouter } from '@angular/router';
4
+
5
+ import { API_URL } from '../tokens';
6
+ import { SESSION_TOKEN_KEY, SESSION_USER_KEY } from '../services/auth.service';
7
+ import { authGuard, roleGuard } from './auth.guard';
8
+
9
+ /**
10
+ * R2.10 — guards de sesión (auth.md §5.3).
11
+ *
12
+ * No son una barrera (el back re-verifica desde R2.6): son el filtro de
13
+ * pantalla. Por eso aquí sólo se comprueba HACIA DÓNDE mandan, no qué
14
+ * protegen.
15
+ */
16
+ describe('authGuard / roleGuard — filtro de pantalla (R2.10)', () => {
17
+ afterEach(() => sessionStorage.clear());
18
+
19
+ /** `role === null` ⇒ sin sesión: no se siembra nada en el storage. */
20
+ function setup(role: string | null = 'admin') {
21
+ sessionStorage.clear();
22
+ if (role !== null) {
23
+ sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok');
24
+ sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role }));
25
+ }
26
+
27
+ TestBed.configureTestingModule({
28
+ providers: [provideHttpClient(), provideRouter([]), { provide: API_URL, useValue: '/api' }],
29
+ });
30
+ return { authGuard, roleGuard, router: TestBed.inject(Router) };
31
+ }
32
+
33
+ /** `route` mínimo con un solo parámetro de ruta. */
34
+ function routeWithId(id: string | null): ActivatedRouteSnapshot {
35
+ return {
36
+ paramMap: {
37
+ get: (key: string) => (key === 'id' ? id : null),
38
+ },
39
+ } as unknown as ActivatedRouteSnapshot;
40
+ }
41
+
42
+ const isUrlTree = (r: unknown): r is UrlTree => r instanceof UrlTree;
43
+
44
+ /**
45
+ * El router invoca un guard DENTRO de un contexto de inyección; una llamada
46
+ * directa desde el spec no lo tiene, y `inject(AuthService)` revienta.
47
+ * `runInInjectionContext` reproducía el contexto real del router.
48
+ */
49
+ function run(guard: typeof authGuard, route: ActivatedRouteSnapshot): boolean | UrlTree {
50
+ const result = TestBed.runInInjectionContext(() => guard(route, {} as never));
51
+ // Los dos guards son síncronos: `MaybeAsync` sólo es ruido de tipos acá.
52
+ return result as boolean | UrlTree;
53
+ }
54
+
55
+ it('con sesión deja pasar', () => {
56
+ const { authGuard: guard } = setup();
57
+ expect(run(guard, {} as ActivatedRouteSnapshot)).toBe(true);
58
+ });
59
+
60
+ it('sin sesión manda a /login', () => {
61
+ const { authGuard: guard, router } = setup(null);
62
+ const result = run(guard, {} as ActivatedRouteSnapshot);
63
+ expect(isUrlTree(result)).toBe(true);
64
+ expect(router.serializeUrl(result as UrlTree)).toBe('/login');
65
+ });
66
+
67
+ it('sin sesión, un `:id` también manda a /login (redundancia barata)', () => {
68
+ const { roleGuard: guard, router } = setup(null);
69
+ const result = run(guard, routeWithId('orders'));
70
+ expect(isUrlTree(result)).toBe(true);
71
+ expect(router.serializeUrl(result as UrlTree)).toBe('/login');
72
+ });
73
+
74
+ it('un `:id` desconocido deja que responda el back (no inventamos política)', () => {
75
+ const { roleGuard: guard } = setup();
76
+ expect(run(guard, routeWithId('no-existe'))).toBe(true);
77
+ });
78
+
79
+ it('una ruta sin `:id` no decide nada', () => {
80
+ const { roleGuard: guard } = setup();
81
+ expect(run(guard, routeWithId(null))).toBe(true);
82
+ });
83
+
84
+ it('un recurso sin `access` declarado se ve con sesión', () => {
85
+ const { roleGuard: guard } = setup('staff');
86
+ expect(run(guard, routeWithId('dashboard'))).toBe(true);
87
+ });
88
+ });
@@ -0,0 +1,48 @@
1
+ import { inject } from '@angular/core';
2
+ import type { CanActivateFn } from '@angular/router';
3
+ import { Router } from '@angular/router';
4
+
5
+ import { getSchema } from '../registry/schema-catalog';
6
+ import { AuthService, LOGIN_PATH } from '../services/auth.service';
7
+
8
+ /**
9
+ * Guards de sesión (auth.md §5.3).
10
+ *
11
+ * Dos puertas y ninguna es una barrera de seguridad: el back re-verifica
12
+ * `accessLevel` en **cada** petición desde R2.6. Éstas sólo evitan pintar
13
+ * una pantalla que no le toca al usuario — enseñarle un formulario que va
14
+ * a recibir 403 es mala UX, no un agujero.
15
+ *
16
+ * `authGuard` va en la ruta madre (todo el shell); `roleGuard` en cada
17
+ * `:id`, que es donde aparece el recurso que puede tener `access`.
18
+ */
19
+
20
+ /** Sin sesión → login. Corre ANTES que cualquier ruta del shell. */
21
+ export const authGuard: CanActivateFn = () => {
22
+ const auth = inject(AuthService);
23
+ if (auth.isAuthenticated()) return true;
24
+ return inject(Router).createUrlTree([LOGIN_PATH]);
25
+ };
26
+
27
+ /**
28
+ * El recurso declara `access` y esta sesión no lo cumple → se cancela la
29
+ * navegación. No hay pantalla de 403 (tampoco la pide §5.3) y el menú ya
30
+ * oculta la entrada, así que queda sólo el caso de alguien tecleando la URL.
31
+ */
32
+ export const roleGuard: CanActivateFn = (route) => {
33
+ const auth = inject(AuthService);
34
+ const router = inject(Router);
35
+
36
+ // Redundante con `authGuard` (la madre ya filtra) pero barato: un hijo
37
+ // nunca debe dejar una sesión muerta navegando sola.
38
+ if (!auth.isAuthenticated()) return router.createUrlTree([LOGIN_PATH]);
39
+
40
+ const id = route.data?.['resourceId'] ?? route.paramMap.get('id');
41
+ // Ruta sin recurso declarado, o id que no existe en el registry → que
42
+ // responda el back (404). No inventar una política sobre datos ajenos.
43
+ if (id === null) return true;
44
+ const schema = getSchema(String(id));
45
+ if (schema === undefined) return true;
46
+
47
+ return auth.canAccess(schema.access);
48
+ };
@@ -0,0 +1,16 @@
1
+ import { CanDeactivateFn } from '@angular/router';
2
+
3
+ /** Componente que expone si se puede salir (base.md §13.9: cambios sin guardar). */
4
+ export interface CanLeaveWhenDirty {
5
+ canDeactivate(): boolean | Promise<boolean>;
6
+ }
7
+
8
+ /**
9
+ * Avisa antes de salir con cambios sin guardar. El componente decide:
10
+ * `canDeactivate()` devuelve `true` para salir limpio o una `Promise` que
11
+ * resuelve según la respuesta del usuario (vía `NotificationService.confirm`).
12
+ */
13
+ export const unsavedChangesGuard: CanDeactivateFn<unknown> = (component) => {
14
+ const candidate = component as Partial<CanLeaveWhenDirty> | null | undefined;
15
+ return candidate?.canDeactivate?.() ?? true;
16
+ };
@@ -0,0 +1,182 @@
1
+ import { HttpClient, provideHttpClient, withInterceptors } from '@angular/common/http';
2
+ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
3
+ import { TestBed } from '@angular/core/testing';
4
+ import { provideRouter } from '@angular/router';
5
+
6
+ import { ApiError } from '../models/api.model';
7
+ import { API_URL } from '../tokens';
8
+ import { AuthService, SESSION_TOKEN_KEY, SESSION_USER_KEY } from '../services/auth.service';
9
+ import { apiErrorInterceptor } from './api-error.interceptor';
10
+
11
+ /**
12
+ * Emite `body` en el canal de éxito (el backend responde 200 con el error
13
+ * dentro) y devuelve el `ApiError` que el interceptor lanzó.
14
+ * A nivel de módulo: lo usan el describe de mensajes y el de R2.9.
15
+ */
16
+ function errorFromBody(body: object): ApiError {
17
+ const http = TestBed.inject(HttpClient);
18
+ const mock = TestBed.inject(HttpTestingController);
19
+ let lanzado: unknown;
20
+ http.get('/recursos').subscribe({
21
+ error: (e: unknown) => {
22
+ lanzado = e;
23
+ },
24
+ });
25
+ mock.expectOne('/recursos').flush(body);
26
+ mock.verify();
27
+ return lanzado as ApiError;
28
+ }
29
+
30
+ /**
31
+ * Plan 7.3 — mensajes en lenguaje humano (guía §2).
32
+ *
33
+ * Dos canales distintos: el body del backend (que ya escribe en español) y
34
+ * el transporte (red caída, proxy, 5xx), que sin normalizar deja en pantalla
35
+ * el texto crudo de Angular: «Http failure response for http://…».
36
+ *
37
+ * **R2.9** — desde que el interceptor entrega los `AUTH_*` a
38
+ * `AuthService.applyAuthCode`, necesita poder inyectarlo: por eso acá hay
39
+ * router y `API_URL`, que son las dependencias de ese servicio.
40
+ */
41
+ describe('apiErrorInterceptor — mensajes humanos', () => {
42
+ beforeEach(() => {
43
+ sessionStorage.clear();
44
+ TestBed.configureTestingModule({
45
+ providers: [
46
+ provideHttpClient(withInterceptors([apiErrorInterceptor])),
47
+ provideHttpClientTesting(),
48
+ provideRouter([]),
49
+ { provide: API_URL, useValue: '/api' },
50
+ ],
51
+ });
52
+ });
53
+
54
+ afterEach(() => sessionStorage.clear());
55
+
56
+ function failWith(status: number, body?: unknown): string {
57
+ const http = TestBed.inject(HttpClient);
58
+ const mock = TestBed.inject(HttpTestingController);
59
+ let message = '';
60
+ http.get('/recursos').subscribe({
61
+ error: (error: unknown) => {
62
+ message = error instanceof ApiError ? error.message : String(error);
63
+ },
64
+ });
65
+ const request = mock.expectOne('/recursos');
66
+ if (status === 0) {
67
+ // Red caída: el backend no llegó a responder.
68
+ request.error(new ErrorEvent('error'));
69
+ } else {
70
+ // `flush` con status fuera de 2xx materializa un `HttpErrorResponse`.
71
+ request.flush(body ?? '', { status, statusText: 'ERR' });
72
+ }
73
+ mock.verify();
74
+ return message;
75
+ }
76
+
77
+ it('un 5xx se explica en español, no con el texto crudo de Angular', () => {
78
+ expect(failWith(500)).toBe(
79
+ 'El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.',
80
+ );
81
+ });
82
+
83
+ it('la red caída (status 0) dice que no hubo conexión', () => {
84
+ expect(failWith(0)).toBe('No se pudo conectar con el servidor. Revisa tu conexión.');
85
+ });
86
+
87
+ it('un 403 habla de permisos', () => {
88
+ expect(failWith(403)).toBe('No tienes permisos para realizar esta acción.');
89
+ });
90
+
91
+ it('conserva el mensaje del backend cuando viene en el body (canal de éxito)', () => {
92
+ expect(
93
+ failWith(200, {
94
+ error: { status: 422, message: 'Ya existe un registro con este valor', path: 'name' },
95
+ }),
96
+ ).toBe('Ya existe un registro con este valor');
97
+ });
98
+
99
+ /**
100
+ * D42 — el422 manda sus rutas en `error.errors[]`, NO en `path`: sin
101
+ * modelar esa lista el form veía «Errores de validación» y no pintaba
102
+ * ningún campo (base.md §5, guía §10).
103
+ */
104
+ it('conserva errors[] completo (path, message, code) en el ApiError', () => {
105
+ const error = errorFromBody({
106
+ error: {
107
+ status: 422,
108
+ message: 'Errores de validación',
109
+ errors: [
110
+ {
111
+ path: 'sku',
112
+ message: 'Debe tener al menos 5 caracteres.',
113
+ code: 'VALIDATION_MIN_LENGTH',
114
+ },
115
+ { path: 'faq[2].answer', message: 'Campo obligatorio.', code: 'VALIDATION_REQUIRED' },
116
+ ],
117
+ },
118
+ });
119
+ expect(error.status).toBe(422);
120
+ expect(error.message).toBe('Errores de validación');
121
+ expect(error.issues).toEqual([
122
+ { path: 'sku', message: 'Debe tener al menos 5 caracteres.', code: 'VALIDATION_MIN_LENGTH' },
123
+ { path: 'faq[2].answer', message: 'Campo obligatorio.', code: 'VALIDATION_REQUIRED' },
124
+ ]);
125
+ // Sin `path` en el sobre no hay fieldPath: las rutas vienen en los issues.
126
+ expect(error.fieldPath).toBeUndefined();
127
+ });
128
+ });
129
+
130
+ /**
131
+ * R2.9 — éste es el ÚNICO puente entre el sobre de error y la tabla de
132
+ * §4.5. Si el interceptor dejara de llamar a `applyAuthCode`, el front se
133
+ * quedaría en una pantalla caducada sin reloguear: por eso el puente se
134
+ * prueba acá y las cinco filas de la tabla, en `auth.service.spec.ts`.
135
+ */
136
+ describe('apiErrorInterceptor — entrega de los códigos de sesión (R2.9)', () => {
137
+ beforeEach(() => {
138
+ sessionStorage.clear();
139
+ sessionStorage.setItem(SESSION_TOKEN_KEY, 'tok-vigente');
140
+ sessionStorage.setItem(SESSION_USER_KEY, JSON.stringify({ id: 'u_1', role: 'admin' }));
141
+ TestBed.configureTestingModule({
142
+ providers: [
143
+ provideHttpClient(withInterceptors([apiErrorInterceptor])),
144
+ provideHttpClientTesting(),
145
+ provideRouter([]),
146
+ { provide: API_URL, useValue: '/api' },
147
+ ],
148
+ });
149
+ });
150
+
151
+ afterEach(() => sessionStorage.clear());
152
+
153
+ it('conserva el `code` en el ApiError que recibe el llamador', () => {
154
+ const error = errorFromBody({
155
+ error: { status: 401, message: 'No autorizado', code: 'AUTH_EXPIRED' },
156
+ });
157
+ expect(error.code).toBe('AUTH_EXPIRED');
158
+ expect(error.status).toBe(401);
159
+ });
160
+
161
+ it('un error SIN código de sesión no toca la sesión', () => {
162
+ errorFromBody({ error: { status: 422, message: 'Datos no válidos', path: 'name' } });
163
+ expect(sessionStorage.getItem(SESSION_TOKEN_KEY)).toBe('tok-vigente');
164
+ expect(TestBed.inject(AuthService).isAuthenticated()).toBe(true);
165
+ });
166
+
167
+ it('FORBIDDEN deja al usuario DENTRO de la sesión (§4.5)', () => {
168
+ errorFromBody({ error: { status: 403, message: 'Prohibido', code: 'FORBIDDEN' } });
169
+ expect(sessionStorage.getItem(SESSION_TOKEN_KEY)).toBe('tok-vigente');
170
+ expect(TestBed.inject(AuthService).isAuthenticated()).toBe(true);
171
+ });
172
+
173
+ it('AUTH_INVALID borra token y usuario ANTES de nada más (§5.4)', () => {
174
+ errorFromBody({ error: { status: 401, message: 'No autorizado', code: 'AUTH_INVALID' } });
175
+ expect(sessionStorage.getItem(SESSION_TOKEN_KEY)).toBeNull();
176
+ expect(sessionStorage.getItem(SESSION_USER_KEY)).toBeNull();
177
+ const auth = TestBed.inject(AuthService);
178
+ expect(auth.token()).toBeNull();
179
+ expect(auth.user()).toBeNull();
180
+ expect(auth.isAuthenticated()).toBe(false);
181
+ });
182
+ });
@@ -0,0 +1,87 @@
1
+ import { HttpErrorResponse, HttpInterceptorFn, HttpResponse } from '@angular/common/http';
2
+ import { inject } from '@angular/core';
3
+ import { catchError, map, throwError } from 'rxjs';
4
+
5
+ import { ApiError } from '../models/api.model';
6
+ import type { ApiErrorBody } from '../models/api.model';
7
+ import { AuthService } from '../services/auth.service';
8
+
9
+ /**
10
+ * Mensaje humano para un error HTTP real (§2).
11
+ *
12
+ * El backend siempre responde 200 con el error dentro del body —eso ya
13
+ * viene en español—, así que esto sólo cubre lo que el transporte falla:
14
+ * red caída, proxy y 5xx. Sin esto el usuario ve el texto crudo de Angular
15
+ * («Http failure response for http://…»), que es inglés técnico.
16
+ */
17
+ function httpMessage(status: number): string {
18
+ switch (status) {
19
+ case 0:
20
+ return 'No se pudo conectar con el servidor. Revisa tu conexión.';
21
+ case 401:
22
+ return 'Tu sesión ha expirado. Vuelve a iniciar sesión.';
23
+ case 403:
24
+ return 'No tienes permisos para realizar esta acción.';
25
+ case 404:
26
+ return 'No se encontró el recurso solicitado.';
27
+ case 409:
28
+ return 'El registro cambió o ya existe. Recarga la página y vuelve a intentarlo.';
29
+ case 413:
30
+ return 'El archivo es demasiado grande.';
31
+ case 422:
32
+ return 'Los datos enviados no son válidos.';
33
+ default:
34
+ if (status >= 500) {
35
+ return 'El servidor tuvo un problema. Vuelve a intentarlo en unos segundos.';
36
+ }
37
+ return status >= 400 ? 'No se pudo completar la operación.' : 'Error inesperado.';
38
+ }
39
+ }
40
+
41
+ function isErrorBody(body: unknown): body is ApiErrorBody {
42
+ if (typeof body !== 'object' || body === null) {
43
+ return false;
44
+ }
45
+ const error = (body as ApiErrorBody).error;
46
+ return typeof error === 'object' && error !== null && typeof error.message === 'string';
47
+ }
48
+
49
+ /**
50
+ * El backend responde SIEMPRE HTTP 200 y pone los errores en el body
51
+ * (api.md §7), así que se detectan en el canal de **éxito** y se relanzan
52
+ * como `ApiError` (con `fieldPath` cuando el backend envía `path`).
53
+ * Los errores HTTP reales (red caída, proxy) también se normalizan.
54
+ *
55
+ * **R2.9 — paso dos de dos.** Éste archivo es el ÚNICO punto donde el
56
+ * sobre de error se abre; lo que decide qué ocurre con un `AUTH_*` es
57
+ * `AuthService.applyAuthCode` (la tabla de §4.5). Aquí sólo se le entrega
58
+ * el código y se propaga el `ApiError` con él — un segundo sitio que
59
+ * «interprete» el código sería una regla duplicada y, a la larga, una que
60
+ * se contradiga con la primera.
61
+ */
62
+ export const apiErrorInterceptor: HttpInterceptorFn = (req, next) => {
63
+ const auth = inject(AuthService);
64
+
65
+ return next(req).pipe(
66
+ map((event) => {
67
+ if (event instanceof HttpResponse && isErrorBody(event.body)) {
68
+ const { status, message, path, code, errors } = event.body.error;
69
+ // R2.9: ésta es la ÚNICA llamada a la tabla de §4.5.
70
+ auth.applyAuthCode(code);
71
+ // `errors[]` es el422 con rutas (D42): va entero al ApiError para que
72
+ // `ErrorMapperService` lo convierta en `serverErrors[prefix + key]`.
73
+ throw new ApiError(status, message, path, code, errors ?? []);
74
+ }
75
+ return event;
76
+ }),
77
+ catchError((err: unknown) => {
78
+ if (err instanceof ApiError) {
79
+ return throwError(() => err);
80
+ }
81
+ if (err instanceof HttpErrorResponse) {
82
+ return throwError(() => new ApiError(err.status, httpMessage(err.status)));
83
+ }
84
+ return throwError(() => err);
85
+ }),
86
+ );
87
+ };
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Contrato HTTP con el backend (doc/baseapi.md §8 «Errores»).
3
+ *
4
+ * El backend responde SIEMPRE HTTP 200; los errores viajan en el body
5
+ * con `{ error: { status, message, errors?: [{ path, message }] } }`.
6
+ * El interceptor (Fase 3) los convierte en `ApiError`.
7
+ */
8
+
9
+ /**
10
+ * Un error con ruta del422 (auth.md §6): `path` para pintarlo en el campo y
11
+ * `code` MAYUS_SNAKE (`VALIDATION_MIN_LENGTH`) para mapearlo sin leer texto.
12
+ */
13
+ export interface ApiIssue {
14
+ /** Ruta del campo afectado (`faq[2].answer`, `sku`). */
15
+ path: string;
16
+ /** Mensaje en español, listo para pintar. */
17
+ message: string;
18
+ /** Código estable del validador (B11); el mensaje puede cambiar, el código no. */
19
+ code?: string;
20
+ }
21
+
22
+ /** Shape del body de error (doc/baseapi.md §8 «Errores»). */
23
+ export interface ApiErrorBody {
24
+ error: {
25
+ status: number;
26
+ message: string;
27
+ /** Ruta del campo afectado cuando el backend la envía (base.md §10). */
28
+ path?: string;
29
+ /**
30
+ * Código de sesión de §4.5 (R2.9): `AUTH_REQUIRED` · `AUTH_INVALID` ·
31
+ * `AUTH_EXPIRED` · `FORBIDDEN` · `RATE_LIMITED`. Aparece SÓLO en esas
32
+ * cinco respuestas — el sobre de error es aditivo (O7 / D10-5).
33
+ */
34
+ code?: string;
35
+ /**
36
+ * Errores de validación con ruta (base.md §5, guía §10): el 422 los manda
37
+ * ACÁ (`error.errors[]`), no en `path`. Sin modelarlos el form veía
38
+ * «Errores de validación» genérico y ningún campo se pintaba (D42).
39
+ */
40
+ errors?: ApiIssue[];
41
+ };
42
+ }
43
+
44
+ /** Error de API estandarizado que lanza el interceptor de errores. */
45
+ export class ApiError extends Error {
46
+ readonly status: number;
47
+ /** Ruta del campo que provocó el error (ej. `faq[2].answer`), si existe. */
48
+ readonly fieldPath?: string;
49
+ /** Código de sesión de §4.5 si el backend lo envió; `undefined` en el resto. */
50
+ readonly code?: string;
51
+ /** Issues con ruta (422); vacío cuando el error no trae `errors[]`. */
52
+ readonly issues: readonly ApiIssue[];
53
+
54
+ constructor(
55
+ status: number,
56
+ message: string,
57
+ fieldPath?: string,
58
+ code?: string,
59
+ issues: readonly ApiIssue[] = [],
60
+ ) {
61
+ super(message);
62
+ this.name = 'ApiError';
63
+ this.status = status;
64
+ this.fieldPath = fieldPath;
65
+ this.code = code;
66
+ this.issues = issues;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * `meta.renewedToken` (auth.md §3.2/§3.4): el back lo adjunta a la respuesta
72
+ * cuando la vida del token bajó del umbral de renovación. Es el ÚNICO modo
73
+ * de que el front se entere — el token nuevo no viaja en cabeceras ni en la
74
+ * URL. Devuelve `null` cuando no hay renovación (que es lo normal).
75
+ *
76
+ * Vive acá y no en el `ApiService` porque lo consumen DOS servicios
77
+ * (`ApiService` y `AuthService`) y éste modelo no importa a ninguno de los
78
+ * dos: sin este archivo no hay ciclo de imports.
79
+ */
80
+ export function renewedTokenOf(body: unknown): string | null {
81
+ if (body === null || typeof body !== 'object') return null;
82
+ const meta = (body as { meta?: unknown }).meta;
83
+ if (meta === null || typeof meta !== 'object') return null;
84
+ const token = (meta as { renewedToken?: unknown }).renewedToken;
85
+ return typeof token === 'string' && token !== '' ? token : null;
86
+ }
87
+
88
+ /**
89
+ * Resultado de una página para `ApiService.listPage()`.
90
+ *
91
+ * El backend envía la metadata de paginación dentro del sobre (típicamente
92
+ * en `meta`); el motor la extrae según la declaración `responseUnwrap.pagination`
93
+ * y la entrega en este shape. `list()` NO usa este tipo: sigue devolviendo `T[]`.
94
+ */
95
+ export interface PageResult<T> {
96
+ /** Registros de la página actual. */
97
+ items: T[];
98
+ /** Cursor para pedir la página siguiente; `null` cuando no hay más. */
99
+ nextCursor: string | null;
100
+ /** Total de registros del conjunto, si el backend lo informa; `0` si no. */
101
+ total: number;
102
+ /** `true` si hay al menos una página más. `false` sin cursor informativo. */
103
+ hasMore: boolean;
104
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Descripciones centralizadas del catálogo de campos del motor `@tinya-code/gas-admin-ui`.
3
+ *
4
+ * Este archivo es el único lugar donde se editan los textos descriptivos de
5
+ * cada propiedad del schema. Los JSDoc de `schema.model.ts` usan estos mismos
6
+ * textos para que el editor (VSCode) muestre tooltips al pasar el cursor
7
+ * sobre una propiedad del schema del consumidor.
8
+ *
9
+ * Estructura: clave de propiedad → descripción en español.
10
+ *
11
+ * Uso en el consumidor (loyalty-admin, etc.):
12
+ * import { schemaDescriptions } from '@tinya-code/gas-admin-ui';
13
+ * schemaDescriptions['docLink']; // 'URL de documentación externa para este campo.'
14
+ *
15
+ * Nota: este objeto no se renderiza en la UI; solo existe para documentación
16
+ * y tooltips del editor. Para renderizar tooltips en el formulario usa
17
+ * `description` en cada campo del schema del consumidor.
18
+ */
19
+
20
+ export const schemaDescriptions = {
21
+ // ── Propiedades comunes a todo campo (FieldBase) ───────────────────────
22
+ key: 'Nombre de la propiedad en el JSON que envía/recibe el back.',
23
+ label: 'Rótulo visible para el usuario en el formulario y la ficha.',
24
+ help: 'Texto corto de ayuda que se muestra debajo del campo.',
25
+ placeholder: 'Texto sugerente que se muestra dentro del input cuando está vacío.',
26
+ required: 'Si es obligatorio. El back también lo valida (R7.4).',
27
+ default: 'Valor por defecto si el registro no trae este campo (ej. `default: "none"` en un `select`/`dropdown`).',
28
+ readonly: 'Si el campo es solo lectura (no editable en el formulario).',
29
+ readonlyOn: 'Bloquea el campo solo en creación, solo en edición o siempre.',
30
+ readonlyWhen: 'Condición bajo la cual el campo es solo lectura (expresión sobre otros campos).',
31
+ visibleWhen: 'Condición bajo la cual el campo se muestra u oculta.',
32
+ hiddenOn: 'Oculta el campo en creación, en edición o siempre.',
33
+ dependsOn: 'Selección dependiente (guía §1: país → ciudad). Clave del campo padre del mismo nivel.',
34
+ validators: 'Reglas de validación (mínimo, máximo, patrón, unicidad, etc.).',
35
+ width: 'Ancho en la grilla del formulario (1–12), igual que un widget del dashboard.',
36
+ section: 'Id de la sección/pestaña del layout a la que pertenece (base.md §4).',
37
+ read: 'Código de nivel de acceso requerido para leer el campo.',
38
+ write: 'Código de nivel de acceso requerido para escribir el campo.',
39
+ description: 'Descripción corta de para qué sirve este campo. Se muestra como tooltip al pasar el cursor sobre el rótulo.',
40
+ docLink: 'URL de documentación externa para este campo (se abre al hacer clic en el tooltip).',
41
+
42
+ // ── Opciones de select/dropdown ───────────────────────────────────────
43
+ value: 'Valor que se envía al back cuando se elige esta opción.',
44
+ labelOption: 'Texto visible de la opción.',
45
+ disabled: 'Si la opción está deshabilitada (no se puede elegir).',
46
+
47
+ // ── Campos específicos ─────────────────────────────────────────────────
48
+ 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`.',
49
+ maxLength: 'Longitud máxima de caracteres permitida.',
50
+ pattern: 'Expresión regular que debe cumplir el valor.',
51
+ rows: 'Número de filas visibles del textarea.',
52
+ minWords: 'Cantidad mínima de palabras.',
53
+ maxWords: 'Cantidad máxima de palabras.',
54
+ tokens: 'Marcadores insertables como chips bajo el área (plantillas de mensaje).',
55
+ from: 'Campo del registro del que se autogenera el slug.',
56
+ lockAfterCreate: 'Bloquea el campo una vez creado el registro (por defecto: sí).',
57
+ min: 'Valor mínimo permitido.',
58
+ max: 'Valor máximo permitido.',
59
+ step: 'Incremento entre valores permitidos.',
60
+ decimals: 'Número de decimales mostrados.',
61
+ currencyFrom: 'Campo del registro del que se toma la moneda (ej. `currency`).',
62
+ trueLabel: 'Texto que se muestra cuando el valor es verdadero.',
63
+ falseLabel: 'Texto que se muestra cuando el valor es falso.',
64
+ source: 'De dónde salen las opciones: `{ resource, view?, params?, … }` (F8 · R6.5).',
65
+ resource: 'Recurso declarado (`resource.id`): su listado es el de las opciones.',
66
+ view: 'Vista declarada en `resource.views` (opcional): filtra en el back.',
67
+ params: 'Valores interpolados para la vista (`{campo}` ← contexto).',
68
+ labelField: 'Etiqueta por defecto de las filas (si no, `titleField` del recurso).',
69
+ valueField: 'Valor por defecto de las filas (si no, `keyField` del recurso).',
70
+ secondaryField: 'Segundo dato a mostrar junto a la etiqueta (ej. "Juan Pérez · DNI 123").',
71
+ onlyActive: 'Cargar solo registros activos (lo exige además la FK en el back).',
72
+ accept: 'Tipos aceptados (ej. `image/*`, `image/png,image/jpeg` o `pdf,docx`).',
73
+ maxSizeMB: 'Tamaño máximo del archivo en megabytes.',
74
+ aspectRatio: 'Relación de aspecto de la vista previa (ej. `16/9`).',
75
+ previewSize: 'Tamaño de la vista previa.',
76
+ formatCurrency: 'Campo del registro del que se toma la moneda (ej. `currency`).',
77
+ display: 'Presentación del grupo: `card`, `collapsible` o `inline`.',
78
+ fields: 'Campos de cada item (cualquier tipo, incluso `group` o `list`).',
79
+ minItems: 'Cantidad mínima de items.',
80
+ maxItems: 'Cantidad máxima de items.',
81
+ sortable: 'Si se puede reordenar (por defecto: sí).',
82
+ positionField: 'Campo donde se guarda el orden dentro de cada item (`position`).',
83
+ addLabel: 'Etiqueta del botón para agregar un item nuevo.',
84
+ emptyText: 'Texto que se muestra cuando no hay items.',
85
+ itemDisplay: 'Cómo se muestra cada item: `card`, `row` o `accordion`.',
86
+ itemFields: 'Campos de cada item (cualquier tipo, incluso `group` o `list`).',
87
+ itemTitle: 'Campo del item usado como título cuando está colapsado.',
88
+ primaryFirst: 'El primer item tiene significado especial (ej. imagen principal).',
89
+ itemType: 'Tipo permitido de cada item (base.md §9: solo `text`, `url` o `email`).',
90
+ sort: 'Si la lista se ordena alfabéticamente o por posición.',
91
+ filter: 'Si el listado permite filtrado por texto.',
92
+ search: 'Campos buscables en el listado.',
93
+ filters: 'Filtros declarados para el listado.',
94
+ listColumns: 'Columnas que se muestran en el listado.',
95
+ searchable: 'Si el campo es buscable.',
96
+ filterable: 'Si el campo es filtrable.',
97
+ sortableItems: 'Si los items de la lista se pueden reordenar.',
98
+ groupBy: 'Campo por el que agrupar los items.',
99
+ cardWidth: 'Ancho de cada tarjeta de opción en la grilla (1–12). Por defecto `4` (3 tarjetas por fila en escritorio).',
100
+ cardCols: 'Cuántas tarjetas mostrar por fila en el punto de ruptura `lg`. Sobrescribe el cálculo automático basado en `cardWidth`.',
101
+ hintMetric: 'Texto secundario bajo la cifra (estático, va en el schema).',
102
+ unitChart: 'Sufijo de las cifras del eje («unidades», «S/»).',
103
+ segments: 'Segmentos de la barra de distribución porcentual.',
104
+ actions: 'Acciones de navegación por fila.',
105
+ labelAction: 'Etiqueta de la acción.',
106
+ icon: 'Icono de la acción.',
107
+ navigateTo: 'Ruta de destino de la acción.',
108
+ variant: 'Variante visual: `primary`, `secondary` u `outline`.',
109
+
110
+ // ── Widgets de dashboard ──────────────────────────────────────────────
111
+ widgets: 'Widgets del panel. Se declara en `ResourceSchema.widgets` — no en `fields`, porque un KPI no es un campo.',
112
+ mobileWidth: 'Ancho en la grilla en mobile (1–12). Por defecto `12` (ancho completo, apilado).',
113
+ sourceWidget: 'De dónde saca sus datos: `DataSource` o `{ kind: "payload" }`.',
114
+ hintWidget: 'Texto secundario bajo la cifra (estático, va en el schema).',
115
+ unitWidget: 'Sufijo de las cifras del eje.',
116
+ } as const;
117
+
118
+ export type SchemaDescriptionKey = keyof typeof schemaDescriptions;