@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,90 @@
1
+ /**
2
+ * Mensaje completo a copiar o compartir.
3
+ *
4
+ * `shareTextTemplate` es el MENSAJE FORMATEADO por el schema (el que trae
5
+ * saludo, ref, PIN o el enlace ya escrito dentro), mientras que `urlTemplate`
6
+ * es sólo el enlace. Los tres caminos —copiar, compartir nativo y WhatsApp—
7
+ * deben ofrecer el mismo texto: antes `copy()` y la acción del listado sólo
8
+ * escribían `url`, así que el operador se quedaba sin el mensaje.
9
+ *
10
+ * Tres casos, sin perder información y sin duplicar el enlace:
11
+ * - sin `shareText` → sólo la URL
12
+ * - `shareText` con el sitio ya mencionado → el mensaje tal cual
13
+ * - `shareText` sin el sitio → mensaje + espacio + URL
14
+ *
15
+ * "Sitio" se decide por ORIGIN, no por la URL exacta: la plantilla puede
16
+ * referenciar sólo la base (`{public_base_url}` → `https://ejemplo.com`)
17
+ * o la URL completa con el path (`…/p/{ref}`); en ambos casos el mensaje
18
+ * YA apunta al sitio y no se le pega nada al final. Antes se comparaba
19
+ * contra la URL completa, así que un mensaje con la base recibía la URL
20
+ * con código duplicada al final.
21
+ *
22
+ * @param url Enlace resuelto (`urlTemplate`).
23
+ * @param shareText Mensaje formateado opcional (`shareTextTemplate`).
24
+ */
25
+ export function buildShareMessage(url: string, shareText?: string): string {
26
+ const text = (shareText ?? '').trim();
27
+ if (!text) return url;
28
+ if (url && mentionsSite(text, url)) return text;
29
+ return url ? `${text} ${url}` : text;
30
+ }
31
+
32
+ /** ¿El mensaje ya apunta al sitio de `url` (origin o URL exacta)? */
33
+ function mentionsSite(text: string, url: string): boolean {
34
+ if (text.includes(url)) return true;
35
+ try {
36
+ const { origin } = new URL(url);
37
+ return origin !== 'null' && text.includes(origin);
38
+ } catch {
39
+ // `url` relativa (`/p/123`): sin origin que comparar, sólo la URL exacta.
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Escribe en el portapapeles sin reventar: primero la API moderna, después
46
+ * el `textarea` + `execCommand` (obsoleto, pero sigue siendo el fallback
47
+ * que existe en contextos no seguros).
48
+ *
49
+ * Único punto del admin que sabe cómo copiar: `copy-share`, las acciones de
50
+ * fila del listado y cualquier widget futuro comparten este camino, en vez
51
+ * de reimplementar cada uno sus propios fallbacks.
52
+ *
53
+ * @returns `true` si el texto quedó copiado por alguna de las vías.
54
+ */
55
+ export async function writeClipboard(text: string): Promise<boolean> {
56
+ const clipboard = typeof navigator !== 'undefined' ? navigator.clipboard : undefined;
57
+ if (clipboard && typeof clipboard.writeText === 'function') {
58
+ try {
59
+ await clipboard.writeText(text);
60
+ return true;
61
+ } catch {
62
+ // Permiso denegado o contexto no seguro: sigue al fallback legado.
63
+ }
64
+ }
65
+ return legacyCopy(text);
66
+ }
67
+
68
+ function legacyCopy(text: string): boolean {
69
+ if (typeof document === 'undefined' || typeof document.execCommand !== 'function') {
70
+ return false;
71
+ }
72
+ const area = document.createElement('textarea');
73
+ area.value = text;
74
+ area.setAttribute('readonly', '');
75
+ area.style.position = 'fixed';
76
+ area.style.top = '0';
77
+ area.style.opacity = '0';
78
+ document.body.appendChild(area);
79
+ area.select();
80
+
81
+ let copied = false;
82
+ try {
83
+ copied = document.execCommand('copy');
84
+ } catch {
85
+ copied = false;
86
+ }
87
+
88
+ document.body.removeChild(area);
89
+ return copied;
90
+ }
@@ -0,0 +1,95 @@
1
+ import { resolveEndpointPath } from './context-resolver';
2
+
3
+ /**
4
+ * Plan `doc/context-sources-plan.md` · Fase 2.
5
+ *
6
+ * TDD: éstos son los tests RED del resolver puro. Congelan las reglas del
7
+ * contrato: resolución anidada, escalares, codificación, pereza (sin
8
+ * marcadores no se llama a ninguna fuente) y TODOS los casos fail-loud.
9
+ */
10
+ describe('context-resolver — marcadores en paths de endpoints', () => {
11
+ const fuentes = {
12
+ auth: () => ({ user: { id: 'u_9', name: 'Ana' }, token: 'tok' }),
13
+ config: () => ({ pais: 'AR' }),
14
+ tenant: () => 'acme',
15
+ numero: () => 7,
16
+ ruta: () => 'a b/c',
17
+ vacio: () => '',
18
+ nulo: () => null,
19
+ objeto: () => ({ a: 1 }),
20
+ rota: () => {
21
+ throw new Error('boom fuente');
22
+ },
23
+ };
24
+
25
+ // ── Casos felices ────────────────────────────────────────────────────────
26
+ it('resuelve una ruta anidada {auth.user.id}', () => {
27
+ expect(resolveEndpointPath('/businesses/{auth.user.id}/orders', fuentes)).toBe(
28
+ '/businesses/u_9/orders',
29
+ );
30
+ });
31
+
32
+ it('resuelve la fuente entera {tenant}', () => {
33
+ expect(resolveEndpointPath('/tenants/{tenant}/orders', fuentes)).toBe('/tenants/acme/orders');
34
+ });
35
+
36
+ it('convierte escalares a texto', () => {
37
+ expect(resolveEndpointPath('/page/{numero}', fuentes)).toBe('/page/7');
38
+ });
39
+
40
+ it('codifica cada valor como segmento de URL', () => {
41
+ expect(resolveEndpointPath('/x/{ruta}/y', fuentes)).toBe('/x/a%20b%2Fc/y');
42
+ });
43
+
44
+ it('resuelve varios marcadores en un mismo path', () => {
45
+ expect(
46
+ resolveEndpointPath('/tenants/{tenant}/businesses/{auth.user.id}/paises/{config.pais}', fuentes),
47
+ ).toBe('/tenants/acme/businesses/u_9/paises/AR');
48
+ });
49
+
50
+ it('sin marcadores devuelve el path tal cual y NO llama a ninguna fuente', () => {
51
+ const perezosa = {
52
+ auth: () => {
53
+ throw new Error('no debe evaluarse sin marcadores');
54
+ },
55
+ };
56
+ expect(resolveEndpointPath('/admin/products', perezosa)).toBe('/admin/products');
57
+ });
58
+
59
+ it('resuelve un marcador dentro de un path que ya lleva query string', () => {
60
+ expect(resolveEndpointPath('/orders?owner={auth.user.id}&limit=10', fuentes)).toBe(
61
+ '/orders?owner=u_9&limit=10',
62
+ );
63
+ });
64
+
65
+ // ── Fail loud ────────────────────────────────────────────────────────────
66
+ it('valor ausente ⇒ error que nombra el path y el marcador', () => {
67
+ const path = '/x/{auth.user.nope}/y';
68
+ expect(() => resolveEndpointPath(path, fuentes)).toThrow(path);
69
+ expect(() => resolveEndpointPath(path, fuentes)).toThrow('auth.user.nope');
70
+ });
71
+
72
+ it('valor vacío ⇒ error, nunca un segmento vacío en la URL', () => {
73
+ expect(() => resolveEndpointPath('/x/{vacio}/y', fuentes)).toThrow('/x/{vacio}/y');
74
+ });
75
+
76
+ it('valor nulo ⇒ error', () => {
77
+ expect(() => resolveEndpointPath('/x/{nulo}', fuentes)).toThrow('nulo');
78
+ });
79
+
80
+ it('fuente no registrada ⇒ error con el nombre de la fuente', () => {
81
+ expect(() => resolveEndpointPath('/x/{desconocida.id}', fuentes)).toThrow('desconocida');
82
+ });
83
+
84
+ it('getter que revienta ⇒ su error se propaga tal cual', () => {
85
+ expect(() => resolveEndpointPath('/x/{rota}', fuentes)).toThrow('boom fuente');
86
+ });
87
+
88
+ it('{key} sin resolver ⇒ error que aclara que sólo aplica a operaciones con clave', () => {
89
+ expect(() => resolveEndpointPath('/x/{key}', fuentes)).toThrow('key');
90
+ });
91
+
92
+ it('valor no escalar ⇒ error, nunca [object Object] en la URL', () => {
93
+ expect(() => resolveEndpointPath('/x/{objeto}', fuentes)).toThrow('objeto');
94
+ });
95
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Resolución de marcadores de contexto en paths de endpoints.
3
+ *
4
+ * Plan `doc/context-sources-plan.md` · Fase 2. Lógica PURA: ni HTTP ni DI —
5
+ * recibe un path y un mapa de getters de fuentes y devuelve el path ya
6
+ * resuelto, o revienta.
7
+ *
8
+ * Reglas del contrato (R1–R4 del plan):
9
+ *
10
+ * - `{<fuente>}` / `{<fuente>.<ruta.anidada>}` → valor de la fuente en el
11
+ * momento de la llamada (los getters se evalúan SOLO si hay marcadores).
12
+ * - Cada valor se codifica como segmento de URL (`encodeURIComponent`).
13
+ * - **Fail loud**: fuente no registrada, valor ausente/vacío, valor no
14
+ * escalar o `{key}` sin resolver ⇒ error que nombra el path y el marcador.
15
+ * Jamás se devuelve un segmento vacío ni el texto literal del marcador.
16
+ * - Sin marcadores ⇒ el path vuelve intacto y ninguna fuente se evalúa.
17
+ *
18
+ * OJO con `interpolateTemplate` (R3): su semántica «ausente ⇒ cadena vacía»
19
+ * es correcta para acciones y fuentes de datos, pero acá sería un 404 mudo.
20
+ * Por eso la lectura y la validación viven en este fichero y ese helper no
21
+ * se toca.
22
+ */
23
+
24
+ /** `{nombre}` o `{nombre.ruta.anidada}` — misma forma que usa el interpolador. */
25
+ const MARKER = /\{([a-zA-Z0-9_.]+)\}/g;
26
+
27
+ /**
28
+ * Resuelve los marcadores de `path` contra `sources`.
29
+ *
30
+ * @param path Path declarado en `ResourceEndpoints`, tal cual vino del schema.
31
+ * @param sources Getters por nombre de fuente (`auth`, `config`, `valueSources`…).
32
+ * @returns El path con cada marcador sustituido y codificado.
33
+ * @throws Error nombrando path y marcador ante cualquier valor irresoluble.
34
+ */
35
+ export function resolveEndpointPath(
36
+ path: string,
37
+ sources: Readonly<Record<string, () => unknown>>,
38
+ ): string {
39
+ if (!path.includes('{')) return path;
40
+ return path.replace(MARKER, (marker, nombre: string) =>
41
+ sustituir(path, marker, nombre, sources),
42
+ );
43
+ }
44
+
45
+ /** Sustituye UN marcador. `path` completo sólo viaja para el mensaje de error. */
46
+ function sustituir(
47
+ path: string,
48
+ marker: string,
49
+ nombre: string,
50
+ sources: Readonly<Record<string, () => unknown>>,
51
+ ): string {
52
+ if (nombre === 'key') {
53
+ throw new Error(
54
+ `[gas-admin-ui] path '${path}': ${marker} llegó sin resolver — ` +
55
+ `la clave sólo la sustituyen get/update/remove antes de resolver el contexto.`,
56
+ );
57
+ }
58
+
59
+ const punto = nombre.indexOf('.');
60
+ const fuente = punto === -1 ? nombre : nombre.slice(0, punto);
61
+ const getter = sources[fuente];
62
+ if (typeof getter !== 'function') {
63
+ throw new Error(
64
+ `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — ` +
65
+ `la fuente '${fuente}' no está registrada.`,
66
+ );
67
+ }
68
+
69
+ // El getter se evalúa acá y sólo acá: si revienta, su error se propaga.
70
+ const valor = punto === -1 ? getter() : leerRuta(getter(), nombre.slice(punto + 1));
71
+
72
+ if (valor === undefined || valor === null) {
73
+ throw new Error(
74
+ `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — sin valor para '${nombre}'.`,
75
+ );
76
+ }
77
+ if (typeof valor === 'object') {
78
+ throw new Error(
79
+ `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — ` +
80
+ `el valor de '${fuente}' no es un segmento de URL (no es escalar).`,
81
+ );
82
+ }
83
+
84
+ const texto = String(valor);
85
+ if (texto === '') {
86
+ throw new Error(
87
+ `[gas-admin-ui] path '${path}': no se pudo resolver ${marker} — valor vacío en '${fuente}'.`,
88
+ );
89
+ }
90
+ return encodeURIComponent(texto);
91
+ }
92
+
93
+ /** Lectura profunda `a.b.c` sobre el valor de la fuente; inexistente ⇒ `undefined`. */
94
+ function leerRuta(valor: unknown, ruta: string): unknown {
95
+ let actual: unknown = valor;
96
+ for (const paso of ruta.split('.')) {
97
+ if (actual === null || typeof actual !== 'object') return undefined;
98
+ actual = (actual as Record<string, unknown>)[paso];
99
+ }
100
+ return actual;
101
+ }
@@ -0,0 +1,70 @@
1
+ import { extractPagination } from './response-unwrapper';
2
+
3
+ // Extracción de metadata de paginación (plan doc/engine-refactor-plan.md · Fase 2.2)
4
+ // Lo consume `listPage()` (Fase 3); acá se prueba la utilidad pura.
5
+ describe('extractPagination', () => {
6
+ it('lee cursor/total/hasMore de meta cuando metaKey está declarado', () => {
7
+ const raw = { data: [{ slug: 'a' }], meta: { cursor: 'c1', count: 10, more: true } };
8
+ const result = extractPagination(raw, {
9
+ dataKey: 'data',
10
+ metaKey: 'meta',
11
+ pagination: { cursorKey: 'cursor', totalKey: 'count', hasMoreKey: 'more' },
12
+ });
13
+ expect(result).toEqual({ nextCursor: 'c1', total: 10, hasMore: true });
14
+ });
15
+
16
+ it('auto-detecta claves comunes (nextCursor, total, hasMore) en el body', () => {
17
+ const raw = { items: [{ slug: 'a' }], nextCursor: 'tok', total: 42, hasMore: true };
18
+ expect(extractPagination(raw, { dataKey: 'items' })).toEqual({
19
+ nextCursor: 'tok',
20
+ total: 42,
21
+ hasMore: true,
22
+ });
23
+ });
24
+
25
+ it('sin cursor ni claves de paginación ⇒ nextCursor null, total 0, hasMore false', () => {
26
+ const raw = { items: [{ slug: 'a' }] };
27
+ expect(extractPagination(raw, undefined)).toEqual({
28
+ nextCursor: null,
29
+ total: 0,
30
+ hasMore: false,
31
+ });
32
+ });
33
+
34
+ it('con cursor presente y sin flag hasMore explícito ⇒ hasMore true (derivado)', () => {
35
+ const raw = { items: [], nextCursor: 'c2' };
36
+ expect(extractPagination(raw, undefined)).toEqual({
37
+ nextCursor: 'c2',
38
+ total: 0,
39
+ hasMore: true,
40
+ });
41
+ });
42
+
43
+ it('metaKey declarado pero ausente en el body ⇒ cae al body', () => {
44
+ const raw = { items: [], nextCursor: 'c3', total: 7 };
45
+ const result = extractPagination(raw, { dataKey: 'items', metaKey: 'meta' });
46
+ expect(result).toEqual({ nextCursor: 'c3', total: 7, hasMore: true });
47
+ });
48
+
49
+ it('cursor vacío string se trata como ausente (nextCursor null, hasMore false)', () => {
50
+ const raw = { items: [], nextCursor: '' };
51
+ expect(extractPagination(raw, undefined)).toEqual({
52
+ nextCursor: null,
53
+ total: 0,
54
+ hasMore: false,
55
+ });
56
+ });
57
+
58
+ it('cuerpo no-objeto ⇒ sin paginación (no lanza)', () => {
59
+ expect(extractPagination(null, undefined)).toEqual({
60
+ nextCursor: null,
61
+ total: 0,
62
+ hasMore: false,
63
+ });
64
+ });
65
+
66
+ it('hasMoreKey explícito tiene prioridad sobre el derivado del cursor', () => {
67
+ const raw = { items: [], nextCursor: 'c9', hasMore: false };
68
+ expect(extractPagination(raw, undefined).hasMore).toBe(false);
69
+ });
70
+ });
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Desempaquetado opt-in de respuestas HTTP anidadas (`{ data, meta }`,
3
+ * `{ items, total }`, o claves arbitrarias).
4
+ *
5
+ * El motor NO asume un sobre: sólo desempaqueta cuando la declaración
6
+ * `ResponseUnwrap` está presente. Sin ella, el comportamiento actual se
7
+ * conserva (arreglo plano, `{ items }`, u objeto crudo).
8
+ *
9
+ * Mantener esta lógica aquí (y no en `ApiService`) deja el servicio enfocado
10
+ * en el despacho y facilita la prueba de las reglas de unwrap.
11
+ */
12
+
13
+ import type { ResponseUnwrap } from '../models/schema.model';
14
+
15
+ export type UnwrapOperation = 'list' | 'get' | 'create' | 'update' | 'remove';
16
+
17
+ /**
18
+ * Devuelve `true` si la declaración `unwrap` aplica a la operación dada.
19
+ * Si `apply` falta, se asume `'all'` (decisión fija del plan).
20
+ */
21
+ export function appliesTo(unwrap: ResponseUnwrap, operation: UnwrapOperation): boolean {
22
+ const apply = unwrap.apply ?? 'all';
23
+ return apply === 'all' || apply === operation;
24
+ }
25
+
26
+ /**
27
+ * Extrae el payload declarado por `unwrap.dataKey` del cuerpo `raw`.
28
+ *
29
+ * - Si `unwrap` no aplica a `operation`, devuelve `raw` tal cual.
30
+ * - Si aplica y `raw` tiene la clave, devuelve su valor.
31
+ * - Si aplica y la clave falta, lanza un `Error` descriptivo (no silencia).
32
+ */
33
+ export function unwrapResponse(raw: unknown, unwrap: ResponseUnwrap | undefined, operation: UnwrapOperation): unknown {
34
+ if (!unwrap || !appliesTo(unwrap, operation)) {
35
+ return raw;
36
+ }
37
+ if (raw !== null && typeof raw === 'object' && !Array.isArray(raw)) {
38
+ const record = raw as Record<string, unknown>;
39
+ if (Object.prototype.hasOwnProperty.call(record, unwrap.dataKey)) {
40
+ return record[unwrap.dataKey];
41
+ }
42
+ }
43
+ throw new Error(
44
+ `Response unwrap declared dataKey '${unwrap.dataKey}' but it is missing in the response for operation '${operation}'`,
45
+ );
46
+ }
47
+
48
+ /** Metadata de paginación extraída del sobre; la consume `ApiService.listPage()`. */
49
+ export interface PaginationMeta {
50
+ nextCursor: string | null;
51
+ total: number;
52
+ hasMore: boolean;
53
+ }
54
+
55
+ const NO_PAGINATION: PaginationMeta = { nextCursor: null, total: 0, hasMore: false };
56
+
57
+ /**
58
+ * Extrae la metadata de paginación del cuerpo `raw` según la declaración
59
+ * `unwrap.pagination` (cursorKey/totalKey/hasMoreKey).
60
+ *
61
+ * - Con `metaKey` declarado Y presente en el body, las claves se leen de ese
62
+ * sub-objeto; si no, del body raíz.
63
+ * - Sin claves declaradas se auto-detectan las comunes: `nextCursor`,
64
+ * `total`, `hasMore`.
65
+ * - `hasMore` explícito (boolean) tiene prioridad; sin él se deriva del
66
+ * cursor: presente ⇒ `true`, ausente/vacío ⇒ `false`.
67
+ * - Cuerpo no-objeto ⇒ sin paginación. Nunca lanza: la paginación es
68
+ * opt-in y su ausencia sólo significa `hasMore = false`.
69
+ */
70
+ export function extractPagination(
71
+ raw: unknown,
72
+ unwrap: ResponseUnwrap | undefined,
73
+ ): PaginationMeta {
74
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
75
+ return { ...NO_PAGINATION };
76
+ }
77
+ const body = raw as Record<string, unknown>;
78
+
79
+ let source = body;
80
+ const metaKey = unwrap?.metaKey;
81
+ if (metaKey) {
82
+ const meta = body[metaKey];
83
+ if (meta !== null && typeof meta === 'object' && !Array.isArray(meta)) {
84
+ source = meta as Record<string, unknown>;
85
+ }
86
+ }
87
+
88
+ const cursorKey = unwrap?.pagination?.cursorKey ?? 'nextCursor';
89
+ const totalKey = unwrap?.pagination?.totalKey ?? 'total';
90
+ const hasMoreKey = unwrap?.pagination?.hasMoreKey ?? 'hasMore';
91
+
92
+ const cursorValue = source[cursorKey] ?? body[cursorKey];
93
+ const nextCursor = typeof cursorValue === 'string' && cursorValue !== '' ? cursorValue : null;
94
+
95
+ const totalValue = source[totalKey] ?? body[totalKey];
96
+ const total = typeof totalValue === 'number' ? totalValue : 0;
97
+
98
+ const hasMoreValue = source[hasMoreKey] ?? body[hasMoreKey];
99
+ const hasMore =
100
+ typeof hasMoreValue === 'boolean' ? hasMoreValue : nextCursor !== null;
101
+
102
+ return { nextCursor, total, hasMore };
103
+ }
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import type { DataSource, ResourceSchema } from '../models/schema.model';
4
+ import { optionsFromRows } from './source-options';
5
+
6
+ const TARGET: ResourceSchema = {
7
+ id: 'categories',
8
+ label: 'Categoría',
9
+ labelPlural: 'Categorías',
10
+ kind: 'collection',
11
+ endpoint: { list: '/admin/categories' },
12
+ keyField: 'slug',
13
+ titleField: 'name',
14
+ listColumns: [],
15
+ fields: [],
16
+ };
17
+
18
+ const FILAS = [
19
+ { slug: 'bebidas', name: 'Bebidas', description: 'Frías', active: true },
20
+ { slug: 'pan', name: '', description: '', active: false },
21
+ ];
22
+
23
+ const source = (extra: Partial<DataSource> = {}): DataSource => ({
24
+ resource: 'categories',
25
+ ...extra,
26
+ });
27
+
28
+ describe('optionsFromRows', () => {
29
+ it('usa valueField/labelField declarados en la fuente', () => {
30
+ const opciones = optionsFromRows(FILAS, {
31
+ source: source({ valueField: 'slug', labelField: 'name' }),
32
+ target: TARGET,
33
+ });
34
+ expect(opciones).toEqual([
35
+ { value: 'bebidas', label: 'Bebidas' },
36
+ { value: 'pan', label: 'pan' }, // label vacío ⇒ cae al valor
37
+ ]);
38
+ });
39
+
40
+ it('sin los campos declarados cae a keyField/titleField del recurso destino', () => {
41
+ const opciones = optionsFromRows(FILAS, { source: source(), target: TARGET });
42
+ expect(opciones[0]).toEqual({ value: 'bebidas', label: 'Bebidas' });
43
+ });
44
+
45
+ it('sin nada declarado el valor es `id` (último recurso conocido)', () => {
46
+ const opciones = optionsFromRows([{ id: 'x' }], {
47
+ source: source(),
48
+ target: undefined,
49
+ });
50
+ expect(opciones).toEqual([{ value: 'x', label: 'x' }]);
51
+ });
52
+
53
+ it('una fila sin valor no es opción: no hay qué seleccionar', () => {
54
+ const opciones = optionsFromRows(
55
+ [{ slug: '', name: 'Vacía' }, { name: 'Sin clave' }, { slug: null, name: 'Nula' }],
56
+ { source: source({ valueField: 'slug', labelField: 'name' }), target: TARGET },
57
+ );
58
+ expect(opciones).toEqual([]);
59
+ });
60
+
61
+ it('`onlyActive` descarta las inactivas en el cliente', () => {
62
+ const opciones = optionsFromRows(FILAS, {
63
+ source: source({ valueField: 'slug', labelField: 'name' }),
64
+ target: TARGET,
65
+ onlyActive: true,
66
+ });
67
+ expect(opciones.map((o) => o.value)).toEqual(['bebidas']);
68
+ });
69
+
70
+ it('secondaryField añade el dato con « · », y se cae si no hay dato', () => {
71
+ const opciones = optionsFromRows(
72
+ [
73
+ { slug: 'a', name: 'A', description: 'Frías' },
74
+ { slug: 'b', name: 'B', description: '' },
75
+ { slug: 'c', name: 'C' },
76
+ ],
77
+ {
78
+ source: source({ valueField: 'slug', labelField: 'name' }),
79
+ target: TARGET,
80
+ secondaryField: 'description',
81
+ },
82
+ );
83
+ expect(opciones.map((o) => o.label)).toEqual(['A · Frías', 'B', 'C']);
84
+ });
85
+
86
+ it('no muta las filas de entrada', () => {
87
+ const filas = [{ slug: 'a', name: 'A', active: false }];
88
+ const copia = JSON.parse(JSON.stringify(filas));
89
+ optionsFromRows(filas, {
90
+ source: source({ valueField: 'slug', labelField: 'name' }),
91
+ target: TARGET,
92
+ onlyActive: true,
93
+ });
94
+ expect(filas).toEqual(copia);
95
+ });
96
+ });
@@ -0,0 +1,54 @@
1
+ import type { DataSource, ResourceSchema, SelectOption } from '../models/schema.model';
2
+
3
+ /** Contexto de mapeo: la fuente declarada más las particularidades del campo. */
4
+ export interface RowsToOptions {
5
+ /** Fuente de la que salieron las filas (F8 · R6.5). */
6
+ source: DataSource;
7
+ /** Schema del recurso destino: aporta `keyField`/`titleField` por defecto. */
8
+ target: ResourceSchema | undefined;
9
+ /** Descarta filas con `active: false` (`relation.onlyActive`). */
10
+ onlyActive?: boolean;
11
+ /** Dato secundario junto a la etiqueta («Juan Pérez · DNI 123»). */
12
+ secondaryField?: string;
13
+ }
14
+
15
+ /**
16
+ * Filas de una fuente declarada → opciones de desplegable (F8 · R6.5).
17
+ *
18
+ * Es el ÚNICO punto que traduce filas a `SelectOption`: antes esa regla
19
+ * estaba escrita dos veces (en `relation` y cada consumidor montaba su
20
+ * propia petición). Con `source` compartido, relaciones y desplegables con
21
+ * datos remotos pasan por acá.
22
+ *
23
+ * Reglas, en orden:
24
+ * - valor: `source.valueField` → `target.keyField` → `id`;
25
+ * - etiqueta: `source.labelField` → `target.titleField` → el valor;
26
+ * - fila sin valor (vacío o inexistente) no es opción: no hay qué seleccionar;
27
+ * - `onlyActive` descarta las inactivas en el cliente — el back ya las excluye
28
+ * de su FK, pero una lista cacheada puede traer una fila vieja.
29
+ */
30
+ export function optionsFromRows(
31
+ rows: readonly Record<string, unknown>[],
32
+ { source, target, onlyActive, secondaryField }: RowsToOptions,
33
+ ): SelectOption[] {
34
+ const valueKey = source.valueField ?? target?.keyField ?? 'id';
35
+ const labelKey = source.labelField ?? target?.titleField ?? valueKey;
36
+ const options: SelectOption[] = [];
37
+ for (const row of rows) {
38
+ if (onlyActive === true && row['active'] === false) continue;
39
+ const value = row[valueKey];
40
+ if (value === undefined || value === null || value === '') continue;
41
+ const label = row[labelKey];
42
+ const primary = typeof label === 'string' && label !== '' ? label : String(value);
43
+ options.push({ value: String(value), label: withSecondary(primary, row, secondaryField) });
44
+ }
45
+ return options;
46
+ }
47
+
48
+ /** Etiqueta con su dato secundario, o la etiqueta tal cual si no hay. */
49
+ function withSecondary(primary: string, row: Record<string, unknown>, key?: string): string {
50
+ if (key === undefined) return primary;
51
+ const secondary = row[key];
52
+ if (secondary === undefined || secondary === null || secondary === '') return primary;
53
+ return `${primary} · ${String(secondary)}`;
54
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { interpolateTemplate } from './template-interpolator';
3
+
4
+ describe('interpolateTemplate', () => {
5
+ it('1. interpola variables simples en la plantilla', () => {
6
+ const template = 'https://ejemplo.com/p/{ref}?cliente={customer_name}';
7
+ const data = { ref: 'ORD-123', customer_name: 'Juan Perez' };
8
+
9
+ const result = interpolateTemplate(template, data);
10
+
11
+ expect(result).toBe('https://ejemplo.com/p/ORD-123?cliente=Juan Perez');
12
+ });
13
+
14
+ it('2. reemplaza variables nulas o indefinidas por cadena vacía', () => {
15
+ const template = 'Pedido {ref} - Estado: {status}';
16
+ const data = { ref: 'ORD-999' };
17
+
18
+ const result = interpolateTemplate(template, data);
19
+
20
+ expect(result).toBe('Pedido ORD-999 - Estado: ');
21
+ });
22
+
23
+ it('3. resuelve claves anidadas usando notación de punto', () => {
24
+ const template = 'ID: {order.meta.id} - Nombre: {order.customer.name}';
25
+ const data = {
26
+ order: {
27
+ meta: { id: 'M-55' },
28
+ customer: { name: 'Maria' },
29
+ },
30
+ };
31
+
32
+ const result = interpolateTemplate(template, data);
33
+
34
+ expect(result).toBe('ID: M-55 - Nombre: Maria');
35
+ });
36
+
37
+ it('4. recurre al contexto adicional si la clave no está en data', () => {
38
+ const template = '{public_base_url}/p/{ref}';
39
+ const data = { ref: 'PED-404' };
40
+ const extraContext = { public_base_url: 'https://retiro.tienda.com' };
41
+
42
+ const result = interpolateTemplate(template, data, extraContext);
43
+
44
+ expect(result).toBe('https://retiro.tienda.com/p/PED-404');
45
+ });
46
+
47
+ it('5. devuelve la misma plantilla si no contiene marcadores', () => {
48
+ const template = 'Texto plano sin variables';
49
+ const result = interpolateTemplate(template, { a: 1 });
50
+
51
+ expect(result).toBe('Texto plano sin variables');
52
+ });
53
+
54
+ it('6. maneja plantillas vacías o nulas sin romper', () => {
55
+ expect(interpolateTemplate('', { ref: '123' })).toBe('');
56
+ // @ts-expect-error probando valor nulo
57
+ expect(interpolateTemplate(null, { ref: '123' })).toBe('');
58
+ });
59
+ });