@sdcorejs/angular 19.1.6 → 19.2.1

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 (374) hide show
  1. package/README.md +10 -0
  2. package/assets/scss/core/menu.scss +18 -0
  3. package/assets/scss/core/utilities/_misc.scss +15 -0
  4. package/assets/scss/sd-core.scss +1 -0
  5. package/components/anchor/src/components/anchor/anchor.component.d.ts +3 -1
  6. package/components/api-contract-builder/index.d.ts +8 -0
  7. package/components/api-contract-builder/src/api-contract-builder.component.d.ts +123 -0
  8. package/components/api-contract-builder/src/api-contract.configuration.d.ts +41 -0
  9. package/components/api-contract-builder/src/api-contract.expression.d.ts +43 -0
  10. package/components/api-contract-builder/src/api-contract.model.d.ts +167 -0
  11. package/components/api-contract-builder/src/api-contract.samples.d.ts +25 -0
  12. package/components/api-contract-builder/src/api-contract.schema.d.ts +126 -0
  13. package/components/api-contract-builder/src/api-contract.serializer.d.ts +17 -0
  14. package/components/api-contract-builder/src/api-contract.validation.d.ts +12 -0
  15. package/components/api-contract-builder/src/components/api-contract-diagnostic-list.component.d.ts +18 -0
  16. package/components/api-contract-builder/src/components/api-contract-node-drawer.component.d.ts +130 -0
  17. package/components/api-contract-builder/src/components/api-contract-node-editor.component.d.ts +35 -0
  18. package/components/api-contract-builder/src/components/api-contract-node-summary.component.d.ts +41 -0
  19. package/components/api-contract-builder/src/components/api-contract-record-editor.component.d.ts +36 -0
  20. package/components/api-contract-builder/src/components/api-contract-source-editor.component.d.ts +86 -0
  21. package/components/api-contract-builder/src/components/api-contract-suggestion.model.d.ts +17 -0
  22. package/components/badge/src/badge.component.d.ts +2 -0
  23. package/components/ckeditor-styles/src/ckeditor-styles.component.d.ts +1 -1
  24. package/components/form-generic/src/components/form-builder/components/expression-builder/expression-builder.component.d.ts +2 -1
  25. package/components/form-generic/src/components/form-builder/form-builder.component.d.ts +3 -1
  26. package/components/form-generic/src/components/form-render/components/item/components/checkbox/checkbox.component.d.ts +34 -0
  27. package/components/form-generic/src/components/form-render/components/item/components/index.d.ts +1 -0
  28. package/components/form-generic/src/components/form-render/components/item/components/table/table.component.d.ts +6 -1
  29. package/components/form-generic/src/components/sd-feel-expression/sd-feel-expression.component.d.ts +1 -1
  30. package/components/form-generic/src/configurations/form.configuration.d.ts +1 -1
  31. package/components/form-generic/src/models/form-generic-component.model.d.ts +8 -8
  32. package/components/form-generic/src/models/form-generic-definition-selection.model.d.ts +5 -5
  33. package/components/form-generic/src/models/form-generic-expression.model.d.ts +12 -12
  34. package/components/form-generic/src/models/index.d.ts +1 -0
  35. package/components/form-generic/src/services/form-generic.service.d.ts +2 -2
  36. package/components/import-excel/src/import-excel.component.d.ts +1 -0
  37. package/components/index.d.ts +7 -1
  38. package/components/mini-editor/src/mini-editor.model.d.ts +1 -1
  39. package/components/modal/src/modal.component.d.ts +3 -2
  40. package/components/modal-resizable/src/modal-resizable.component.d.ts +92 -0
  41. package/components/modal-resizable/src/modal-resizable.registry.d.ts +22 -0
  42. package/components/org-chart/src/org-chart.component.d.ts +6 -0
  43. package/components/preview/src/preview-image/preview-image.component.d.ts +1 -0
  44. package/components/query-bar/src/components/actions-bar/actions-bar.component.d.ts +6 -2
  45. package/components/query-bar/src/components/build-chip/build-chip.component.d.ts +4 -1
  46. package/components/query-bar/src/components/chip-popover/chip-popover.component.d.ts +8 -0
  47. package/components/query-bar/src/components/field-picker/field-picker.component.d.ts +2 -0
  48. package/components/query-bar/src/components/inline-chip/inline-chip.component.d.ts +2 -0
  49. package/components/query-bar/src/components/inline-value-chip/inline-value-chip.component.d.ts +3 -3
  50. package/components/query-bar/src/components/saved-filters-menu/saved-filters-menu.component.d.ts +9 -4
  51. package/components/query-bar/src/query-bar.component.d.ts +32 -5
  52. package/components/query-bar/src/query-bar.model.d.ts +15 -12
  53. package/components/query-builder/src/query-builder.component.d.ts +76 -39
  54. package/components/query-builder/src/query-builder.model.d.ts +59 -52
  55. package/components/query-builder/src/query-builder.serializer.d.ts +15 -6
  56. package/components/section/src/section.component.d.ts +5 -0
  57. package/components/side-drawer/src/body-scroll-lock.service.d.ts +10 -0
  58. package/components/splitter/src/splitter-handle/splitter-handle.component.d.ts +1 -0
  59. package/components/tab-router/index.d.ts +2 -1
  60. package/components/tab-router/src/actions/tab-router.action.d.ts +2 -2
  61. package/components/tab-router/src/components/tab-router-item/tab-router-item.component.d.ts +2 -2
  62. package/components/tab-router/src/components/tab-router-nav/tab-router-nav.component.d.ts +3 -3
  63. package/components/tab-router/src/components/tab-router-outlet/tab-router-outlet.component.d.ts +3 -3
  64. package/components/tab-router/src/decorators/tab.decorator.d.ts +13 -1
  65. package/components/tab-router/src/events/tab-router.event.d.ts +3 -3
  66. package/components/tab-router/src/models/tab-router.model.d.ts +2 -2
  67. package/components/tab-router/src/pipes/tab-info.pipe.d.ts +2 -2
  68. package/components/tab-router/src/services/tab-router.service.d.ts +9 -9
  69. package/components/table/index.d.ts +1 -0
  70. package/components/table/src/components/config/config.component.d.ts +6 -1
  71. package/components/table/src/components/desktop-cell/view/view.component.d.ts +1 -0
  72. package/components/table/src/components/filter/external-filter/external-filter.component.d.ts +1 -1
  73. package/components/table/src/components/filter/mobile-filter/mobile-filter.component.d.ts +1 -1
  74. package/components/table/src/components/selector-action/action-filter.pipe.d.ts +8 -1
  75. package/components/table/src/components/selector-action/selector-action.component.d.ts +2 -0
  76. package/components/table/src/directives/index.d.ts +1 -0
  77. package/components/table/src/directives/sd-table-command-header-def.directive.d.ts +24 -0
  78. package/components/table/src/models/table-column.model.d.ts +30 -13
  79. package/components/table/src/models/table-item.model.d.ts +30 -1
  80. package/components/table/src/models/table-option-selector.model.d.ts +5 -4
  81. package/components/table/src/models/table-option.model.d.ts +12 -0
  82. package/components/table/src/pipes/sd-group.pipe.d.ts +6 -2
  83. package/components/table/src/services/table-filter/table-filter.model.d.ts +15 -15
  84. package/components/table/src/services/table-filter/table-filter.service.d.ts +2 -2
  85. package/components/table/src/services/table-format/table-format.service.d.ts +1 -1
  86. package/components/table/src/services/table-selection/selection-action.util.d.ts +24 -0
  87. package/components/table/src/services/table-selection/table-selection.util.d.ts +31 -0
  88. package/components/table/src/services/tree/tree.util.d.ts +3 -2
  89. package/components/table/src/table.component.d.ts +51 -5
  90. package/components/tree/src/tree-item-def.directive.d.ts +1 -1
  91. package/components/tree/src/tree.component.d.ts +16 -4
  92. package/components/tree/src/tree.model.d.ts +31 -25
  93. package/components/upload-file/src/configurations/upload-file.configuration.d.ts +2 -2
  94. package/components/upload-file/src/upload-file.component.d.ts +4 -1
  95. package/configurations/src/sd-core.configuration.d.ts +1 -2
  96. package/fesm2022/sdcorejs-angular-components-anchor.mjs +36 -12
  97. package/fesm2022/sdcorejs-angular-components-anchor.mjs.map +1 -1
  98. package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs +3411 -0
  99. package/fesm2022/sdcorejs-angular-components-api-contract-builder.mjs.map +1 -0
  100. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs +2 -2
  101. package/fesm2022/sdcorejs-angular-components-autoid-inspector.mjs.map +1 -1
  102. package/fesm2022/sdcorejs-angular-components-badge.mjs +27 -2
  103. package/fesm2022/sdcorejs-angular-components-badge.mjs.map +1 -1
  104. package/fesm2022/sdcorejs-angular-components-button.mjs +2 -2
  105. package/fesm2022/sdcorejs-angular-components-button.mjs.map +1 -1
  106. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs +1 -1
  107. package/fesm2022/sdcorejs-angular-components-ckeditor-styles.mjs.map +1 -1
  108. package/fesm2022/sdcorejs-angular-components-code-editor.mjs +3 -3
  109. package/fesm2022/sdcorejs-angular-components-code-editor.mjs.map +1 -1
  110. package/fesm2022/sdcorejs-angular-components-data-state.mjs +3 -3
  111. package/fesm2022/sdcorejs-angular-components-data-state.mjs.map +1 -1
  112. package/fesm2022/sdcorejs-angular-components-editor.mjs +2 -2
  113. package/fesm2022/sdcorejs-angular-components-editor.mjs.map +1 -1
  114. package/fesm2022/sdcorejs-angular-components-form-generic.mjs +324 -192
  115. package/fesm2022/sdcorejs-angular-components-form-generic.mjs.map +1 -1
  116. package/fesm2022/sdcorejs-angular-components-history.mjs +4 -4
  117. package/fesm2022/sdcorejs-angular-components-history.mjs.map +1 -1
  118. package/fesm2022/sdcorejs-angular-components-import-excel.mjs +13 -5
  119. package/fesm2022/sdcorejs-angular-components-import-excel.mjs.map +1 -1
  120. package/fesm2022/sdcorejs-angular-components-inform.mjs +3 -3
  121. package/fesm2022/sdcorejs-angular-components-inform.mjs.map +1 -1
  122. package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs +164 -8
  123. package/fesm2022/sdcorejs-angular-components-modal-resizable.mjs.map +1 -1
  124. package/fesm2022/sdcorejs-angular-components-modal.mjs +30 -18
  125. package/fesm2022/sdcorejs-angular-components-modal.mjs.map +1 -1
  126. package/fesm2022/sdcorejs-angular-components-operator.mjs +2 -2
  127. package/fesm2022/sdcorejs-angular-components-operator.mjs.map +1 -1
  128. package/fesm2022/sdcorejs-angular-components-org-chart.mjs +60 -13
  129. package/fesm2022/sdcorejs-angular-components-org-chart.mjs.map +1 -1
  130. package/fesm2022/sdcorejs-angular-components-preview.mjs +27 -17
  131. package/fesm2022/sdcorejs-angular-components-preview.mjs.map +1 -1
  132. package/fesm2022/sdcorejs-angular-components-query-bar.mjs +192 -30
  133. package/fesm2022/sdcorejs-angular-components-query-bar.mjs.map +1 -1
  134. package/fesm2022/sdcorejs-angular-components-query-builder.mjs +187 -127
  135. package/fesm2022/sdcorejs-angular-components-query-builder.mjs.map +1 -1
  136. package/fesm2022/sdcorejs-angular-components-section.mjs +33 -2
  137. package/fesm2022/sdcorejs-angular-components-section.mjs.map +1 -1
  138. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs +83 -24
  139. package/fesm2022/sdcorejs-angular-components-side-drawer.mjs.map +1 -1
  140. package/fesm2022/sdcorejs-angular-components-splitter.mjs +57 -21
  141. package/fesm2022/sdcorejs-angular-components-splitter.mjs.map +1 -1
  142. package/fesm2022/sdcorejs-angular-components-stepper.mjs +2 -2
  143. package/fesm2022/sdcorejs-angular-components-stepper.mjs.map +1 -1
  144. package/fesm2022/sdcorejs-angular-components-tab-router.mjs +68 -20
  145. package/fesm2022/sdcorejs-angular-components-tab-router.mjs.map +1 -1
  146. package/fesm2022/sdcorejs-angular-components-tab.mjs +3 -2
  147. package/fesm2022/sdcorejs-angular-components-tab.mjs.map +1 -1
  148. package/fesm2022/sdcorejs-angular-components-table.mjs +555 -267
  149. package/fesm2022/sdcorejs-angular-components-table.mjs.map +1 -1
  150. package/fesm2022/sdcorejs-angular-components-tree.mjs +80 -17
  151. package/fesm2022/sdcorejs-angular-components-tree.mjs.map +1 -1
  152. package/fesm2022/sdcorejs-angular-components-upload-file.mjs +48 -13
  153. package/fesm2022/sdcorejs-angular-components-upload-file.mjs.map +1 -1
  154. package/fesm2022/sdcorejs-angular-components.mjs +7 -1
  155. package/fesm2022/sdcorejs-angular-components.mjs.map +1 -1
  156. package/fesm2022/sdcorejs-angular-configurations.mjs.map +1 -1
  157. package/fesm2022/sdcorejs-angular-directives.mjs +45 -8
  158. package/fesm2022/sdcorejs-angular-directives.mjs.map +1 -1
  159. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs +40 -29
  160. package/fesm2022/sdcorejs-angular-forms-autocomplete.mjs.map +1 -1
  161. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs +5 -5
  162. package/fesm2022/sdcorejs-angular-forms-checkbox.mjs.map +1 -1
  163. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs +67 -50
  164. package/fesm2022/sdcorejs-angular-forms-chip-calendar.mjs.map +1 -1
  165. package/fesm2022/sdcorejs-angular-forms-chip.mjs +57 -43
  166. package/fesm2022/sdcorejs-angular-forms-chip.mjs.map +1 -1
  167. package/fesm2022/sdcorejs-angular-forms-date-range.mjs +231 -60
  168. package/fesm2022/sdcorejs-angular-forms-date-range.mjs.map +1 -1
  169. package/fesm2022/sdcorejs-angular-forms-date.mjs +95 -23
  170. package/fesm2022/sdcorejs-angular-forms-date.mjs.map +1 -1
  171. package/fesm2022/sdcorejs-angular-forms-datetime.mjs +130 -26
  172. package/fesm2022/sdcorejs-angular-forms-datetime.mjs.map +1 -1
  173. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs +39 -15
  174. package/fesm2022/sdcorejs-angular-forms-entity-picker.mjs.map +1 -1
  175. package/fesm2022/sdcorejs-angular-forms-input-color.mjs +16 -5
  176. package/fesm2022/sdcorejs-angular-forms-input-color.mjs.map +1 -1
  177. package/fesm2022/sdcorejs-angular-forms-input-number.mjs +24 -9
  178. package/fesm2022/sdcorejs-angular-forms-input-number.mjs.map +1 -1
  179. package/fesm2022/sdcorejs-angular-forms-input.mjs +58 -10
  180. package/fesm2022/sdcorejs-angular-forms-input.mjs.map +1 -1
  181. package/fesm2022/sdcorejs-angular-forms-models.mjs +227 -6
  182. package/fesm2022/sdcorejs-angular-forms-models.mjs.map +1 -1
  183. package/fesm2022/sdcorejs-angular-forms-radio.mjs +4 -4
  184. package/fesm2022/sdcorejs-angular-forms-radio.mjs.map +1 -1
  185. package/fesm2022/sdcorejs-angular-forms-select.mjs +135 -37
  186. package/fesm2022/sdcorejs-angular-forms-select.mjs.map +1 -1
  187. package/fesm2022/sdcorejs-angular-forms-switch.mjs +18 -3
  188. package/fesm2022/sdcorejs-angular-forms-switch.mjs.map +1 -1
  189. package/fesm2022/sdcorejs-angular-forms-textarea.mjs +51 -62
  190. package/fesm2022/sdcorejs-angular-forms-textarea.mjs.map +1 -1
  191. package/fesm2022/sdcorejs-angular-forms-time-range.mjs +57 -12
  192. package/fesm2022/sdcorejs-angular-forms-time-range.mjs.map +1 -1
  193. package/fesm2022/sdcorejs-angular-forms-time.mjs +16 -5
  194. package/fesm2022/sdcorejs-angular-forms-time.mjs.map +1 -1
  195. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs +100 -9
  196. package/fesm2022/sdcorejs-angular-forms-tree-select.mjs.map +1 -1
  197. package/fesm2022/sdcorejs-angular-forms.mjs +91 -46
  198. package/fesm2022/sdcorejs-angular-forms.mjs.map +1 -1
  199. package/fesm2022/sdcorejs-angular-handlers.mjs +18 -3
  200. package/fesm2022/sdcorejs-angular-handlers.mjs.map +1 -1
  201. package/fesm2022/sdcorejs-angular-i18n.mjs +895 -68
  202. package/fesm2022/sdcorejs-angular-i18n.mjs.map +1 -1
  203. package/fesm2022/sdcorejs-angular-interceptors.mjs +87 -29
  204. package/fesm2022/sdcorejs-angular-interceptors.mjs.map +1 -1
  205. package/fesm2022/sdcorejs-angular-modules-auth.mjs +25 -15
  206. package/fesm2022/sdcorejs-angular-modules-auth.mjs.map +1 -1
  207. package/fesm2022/sdcorejs-angular-modules-icon.mjs +2 -2
  208. package/fesm2022/sdcorejs-angular-modules-icon.mjs.map +1 -1
  209. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs +32 -3
  210. package/fesm2022/sdcorejs-angular-modules-keycloak.mjs.map +1 -1
  211. package/fesm2022/sdcorejs-angular-modules-layout.mjs +342 -111
  212. package/fesm2022/sdcorejs-angular-modules-layout.mjs.map +1 -1
  213. package/fesm2022/sdcorejs-angular-modules-permission.mjs +177 -40
  214. package/fesm2022/sdcorejs-angular-modules-permission.mjs.map +1 -1
  215. package/fesm2022/sdcorejs-angular-pipes.mjs +62 -28
  216. package/fesm2022/sdcorejs-angular-pipes.mjs.map +1 -1
  217. package/fesm2022/sdcorejs-angular-services-api.mjs +123 -13
  218. package/fesm2022/sdcorejs-angular-services-api.mjs.map +1 -1
  219. package/fesm2022/sdcorejs-angular-services-cache.mjs +53 -3
  220. package/fesm2022/sdcorejs-angular-services-cache.mjs.map +1 -1
  221. package/fesm2022/sdcorejs-angular-services-confirm.mjs +25 -73
  222. package/fesm2022/sdcorejs-angular-services-confirm.mjs.map +1 -1
  223. package/fesm2022/sdcorejs-angular-services-excel.mjs +12 -10
  224. package/fesm2022/sdcorejs-angular-services-excel.mjs.map +1 -1
  225. package/fesm2022/sdcorejs-angular-services-loading.mjs +75 -84
  226. package/fesm2022/sdcorejs-angular-services-loading.mjs.map +1 -1
  227. package/fesm2022/sdcorejs-angular-services-notify.mjs +46 -5
  228. package/fesm2022/sdcorejs-angular-services-notify.mjs.map +1 -1
  229. package/fesm2022/sdcorejs-angular-services-storage.mjs +4 -0
  230. package/fesm2022/sdcorejs-angular-services-storage.mjs.map +1 -1
  231. package/fesm2022/sdcorejs-angular-services.mjs +0 -1
  232. package/fesm2022/sdcorejs-angular-services.mjs.map +1 -1
  233. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs +225 -80
  234. package/fesm2022/sdcorejs-angular-utilities-extensions.mjs.map +1 -1
  235. package/fesm2022/sdcorejs-angular-utilities-models.mjs +1 -63
  236. package/fesm2022/sdcorejs-angular-utilities-models.mjs.map +1 -1
  237. package/fesm2022/sdcorejs-angular.mjs +17 -10
  238. package/fesm2022/sdcorejs-angular.mjs.map +1 -1
  239. package/forms/autocomplete/src/autocomplete.component.d.ts +5 -2
  240. package/forms/chip-calendar/src/chip-calendar.component.d.ts +10 -0
  241. package/forms/date/src/date.component.d.ts +13 -2
  242. package/forms/date-range/src/date-range.component.d.ts +13 -2
  243. package/forms/datetime/src/datetime.component.d.ts +13 -2
  244. package/forms/entity-picker/src/entity-picker.component.d.ts +9 -3
  245. package/forms/input/src/input.component.d.ts +3 -0
  246. package/forms/input-color/src/input-color.component.d.ts +4 -0
  247. package/forms/input-number/src/input-number.component.d.ts +3 -0
  248. package/forms/models/index.d.ts +3 -0
  249. package/forms/models/src/sd-form-control-connector.d.ts +12 -0
  250. package/forms/models/src/sd-model-facing-control.d.ts +43 -0
  251. package/forms/models/src/sd-search.model.d.ts +1 -1
  252. package/forms/models/src/sd-selection-data.model.d.ts +1 -1
  253. package/forms/models/src/sd-temporal-transform.d.ts +31 -0
  254. package/forms/models/src/sd-timer-scope.d.ts +29 -0
  255. package/forms/select/src/select.component.d.ts +27 -4
  256. package/forms/src/forms.module.d.ts +22 -13
  257. package/forms/switch/src/switch.component.d.ts +5 -0
  258. package/forms/textarea/src/textarea.component.d.ts +5 -3
  259. package/forms/time-range/src/time-range.component.d.ts +22 -1
  260. package/forms/tree-select/src/tree-select.component.d.ts +36 -5
  261. package/i18n/src/en.d.ts +170 -10
  262. package/i18n/src/i18n.messages.d.ts +1 -1
  263. package/i18n/src/i18n.pipe.d.ts +3 -3
  264. package/i18n/src/i18n.service.d.ts +2 -1
  265. package/i18n/src/i18n.types.d.ts +0 -3
  266. package/interceptors/no-internet/no-internet.interceptor.d.ts +11 -1
  267. package/modules/auth/services/auth.model.d.ts +1 -1
  268. package/modules/keycloak/keycloak.service.d.ts +1 -0
  269. package/modules/layout/components/shared/menu-tree/menu-tree.component.d.ts +2 -0
  270. package/modules/layout/components/sidebar-mobile-v1/components/sidebar/sidebar.component.d.ts +3 -3
  271. package/modules/layout/components/sidebar-mobile-v1/main.component.d.ts +3 -3
  272. package/modules/layout/components/sidebar-mobile-v2/main.component.d.ts +3 -3
  273. package/modules/layout/components/sidebar-mobile-v3/main.component.d.ts +3 -3
  274. package/modules/layout/components/sidebar-v1/components/sidebar/sidebar.component.d.ts +16 -3
  275. package/modules/layout/components/sidebar-v1/main.component.d.ts +3 -3
  276. package/modules/layout/components/sidebar-v2/main.component.d.ts +3 -3
  277. package/modules/layout/components/sidebar-v3/main.component.d.ts +3 -3
  278. package/modules/layout/modules/forbidden/pages/root/root.component.d.ts +5 -0
  279. package/modules/layout/modules/not-found/pages/root/root.component.d.ts +5 -0
  280. package/modules/layout/services/layout.service.d.ts +21 -0
  281. package/modules/layout/services/storage/storage.service.d.ts +52 -7
  282. package/modules/permission/src/configurations/permission.configuration.d.ts +11 -0
  283. package/modules/permission/src/directives/permission.directive.d.ts +2 -1
  284. package/modules/permission/src/services/permission.service.d.ts +52 -1
  285. package/package.json +40 -29
  286. package/pipes/src/safe-html.pipe.d.ts +26 -4
  287. package/pipes/src/time-different.pipe.d.ts +1 -0
  288. package/public-api.d.ts +11 -10
  289. package/services/api/src/api-host.d.ts +15 -0
  290. package/services/api/src/api.model.d.ts +14 -0
  291. package/services/api/src/api.module.d.ts +6 -0
  292. package/services/cache/src/cache.model.d.ts +10 -0
  293. package/services/index.d.ts +0 -1
  294. package/services/notify/index.d.ts +1 -1
  295. package/utilities/extensions/index.d.ts +1 -5
  296. package/utilities/extensions/src/url-safety.d.ts +72 -0
  297. package/utilities/extensions/src/utility.extension.d.ts +9 -5
  298. package/utilities/models/index.d.ts +0 -10
  299. package/components/base/index.d.ts +0 -1
  300. package/components/base/src/base-secure.component.d.ts +0 -8
  301. package/components/document-builder/index.d.ts +0 -4
  302. package/components/document-builder/src/components/header-footer-builder/header-footer-builder.component.d.ts +0 -15
  303. package/components/document-builder/src/components/header-footer-builder/plugins/page-number.plugin.d.ts +0 -4
  304. package/components/document-builder/src/components/index.d.ts +0 -1
  305. package/components/document-builder/src/document-builder.component.d.ts +0 -53
  306. package/components/document-builder/src/document-builder.config.d.ts +0 -21
  307. package/components/document-builder/src/document-builder.model.d.ts +0 -41
  308. package/components/document-builder/src/document-builder.utils.d.ts +0 -10
  309. package/components/document-builder/src/plugins/block-space/block-space.plugin.d.ts +0 -9
  310. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.d.ts +0 -44
  311. package/components/document-builder/src/plugins/ck-comment/ck-comment.plugin.model.d.ts +0 -57
  312. package/components/document-builder/src/plugins/heading/heading.plugin.d.ts +0 -5
  313. package/components/document-builder/src/plugins/highlight-range/highlight-range.plugin.d.ts +0 -4
  314. package/components/document-builder/src/plugins/image-custom/image-custom.plugin.d.ts +0 -31
  315. package/components/document-builder/src/plugins/image-upload/image-upload.plugin.d.ts +0 -5
  316. package/components/document-builder/src/plugins/index.d.ts +0 -11
  317. package/components/document-builder/src/plugins/page-orientation/page-orientation.plugin.d.ts +0 -24
  318. package/components/document-builder/src/plugins/paste-handler/filters/bookmark.d.ts +0 -14
  319. package/components/document-builder/src/plugins/paste-handler/filters/br.d.ts +0 -15
  320. package/components/document-builder/src/plugins/paste-handler/filters/image.d.ts +0 -25
  321. package/components/document-builder/src/plugins/paste-handler/filters/list.d.ts +0 -29
  322. package/components/document-builder/src/plugins/paste-handler/filters/parse.d.ts +0 -35
  323. package/components/document-builder/src/plugins/paste-handler/filters/removeboldwrapper.d.ts +0 -15
  324. package/components/document-builder/src/plugins/paste-handler/filters/removegooglesheetstag.d.ts +0 -15
  325. package/components/document-builder/src/plugins/paste-handler/filters/removeinvalidtablewidth.d.ts +0 -15
  326. package/components/document-builder/src/plugins/paste-handler/filters/removemsattributes.d.ts +0 -15
  327. package/components/document-builder/src/plugins/paste-handler/filters/removestyleblock.d.ts +0 -15
  328. package/components/document-builder/src/plugins/paste-handler/filters/removexmlns.d.ts +0 -15
  329. package/components/document-builder/src/plugins/paste-handler/filters/replacemsfootnotes.d.ts +0 -54
  330. package/components/document-builder/src/plugins/paste-handler/filters/space.d.ts +0 -27
  331. package/components/document-builder/src/plugins/paste-handler/filters/table.d.ts +0 -16
  332. package/components/document-builder/src/plugins/paste-handler/filters/utils.d.ts +0 -25
  333. package/components/document-builder/src/plugins/paste-handler/index.d.ts +0 -35
  334. package/components/document-builder/src/plugins/paste-handler/normalizers/googledocsnormalizer.d.ts +0 -31
  335. package/components/document-builder/src/plugins/paste-handler/normalizers/googlesheetsnormalizer.d.ts +0 -31
  336. package/components/document-builder/src/plugins/paste-handler/normalizers/mswordnormalizer.d.ts +0 -29
  337. package/components/document-builder/src/plugins/paste-handler/types.d.ts +0 -30
  338. package/components/document-builder/src/plugins/table-custom/index.d.ts +0 -34
  339. package/components/document-builder/src/plugins/variable/variable.plugin.d.ts +0 -51
  340. package/components/table/src/pipes/selection-visible-select-all.pipe.d.ts +0 -9
  341. package/fesm2022/sdcorejs-angular-components-base.mjs +0 -23
  342. package/fesm2022/sdcorejs-angular-components-base.mjs.map +0 -1
  343. package/fesm2022/sdcorejs-angular-components-document-builder.mjs +0 -5038
  344. package/fesm2022/sdcorejs-angular-components-document-builder.mjs.map +0 -1
  345. package/fesm2022/sdcorejs-angular-models.mjs +0 -6
  346. package/fesm2022/sdcorejs-angular-models.mjs.map +0 -1
  347. package/fesm2022/sdcorejs-angular-services-docx.mjs +0 -291
  348. package/fesm2022/sdcorejs-angular-services-docx.mjs.map +0 -1
  349. package/fesm2022/sdcorejs-angular-services-license.mjs +0 -97
  350. package/fesm2022/sdcorejs-angular-services-license.mjs.map +0 -1
  351. package/models/index.d.ts +0 -1
  352. package/models/src/language.model.d.ts +0 -2
  353. package/services/docx/index.d.ts +0 -1
  354. package/services/docx/src/lib/docx.model.d.ts +0 -9
  355. package/services/docx/src/lib/docx.service.d.ts +0 -14
  356. package/services/docx/src/lib/pandoc-core.d.ts +0 -11
  357. package/services/docx/src/public-api.d.ts +0 -2
  358. package/services/license/index.d.ts +0 -1
  359. package/services/license/src/license.service.d.ts +0 -9
  360. package/utilities/extensions/src/array.extension.d.ts +0 -1
  361. package/utilities/extensions/src/color.extension.d.ts +0 -5
  362. package/utilities/extensions/src/date.extension.d.ts +0 -1
  363. package/utilities/extensions/src/number.extension.d.ts +0 -1
  364. package/utilities/extensions/src/string.extension.d.ts +0 -50
  365. package/utilities/models/src/color.model.d.ts +0 -4
  366. package/utilities/models/src/empty.model.d.ts +0 -4
  367. package/utilities/models/src/filter.model.d.ts +0 -12
  368. package/utilities/models/src/maybe-async.model.d.ts +0 -10
  369. package/utilities/models/src/nested-key-of.model.d.ts +0 -4
  370. package/utilities/models/src/operator.model.d.ts +0 -13
  371. package/utilities/models/src/order.model.d.ts +0 -4
  372. package/utilities/models/src/paging.model.d.ts +0 -8
  373. package/utilities/models/src/pattern.model.d.ts +0 -21
  374. package/utilities/models/src/size.model.d.ts +0 -4
@@ -0,0 +1,3411 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, makeEnvironmentProviders, input, output, computed, ChangeDetectionStrategy, Component, inject, booleanAttribute, linkedSignal, viewChild, signal, model, effect, untracked, HostBinding } from '@angular/core';
3
+ import { sdIsTemporalValueTransform } from '@sdcorejs/angular/forms/models';
4
+ import { SdTranslatePipe, I18nService } from '@sdcorejs/angular/i18n';
5
+ import { SdButton } from '@sdcorejs/angular/components/button';
6
+ import { SdCodeEditor } from '@sdcorejs/angular/components/code-editor';
7
+ import { SdIcon } from '@sdcorejs/angular/modules/icon';
8
+ import { SdInput } from '@sdcorejs/angular/forms/input';
9
+ import { SdSelect } from '@sdcorejs/angular/forms/select';
10
+ import { SdSideDrawer } from '@sdcorejs/angular/components/side-drawer';
11
+ import { SdDate } from '@sdcorejs/angular/forms/date';
12
+ import { SdDatetime } from '@sdcorejs/angular/forms/datetime';
13
+ import { SdInputNumber } from '@sdcorejs/angular/forms/input-number';
14
+
15
+ const SD_API_CONTRACT_DATA_TYPES = [
16
+ 'string',
17
+ 'number',
18
+ 'boolean',
19
+ 'date',
20
+ 'datetime',
21
+ 'object',
22
+ 'array',
23
+ ];
24
+ const SD_API_CONTRACT_SCALAR_DATA_TYPES = [
25
+ 'string',
26
+ 'number',
27
+ 'boolean',
28
+ 'date',
29
+ 'datetime',
30
+ ];
31
+ function sdIsApiContractDataType(value) {
32
+ return typeof value === 'string' && SD_API_CONTRACT_DATA_TYPES.includes(value);
33
+ }
34
+ function sdIsApiContractScalarDataType(value) {
35
+ return typeof value === 'string' && SD_API_CONTRACT_SCALAR_DATA_TYPES.includes(value);
36
+ }
37
+ function sdIsApiContractTemporalDataType(value) {
38
+ return value === 'date' || value === 'datetime';
39
+ }
40
+ const SD_API_CONTRACT_HTTP_METHODS = [
41
+ 'GET',
42
+ 'POST',
43
+ 'PUT',
44
+ 'PATCH',
45
+ 'DELETE',
46
+ 'HEAD',
47
+ 'OPTIONS',
48
+ ];
49
+ function sdIsApiContractHttpMethod(value) {
50
+ return typeof value === 'string' && SD_API_CONTRACT_HTTP_METHODS.includes(value);
51
+ }
52
+ // ---------------------------------------------------------------------------
53
+ // Contract
54
+ // ---------------------------------------------------------------------------
55
+ /** The only `contractVersion` this release understands. */
56
+ const SD_API_CONTRACT_VERSION = 1;
57
+ const SD_API_CONTRACT_EXPRESSION_ROOTS = ['input', 'env', 'res'];
58
+ const SD_API_CONTRACT_ALLOWED_ROOTS = {
59
+ request: ['input', 'env'],
60
+ output: ['res', 'input', 'env'],
61
+ };
62
+
63
+ /** What the builder falls back to when the host application provides no configuration. */
64
+ const SD_API_CONTRACT_EMPTY_CONFIGURATION = Object.freeze({ env: Object.freeze({}) });
65
+ const SD_API_CONTRACT_CONFIGURATION = new InjectionToken('sd-api-contract.configuration');
66
+ /**
67
+ * Registers the env catalog available to every `<sd-api-contract-builder>` in the injector.
68
+ *
69
+ * ```ts
70
+ * provideSdApiContract({
71
+ * env: {
72
+ * baseUrl: { type: 'string', label: 'Backend base URL' },
73
+ * token: { type: 'string', label: 'Access token', sensitive: true },
74
+ * },
75
+ * });
76
+ * ```
77
+ */
78
+ function provideSdApiContract(configuration) {
79
+ return makeEnvironmentProviders([{ provide: SD_API_CONTRACT_CONFIGURATION, useValue: configuration }]);
80
+ }
81
+ /** Normalizes an optionally-injected configuration into one that is always safe to read. */
82
+ function resolveSdApiContractConfiguration(configuration) {
83
+ if (!configuration || typeof configuration !== 'object' || !configuration.env || typeof configuration.env !== 'object') {
84
+ return SD_API_CONTRACT_EMPTY_CONFIGURATION;
85
+ }
86
+ return configuration;
87
+ }
88
+
89
+ const OPEN = '${';
90
+ const CLOSE = '}';
91
+ // why: chỉ chấp nhận identifier thuần. Mọi thứ khác — `a[0]`, `a.toUpperCase()`, `1 + 1`, `a ? b : c`
92
+ // — trượt regex này và bị từ chối NGAY tại parser, nên không có nhánh nào có thể chạy JavaScript.
93
+ const IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
94
+ // why: ba tên này là đường vào prototype chain. Chặn ở parser (không phải ở resolver) để mọi
95
+ // consumer của reference — validator, autocomplete, executor tương lai — đều được bảo vệ như nhau.
96
+ const FORBIDDEN_SEGMENTS = new Set(['__proto__', 'prototype', 'constructor']);
97
+ function isKnownRoot(value) {
98
+ return SD_API_CONTRACT_EXPRESSION_ROOTS.includes(value);
99
+ }
100
+ /**
101
+ * Parses a `source` / URL template into references and errors.
102
+ *
103
+ * Pure string scanning — no `eval`, no `new Function`, no expression evaluation of any kind. The
104
+ * grammar accepts exactly `${<root>.<identifier>(.<identifier>)*}` and nothing else.
105
+ */
106
+ function parseSdApiContractTemplate(source) {
107
+ if (typeof source !== 'string') {
108
+ return { kind: 'literal', valid: true, references: [], errors: [] };
109
+ }
110
+ const spans = [];
111
+ const errors = [];
112
+ let cursor = 0;
113
+ while (cursor <= source.length) {
114
+ const open = source.indexOf(OPEN, cursor);
115
+ if (open < 0)
116
+ break;
117
+ const close = source.indexOf(CLOSE, open + OPEN.length);
118
+ if (close < 0) {
119
+ const raw = source.slice(open);
120
+ spans.push({ open, close: source.length - 1, raw, reference: null });
121
+ errors.push({ code: 'template.unterminated', message: `Expression "${raw}" is missing its closing "}".`, index: open, raw });
122
+ break;
123
+ }
124
+ const raw = source.slice(open, close + 1);
125
+ const inner = source.slice(open + OPEN.length, close);
126
+ const error = validateInner(inner, raw, open);
127
+ if (error) {
128
+ errors.push(error);
129
+ spans.push({ open, close, raw, reference: null });
130
+ }
131
+ else {
132
+ const segments = inner.split('.');
133
+ spans.push({
134
+ open,
135
+ close,
136
+ raw,
137
+ reference: {
138
+ root: segments[0],
139
+ path: segments.slice(1),
140
+ expression: inner,
141
+ raw,
142
+ start: open,
143
+ end: close + 1,
144
+ },
145
+ });
146
+ }
147
+ cursor = close + 1;
148
+ }
149
+ return {
150
+ kind: resolveKind(source, spans),
151
+ valid: errors.length === 0,
152
+ references: spans.map(span => span.reference).filter((reference) => reference !== null),
153
+ errors,
154
+ };
155
+ }
156
+ /** The well-formed references of a template. Malformed expressions are dropped, not thrown. */
157
+ function extractSdApiContractReferences(source) {
158
+ return parseSdApiContractTemplate(source).references;
159
+ }
160
+ function validateInner(inner, raw, index) {
161
+ if (inner.includes(OPEN)) {
162
+ return { code: 'template.nested', message: `Expression "${raw}" nests another "\${".`, index, raw };
163
+ }
164
+ if (inner.trim() === '') {
165
+ return { code: 'template.empty', message: 'Expression "${}" declares no path.', index, raw };
166
+ }
167
+ const segments = inner.split('.');
168
+ // why: kiểm identifier TRƯỚC root — `${input["a"]}` phải báo là path sai, không phải root lạ.
169
+ if (segments.some(segment => !IDENTIFIER.test(segment))) {
170
+ return {
171
+ code: 'template.invalid-path',
172
+ message: `Expression "${raw}" is not a plain dotted path of identifiers.`,
173
+ index,
174
+ raw,
175
+ };
176
+ }
177
+ if (segments.length < 2) {
178
+ return {
179
+ code: 'template.invalid-path',
180
+ message: `Expression "${raw}" needs a root and at least one segment, e.g. "\${env.baseUrl}".`,
181
+ index,
182
+ raw,
183
+ };
184
+ }
185
+ if (!isKnownRoot(segments[0])) {
186
+ return {
187
+ code: 'template.unknown-root',
188
+ message: `Unknown expression root "${segments[0]}" — expected one of ${SD_API_CONTRACT_EXPRESSION_ROOTS.join(', ')}.`,
189
+ index,
190
+ raw,
191
+ };
192
+ }
193
+ const forbidden = segments.slice(1).find(segment => FORBIDDEN_SEGMENTS.has(segment));
194
+ if (forbidden) {
195
+ return { code: 'template.forbidden-segment', message: `Segment "${forbidden}" is not addressable.`, index, raw };
196
+ }
197
+ if (segments[0] === 'env' && segments.length !== 2) {
198
+ return {
199
+ code: 'template.invalid-path',
200
+ message: `Environment references address a single key, e.g. "\${env.baseUrl}" — got "${raw}".`,
201
+ index,
202
+ raw,
203
+ };
204
+ }
205
+ return null;
206
+ }
207
+ function resolveKind(source, spans) {
208
+ if (spans.length === 0)
209
+ return 'literal';
210
+ const only = spans[0];
211
+ const isWholeString = spans.length === 1 && only.reference !== null && only.open === 0 && only.close === source.length - 1;
212
+ return isWholeString ? 'exact' : 'interpolated';
213
+ }
214
+
215
+ // why: `in` và truy cập trực tiếp đều đi qua prototype chain — `properties['constructor']` trả về
216
+ // hàm dựng của Object chứ không phải undefined. Mọi lần đọc key động trong file này phải qua đây.
217
+ function hasOwn$1(record, key) {
218
+ return Object.prototype.hasOwnProperty.call(record, key);
219
+ }
220
+ const PLACEHOLDER_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
221
+ // ---------------------------------------------------------------------------
222
+ // Traversal
223
+ // ---------------------------------------------------------------------------
224
+ /** Flattens a node tree into addressable fields. See `SdApiContractSchemaField` for the convention. */
225
+ function listSdApiContractSchemaFields(node, options) {
226
+ const flatten = (options?.arrays ?? 'flatten') === 'flatten';
227
+ const base = options?.basePath ? options.basePath.split('.') : [];
228
+ const fields = [];
229
+ const descendsInto = (candidate) => {
230
+ if (candidate.type === 'object')
231
+ return true;
232
+ if (candidate.type !== 'array' || !flatten)
233
+ return false;
234
+ const items = candidate.items;
235
+ return !!items && (items.type === 'object' || items.type === 'array');
236
+ };
237
+ const walk = (current, segments, arrayItem) => {
238
+ if (current.type === 'object') {
239
+ const properties = current.properties;
240
+ if (!properties)
241
+ return;
242
+ for (const key of Object.keys(properties)) {
243
+ const child = properties[key];
244
+ if (!child)
245
+ continue;
246
+ const childSegments = [...segments, key];
247
+ fields.push({
248
+ path: childSegments.join('.'),
249
+ segments: childSegments,
250
+ type: child.type,
251
+ required: child.required,
252
+ label: child.label,
253
+ description: child.description,
254
+ leaf: !descendsInto(child),
255
+ arrayItem,
256
+ });
257
+ walk(child, childSegments, arrayItem);
258
+ }
259
+ return;
260
+ }
261
+ if (current.type === 'array' && flatten && current.items) {
262
+ walk(current.items, segments, true);
263
+ }
264
+ };
265
+ walk(node, base, false);
266
+ return fields;
267
+ }
268
+ /** Every `${res.…}` path the output layer may address, in a stable order. */
269
+ function listSdApiContractResponseFields(response) {
270
+ const fields = [
271
+ { path: 'status', segments: ['status'], type: 'number', required: true, leaf: true, arrayItem: false },
272
+ ];
273
+ const headers = response.headers;
274
+ if (headers) {
275
+ for (const key of Object.keys(headers)) {
276
+ const node = headers[key];
277
+ if (!node)
278
+ continue;
279
+ fields.push({
280
+ path: `headers.${key}`,
281
+ segments: ['headers', key],
282
+ type: node.type,
283
+ required: node.required,
284
+ label: node.label,
285
+ description: node.description,
286
+ leaf: true,
287
+ arrayItem: false,
288
+ });
289
+ }
290
+ }
291
+ const body = response.body;
292
+ if (body) {
293
+ fields.push({
294
+ path: 'body',
295
+ segments: ['body'],
296
+ type: body.type,
297
+ required: body.required,
298
+ label: body.label,
299
+ description: body.description,
300
+ leaf: body.type !== 'object',
301
+ arrayItem: false,
302
+ });
303
+ fields.push(...listSdApiContractSchemaFields(body, { arrays: 'stop', basePath: 'body' }));
304
+ }
305
+ return fields;
306
+ }
307
+ /**
308
+ * Resolves a *logical* reference path (`customer.id`) against a schema.
309
+ *
310
+ * Arrays are terminal: `${res.body.items}` addresses the whole array, `${res.body.items.id}` does
311
+ * not exist because there is no element to address. Per-item projection is out of scope.
312
+ */
313
+ function resolveSdApiContractSchemaPath(root, path) {
314
+ let current = root;
315
+ for (const segment of path) {
316
+ if (!current || current.type !== 'object')
317
+ return null;
318
+ const properties = current.properties;
319
+ if (!properties || !hasOwn$1(properties, segment))
320
+ return null;
321
+ current = properties[segment];
322
+ }
323
+ return current ?? null;
324
+ }
325
+ /** Resolves `status` / `headers.<name>` / `body.<path>` against a response declaration. */
326
+ function resolveSdApiContractResponsePath(response, path) {
327
+ if (path.length === 0)
328
+ return null;
329
+ const [section, ...rest] = path;
330
+ if (section === 'status') {
331
+ return rest.length === 0 ? { type: 'number', required: true, node: null } : null;
332
+ }
333
+ if (section === 'headers') {
334
+ const headers = response.headers;
335
+ if (rest.length !== 1 || !headers || !hasOwn$1(headers, rest[0]))
336
+ return null;
337
+ return describe(headers[rest[0]]);
338
+ }
339
+ if (section === 'body') {
340
+ const body = response.body;
341
+ if (!body)
342
+ return null;
343
+ const node = resolveSdApiContractSchemaPath(body, rest);
344
+ return node ? describe(node) : null;
345
+ }
346
+ return null;
347
+ }
348
+ function describe(node) {
349
+ return { type: node.type, required: node.required, label: node.label, description: node.description, node };
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // Immutable structural editing
353
+ // ---------------------------------------------------------------------------
354
+ /** Reads the node a structural pointer addresses, or `null` when the pointer does not resolve. */
355
+ function getSdApiContractNodeAt(root, pointer) {
356
+ let current = root;
357
+ for (let index = 0; index < pointer.length; index += 1) {
358
+ if (!current)
359
+ return null;
360
+ const segment = pointer[index];
361
+ if (segment === 'properties') {
362
+ const key = pointer[index + 1];
363
+ const properties = current.properties;
364
+ if (key === undefined || !properties || !hasOwn$1(properties, key))
365
+ return null;
366
+ current = properties[key];
367
+ index += 1;
368
+ }
369
+ else if (segment === 'items') {
370
+ current = current.items;
371
+ }
372
+ else {
373
+ return null;
374
+ }
375
+ }
376
+ return current ?? null;
377
+ }
378
+ /** Replaces the node a pointer addresses, rebuilding only the spine. Never mutates `root`. */
379
+ function setSdApiContractNodeAt(root, pointer, node) {
380
+ return replaceAt(root, pointer, 0, node);
381
+ }
382
+ function replaceAt(current, pointer, index, next) {
383
+ if (index >= pointer.length)
384
+ return next;
385
+ const segment = pointer[index];
386
+ if (segment === 'properties') {
387
+ const key = pointer[index + 1];
388
+ const properties = current.properties;
389
+ // why: pointer trỏ vào chỗ không tồn tại thì trả nguyên object cũ — im lặng bỏ qua an toàn hơn
390
+ // là dựng ra nhánh rỗng mà người dùng không hề khai báo.
391
+ if (key === undefined || !properties || !hasOwn$1(properties, key))
392
+ return current;
393
+ return { ...current, properties: { ...properties, [key]: replaceAt(properties[key], pointer, index + 2, next) } };
394
+ }
395
+ if (segment === 'items') {
396
+ if (!current.items)
397
+ return current;
398
+ return { ...current, items: replaceAt(current.items, pointer, index + 1, next) };
399
+ }
400
+ return current;
401
+ }
402
+ /** Appends a property. A key that already exists is left untouched — the caller must dedupe first. */
403
+ function addSdApiContractProperty(node, key, child) {
404
+ const properties = node.properties ?? {};
405
+ if (!key || hasOwn$1(properties, key))
406
+ return asObjectShape(node, properties);
407
+ return { ...node, type: 'object', properties: { ...properties, [key]: child } };
408
+ }
409
+ /** Renames a property **in place in the key order**, so the JSON diff stays readable. */
410
+ function renameSdApiContractProperty(node, from, to) {
411
+ const properties = node.properties;
412
+ if (!properties || !hasOwn$1(properties, from) || !to || from === to || hasOwn$1(properties, to)) {
413
+ return node;
414
+ }
415
+ const next = {};
416
+ for (const key of Object.keys(properties))
417
+ next[key === from ? to : key] = properties[key];
418
+ return { ...node, type: 'object', properties: next };
419
+ }
420
+ function removeSdApiContractProperty(node, key) {
421
+ const properties = node.properties;
422
+ if (!properties || !hasOwn$1(properties, key))
423
+ return node;
424
+ const next = {};
425
+ for (const existing of Object.keys(properties)) {
426
+ if (existing !== key)
427
+ next[existing] = properties[existing];
428
+ }
429
+ return { ...node, type: 'object', properties: next };
430
+ }
431
+ function asObjectShape(node, properties) {
432
+ return node.type === 'object' && node.properties ? node : { ...node, type: 'object', properties };
433
+ }
434
+ /** A minimal well-formed node of the given type. */
435
+ function createSdApiContractNode(type) {
436
+ if (type === 'object')
437
+ return { type: 'object', properties: {} };
438
+ if (type === 'array')
439
+ return { type: 'array', items: { type: 'string' } };
440
+ return { type };
441
+ }
442
+ /**
443
+ * Retypes a node, dropping the members the new type cannot carry.
444
+ *
445
+ * Returns the same reference when the type is unchanged, so an idempotent UI write never produces a
446
+ * spurious `modelChange`.
447
+ */
448
+ function changeSdApiContractNodeType(node, type) {
449
+ if (node.type === type)
450
+ return node;
451
+ const next = { type };
452
+ if (node.required !== undefined)
453
+ next.required = node.required;
454
+ if (node.label !== undefined)
455
+ next.label = node.label;
456
+ if (node.description !== undefined)
457
+ next.description = node.description;
458
+ if (node.source !== undefined)
459
+ next.source = node.source;
460
+ if (node.value !== undefined)
461
+ next.value = node.value;
462
+ if (node.transform !== undefined && sdIsApiContractTemporalDataType(type))
463
+ next.transform = node.transform;
464
+ if (type === 'object')
465
+ next.properties = {};
466
+ if (type === 'array')
467
+ next.items = { type: 'string' };
468
+ return next;
469
+ }
470
+ /** Deep copy of a node subtree. Used when a response subtree is adopted as the output schema. */
471
+ function cloneSdApiContractNode(node) {
472
+ return deepClone(node);
473
+ }
474
+ /**
475
+ * Deep copy of a whole contract.
476
+ *
477
+ * The builder clones on the way in so the object a parent owns is never reachable from an edit, and
478
+ * a consumer can do the same before handing a contract to anything that might mutate it.
479
+ */
480
+ function cloneSdApiContract(contract) {
481
+ return deepClone(contract);
482
+ }
483
+ /** Deep copy of any JSON-shaped value. Own enumerable keys only, so nothing inherited leaks in. */
484
+ function deepClone(value) {
485
+ if (Array.isArray(value))
486
+ return value.map(item => deepClone(item));
487
+ if (value && typeof value === 'object') {
488
+ const source = value;
489
+ const out = {};
490
+ for (const key of Object.keys(source))
491
+ out[key] = deepClone(source[key]);
492
+ return out;
493
+ }
494
+ return value;
495
+ }
496
+ // ---------------------------------------------------------------------------
497
+ // Record helpers (`req.path` / `req.query` / `req.headers` / `res.headers`)
498
+ // ---------------------------------------------------------------------------
499
+ function sdApiContractRecordSet(record, key, value) {
500
+ return { ...(record ?? {}), [key]: value };
501
+ }
502
+ function sdApiContractRecordRemove(record, key) {
503
+ if (!hasOwn$1(record, key))
504
+ return record;
505
+ const next = {};
506
+ for (const existing of Object.keys(record)) {
507
+ if (existing !== key)
508
+ next[existing] = record[existing];
509
+ }
510
+ return next;
511
+ }
512
+ /** Renames a key in place. A collision or an empty target is a no-op — the caller reports it. */
513
+ function sdApiContractRecordRename(record, from, to) {
514
+ if (!hasOwn$1(record, from) || !to || from === to || hasOwn$1(record, to))
515
+ return record;
516
+ const next = {};
517
+ for (const key of Object.keys(record))
518
+ next[key === from ? to : key] = record[key];
519
+ return next;
520
+ }
521
+ // ---------------------------------------------------------------------------
522
+ // Formatting
523
+ // ---------------------------------------------------------------------------
524
+ /** Builds the canonical expression text — the inverse of `parseSdApiContractTemplate`. */
525
+ function formatSdApiContractExpression(root, path) {
526
+ return `\${${[root, ...path].join('.')}}`;
527
+ }
528
+ /** Joins a diagnostic base path with a structural pointer, e.g. `req.body` + `properties.x`. */
529
+ function formatSdApiContractPointer(base, pointer) {
530
+ return pointer.length === 0 ? base : `${base}.${pointer.join('.')}`;
531
+ }
532
+ /**
533
+ * Reads REST placeholders out of a URL template.
534
+ *
535
+ * `${…}` interpolation is masked out first, so `${env.baseUrl}` is never mistaken for a `{…}`
536
+ * path placeholder.
537
+ */
538
+ function parseSdApiContractUrlPlaceholders(url) {
539
+ if (typeof url !== 'string')
540
+ return { names: [], duplicates: [], malformed: [] };
541
+ const masked = url.replace(/\$\{[^}]*\}/g, match => ' '.repeat(match.length));
542
+ const names = [];
543
+ const seen = new Set();
544
+ const duplicates = [];
545
+ const malformed = [];
546
+ let cursor = 0;
547
+ while (cursor < masked.length) {
548
+ const open = masked.indexOf('{', cursor);
549
+ if (open < 0)
550
+ break;
551
+ const close = masked.indexOf('}', open + 1);
552
+ if (close < 0) {
553
+ malformed.push(url.slice(open));
554
+ break;
555
+ }
556
+ const inner = masked.slice(open + 1, close);
557
+ if (!PLACEHOLDER_NAME.test(inner)) {
558
+ malformed.push(url.slice(open, close + 1));
559
+ }
560
+ else if (seen.has(inner)) {
561
+ if (!duplicates.includes(inner))
562
+ duplicates.push(inner);
563
+ }
564
+ else {
565
+ seen.add(inner);
566
+ names.push(inner);
567
+ }
568
+ cursor = close + 1;
569
+ }
570
+ return { names, duplicates, malformed };
571
+ }
572
+
573
+ /**
574
+ * Deterministic JSON for an API contract.
575
+ *
576
+ * Three guarantees the persisted file depends on:
577
+ *
578
+ * 1. **System keys are ordered**, so two authors editing the same contract produce the same bytes
579
+ * and a `git diff` shows the semantic change instead of a reshuffle.
580
+ * 2. **User-declared keys keep their order** (`properties`, `query`, `headers`, …) — that order is
581
+ * authored information, and sorting it would churn every diff.
582
+ * 3. **Only contract vocabulary survives.** The builder's transient UI state (expansion, selection,
583
+ * internal ids) is dropped by construction: the serializer copies a fixed key whitelist rather
584
+ * than the object it was handed, so a new piece of UI state can never leak into the file.
585
+ *
586
+ * `undefined` members are omitted; declared `false`, `0`, `null` and `""` are kept.
587
+ */
588
+ function serializeSdApiContract(contract) {
589
+ return JSON.stringify(normalizeContract(contract), null, 2);
590
+ }
591
+ function isRecord$1(value) {
592
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
593
+ }
594
+ function put(target, key, value) {
595
+ if (value !== undefined)
596
+ target[key] = value;
597
+ }
598
+ function normalizeContract(contract) {
599
+ if (!isRecord$1(contract))
600
+ return null;
601
+ const out = {};
602
+ put(out, 'contractVersion', contract['contractVersion']);
603
+ put(out, 'code', contract['code']);
604
+ put(out, 'name', contract['name']);
605
+ put(out, 'description', contract['description']);
606
+ put(out, 'input', normalizeSchemaHolder(contract['input']));
607
+ put(out, 'req', normalizeRequest(contract['req']));
608
+ put(out, 'res', normalizeResponse(contract['res']));
609
+ put(out, 'output', normalizeSchemaHolder(contract['output']));
610
+ return out;
611
+ }
612
+ function normalizeSchemaHolder(holder) {
613
+ if (!isRecord$1(holder))
614
+ return undefined;
615
+ const out = {};
616
+ put(out, 'schema', normalizeNode(holder['schema']));
617
+ return out;
618
+ }
619
+ function normalizeRequest(request) {
620
+ if (!isRecord$1(request))
621
+ return undefined;
622
+ const out = {};
623
+ put(out, 'method', request['method']);
624
+ put(out, 'url', request['url']);
625
+ put(out, 'path', normalizeNodeRecord(request['path']));
626
+ put(out, 'query', normalizeNodeRecord(request['query']));
627
+ put(out, 'headers', normalizeNodeRecord(request['headers']));
628
+ put(out, 'body', normalizeNode(request['body']));
629
+ return out;
630
+ }
631
+ function normalizeResponse(response) {
632
+ if (!isRecord$1(response))
633
+ return undefined;
634
+ const out = {};
635
+ put(out, 'status', Array.isArray(response['status']) ? [...response['status']] : response['status']);
636
+ put(out, 'headers', normalizeNodeRecord(response['headers']));
637
+ put(out, 'body', normalizeNode(response['body']));
638
+ return out;
639
+ }
640
+ function normalizeNode(node) {
641
+ if (!isRecord$1(node))
642
+ return undefined;
643
+ const out = {};
644
+ put(out, 'type', node['type']);
645
+ put(out, 'required', node['required']);
646
+ put(out, 'label', node['label']);
647
+ put(out, 'description', node['description']);
648
+ put(out, 'transform', node['transform']);
649
+ put(out, 'source', node['source']);
650
+ put(out, 'value', normalizeJsonValue(node['value']));
651
+ put(out, 'properties', normalizeNodeRecord(node['properties']));
652
+ put(out, 'items', normalizeNode(node['items']));
653
+ return out;
654
+ }
655
+ function normalizeNodeRecord(record) {
656
+ if (!isRecord$1(record))
657
+ return undefined;
658
+ const out = {};
659
+ for (const key of Object.keys(record)) {
660
+ const node = normalizeNode(record[key]);
661
+ if (node !== undefined)
662
+ out[key] = node;
663
+ }
664
+ return out;
665
+ }
666
+ // why: static literal là dữ liệu tự do của người dùng — copy nguyên hình dạng (giữ null, 0, '',
667
+ // thứ tự key) thay vì lọc theo whitelist như node.
668
+ function normalizeJsonValue(value) {
669
+ if (Array.isArray(value))
670
+ return value.map(item => normalizeJsonValue(item));
671
+ if (isRecord$1(value)) {
672
+ const out = {};
673
+ for (const key of Object.keys(value)) {
674
+ const normalized = normalizeJsonValue(value[key]);
675
+ if (normalized !== undefined)
676
+ out[key] = normalized;
677
+ }
678
+ return out;
679
+ }
680
+ return value;
681
+ }
682
+
683
+ /**
684
+ * Validates a contract against the grammar, the schema rules, the REST rules and the injected env
685
+ * catalog.
686
+ *
687
+ * Pure and UI-free: it takes `unknown` because an externally supplied contract may be malformed,
688
+ * and it **never repairs anything** — a silent fix would hide the very mistake the author needs to
689
+ * see. Diagnostics come back in a fixed traversal order (metadata → `input` → `req` → `res` →
690
+ * `output`, declaration order within each), so the same contract always yields the same list.
691
+ */
692
+ function validateSdApiContract(contract, configuration) {
693
+ const diagnostics = [];
694
+ if (!isRecord(contract)) {
695
+ return [{ code: 'contract.invalid', severity: 'error', path: '', message: 'A contract must be an object.' }];
696
+ }
697
+ const env = resolveSdApiContractConfiguration(configuration).env;
698
+ const inputSchema = readSchema(contract['input']);
699
+ const response = isRecord(contract['res']) ? contract['res'] : null;
700
+ const push = (code, severity, path, message) => {
701
+ diagnostics.push({ code, severity, path, message });
702
+ };
703
+ // -------------------------------------------------------------------------
704
+ // Reference resolution
705
+ // -------------------------------------------------------------------------
706
+ const resolveReference = (reference) => {
707
+ if (reference.root === 'env') {
708
+ const key = reference.path[0];
709
+ if (!key || !hasOwn(env, key))
710
+ return null;
711
+ const variable = env[key];
712
+ return { type: variable.type, required: true, label: variable.label, description: variable.description, node: null };
713
+ }
714
+ if (reference.root === 'input') {
715
+ if (!inputSchema)
716
+ return null;
717
+ const node = resolveSdApiContractSchemaPath(inputSchema, reference.path);
718
+ return node ? { type: node.type, required: node.required, label: node.label, description: node.description, node } : null;
719
+ }
720
+ if (!response)
721
+ return null;
722
+ return resolveSdApiContractResponsePath(response, reference.path);
723
+ };
724
+ /** Checks roots and existence for every reference. Returns `false` when a root was rejected. */
725
+ const checkReferences = (references, path, context) => {
726
+ const allowed = SD_API_CONTRACT_ALLOWED_ROOTS[context];
727
+ let rootsOk = true;
728
+ for (const reference of references) {
729
+ if (!allowed.includes(reference.root)) {
730
+ rootsOk = false;
731
+ push('mapping.root.forbidden', 'error', path, `"${reference.raw}" is not readable here — this position accepts ${allowed.map(root => `\${${root}.…}`).join(', ')}.`);
732
+ continue;
733
+ }
734
+ if (resolveReference(reference))
735
+ continue;
736
+ if (reference.root === 'env') {
737
+ push('mapping.env.unknown', 'error', path, `Environment variable "${reference.path.join('.')}" is not declared in the injected configuration.`);
738
+ }
739
+ else {
740
+ push('mapping.reference.missing', 'error', path, `"${reference.raw}" does not resolve to a declared field.`);
741
+ }
742
+ }
743
+ return rootsOk;
744
+ };
745
+ // -------------------------------------------------------------------------
746
+ // Node structure — shared by every layer
747
+ // -------------------------------------------------------------------------
748
+ const validateStructure = (node, path, allowTransform, requireContainerMembers) => {
749
+ const type = node['type'];
750
+ if (!sdIsApiContractDataType(type)) {
751
+ push('schema.type.invalid', 'error', path, `"${String(type)}" is not a supported data type.`);
752
+ return false;
753
+ }
754
+ if (type === 'object' && requireContainerMembers && !isRecord(node['properties'])) {
755
+ push('schema.object.properties.missing', 'error', path, 'An object node must declare "properties".');
756
+ }
757
+ if (type === 'array' && !isRecord(node['items'])) {
758
+ push('schema.array.items.missing', 'error', path, 'An array node must declare "items".');
759
+ }
760
+ if (sdIsApiContractScalarDataType(type)) {
761
+ if (node['properties'] !== undefined)
762
+ push('schema.scalar.properties.forbidden', 'error', path, `A "${type}" node cannot declare "properties".`);
763
+ if (node['items'] !== undefined)
764
+ push('schema.scalar.items.forbidden', 'error', path, `A "${type}" node cannot declare "items".`);
765
+ }
766
+ const required = node['required'];
767
+ if (required !== undefined && typeof required !== 'boolean') {
768
+ push('schema.required.invalid', 'error', path, '"required" must be true, false, or omitted.');
769
+ }
770
+ const transform = node['transform'];
771
+ if (transform !== undefined) {
772
+ if (!allowTransform || !sdIsApiContractTemporalDataType(type)) {
773
+ push('schema.transform.invalid', 'error', path, '"transform" is only valid on a date or datetime node in input.schema / output.schema.');
774
+ }
775
+ else if (!sdIsTemporalValueTransform(transform)) {
776
+ push('schema.transform.unknown', 'error', path, `"${String(transform)}" is not a known temporal transform.`);
777
+ }
778
+ }
779
+ return true;
780
+ };
781
+ const checkDuplicateKeysIgnoringCase = (record, basePath) => {
782
+ const seen = new Map();
783
+ for (const key of Object.keys(record)) {
784
+ const normalized = key.trim().toLowerCase();
785
+ if (!normalized)
786
+ continue;
787
+ const previous = seen.get(normalized);
788
+ if (previous !== undefined) {
789
+ push('schema.property.key.duplicate', 'error', `${basePath}.${key}`, `"${key}" collides with "${previous}" — header names are case-insensitive.`);
790
+ }
791
+ else {
792
+ seen.set(normalized, key);
793
+ }
794
+ }
795
+ };
796
+ // -------------------------------------------------------------------------
797
+ // Declaration layers — `input.schema`, `res.headers`, `res.body`
798
+ // -------------------------------------------------------------------------
799
+ const validateDeclaration = (node, path, allowTransform) => {
800
+ if (!isRecord(node))
801
+ return;
802
+ if (!validateStructure(node, path, allowTransform, true))
803
+ return;
804
+ if (node['source'] !== undefined || node['value'] !== undefined) {
805
+ push('schema.mapping.forbidden', 'error', path, 'A declaration cannot carry "source" or "value" — nothing maps into it.');
806
+ }
807
+ const type = node['type'];
808
+ const properties = node['properties'];
809
+ if (type === 'object' && isRecord(properties)) {
810
+ for (const key of Object.keys(properties)) {
811
+ const childPath = `${path}.properties.${key}`;
812
+ if (!key.trim())
813
+ push('schema.property.key.empty', 'error', childPath, 'A property name cannot be empty.');
814
+ validateDeclaration(properties[key], childPath, allowTransform);
815
+ }
816
+ }
817
+ if (type === 'array' && isRecord(node['items'])) {
818
+ validateDeclaration(node['items'], `${path}.items`, allowTransform);
819
+ }
820
+ };
821
+ // -------------------------------------------------------------------------
822
+ // Mapped layers — `req.*`, `output.schema`
823
+ // -------------------------------------------------------------------------
824
+ const validateStaticValue = (value, type, path) => {
825
+ if (value === null)
826
+ return;
827
+ const ok = type === 'string' || type === 'date' || type === 'datetime'
828
+ ? typeof value === 'string'
829
+ : type === 'number'
830
+ ? typeof value === 'number' && Number.isFinite(value)
831
+ : type === 'boolean'
832
+ ? typeof value === 'boolean'
833
+ : type === 'object'
834
+ ? isRecord(value)
835
+ : Array.isArray(value);
836
+ if (!ok)
837
+ push('mapping.value.type-mismatch', 'error', path, `The static value does not fit the declared type "${type}".`);
838
+ };
839
+ const validateSource = (node, type, path, context) => {
840
+ const source = node['source'];
841
+ if (typeof source !== 'string') {
842
+ push('mapping.template.invalid', 'error', path, '"source" must be a string template.');
843
+ return;
844
+ }
845
+ const template = parseSdApiContractTemplate(source);
846
+ if (!template.valid) {
847
+ push('mapping.template.invalid', 'error', path, template.errors[0].message);
848
+ return;
849
+ }
850
+ if (!checkReferences(template.references, path, context))
851
+ return;
852
+ if (template.kind === 'exact') {
853
+ const resolved = resolveReference(template.references[0]);
854
+ if (!resolved)
855
+ return;
856
+ if (!isTypeCompatible(resolved.type, type)) {
857
+ push('mapping.type.mismatch', 'error', path, `"${source}" resolves to "${resolved.type}", which cannot fill a "${type}" node.`);
858
+ }
859
+ if (context === 'output' && node['required'] === true && resolved.required !== true) {
860
+ push('mapping.required.optional-source', 'warning', path, `A required output field is fed by "${source}", which is not declared required.`);
861
+ }
862
+ return;
863
+ }
864
+ // why: literal + interpolated đều cho ra string. `req.url` không đi qua đây nên vẫn nội suy tự do.
865
+ if (type !== 'string') {
866
+ push('mapping.interpolation.forbidden', 'error', path, `String interpolation can only fill a "string" node, not "${type}".`);
867
+ }
868
+ };
869
+ const validateMapped = (node, path, context, covered, allowTransform) => {
870
+ if (!isRecord(node))
871
+ return;
872
+ const hasSource = node['source'] !== undefined;
873
+ const hasValue = node['value'] !== undefined;
874
+ const wholeNodeMapped = hasSource || hasValue;
875
+ if (!validateStructure(node, path, allowTransform, !wholeNodeMapped))
876
+ return;
877
+ const type = node['type'];
878
+ if (hasSource && hasValue) {
879
+ push('mapping.source-and-value', 'error', path, '"source" and "value" are mutually exclusive.');
880
+ }
881
+ const properties = node['properties'];
882
+ const childCount = type === 'object' && isRecord(properties) ? Object.keys(properties).length : 0;
883
+ if (wholeNodeMapped && childCount > 0) {
884
+ push('mapping.object.conflict', 'error', path, 'An object mapped as a whole cannot also map its properties.');
885
+ }
886
+ if (hasSource)
887
+ validateSource(node, type, path, context);
888
+ else if (hasValue)
889
+ validateStaticValue(node['value'], type, path);
890
+ else if (!covered && (type !== 'object' || childCount === 0)) {
891
+ push('mapping.node.unmapped', 'warning', path, 'This node receives no value — declare a "source" or a static "value".');
892
+ }
893
+ const childCovered = covered || wholeNodeMapped;
894
+ if (type === 'object' && isRecord(properties)) {
895
+ for (const key of Object.keys(properties)) {
896
+ const childPath = `${path}.properties.${key}`;
897
+ if (!key.trim())
898
+ push('schema.property.key.empty', 'error', childPath, 'A property name cannot be empty.');
899
+ validateMapped(properties[key], childPath, context, childCovered, allowTransform);
900
+ }
901
+ }
902
+ if (type === 'array' && isRecord(node['items'])) {
903
+ validateMapped(node['items'], `${path}.items`, context, childCovered, allowTransform);
904
+ }
905
+ };
906
+ // -------------------------------------------------------------------------
907
+ // 1. Contract metadata
908
+ // -------------------------------------------------------------------------
909
+ if (contract['contractVersion'] !== SD_API_CONTRACT_VERSION) {
910
+ push('contract.version.invalid', 'error', 'contractVersion', `"contractVersion" must be ${SD_API_CONTRACT_VERSION}.`);
911
+ }
912
+ if (!isFilledString(contract['code']))
913
+ push('contract.code.empty', 'error', 'code', 'A contract needs a non-empty "code".');
914
+ if (!isFilledString(contract['name']))
915
+ push('contract.name.empty', 'error', 'name', 'A contract needs a non-empty "name".');
916
+ // -------------------------------------------------------------------------
917
+ // 2. input.schema
918
+ // -------------------------------------------------------------------------
919
+ if (!inputSchema)
920
+ push('schema.missing', 'error', 'input.schema', '"input.schema" is missing.');
921
+ else
922
+ validateDeclaration(inputSchema, 'input.schema', true);
923
+ // -------------------------------------------------------------------------
924
+ // 3. req
925
+ // -------------------------------------------------------------------------
926
+ const request = isRecord(contract['req']) ? contract['req'] : null;
927
+ if (!request) {
928
+ push('req.method.invalid', 'error', 'req.method', 'The request is missing.');
929
+ push('req.url.empty', 'error', 'req.url', 'The request is missing.');
930
+ }
931
+ else {
932
+ const method = request['method'];
933
+ if (!sdIsApiContractHttpMethod(method)) {
934
+ push('req.method.invalid', 'error', 'req.method', `"${String(method)}" is not a supported HTTP method.`);
935
+ }
936
+ const url = request['url'];
937
+ const placeholders = isFilledString(url) ? parseSdApiContractUrlPlaceholders(url) : { names: [], duplicates: [], malformed: [] };
938
+ if (!isFilledString(url)) {
939
+ push('req.url.empty', 'error', 'req.url', 'A request needs a non-empty "url".');
940
+ }
941
+ else {
942
+ const template = parseSdApiContractTemplate(url);
943
+ if (!template.valid)
944
+ push('req.url.template.invalid', 'error', 'req.url', template.errors[0].message);
945
+ else
946
+ checkReferences(template.references, 'req.url', 'request');
947
+ for (const fragment of placeholders.malformed) {
948
+ push('req.url.placeholder.malformed', 'error', 'req.url', `"${fragment}" is not a valid path placeholder.`);
949
+ }
950
+ for (const name of placeholders.duplicates) {
951
+ push('req.url.placeholder.duplicate', 'error', 'req.url', `Path placeholder "{${name}}" appears more than once.`);
952
+ }
953
+ }
954
+ const pathRecord = isRecord(request['path']) ? request['path'] : null;
955
+ const declaredPathKeys = pathRecord ? Object.keys(pathRecord) : [];
956
+ for (const name of placeholders.names) {
957
+ if (!declaredPathKeys.includes(name)) {
958
+ push('req.path.missing', 'error', `req.path.${name}`, `The url declares "{${name}}" but "req.path" has no entry for it.`);
959
+ }
960
+ }
961
+ for (const key of declaredPathKeys) {
962
+ if (!placeholders.names.includes(key)) {
963
+ push('req.path.unused', 'error', `req.path.${key}`, `"req.path.${key}" has no matching "{${key}}" in the url.`);
964
+ }
965
+ }
966
+ if (pathRecord) {
967
+ for (const key of declaredPathKeys) {
968
+ const entryPath = `req.path.${key}`;
969
+ const entry = pathRecord[key];
970
+ if (!key.trim())
971
+ push('schema.property.key.empty', 'error', entryPath, 'A path parameter name cannot be empty.');
972
+ if (isRecord(entry)) {
973
+ if (entry['required'] !== true) {
974
+ push('req.path.required', 'error', entryPath, 'A path parameter is part of the url and must be declared "required": true.');
975
+ }
976
+ if (entry['type'] !== undefined && !sdIsApiContractScalarDataType(entry['type'])) {
977
+ push('req.path.type.invalid', 'error', entryPath, 'A path parameter must be a scalar.');
978
+ }
979
+ }
980
+ validateMapped(entry, entryPath, 'request', false, false);
981
+ }
982
+ }
983
+ const queryRecord = isRecord(request['query']) ? request['query'] : null;
984
+ if (queryRecord) {
985
+ for (const key of Object.keys(queryRecord)) {
986
+ const entryPath = `req.query.${key}`;
987
+ const entry = queryRecord[key];
988
+ if (!key.trim())
989
+ push('schema.property.key.empty', 'error', entryPath, 'A query parameter name cannot be empty.');
990
+ if (isRecord(entry) && entry['type'] !== undefined && !sdIsApiContractScalarDataType(entry['type']) && entry['type'] !== 'array') {
991
+ push('req.query.type.invalid', 'error', entryPath, 'A query parameter must be a scalar or an array of scalars.');
992
+ }
993
+ validateMapped(entry, entryPath, 'request', false, false);
994
+ }
995
+ }
996
+ const headerRecord = isRecord(request['headers']) ? request['headers'] : null;
997
+ if (headerRecord) {
998
+ checkDuplicateKeysIgnoringCase(headerRecord, 'req.headers');
999
+ for (const key of Object.keys(headerRecord)) {
1000
+ const entryPath = `req.headers.${key}`;
1001
+ const entry = headerRecord[key];
1002
+ if (!key.trim())
1003
+ push('req.header.name.empty', 'error', entryPath, 'A header name cannot be empty.');
1004
+ if (isRecord(entry) && entry['type'] !== undefined && !sdIsApiContractScalarDataType(entry['type'])) {
1005
+ push('req.header.type.invalid', 'error', entryPath, 'A header must be a scalar.');
1006
+ }
1007
+ validateMapped(entry, entryPath, 'request', false, false);
1008
+ }
1009
+ }
1010
+ if (request['body'] !== undefined) {
1011
+ if (method === 'GET' || method === 'HEAD') {
1012
+ push('req.body.unexpected', 'warning', 'req.body', `A ${method} request with a body is ignored by many clients and proxies.`);
1013
+ }
1014
+ validateMapped(request['body'], 'req.body', 'request', false, false);
1015
+ }
1016
+ }
1017
+ // -------------------------------------------------------------------------
1018
+ // 4. res
1019
+ // -------------------------------------------------------------------------
1020
+ if (!response) {
1021
+ push('res.status.invalid', 'error', 'res.status', 'The response declaration is missing.');
1022
+ }
1023
+ else {
1024
+ const rawStatus = response['status'];
1025
+ const statuses = Array.isArray(rawStatus) ? rawStatus : [rawStatus];
1026
+ if (Array.isArray(rawStatus) && rawStatus.length === 0) {
1027
+ push('res.status.empty', 'error', 'res.status', 'Declare at least one success status.');
1028
+ }
1029
+ const seenStatus = new Set();
1030
+ for (const status of statuses) {
1031
+ if (typeof status !== 'number' || !Number.isInteger(status) || status < 100 || status > 599) {
1032
+ push('res.status.invalid', 'error', 'res.status', `"${String(status)}" is not an HTTP status code between 100 and 599.`);
1033
+ }
1034
+ else if (seenStatus.has(status)) {
1035
+ push('res.status.duplicate', 'error', 'res.status', `Status ${status} is declared more than once.`);
1036
+ }
1037
+ else {
1038
+ seenStatus.add(status);
1039
+ }
1040
+ }
1041
+ const responseHeaders = isRecord(response['headers']) ? response['headers'] : null;
1042
+ if (responseHeaders) {
1043
+ checkDuplicateKeysIgnoringCase(responseHeaders, 'res.headers');
1044
+ for (const key of Object.keys(responseHeaders)) {
1045
+ const entryPath = `res.headers.${key}`;
1046
+ if (!key.trim())
1047
+ push('req.header.name.empty', 'error', entryPath, 'A header name cannot be empty.');
1048
+ validateDeclaration(responseHeaders[key], entryPath, false);
1049
+ }
1050
+ }
1051
+ if (response['body'] !== undefined) {
1052
+ if (seenStatus.has(204)) {
1053
+ push('res.body.unexpected', 'warning', 'res.body', 'HTTP 204 means "no content" — a declared body will never arrive.');
1054
+ }
1055
+ validateDeclaration(response['body'], 'res.body', false);
1056
+ }
1057
+ }
1058
+ // -------------------------------------------------------------------------
1059
+ // 5. output.schema
1060
+ // -------------------------------------------------------------------------
1061
+ const outputSchema = readSchema(contract['output']);
1062
+ if (!outputSchema)
1063
+ push('schema.missing', 'error', 'output.schema', '"output.schema" is missing.');
1064
+ else
1065
+ validateMapped(outputSchema, 'output.schema', 'output', false, true);
1066
+ return diagnostics;
1067
+ }
1068
+ /** `true` when a source of `sourceType` may fill a target of `targetType`. */
1069
+ function isTypeCompatible(sourceType, targetType) {
1070
+ if (sourceType === targetType)
1071
+ return true;
1072
+ // why: date/datetime là kiểu LOGIC, trên dây luôn là string — nên string ↔ temporal đi được cả hai
1073
+ // chiều. Mọi cặp khác phải khớp chính xác, vì exact expression giữ nguyên type của nguồn.
1074
+ const sourceTemporalish = sdIsApiContractTemporalDataType(sourceType) || sourceType === 'string';
1075
+ const targetTemporalish = sdIsApiContractTemporalDataType(targetType) || targetType === 'string';
1076
+ return sourceTemporalish && targetTemporalish;
1077
+ }
1078
+ function readSchema(holder) {
1079
+ if (!isRecord(holder) || !isRecord(holder['schema']))
1080
+ return null;
1081
+ return holder['schema'];
1082
+ }
1083
+ function isRecord(value) {
1084
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
1085
+ }
1086
+ function isFilledString(value) {
1087
+ return typeof value === 'string' && value.trim().length > 0;
1088
+ }
1089
+ function hasOwn(record, key) {
1090
+ return Object.prototype.hasOwnProperty.call(record, key);
1091
+ }
1092
+
1093
+ /**
1094
+ * Reference contracts, shared by the docs, the showcase and the test-suite so the canonical example
1095
+ * can never drift between them.
1096
+ *
1097
+ * Each is a **factory**, not a constant: the builder takes a two-way `[(model)]`, and handing two
1098
+ * demos the same object would let one seed the other.
1099
+ */
1100
+ /** The env catalog the samples reference. Definitions only — no secret ever has a value here. */
1101
+ const SD_API_CONTRACT_SAMPLE_ENVIRONMENT = {
1102
+ env: {
1103
+ baseUrl: { type: 'string', label: 'Backend base URL' },
1104
+ token: { type: 'string', label: 'Access token', sensitive: true },
1105
+ userId: { type: 'string', label: 'Current user ID' },
1106
+ },
1107
+ };
1108
+ /** `GET` list endpoint whose output is a root array — the dropdown / table shape. */
1109
+ function sdApiContractSearchSample() {
1110
+ return {
1111
+ contractVersion: 1,
1112
+ code: 'product.search',
1113
+ name: 'Search products',
1114
+ description: 'Search active products for dropdown or table',
1115
+ input: {
1116
+ schema: {
1117
+ type: 'object',
1118
+ properties: {
1119
+ keyword: { type: 'string', required: false },
1120
+ page: { type: 'number' },
1121
+ createdFrom: { type: 'datetime', transform: 'ISOString' },
1122
+ },
1123
+ },
1124
+ },
1125
+ req: {
1126
+ method: 'GET',
1127
+ url: '${env.baseUrl}/products',
1128
+ query: {
1129
+ keyword: { type: 'string', source: '${input.keyword}' },
1130
+ page: { type: 'number', source: '${input.page}' },
1131
+ createdFrom: { type: 'datetime', source: '${input.createdFrom}' },
1132
+ },
1133
+ headers: {
1134
+ Authorization: { type: 'string', source: 'Bearer ${env.token}' },
1135
+ 'x-user-id': { type: 'string', source: '${env.userId}' },
1136
+ },
1137
+ },
1138
+ res: {
1139
+ status: 200,
1140
+ headers: { 'x-request-id': { type: 'string', required: false } },
1141
+ body: {
1142
+ type: 'object',
1143
+ properties: {
1144
+ items: {
1145
+ type: 'array',
1146
+ required: true,
1147
+ items: {
1148
+ type: 'object',
1149
+ properties: {
1150
+ id: { type: 'string', required: true },
1151
+ name: { type: 'string', required: true },
1152
+ createdAt: { type: 'datetime' },
1153
+ },
1154
+ },
1155
+ },
1156
+ total: { type: 'number', required: true },
1157
+ },
1158
+ },
1159
+ },
1160
+ output: {
1161
+ schema: {
1162
+ type: 'array',
1163
+ source: '${res.body.items}',
1164
+ items: {
1165
+ type: 'object',
1166
+ properties: {
1167
+ id: { type: 'string', required: true },
1168
+ name: { type: 'string', required: true },
1169
+ createdAt: { type: 'datetime' },
1170
+ },
1171
+ },
1172
+ },
1173
+ },
1174
+ };
1175
+ }
1176
+ /**
1177
+ * `POST` endpoint showing every mapping flavour at once:
1178
+ * `input.a → req.body.x`, `input.b → req.body.y`, `input.c → req.body.z`,
1179
+ * `env.userId → req.body.u`, and a static literal in `req.body.v`.
1180
+ */
1181
+ function sdApiContractCreateSample() {
1182
+ return {
1183
+ contractVersion: 1,
1184
+ code: 'order.create',
1185
+ name: 'Create order',
1186
+ description: 'Maps a frontend payload onto the backend order body',
1187
+ input: {
1188
+ schema: {
1189
+ type: 'object',
1190
+ properties: {
1191
+ a: { type: 'string', required: true, label: 'Order code' },
1192
+ b: { type: 'number', label: 'Quantity' },
1193
+ c: { type: 'array', items: { type: 'string' }, label: 'Tags' },
1194
+ },
1195
+ },
1196
+ },
1197
+ req: {
1198
+ method: 'POST',
1199
+ url: '${env.baseUrl}/orders',
1200
+ headers: { Authorization: { type: 'string', source: 'Bearer ${env.token}' } },
1201
+ body: {
1202
+ type: 'object',
1203
+ properties: {
1204
+ x: { type: 'string', required: true, source: '${input.a}' },
1205
+ y: { type: 'number', source: '${input.b}' },
1206
+ z: { type: 'array', source: '${input.c}', items: { type: 'string' } },
1207
+ u: { type: 'string', source: '${env.userId}' },
1208
+ v: { type: 'string', value: 'STATIC VALUE' },
1209
+ },
1210
+ },
1211
+ },
1212
+ res: {
1213
+ status: [200, 201],
1214
+ body: {
1215
+ type: 'object',
1216
+ properties: {
1217
+ id: { type: 'string', required: true },
1218
+ createdAt: { type: 'datetime' },
1219
+ },
1220
+ },
1221
+ },
1222
+ output: {
1223
+ schema: {
1224
+ type: 'object',
1225
+ properties: {
1226
+ id: { type: 'string', required: true, source: '${res.body.id}' },
1227
+ createdAt: { type: 'datetime', source: '${res.body.createdAt}' },
1228
+ },
1229
+ },
1230
+ },
1231
+ };
1232
+ }
1233
+ /**
1234
+ * Deliberately broken contract used to demonstrate the diagnostics: an undeclared env variable, a
1235
+ * `{id}` placeholder with no `req.path` entry, a `${input.page}` that does not exist, and an output
1236
+ * source pointing at a scalar while the output declares an array.
1237
+ */
1238
+ function sdApiContractInvalidSample() {
1239
+ return {
1240
+ contractVersion: 1,
1241
+ code: 'product.broken',
1242
+ name: 'Broken product search',
1243
+ description: 'Every diagnostic class in one contract',
1244
+ input: {
1245
+ schema: {
1246
+ type: 'object',
1247
+ properties: { keyword: { type: 'string' } },
1248
+ },
1249
+ },
1250
+ req: {
1251
+ method: 'GET',
1252
+ url: '${env.baseUrl}/products/{id}',
1253
+ query: { page: { type: 'number', source: '${input.page}' } },
1254
+ headers: { Authorization: { type: 'string', source: 'Bearer ${env.unknown}' } },
1255
+ },
1256
+ res: {
1257
+ status: 200,
1258
+ body: {
1259
+ type: 'object',
1260
+ properties: {
1261
+ items: { type: 'array', required: true, items: { type: 'object', properties: { id: { type: 'string' } } } },
1262
+ total: { type: 'number' },
1263
+ },
1264
+ },
1265
+ },
1266
+ output: {
1267
+ schema: {
1268
+ type: 'array',
1269
+ source: '${res.body.total}',
1270
+ items: { type: 'object', properties: { id: { type: 'string' } } },
1271
+ },
1272
+ },
1273
+ };
1274
+ }
1275
+
1276
+ /**
1277
+ * Validation summary + the diagnostic list.
1278
+ *
1279
+ * Severity is never signalled by colour alone: every row carries an icon, the severity word, the
1280
+ * stable `code` and the structural `path`, so the list stays readable for a colour-blind reader and
1281
+ * usable from a screen reader.
1282
+ */
1283
+ class SdApiContractDiagnosticList {
1284
+ diagnostics = input([]);
1285
+ autoId = input();
1286
+ navigate = output();
1287
+ errorCount = computed(() => this.diagnostics().filter(diagnostic => diagnostic.severity === 'error').length);
1288
+ warningCount = computed(() => this.diagnostics().filter(diagnostic => diagnostic.severity === 'warning').length);
1289
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractDiagnosticList, deps: [], target: i0.ɵɵFactoryTarget.Component });
1290
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractDiagnosticList, isStandalone: true, selector: "sd-api-contract-diagnostic-list", inputs: { diagnostics: { classPropertyName: "diagnostics", publicName: "diagnostics", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { navigate: "navigate" }, ngImport: i0, template: `
1291
+ @let _diagnostics = diagnostics();
1292
+ @let _errors = errorCount();
1293
+ @let _warnings = warningCount();
1294
+ @let _autoId = autoId();
1295
+
1296
+ <div class="sd-acb-diagnostics" [attr.data-autoId]="_autoId">
1297
+ <div class="sd-acb-diagnostics__summary" role="status">
1298
+ @if (!_diagnostics.length) {
1299
+ <span class="sd-acb-diagnostics__badge" data-severity="ok">
1300
+ <sd-icon name="check_circle" size="sm"></sd-icon>
1301
+ <span>{{ 'core.component.api-contract-builder.review.valid' | sdTranslate }}</span>
1302
+ </span>
1303
+ } @else {
1304
+ <span class="sd-acb-diagnostics__badge" data-severity="error">
1305
+ <sd-icon name="error" size="sm"></sd-icon>
1306
+ <span>{{ 'core.component.api-contract-builder.review.errors' | sdTranslate: { count: _errors } }}</span>
1307
+ </span>
1308
+ <span class="sd-acb-diagnostics__badge" data-severity="warning">
1309
+ <sd-icon name="warning" size="sm"></sd-icon>
1310
+ <span>{{ 'core.component.api-contract-builder.review.warnings' | sdTranslate: { count: _warnings } }}</span>
1311
+ </span>
1312
+ }
1313
+ </div>
1314
+
1315
+ @if (_diagnostics.length) {
1316
+ <ul class="sd-acb-diagnostics__list">
1317
+ @for (diagnostic of _diagnostics; track diagnostic.code + '|' + diagnostic.path + '|' + $index) {
1318
+ <li class="sd-acb-diagnostics__item" [attr.data-severity]="diagnostic.severity" [attr.data-code]="diagnostic.code">
1319
+ <button
1320
+ type="button"
1321
+ class="sd-acb-diagnostics__nav"
1322
+ [attr.data-autoId]="_autoId ? _autoId + '-goto-' + $index : null"
1323
+ (click)="navigate.emit(diagnostic)">
1324
+ <sd-icon [name]="diagnostic.severity === 'error' ? 'error' : 'warning'" size="sm"></sd-icon>
1325
+ <span class="sd-acb-diagnostics__path">{{ diagnostic.path || '(contract)' }}</span>
1326
+ <span class="sd-acb-diagnostics__message">{{ diagnostic.message }}</span>
1327
+ <span class="sd-acb-diagnostics__code">{{ diagnostic.code }}</span>
1328
+ </button>
1329
+ </li>
1330
+ }
1331
+ </ul>
1332
+ }
1333
+ </div>
1334
+ `, isInline: true, styles: [":host{display:block}.sd-acb-diagnostics__summary{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px}.sd-acb-diagnostics__badge{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;&[data-severity=ok]{color:var(--sd-success, #1c6c3a)}&[data-severity=error]{color:var(--sd-error, #b3261e)}&[data-severity=warning]{color:var(--sd-warning, #8a5a00)}}.sd-acb-diagnostics__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}.sd-acb-diagnostics__nav{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;width:100%;text-align:left;background:none;border:1px solid transparent;border-radius:6px;padding:6px 8px;cursor:pointer;font:inherit;font-size:12px;&:hover,&:focus-visible{border-color:var(--sd-border-color, #e6e6e6);background:var(--sd-surface-muted, #f3f5f8)}}.sd-acb-diagnostics__item[data-severity=error] .sd-acb-diagnostics__nav{color:var(--sd-error, #b3261e)}.sd-acb-diagnostics__item[data-severity=warning] .sd-acb-diagnostics__nav{color:var(--sd-warning, #8a5a00)}.sd-acb-diagnostics__path{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-weight:600}.sd-acb-diagnostics__message{color:var(--sd-text, #1f2937)}.sd-acb-diagnostics__code{margin-left:auto;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;color:var(--sd-text-secondary, #6b6b6b)}\n"], dependencies: [{ kind: "component", type: SdIcon, selector: "sd-icon", inputs: ["name", "fontIcon", "color", "set", "fontSet", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1335
+ }
1336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractDiagnosticList, decorators: [{
1337
+ type: Component,
1338
+ args: [{ selector: 'sd-api-contract-diagnostic-list', standalone: true, imports: [SdIcon, SdTranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1339
+ @let _diagnostics = diagnostics();
1340
+ @let _errors = errorCount();
1341
+ @let _warnings = warningCount();
1342
+ @let _autoId = autoId();
1343
+
1344
+ <div class="sd-acb-diagnostics" [attr.data-autoId]="_autoId">
1345
+ <div class="sd-acb-diagnostics__summary" role="status">
1346
+ @if (!_diagnostics.length) {
1347
+ <span class="sd-acb-diagnostics__badge" data-severity="ok">
1348
+ <sd-icon name="check_circle" size="sm"></sd-icon>
1349
+ <span>{{ 'core.component.api-contract-builder.review.valid' | sdTranslate }}</span>
1350
+ </span>
1351
+ } @else {
1352
+ <span class="sd-acb-diagnostics__badge" data-severity="error">
1353
+ <sd-icon name="error" size="sm"></sd-icon>
1354
+ <span>{{ 'core.component.api-contract-builder.review.errors' | sdTranslate: { count: _errors } }}</span>
1355
+ </span>
1356
+ <span class="sd-acb-diagnostics__badge" data-severity="warning">
1357
+ <sd-icon name="warning" size="sm"></sd-icon>
1358
+ <span>{{ 'core.component.api-contract-builder.review.warnings' | sdTranslate: { count: _warnings } }}</span>
1359
+ </span>
1360
+ }
1361
+ </div>
1362
+
1363
+ @if (_diagnostics.length) {
1364
+ <ul class="sd-acb-diagnostics__list">
1365
+ @for (diagnostic of _diagnostics; track diagnostic.code + '|' + diagnostic.path + '|' + $index) {
1366
+ <li class="sd-acb-diagnostics__item" [attr.data-severity]="diagnostic.severity" [attr.data-code]="diagnostic.code">
1367
+ <button
1368
+ type="button"
1369
+ class="sd-acb-diagnostics__nav"
1370
+ [attr.data-autoId]="_autoId ? _autoId + '-goto-' + $index : null"
1371
+ (click)="navigate.emit(diagnostic)">
1372
+ <sd-icon [name]="diagnostic.severity === 'error' ? 'error' : 'warning'" size="sm"></sd-icon>
1373
+ <span class="sd-acb-diagnostics__path">{{ diagnostic.path || '(contract)' }}</span>
1374
+ <span class="sd-acb-diagnostics__message">{{ diagnostic.message }}</span>
1375
+ <span class="sd-acb-diagnostics__code">{{ diagnostic.code }}</span>
1376
+ </button>
1377
+ </li>
1378
+ }
1379
+ </ul>
1380
+ }
1381
+ </div>
1382
+ `, styles: [":host{display:block}.sd-acb-diagnostics__summary{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px}.sd-acb-diagnostics__badge{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:600;&[data-severity=ok]{color:var(--sd-success, #1c6c3a)}&[data-severity=error]{color:var(--sd-error, #b3261e)}&[data-severity=warning]{color:var(--sd-warning, #8a5a00)}}.sd-acb-diagnostics__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:4px}.sd-acb-diagnostics__nav{display:flex;flex-wrap:wrap;align-items:baseline;gap:8px;width:100%;text-align:left;background:none;border:1px solid transparent;border-radius:6px;padding:6px 8px;cursor:pointer;font:inherit;font-size:12px;&:hover,&:focus-visible{border-color:var(--sd-border-color, #e6e6e6);background:var(--sd-surface-muted, #f3f5f8)}}.sd-acb-diagnostics__item[data-severity=error] .sd-acb-diagnostics__nav{color:var(--sd-error, #b3261e)}.sd-acb-diagnostics__item[data-severity=warning] .sd-acb-diagnostics__nav{color:var(--sd-warning, #8a5a00)}.sd-acb-diagnostics__path{font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-weight:600}.sd-acb-diagnostics__message{color:var(--sd-text, #1f2937)}.sd-acb-diagnostics__code{margin-left:auto;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;color:var(--sd-text-secondary, #6b6b6b)}\n"] }]
1383
+ }] });
1384
+
1385
+ /**
1386
+ * One collapsed, read-only row for a node in a layer list.
1387
+ *
1388
+ * The whole point is that it holds NO editable control. Editing a node happens in the drawer, so the
1389
+ * list can stay a thing you scan rather than a wall of inputs — and so a node never has two editors
1390
+ * writing it at once.
1391
+ *
1392
+ * The row shows what an author needs to spot a mistake without opening anything: the name, the
1393
+ * declared type, whether it is required, and where its value comes from.
1394
+ */
1395
+ class SdApiContractNodeSummary {
1396
+ #i18n = inject(I18nService);
1397
+ name = input.required();
1398
+ node = input.required();
1399
+ /** `view` mode and `disabled` both land here: the row becomes text, with no action at all. */
1400
+ readonly = input(false, { transform: booleanAttribute });
1401
+ autoId = input();
1402
+ edit = output();
1403
+ remove = output();
1404
+ requiredLabel = this.#i18n.t('core.component.api-contract-builder.field.required');
1405
+ removeLabel = this.#i18n.t('core.component.api-contract-builder.node.remove');
1406
+ unmapped = computed(() => {
1407
+ const node = this.node();
1408
+ if (node.source !== undefined || node.value !== undefined)
1409
+ return false;
1410
+ return !(node.type === 'object' && node.properties);
1411
+ });
1412
+ /**
1413
+ * One line describing where the value comes from.
1414
+ *
1415
+ * why bốn dạng khác nhau: `${input.keyword}` rút về `input.keyword` vì dấu `${}` là cú pháp chứ
1416
+ * không phải thông tin; còn template ghép giữ NGUYÊN VĂN vì rút gọn nó sẽ sai. Literal hiện giá trị
1417
+ * để thấy ngay, object hiện số trường để biết còn bao nhiêu phải mở ra xem.
1418
+ */
1419
+ mappingSummary = computed(() => {
1420
+ const node = this.node();
1421
+ if (typeof node.source === 'string' && node.source !== '') {
1422
+ const parsed = parseSdApiContractTemplate(node.source);
1423
+ const reference = parsed.kind === 'exact' ? parsed.references[0] : undefined;
1424
+ return `← ${reference ? reference.expression : node.source}`;
1425
+ }
1426
+ if (node.value !== undefined)
1427
+ return `= ${formatLiteral(node.value)}`;
1428
+ if (node.type === 'object' && node.properties) {
1429
+ return this.#i18n.t('core.component.api-contract-builder.summary.field-count', {
1430
+ count: Object.keys(node.properties).length,
1431
+ });
1432
+ }
1433
+ return this.#i18n.t('core.component.api-contract-builder.summary.unmapped');
1434
+ });
1435
+ requestEdit() {
1436
+ if (this.readonly())
1437
+ return;
1438
+ this.edit.emit();
1439
+ }
1440
+ /**
1441
+ * why stopPropagation: nút xoá nằm TRONG hàng có `(click)`, nên không chặn thì bấm xoá cũng mở
1442
+ * drawer — người dùng bấm xoá lại thấy form hiện ra.
1443
+ */
1444
+ requestRemove(event) {
1445
+ event.stopPropagation();
1446
+ if (this.readonly())
1447
+ return;
1448
+ this.remove.emit();
1449
+ }
1450
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeSummary, deps: [], target: i0.ɵɵFactoryTarget.Component });
1451
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractNodeSummary, isStandalone: true, selector: "sd-api-contract-node-summary", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { edit: "edit", remove: "remove" }, ngImport: i0, template: `
1452
+ @let _autoId = autoId();
1453
+ @let _readonly = readonly();
1454
+
1455
+ <div
1456
+ class="sd-acb-row"
1457
+ [class.sd-acb-row--readonly]="_readonly"
1458
+ [attr.role]="_readonly ? null : 'button'"
1459
+ [attr.tabindex]="_readonly ? null : 0"
1460
+ [attr.data-autoid]="_autoId || null"
1461
+ (click)="requestEdit()"
1462
+ (keydown.enter)="requestEdit()"
1463
+ (keydown.space)="requestEdit()">
1464
+ <span class="sd-acb-row__name">{{ name() }}</span>
1465
+ <span class="sd-acb-row__type">{{ node().type }}</span>
1466
+
1467
+ @if (node().required === true) {
1468
+ <span class="sd-acb-row__required">{{ requiredLabel }}</span>
1469
+ }
1470
+
1471
+ <span class="sd-acb-row__mapping" [class.sd-acb-row__mapping--unmapped]="unmapped()">{{ mappingSummary() }}</span>
1472
+
1473
+ @if (!_readonly) {
1474
+ <button
1475
+ type="button"
1476
+ class="sd-acb-row__remove"
1477
+ [attr.data-autoid]="_autoId ? _autoId + '-remove' : null"
1478
+ [attr.aria-label]="removeLabel"
1479
+ [title]="removeLabel"
1480
+ (click)="requestRemove($event)">
1481
+ <sd-icon name="delete" size="sm"></sd-icon>
1482
+ </button>
1483
+ }
1484
+ </div>
1485
+ `, isInline: true, styles: [":host{display:block;width:100%}.sd-acb-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border:1px solid var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface, #fff);font-size:13px;cursor:pointer}.sd-acb-row:hover{border-color:var(--sd-primary, #005cbb)}.sd-acb-row--readonly{cursor:default}.sd-acb-row--readonly:hover{border-color:var(--sd-border-color, #e6e6e6)}.sd-acb-row__name{min-width:0;font-weight:600;overflow-wrap:anywhere}.sd-acb-row__type{padding:1px 6px;border-radius:4px;background:var(--sd-surface-muted, #f3f5f8);color:var(--sd-text-secondary, #6b6b6b);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px}.sd-acb-row__required{padding:1px 6px;border-radius:4px;background:color-mix(in srgb,var(--sd-primary, #005cbb) 12%,transparent);color:var(--sd-primary, #005cbb);font-size:11px}.sd-acb-row__mapping{min-width:0;margin-left:auto;overflow:hidden;color:var(--sd-text-secondary, #6b6b6b);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.sd-acb-row__mapping--unmapped{color:var(--sd-warning, #8a5a00);font-style:italic}.sd-acb-row__remove{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--sd-error, #b3261e);cursor:pointer}.sd-acb-row__remove:hover{background:color-mix(in srgb,var(--sd-error, #b3261e) 12%,transparent)}\n"], dependencies: [{ kind: "component", type: SdIcon, selector: "sd-icon", inputs: ["name", "fontIcon", "color", "set", "fontSet", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1486
+ }
1487
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeSummary, decorators: [{
1488
+ type: Component,
1489
+ args: [{ selector: 'sd-api-contract-node-summary', standalone: true, imports: [SdIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1490
+ @let _autoId = autoId();
1491
+ @let _readonly = readonly();
1492
+
1493
+ <div
1494
+ class="sd-acb-row"
1495
+ [class.sd-acb-row--readonly]="_readonly"
1496
+ [attr.role]="_readonly ? null : 'button'"
1497
+ [attr.tabindex]="_readonly ? null : 0"
1498
+ [attr.data-autoid]="_autoId || null"
1499
+ (click)="requestEdit()"
1500
+ (keydown.enter)="requestEdit()"
1501
+ (keydown.space)="requestEdit()">
1502
+ <span class="sd-acb-row__name">{{ name() }}</span>
1503
+ <span class="sd-acb-row__type">{{ node().type }}</span>
1504
+
1505
+ @if (node().required === true) {
1506
+ <span class="sd-acb-row__required">{{ requiredLabel }}</span>
1507
+ }
1508
+
1509
+ <span class="sd-acb-row__mapping" [class.sd-acb-row__mapping--unmapped]="unmapped()">{{ mappingSummary() }}</span>
1510
+
1511
+ @if (!_readonly) {
1512
+ <button
1513
+ type="button"
1514
+ class="sd-acb-row__remove"
1515
+ [attr.data-autoid]="_autoId ? _autoId + '-remove' : null"
1516
+ [attr.aria-label]="removeLabel"
1517
+ [title]="removeLabel"
1518
+ (click)="requestRemove($event)">
1519
+ <sd-icon name="delete" size="sm"></sd-icon>
1520
+ </button>
1521
+ }
1522
+ </div>
1523
+ `, styles: [":host{display:block;width:100%}.sd-acb-row{display:flex;gap:10px;align-items:center;padding:8px 10px;border:1px solid var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface, #fff);font-size:13px;cursor:pointer}.sd-acb-row:hover{border-color:var(--sd-primary, #005cbb)}.sd-acb-row--readonly{cursor:default}.sd-acb-row--readonly:hover{border-color:var(--sd-border-color, #e6e6e6)}.sd-acb-row__name{min-width:0;font-weight:600;overflow-wrap:anywhere}.sd-acb-row__type{padding:1px 6px;border-radius:4px;background:var(--sd-surface-muted, #f3f5f8);color:var(--sd-text-secondary, #6b6b6b);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px}.sd-acb-row__required{padding:1px 6px;border-radius:4px;background:color-mix(in srgb,var(--sd-primary, #005cbb) 12%,transparent);color:var(--sd-primary, #005cbb);font-size:11px}.sd-acb-row__mapping{min-width:0;margin-left:auto;overflow:hidden;color:var(--sd-text-secondary, #6b6b6b);font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;font-size:12px;text-overflow:ellipsis;white-space:nowrap}.sd-acb-row__mapping--unmapped{color:var(--sd-warning, #8a5a00);font-style:italic}.sd-acb-row__remove{display:inline-flex;flex:0 0 auto;align-items:center;justify-content:center;width:24px;height:24px;padding:0;border:0;border-radius:50%;background:transparent;color:var(--sd-error, #b3261e);cursor:pointer}.sd-acb-row__remove:hover{background:color-mix(in srgb,var(--sd-error, #b3261e) 12%,transparent)}\n"] }]
1524
+ }] });
1525
+ /** Renders a literal short enough for one row: strings quoted, composites collapsed. */
1526
+ function formatLiteral(value) {
1527
+ if (typeof value === 'string')
1528
+ return JSON.stringify(value);
1529
+ if (value === null || typeof value !== 'object')
1530
+ return String(value);
1531
+ if (Array.isArray(value))
1532
+ return `[${value.length}]`;
1533
+ return `{${Object.keys(value).length}}`;
1534
+ }
1535
+
1536
+ /**
1537
+ * Value editor for one mapped node: a mode picker plus one control chosen by the mode.
1538
+ *
1539
+ * `source` shows a single dropdown of the references the surrounding schemas and the injected env
1540
+ * catalog make available — the author never types `${…}` for the common case. `advanced` keeps a raw
1541
+ * expression field for what a dropdown cannot express (`Bearer ${env.token}`). `static` renders a
1542
+ * control matching the declared type, including a JSON editor for `object` / `array` literals.
1543
+ *
1544
+ * The JSON editor for a composite literal only writes when the text parses. While it is half-typed it
1545
+ * emits the raw string, and that string is dropped — otherwise one keystroke would turn a literal
1546
+ * object into a garbage string in the contract.
1547
+ */
1548
+ class SdApiContractSourceEditor {
1549
+ #i18n = inject(I18nService);
1550
+ node = input.required();
1551
+ suggestions = input([]);
1552
+ disabled = input(false, { transform: booleanAttribute });
1553
+ autoId = input();
1554
+ nodeChange = output();
1555
+ advancedPlaceholder = 'Bearer ${env.token}';
1556
+ // why: dựng MỘT lần trong field initializer. `[items]` nhận mảng mới mỗi CD sẽ kích
1557
+ // `toObservable(items)` của sd-select → markForCheck → CD mới → treo trình duyệt (bug OOM
1558
+ // đã gặp ở query-builder). Ngôn ngữ chỉ đổi kèm reload trang nên dựng một lần là đủ.
1559
+ booleanOptions = [
1560
+ { value: 'true', label: this.#i18n.t('core.component.api-contract-builder.boolean.true') },
1561
+ { value: 'false', label: this.#i18n.t('core.component.api-contract-builder.boolean.false') },
1562
+ ];
1563
+ #sourceMode = {
1564
+ value: 'source',
1565
+ label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.source'),
1566
+ };
1567
+ #advancedMode = {
1568
+ value: 'advanced',
1569
+ label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.advanced'),
1570
+ };
1571
+ #scalarModes = [
1572
+ this.#sourceMode,
1573
+ { value: 'static', label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.static') },
1574
+ this.#advancedMode,
1575
+ ];
1576
+ #objectModes = [
1577
+ this.#sourceMode,
1578
+ { value: 'static', label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.static') },
1579
+ this.#advancedMode,
1580
+ { value: 'nested', label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.nested') },
1581
+ ];
1582
+ #arrayModes = [
1583
+ this.#sourceMode,
1584
+ { value: 'static', label: this.#i18n.t('core.component.api-contract-builder.mapping.mode.static') },
1585
+ this.#advancedMode,
1586
+ ];
1587
+ // why: mode là state của GIAO DIỆN, không nằm trong contract — nó được suy lại từ node mỗi khi cha
1588
+ // đẩy giá trị mới. `linkedSignal` (không phải `computed`) vì có đúng một trường hợp phải GHI ĐÈ
1589
+ // suy diễn: người dùng đang ở `advanced` và gõ tới lúc chuỗi tình cờ còn đúng một reference sạch.
1590
+ // Nếu để suy diễn thắng, ô text đang gõ sẽ biến thành dropdown giữa lúc gõ.
1591
+ mode = linkedSignal({
1592
+ source: () => this.node(),
1593
+ computation: (node, previous) => {
1594
+ const derived = deriveMode(node);
1595
+ if (previous?.value === 'advanced' && derived === 'source')
1596
+ return 'advanced';
1597
+ return derived;
1598
+ },
1599
+ });
1600
+ /**
1601
+ * Giá trị hiển thị của picker.
1602
+ *
1603
+ * why: `none` không còn là một option, nên bind nó vào `[model]` sẽ ra một ô trống mà người dùng
1604
+ * không hiểu tại sao. Trả `null` để picker rỗng THẬT — cộng `required`, một node chưa gán trở thành
1605
+ * lỗi thấy được ngay trên hàng, khớp với diagnostic `mapping.node.unmapped` mà validation đã báo.
1606
+ */
1607
+ modeValue = computed(() => {
1608
+ const mode = this.mode();
1609
+ return mode === 'none' ? null : mode;
1610
+ });
1611
+ modeOptions = computed(() => {
1612
+ const type = this.node().type;
1613
+ if (type === 'object')
1614
+ return this.#objectModes;
1615
+ if (type === 'array')
1616
+ return this.#arrayModes;
1617
+ return this.#scalarModes;
1618
+ });
1619
+ suggestionOptions = computed(() => {
1620
+ const type = this.node().type;
1621
+ const all = [...this.suggestions()];
1622
+ // why: lọc mềm — ưu tiên gợi ý hợp type, nhưng trả toàn bộ khi không cái nào khớp, để template
1623
+ // string (`Bearer ${env.token}`) và các cặp type tương thích không bị giấu mất.
1624
+ const compatible = all.filter(suggestion => isAssignable(suggestion.type, type));
1625
+ return compatible.length > 0 ? compatible : all;
1626
+ });
1627
+ /**
1628
+ * Options for the source dropdown.
1629
+ *
1630
+ * why: một `source` đang lưu có thể trỏ vào trường đã bị xoá hoặc đổi tên — nó KHÔNG còn trong
1631
+ * danh sách gợi ý. `sd-select` không tìm thấy value sẽ hiện ô rỗng, và giá trị vẫn nằm trong
1632
+ * contract mà người dùng không thấy để sửa. Thêm một option dựng từ chính giá trị đó để nó luôn
1633
+ * hiển thị; validation lo phần báo `mapping.reference.missing`.
1634
+ */
1635
+ sourceOptions = computed(() => {
1636
+ const options = this.suggestionOptions();
1637
+ const current = this.node().source;
1638
+ if (typeof current !== 'string' || current === '')
1639
+ return options;
1640
+ if (options.some(option => option.expression === current))
1641
+ return options;
1642
+ const parsed = parseSdApiContractTemplate(current);
1643
+ if (parsed.kind !== 'exact')
1644
+ return options;
1645
+ const reference = parsed.references[0];
1646
+ return [
1647
+ ...options,
1648
+ {
1649
+ expression: current,
1650
+ path: reference?.expression ?? current,
1651
+ root: reference?.root ?? 'input',
1652
+ type: this.node().type,
1653
+ display: reference?.expression ?? current,
1654
+ },
1655
+ ];
1656
+ });
1657
+ staticText = computed(() => {
1658
+ const value = this.node().value;
1659
+ return value === undefined || value === null ? '' : String(value);
1660
+ });
1661
+ staticNumber = computed(() => {
1662
+ const value = this.node().value;
1663
+ return typeof value === 'number' ? value : null;
1664
+ });
1665
+ setMode(mode) {
1666
+ const node = this.node();
1667
+ // why: đặt mode TRƯỚC khi emit. Node quay lại từ cha sẽ kích `linkedSignal` tính lại, và nó đọc
1668
+ // `previous.value` — nếu chưa đặt, carve-out `advanced` sẽ giữ lại mode cũ và chặn việc rời khỏi
1669
+ // `advanced` sang `source`.
1670
+ if (mode === 'source' || mode === 'advanced') {
1671
+ this.mode.set(mode);
1672
+ // Rời `advanced` sang `source` thì template ghép không còn diễn đạt được bằng dropdown — xoá để
1673
+ // dropdown bắt đầu sạch. Ngược lại, đã có key `source` thì đổi mode không đụng tới model.
1674
+ const abandonsTemplate = mode === 'source' && parseSdApiContractTemplate(node.source).kind === 'interpolated';
1675
+ if (node.source === undefined || abandonsTemplate) {
1676
+ this.nodeChange.emit({ ...withoutMapping(node), source: '' });
1677
+ }
1678
+ return;
1679
+ }
1680
+ if (mode === 'static') {
1681
+ this.mode.set('static');
1682
+ this.nodeChange.emit({ ...withoutMapping(node), value: defaultStatic(node.type) });
1683
+ return;
1684
+ }
1685
+ this.mode.set(node.type === 'object' ? 'nested' : 'none');
1686
+ this.nodeChange.emit(withoutMapping(node));
1687
+ }
1688
+ setSource(value) {
1689
+ this.nodeChange.emit({ ...withoutMapping(this.node()), source: typeof value === 'string' ? value : '' });
1690
+ }
1691
+ setStaticScalar(value) {
1692
+ const node = this.node();
1693
+ this.nodeChange.emit({ ...withoutMapping(node), value: value === null || value === undefined ? '' : String(value) });
1694
+ }
1695
+ setStaticNumber(value) {
1696
+ const node = this.node();
1697
+ const parsed = typeof value === 'number' ? value : Number(value === null || value === undefined ? '' : String(value).trim());
1698
+ this.nodeChange.emit({ ...withoutMapping(node), value: Number.isFinite(parsed) ? parsed : 0 });
1699
+ }
1700
+ /**
1701
+ * Stores a `date` / `datetime` literal.
1702
+ *
1703
+ * why: `<sd-date>` / `<sd-datetime>` có thể bắn ra một `Date`. `String(new Date())` ra
1704
+ * `Mon Aug 17 2026 …` — không parse lại được và đổi theo locale của máy tác giả, nên contract sẽ
1705
+ * mang một giá trị không portable. Chuẩn hoá về ISO.
1706
+ */
1707
+ setStaticTemporal(value) {
1708
+ const node = this.node();
1709
+ if (value instanceof Date) {
1710
+ const iso = Number.isNaN(value.getTime()) ? '' : value.toISOString();
1711
+ this.nodeChange.emit({ ...withoutMapping(node), value: iso });
1712
+ return;
1713
+ }
1714
+ this.nodeChange.emit({ ...withoutMapping(node), value: value === null || value === undefined ? '' : String(value) });
1715
+ }
1716
+ /**
1717
+ * Stores an `object` / `array` literal from the JSON editor.
1718
+ *
1719
+ * why: `<sd-code-editor language="json">` bắn ra STRING khi cú pháp còn dở (`{"a":`). Ghi chuỗi đó
1720
+ * vào contract sẽ biến một literal object thành rác, nên bỏ qua và giữ giá trị hợp lệ cuối cùng —
1721
+ * người dùng vẫn thấy nguyên văn mình đang gõ trong editor.
1722
+ */
1723
+ setStaticJson(value) {
1724
+ if (value === null || typeof value !== 'object')
1725
+ return;
1726
+ this.nodeChange.emit({ ...withoutMapping(this.node()), value: value });
1727
+ }
1728
+ setStaticBoolean(value) {
1729
+ this.nodeChange.emit({ ...withoutMapping(this.node()), value: value === 'true' || value === true });
1730
+ }
1731
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractSourceEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
1732
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractSourceEditor, isStandalone: true, selector: "sd-api-contract-source-editor", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeChange: "nodeChange" }, ngImport: i0, template: `
1733
+ @let _node = node();
1734
+ @let _disabled = disabled();
1735
+ @let _mode = mode();
1736
+ @let _autoId = autoId();
1737
+ @let _suggestions = sourceOptions();
1738
+
1739
+ <div class="sd-acb-source">
1740
+ <sd-select
1741
+ class="sd-acb-source__mode"
1742
+ size="sm"
1743
+ hideInlineError
1744
+ [clearable]="false"
1745
+ [autoId]="_autoId ? _autoId + '-mode' : undefined"
1746
+ [label]="'core.component.api-contract-builder.mapping.mode' | sdTranslate"
1747
+ [items]="modeOptions()"
1748
+ valueField="value"
1749
+ displayField="label"
1750
+ [required]="true"
1751
+ [model]="modeValue()"
1752
+ [disabled]="_disabled"
1753
+ (sdChange)="setMode($event)"></sd-select>
1754
+
1755
+ @if (_mode === 'source') {
1756
+ <sd-select
1757
+ class="sd-acb-source__expression"
1758
+ size="sm"
1759
+ hideInlineError
1760
+ [clearable]="false"
1761
+ [autoId]="_autoId ? _autoId + '-source' : undefined"
1762
+ [label]="'core.component.api-contract-builder.mapping.source' | sdTranslate"
1763
+ [items]="_suggestions"
1764
+ valueField="expression"
1765
+ displayField="display"
1766
+ [model]="_node.source ?? ''"
1767
+ [disabled]="_disabled"
1768
+ (sdChange)="setSource($event)"></sd-select>
1769
+ } @else if (_mode === 'advanced') {
1770
+ <sd-input
1771
+ class="sd-acb-source__expression"
1772
+ size="sm"
1773
+ hideInlineError
1774
+ [autoId]="_autoId ? _autoId + '-advanced' : undefined"
1775
+ [label]="'core.component.api-contract-builder.mapping.source' | sdTranslate"
1776
+ [placeholder]="advancedPlaceholder"
1777
+ [model]="_node.source ?? ''"
1778
+ [disabled]="_disabled"
1779
+ (sdChange)="setSource($event)"></sd-input>
1780
+ } @else if (_mode === 'static') {
1781
+ @let _staticAutoId = _autoId ? _autoId + '-static' : undefined;
1782
+ @let _staticLabel = 'core.component.api-contract-builder.mapping.value' | sdTranslate;
1783
+
1784
+ @switch (_node.type) {
1785
+ @case ('boolean') {
1786
+ <sd-select
1787
+ class="sd-acb-source__static"
1788
+ size="sm"
1789
+ hideInlineError
1790
+ [clearable]="false"
1791
+ [autoId]="_staticAutoId"
1792
+ [label]="_staticLabel"
1793
+ [items]="booleanOptions"
1794
+ valueField="value"
1795
+ displayField="label"
1796
+ [model]="_node.value === true ? 'true' : 'false'"
1797
+ [disabled]="_disabled"
1798
+ (sdChange)="setStaticBoolean($event)"></sd-select>
1799
+ }
1800
+ @case ('number') {
1801
+ <sd-input-number
1802
+ class="sd-acb-source__static"
1803
+ size="sm"
1804
+ hideInlineError
1805
+ [autoId]="_staticAutoId"
1806
+ [label]="_staticLabel"
1807
+ [model]="staticNumber()"
1808
+ [disabled]="_disabled"
1809
+ (sdChange)="setStaticNumber($event)"></sd-input-number>
1810
+ }
1811
+ @case ('date') {
1812
+ <sd-date
1813
+ class="sd-acb-source__static"
1814
+ size="sm"
1815
+ hideInlineError
1816
+ [autoId]="_staticAutoId"
1817
+ [label]="_staticLabel"
1818
+ [model]="staticText()"
1819
+ [disabled]="_disabled"
1820
+ (sdChange)="setStaticTemporal($event)"></sd-date>
1821
+ }
1822
+ @case ('datetime') {
1823
+ <sd-datetime
1824
+ class="sd-acb-source__static"
1825
+ size="sm"
1826
+ hideInlineError
1827
+ [autoId]="_staticAutoId"
1828
+ [label]="_staticLabel"
1829
+ [model]="staticText()"
1830
+ [disabled]="_disabled"
1831
+ (sdChange)="setStaticTemporal($event)"></sd-datetime>
1832
+ }
1833
+ @case ('object') {
1834
+ <sd-code-editor
1835
+ class="sd-acb-source__static sd-acb-source__static--json"
1836
+ language="json"
1837
+ maxHeight="220px"
1838
+ [model]="_node.value"
1839
+ [viewed]="_disabled"
1840
+ (modelChange)="setStaticJson($event)"></sd-code-editor>
1841
+ }
1842
+ @case ('array') {
1843
+ <sd-code-editor
1844
+ class="sd-acb-source__static sd-acb-source__static--json"
1845
+ language="json"
1846
+ maxHeight="220px"
1847
+ [model]="_node.value"
1848
+ [viewed]="_disabled"
1849
+ (modelChange)="setStaticJson($event)"></sd-code-editor>
1850
+ }
1851
+ @default {
1852
+ <sd-input
1853
+ class="sd-acb-source__static"
1854
+ size="sm"
1855
+ hideInlineError
1856
+ [autoId]="_staticAutoId"
1857
+ [label]="_staticLabel"
1858
+ [model]="staticText()"
1859
+ [disabled]="_disabled"
1860
+ (sdChange)="setStaticScalar($event)"></sd-input>
1861
+ }
1862
+ }
1863
+ }
1864
+ </div>
1865
+ `, isInline: true, styles: [":host{display:block;width:100%}.sd-acb-source{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}.sd-acb-source__mode{flex:0 0 200px;min-width:180px}.sd-acb-source__expression,.sd-acb-source__static{flex:1 1 260px;min-width:220px}.sd-acb-source__static--json{flex:1 1 100%;min-width:0}\n"], dependencies: [{ kind: "component", type: SdCodeEditor, selector: "sd-code-editor", inputs: ["model", "language", "maxHeight", "viewed"], outputs: ["modelChange"] }, { kind: "component", type: SdDate, selector: "sd-date", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "transform", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "component", type: SdDatetime, selector: "sd-datetime", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "required", "disabled", "viewed", "showSeconds", "clearable", "inlineError", "hyperlink", "appearance", "floatLabel", "min", "minDate", "max", "maxDate", "transform", "model"], outputs: ["modelChange", "sdChange", "sdFocus"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "mask", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdInputNumber, selector: "sd-input-number", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "type", "precision", "format", "min", "max", "validator", "inlineError", "hyperlink", "appearance", "floatLabel", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "showSelectAll", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1866
+ }
1867
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractSourceEditor, decorators: [{
1868
+ type: Component,
1869
+ args: [{ selector: 'sd-api-contract-source-editor', standalone: true, imports: [SdCodeEditor, SdDate, SdDatetime, SdInput, SdInputNumber, SdSelect, SdTranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
1870
+ @let _node = node();
1871
+ @let _disabled = disabled();
1872
+ @let _mode = mode();
1873
+ @let _autoId = autoId();
1874
+ @let _suggestions = sourceOptions();
1875
+
1876
+ <div class="sd-acb-source">
1877
+ <sd-select
1878
+ class="sd-acb-source__mode"
1879
+ size="sm"
1880
+ hideInlineError
1881
+ [clearable]="false"
1882
+ [autoId]="_autoId ? _autoId + '-mode' : undefined"
1883
+ [label]="'core.component.api-contract-builder.mapping.mode' | sdTranslate"
1884
+ [items]="modeOptions()"
1885
+ valueField="value"
1886
+ displayField="label"
1887
+ [required]="true"
1888
+ [model]="modeValue()"
1889
+ [disabled]="_disabled"
1890
+ (sdChange)="setMode($event)"></sd-select>
1891
+
1892
+ @if (_mode === 'source') {
1893
+ <sd-select
1894
+ class="sd-acb-source__expression"
1895
+ size="sm"
1896
+ hideInlineError
1897
+ [clearable]="false"
1898
+ [autoId]="_autoId ? _autoId + '-source' : undefined"
1899
+ [label]="'core.component.api-contract-builder.mapping.source' | sdTranslate"
1900
+ [items]="_suggestions"
1901
+ valueField="expression"
1902
+ displayField="display"
1903
+ [model]="_node.source ?? ''"
1904
+ [disabled]="_disabled"
1905
+ (sdChange)="setSource($event)"></sd-select>
1906
+ } @else if (_mode === 'advanced') {
1907
+ <sd-input
1908
+ class="sd-acb-source__expression"
1909
+ size="sm"
1910
+ hideInlineError
1911
+ [autoId]="_autoId ? _autoId + '-advanced' : undefined"
1912
+ [label]="'core.component.api-contract-builder.mapping.source' | sdTranslate"
1913
+ [placeholder]="advancedPlaceholder"
1914
+ [model]="_node.source ?? ''"
1915
+ [disabled]="_disabled"
1916
+ (sdChange)="setSource($event)"></sd-input>
1917
+ } @else if (_mode === 'static') {
1918
+ @let _staticAutoId = _autoId ? _autoId + '-static' : undefined;
1919
+ @let _staticLabel = 'core.component.api-contract-builder.mapping.value' | sdTranslate;
1920
+
1921
+ @switch (_node.type) {
1922
+ @case ('boolean') {
1923
+ <sd-select
1924
+ class="sd-acb-source__static"
1925
+ size="sm"
1926
+ hideInlineError
1927
+ [clearable]="false"
1928
+ [autoId]="_staticAutoId"
1929
+ [label]="_staticLabel"
1930
+ [items]="booleanOptions"
1931
+ valueField="value"
1932
+ displayField="label"
1933
+ [model]="_node.value === true ? 'true' : 'false'"
1934
+ [disabled]="_disabled"
1935
+ (sdChange)="setStaticBoolean($event)"></sd-select>
1936
+ }
1937
+ @case ('number') {
1938
+ <sd-input-number
1939
+ class="sd-acb-source__static"
1940
+ size="sm"
1941
+ hideInlineError
1942
+ [autoId]="_staticAutoId"
1943
+ [label]="_staticLabel"
1944
+ [model]="staticNumber()"
1945
+ [disabled]="_disabled"
1946
+ (sdChange)="setStaticNumber($event)"></sd-input-number>
1947
+ }
1948
+ @case ('date') {
1949
+ <sd-date
1950
+ class="sd-acb-source__static"
1951
+ size="sm"
1952
+ hideInlineError
1953
+ [autoId]="_staticAutoId"
1954
+ [label]="_staticLabel"
1955
+ [model]="staticText()"
1956
+ [disabled]="_disabled"
1957
+ (sdChange)="setStaticTemporal($event)"></sd-date>
1958
+ }
1959
+ @case ('datetime') {
1960
+ <sd-datetime
1961
+ class="sd-acb-source__static"
1962
+ size="sm"
1963
+ hideInlineError
1964
+ [autoId]="_staticAutoId"
1965
+ [label]="_staticLabel"
1966
+ [model]="staticText()"
1967
+ [disabled]="_disabled"
1968
+ (sdChange)="setStaticTemporal($event)"></sd-datetime>
1969
+ }
1970
+ @case ('object') {
1971
+ <sd-code-editor
1972
+ class="sd-acb-source__static sd-acb-source__static--json"
1973
+ language="json"
1974
+ maxHeight="220px"
1975
+ [model]="_node.value"
1976
+ [viewed]="_disabled"
1977
+ (modelChange)="setStaticJson($event)"></sd-code-editor>
1978
+ }
1979
+ @case ('array') {
1980
+ <sd-code-editor
1981
+ class="sd-acb-source__static sd-acb-source__static--json"
1982
+ language="json"
1983
+ maxHeight="220px"
1984
+ [model]="_node.value"
1985
+ [viewed]="_disabled"
1986
+ (modelChange)="setStaticJson($event)"></sd-code-editor>
1987
+ }
1988
+ @default {
1989
+ <sd-input
1990
+ class="sd-acb-source__static"
1991
+ size="sm"
1992
+ hideInlineError
1993
+ [autoId]="_staticAutoId"
1994
+ [label]="_staticLabel"
1995
+ [model]="staticText()"
1996
+ [disabled]="_disabled"
1997
+ (sdChange)="setStaticScalar($event)"></sd-input>
1998
+ }
1999
+ }
2000
+ }
2001
+ </div>
2002
+ `, styles: [":host{display:block;width:100%}.sd-acb-source{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-start}.sd-acb-source__mode{flex:0 0 200px;min-width:180px}.sd-acb-source__expression,.sd-acb-source__static{flex:1 1 260px;min-width:220px}.sd-acb-source__static--json{flex:1 1 100%;min-width:0}\n"] }]
2003
+ }] });
2004
+ /**
2005
+ * Reads the mode out of the node alone — the contract never stores it.
2006
+ *
2007
+ * An EMPTY `source` means "Lấy từ nguồn đã chọn, chưa pick" and stays in `source`; anything the
2008
+ * dropdown cannot express (a template with literal text, or text with no reference at all) is
2009
+ * `advanced`. A clean single reference stays in `source` even when it resolves to nothing, so a typo
2010
+ * is fixed in the friendly picker while validation reports `mapping.reference.missing`.
2011
+ */
2012
+ function deriveMode(node) {
2013
+ if (node.source !== undefined) {
2014
+ if (node.source === '')
2015
+ return 'source';
2016
+ return parseSdApiContractTemplate(node.source).kind === 'exact' ? 'source' : 'advanced';
2017
+ }
2018
+ if (node.value !== undefined)
2019
+ return 'static';
2020
+ return node.type === 'object' ? 'nested' : 'none';
2021
+ }
2022
+ /** Rebuilds a node without `source` / `value`, so the key is gone rather than set to `undefined`. */
2023
+ function withoutMapping(node) {
2024
+ const next = {};
2025
+ for (const key of Object.keys(node)) {
2026
+ if (key === 'source' || key === 'value')
2027
+ continue;
2028
+ next[key] = node[key];
2029
+ }
2030
+ return next;
2031
+ }
2032
+ function defaultStatic(type) {
2033
+ if (type === 'number')
2034
+ return 0;
2035
+ if (type === 'boolean')
2036
+ return false;
2037
+ if (type === 'object')
2038
+ return {};
2039
+ if (type === 'array')
2040
+ return [];
2041
+ return '';
2042
+ }
2043
+ function isAssignable(sourceType, targetType) {
2044
+ if (sourceType === targetType)
2045
+ return true;
2046
+ const temporalish = (type) => type === 'string' || type === 'date' || type === 'datetime';
2047
+ return temporalish(sourceType) && temporalish(targetType);
2048
+ }
2049
+
2050
+ /**
2051
+ * Editing surface for exactly one node, staged and committed in one go.
2052
+ *
2053
+ * The layer lists outside are read-only, so this drawer is the only place a node changes. It holds a
2054
+ * DEEP COPY of the node it was seeded from and emits `nodeCommit` once, when the author saves — that
2055
+ * is what makes "Huỷ" able to actually cancel, and what keeps the tree from changing under the
2056
+ * author's cursor on every keystroke.
2057
+ *
2058
+ * Save is blocked only for the two problems that stop a node from existing at all: no name, or a name
2059
+ * a sibling already holds. Everything else — a reference to a field that does not exist yet, a type
2060
+ * that will not fit — saves and is reported by `validateSdApiContract`, because inventing rules here
2061
+ * would stop an author declaring one end of a mapping before the other exists.
2062
+ */
2063
+ class SdApiContractNodeDrawer {
2064
+ #i18n = inject(I18nService);
2065
+ /** `mapping` shows the value editor; `schema` is a plain declaration with no mapping. */
2066
+ layer = input('mapping');
2067
+ allowTransform = input(false, { transform: booleanAttribute });
2068
+ suggestions = input([]);
2069
+ autoId = input();
2070
+ nodeCommit = output();
2071
+ // why không phải ES-private: Angular từ chối `viewChild` trên field `#` (diagnostic 1053).
2072
+ drawerRef = viewChild.required('drawer');
2073
+ // why: seed là mốc so sánh để biết draft có bẩn hay chưa. Không có nó thì "Huỷ" phải đoán, và mọi
2074
+ // lần mở drawer đều bị coi là đã sửa.
2075
+ #seedName = '';
2076
+ #seedJson = '';
2077
+ #draftName = signal('');
2078
+ #draft = signal(null);
2079
+ #rootSiblings = signal([]);
2080
+ #editingName = signal(null);
2081
+ #discardPrompt = signal(false);
2082
+ // why pointer chứ không phải "node hiện tại": draft luôn là GỐC của subtree, và một lần Lưu commit
2083
+ // cả gốc. Giữ node con rời ra sẽ phải ghép lại lúc Save, và ghép sai là mất nhánh.
2084
+ #pointer = signal([]);
2085
+ // why: tên đang gõ ở CẤP HIỆN TẠI, kể cả khi nó chưa áp được vào draft.
2086
+ #typedName = signal('');
2087
+ draftName = this.#draftName.asReadonly();
2088
+ discardPrompt = this.#discardPrompt.asReadonly();
2089
+ /** The node the author is looking at right now — the draft root, or a descendant of it. */
2090
+ current = computed(() => {
2091
+ const draft = this.#draft();
2092
+ if (!draft)
2093
+ return null;
2094
+ return getSdApiContractNodeAt(draft, this.#pointer());
2095
+ });
2096
+ /** Every key on the breadcrumb. The pointer alternates `properties`/key, so keys sit on odd slots. */
2097
+ breadcrumb = computed(() => {
2098
+ const pointer = this.#pointer();
2099
+ const keys = pointer.filter((_, index) => index % 2 === 1);
2100
+ return [this.#draftName() || this.#i18n.t('core.component.api-contract-builder.drawer.add-title'), ...keys];
2101
+ });
2102
+ /**
2103
+ * What the name field shows.
2104
+ *
2105
+ * why một signal riêng chứ không đọc thẳng từ draft: một tên KHÔNG áp được — rỗng, hoặc trùng
2106
+ * sibling — vẫn phải hiện ra để người dùng thấy mình vừa gõ gì và đọc được lý do bị chặn. Đọc
2107
+ * thẳng từ draft thì ô input nhảy về tên cũ ngay khi gõ, tức là từ chối im lặng.
2108
+ */
2109
+ currentName = this.#typedName.asReadonly();
2110
+ children = computed(() => {
2111
+ const node = this.current();
2112
+ const properties = node?.properties;
2113
+ if (!properties)
2114
+ return [];
2115
+ return Object.keys(properties).map(key => ({ key, node: properties[key] }));
2116
+ });
2117
+ /** Siblings on the level the author is on — the root list, or the parent object's keys. */
2118
+ siblingNames = computed(() => {
2119
+ const pointer = this.#pointer();
2120
+ if (pointer.length === 0)
2121
+ return this.#rootSiblings();
2122
+ const draft = this.#draft();
2123
+ if (!draft)
2124
+ return [];
2125
+ const parent = getSdApiContractNodeAt(draft, pointer.slice(0, -2));
2126
+ return parent?.properties ? Object.keys(parent.properties) : [];
2127
+ });
2128
+ typeOptions = SD_API_CONTRACT_DATA_TYPES.map(type => ({
2129
+ value: type,
2130
+ label: type,
2131
+ }));
2132
+ requiredOptions = [
2133
+ { value: 'true', label: this.#i18n.t('core.component.api-contract-builder.required.true') },
2134
+ { value: 'false', label: this.#i18n.t('core.component.api-contract-builder.required.false') },
2135
+ ];
2136
+ transformOptions = [
2137
+ { value: '', label: this.#i18n.t('core.component.api-contract-builder.transform.none') },
2138
+ { value: 'ISOString', label: 'ISOString' },
2139
+ { value: 'UTCString', label: 'UTCString' },
2140
+ ];
2141
+ title = computed(() => this.#editingName() === null
2142
+ ? this.#i18n.t('core.component.api-contract-builder.drawer.add-title')
2143
+ : this.#i18n.t('core.component.api-contract-builder.drawer.edit-title'));
2144
+ dirty = computed(() => {
2145
+ const draft = this.#draft();
2146
+ if (!draft)
2147
+ return false;
2148
+ return this.#draftName() !== this.#seedName || JSON.stringify(draft) !== this.#seedJson;
2149
+ });
2150
+ nameError = computed(() => {
2151
+ const name = this.currentName().trim();
2152
+ if (name === '')
2153
+ return this.#i18n.t('core.component.api-contract-builder.node.name-required');
2154
+ // why: node đang sửa luôn nằm trong danh sách sibling của chính cấp nó — so với chính nó là tự báo
2155
+ // trùng. "Chính nó" phải là key THẬT đang lưu, không phải chữ đang gõ: lấy chữ đang gõ thì gõ đúng
2156
+ // tên một sibling khác cũng tự loại chính mình và lỗi trùng không bao giờ nổi lên.
2157
+ const pointer = this.#pointer();
2158
+ const self = pointer.length === 0 ? this.#editingName() : pointer[pointer.length - 1];
2159
+ const taken = this.siblingNames().some(sibling => sibling === name && sibling !== self);
2160
+ return taken ? this.#i18n.t('core.component.api-contract-builder.node.duplicate-key') : null;
2161
+ });
2162
+ // why chỉ xét lỗi ở GỐC: Lưu commit cả subtree, và tên đang gõ ở cấp sâu đã được `setName` áp vào
2163
+ // draft ngay. Nếu chặn theo cấp hiện tại thì đứng ở cấp sâu sẽ không Lưu được gốc hợp lệ.
2164
+ canSave = computed(() => {
2165
+ if (this.#draft() === null)
2166
+ return false;
2167
+ if (this.#draftName().trim() === '')
2168
+ return false;
2169
+ return this.nameError() === null;
2170
+ });
2171
+ /**
2172
+ * `beforeClose` guard for `<sd-side-drawer>`.
2173
+ *
2174
+ * why an arrow field, not a method: the drawer takes it as an input value, so it must keep `this`
2175
+ * without the template having to bind it.
2176
+ */
2177
+ closeGuard = () => this.guardClose();
2178
+ // -------------------------------------------------------------------------
2179
+ // Public surface the owning builder drives
2180
+ // -------------------------------------------------------------------------
2181
+ openForAdd(siblingNames = [], type = 'string') {
2182
+ this.#seed(null, createSdApiContractNode(type), siblingNames);
2183
+ this.drawerRef().open();
2184
+ }
2185
+ openForEdit(name, node, siblingNames = []) {
2186
+ this.#seed(name, cloneSdApiContractNode(node), siblingNames);
2187
+ this.drawerRef().open();
2188
+ }
2189
+ // -------------------------------------------------------------------------
2190
+ // Draft edits
2191
+ // -------------------------------------------------------------------------
2192
+ /**
2193
+ * Types into the name field.
2194
+ *
2195
+ * The typed text is ALWAYS kept, then applied to the draft only when it can be. A name that cannot
2196
+ * be applied — empty, or already taken by a sibling — stays visible and `nameError()` explains it,
2197
+ * which is what blocks Save. Refusing the keystroke instead would leave the field showing one name
2198
+ * while the contract holds another.
2199
+ */
2200
+ setName(value) {
2201
+ const name = typeof value === 'string' ? value : '';
2202
+ this.#typedName.set(name);
2203
+ const pointer = this.#pointer();
2204
+ if (pointer.length === 0) {
2205
+ // why chỉ ghi khi hợp lệ: `#draftName` là thứ `save()` phát ra. Ghi tên rỗng vào đó rồi chặn ở
2206
+ // `canSave` cũng được, nhưng giữ nó luôn hợp lệ thì `save()` không cần tin vào cổng chặn.
2207
+ if (name.trim() !== '')
2208
+ this.#draftName.set(name);
2209
+ return;
2210
+ }
2211
+ const draft = this.#draft();
2212
+ const from = pointer[pointer.length - 1];
2213
+ if (!draft || name.trim() === '' || name === from)
2214
+ return;
2215
+ const parentPointer = pointer.slice(0, -2);
2216
+ const parent = getSdApiContractNodeAt(draft, parentPointer);
2217
+ if (!parent)
2218
+ return;
2219
+ const renamed = renameSdApiContractProperty(parent, from, name);
2220
+ // why: helper trả về CHÍNH object cũ khi trùng key. Không áp được thì thôi — `#typedName` đã giữ
2221
+ // chữ người dùng gõ và `nameError()` đã báo, nên không có gì bị nuốt im lặng.
2222
+ if (renamed === parent)
2223
+ return;
2224
+ this.#draft.set(setSdApiContractNodeAt(draft, parentPointer, renamed));
2225
+ // why dời pointer theo: nếu không, pointer vẫn trỏ key CŨ — `current()` trả null và form trắng xoá.
2226
+ this.#pointer.set([...parentPointer, 'properties', name]);
2227
+ }
2228
+ setType(value) {
2229
+ const draft = this.#draft();
2230
+ if (!draft || typeof value !== 'string')
2231
+ return;
2232
+ if (!SD_API_CONTRACT_DATA_TYPES.includes(value))
2233
+ return;
2234
+ this.applyCurrent(changeSdApiContractNodeType(draft, value));
2235
+ }
2236
+ setRequired(value) {
2237
+ const draft = this.#draft();
2238
+ if (!draft)
2239
+ return;
2240
+ if (value === 'true') {
2241
+ this.applyCurrent({ ...draft, required: true });
2242
+ return;
2243
+ }
2244
+ // why: bỏ hẳn key thay vì ghi `false` — key vắng và `false` nghĩa như nhau với consumer, và JSON
2245
+ // gọn hơn. Một `false` do người khác viết tay vẫn được giữ khi nạp vào.
2246
+ const next = { ...draft };
2247
+ delete next['required'];
2248
+ this.applyCurrent(next);
2249
+ }
2250
+ setText(key, value) {
2251
+ const draft = this.#draft();
2252
+ if (!draft)
2253
+ return;
2254
+ const text = typeof value === 'string' ? value.trim() : '';
2255
+ const next = { ...draft };
2256
+ if (text === '')
2257
+ delete next[key];
2258
+ else
2259
+ next[key] = text;
2260
+ this.applyCurrent(next);
2261
+ }
2262
+ setTransform(value) {
2263
+ const draft = this.#draft();
2264
+ if (!draft)
2265
+ return;
2266
+ const next = { ...draft };
2267
+ if (value === 'ISOString' || value === 'UTCString')
2268
+ next['transform'] = value;
2269
+ else
2270
+ delete next['transform'];
2271
+ this.applyCurrent(next);
2272
+ }
2273
+ applyCurrent(node) {
2274
+ const draft = this.#draft();
2275
+ if (!draft)
2276
+ return;
2277
+ this.#draft.set(setSdApiContractNodeAt(draft, this.#pointer(), node));
2278
+ }
2279
+ enter(key) {
2280
+ this.#pointer.set([...this.#pointer(), 'properties', key]);
2281
+ this.#syncTypedName();
2282
+ this.#discardPrompt.set(false);
2283
+ }
2284
+ /** `depth` counts breadcrumb entries, so 0 is the draft root. */
2285
+ backTo(depth) {
2286
+ this.#pointer.set(this.#pointer().slice(0, depth * 2));
2287
+ this.#syncTypedName();
2288
+ this.#discardPrompt.set(false);
2289
+ }
2290
+ addChild() {
2291
+ const node = this.current();
2292
+ const draft = this.#draft();
2293
+ if (!node || !draft)
2294
+ return;
2295
+ const key = uniqueChildKey(node.properties ?? {});
2296
+ const next = addSdApiContractProperty(node, key, createSdApiContractNode('string'));
2297
+ this.#draft.set(setSdApiContractNodeAt(draft, this.#pointer(), next));
2298
+ }
2299
+ removeChild(key) {
2300
+ const node = this.current();
2301
+ const draft = this.#draft();
2302
+ if (!node || !draft)
2303
+ return;
2304
+ this.#draft.set(setSdApiContractNodeAt(draft, this.#pointer(), removeSdApiContractProperty(node, key)));
2305
+ }
2306
+ // -------------------------------------------------------------------------
2307
+ // Chốt sổ
2308
+ // -------------------------------------------------------------------------
2309
+ save() {
2310
+ const draft = this.#draft();
2311
+ if (!draft || !this.canSave())
2312
+ return;
2313
+ this.#discardPrompt.set(false);
2314
+ this.nodeCommit.emit({ name: this.#draftName().trim(), node: draft });
2315
+ // why: forceClose bỏ qua `beforeClose`. Save đã là chủ ý rõ ràng — hỏi lại "bỏ thay đổi?" ngay sau
2316
+ // khi vừa lưu là vô nghĩa.
2317
+ this.#markCommitted();
2318
+ this.drawerRef().forceClose();
2319
+ }
2320
+ requestCancel() {
2321
+ void this.drawerRef().requestClose();
2322
+ }
2323
+ confirmDiscard() {
2324
+ this.#discardPrompt.set(false);
2325
+ this.#markCommitted();
2326
+ this.drawerRef().forceClose();
2327
+ }
2328
+ cancelDiscard() {
2329
+ this.#discardPrompt.set(false);
2330
+ }
2331
+ /**
2332
+ * Blocks the close while the draft is dirty and raises an inline prompt instead.
2333
+ *
2334
+ * why inline chứ không `window.confirm`: một component thư viện không được dựng dialog của browser
2335
+ * — nó không style được, không test được, và chặn cả tab.
2336
+ */
2337
+ guardClose() {
2338
+ if (!this.dirty())
2339
+ return true;
2340
+ this.#discardPrompt.set(true);
2341
+ return false;
2342
+ }
2343
+ onClosed() {
2344
+ this.#discardPrompt.set(false);
2345
+ }
2346
+ // -------------------------------------------------------------------------
2347
+ #seed(name, node, siblingNames) {
2348
+ this.#editingName.set(name);
2349
+ this.#draftName.set(name ?? '');
2350
+ this.#draft.set(node);
2351
+ this.#pointer.set([]);
2352
+ this.#typedName.set(name ?? '');
2353
+ this.#rootSiblings.set([...siblingNames]);
2354
+ this.#discardPrompt.set(false);
2355
+ this.#seedName = name ?? '';
2356
+ this.#seedJson = JSON.stringify(node);
2357
+ }
2358
+ /** Re-points the name field at whatever level the author just moved to. */
2359
+ #syncTypedName() {
2360
+ const pointer = this.#pointer();
2361
+ this.#typedName.set(pointer.length === 0 ? this.#draftName() : pointer[pointer.length - 1]);
2362
+ }
2363
+ /** Re-baselines the seed so a closing drawer is never considered dirty any more. */
2364
+ #markCommitted() {
2365
+ const draft = this.#draft();
2366
+ this.#seedName = this.#draftName();
2367
+ this.#seedJson = draft ? JSON.stringify(draft) : '';
2368
+ }
2369
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeDrawer, deps: [], target: i0.ɵɵFactoryTarget.Component });
2370
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractNodeDrawer, isStandalone: true, selector: "sd-api-contract-node-drawer", inputs: { layer: { classPropertyName: "layer", publicName: "layer", isSignal: true, isRequired: false, transformFunction: null }, allowTransform: { classPropertyName: "allowTransform", publicName: "allowTransform", isSignal: true, isRequired: false, transformFunction: null }, suggestions: { classPropertyName: "suggestions", publicName: "suggestions", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeCommit: "nodeCommit" }, viewQueries: [{ propertyName: "drawerRef", first: true, predicate: ["drawer"], descendants: true, isSignal: true }], ngImport: i0, template: `
2371
+ @let _autoId = autoId();
2372
+ @let _current = current();
2373
+ @let _nameError = nameError();
2374
+
2375
+ <sd-side-drawer
2376
+ #drawer
2377
+ width="560px"
2378
+ disableBackdropClose
2379
+ [title]="title()"
2380
+ [autoId]="_autoId"
2381
+ [beforeClose]="closeGuard"
2382
+ (sdClosed)="onClosed()">
2383
+ @if (_current) {
2384
+ <div class="sd-acb-drawer">
2385
+ @let _breadcrumb = breadcrumb();
2386
+ @if (_breadcrumb.length > 1) {
2387
+ <nav class="sd-acb-drawer__crumbs" [attr.aria-label]="title()">
2388
+ @for (crumb of _breadcrumb; track $index) {
2389
+ @if ($index < _breadcrumb.length - 1) {
2390
+ <button
2391
+ type="button"
2392
+ class="sd-acb-drawer__crumb"
2393
+ [attr.data-autoid]="_autoId ? _autoId + '-crumb-' + $index : null"
2394
+ (click)="backTo($index)">
2395
+ {{ crumb }}
2396
+ </button>
2397
+ <span class="sd-acb-drawer__crumb-sep">›</span>
2398
+ } @else {
2399
+ <span class="sd-acb-drawer__crumb sd-acb-drawer__crumb--current">{{ crumb }}</span>
2400
+ }
2401
+ }
2402
+ </nav>
2403
+ }
2404
+
2405
+ <sd-input
2406
+ size="sm"
2407
+ [autoId]="_autoId ? _autoId + '-name' : undefined"
2408
+ [label]="'core.component.api-contract-builder.node.name' | sdTranslate"
2409
+ [model]="currentName()"
2410
+ [inlineError]="_nameError ?? undefined"
2411
+ (sdChange)="setName($event)"></sd-input>
2412
+
2413
+ <sd-select
2414
+ size="sm"
2415
+ hideInlineError
2416
+ [clearable]="false"
2417
+ [autoId]="_autoId ? _autoId + '-type' : undefined"
2418
+ [label]="'core.component.api-contract-builder.node.type' | sdTranslate"
2419
+ [items]="typeOptions"
2420
+ valueField="value"
2421
+ displayField="label"
2422
+ [model]="_current.type"
2423
+ (sdChange)="setType($event)"></sd-select>
2424
+
2425
+ <sd-select
2426
+ size="sm"
2427
+ hideInlineError
2428
+ [clearable]="false"
2429
+ [autoId]="_autoId ? _autoId + '-required' : undefined"
2430
+ [label]="'core.component.api-contract-builder.node.required' | sdTranslate"
2431
+ [items]="requiredOptions"
2432
+ valueField="value"
2433
+ displayField="label"
2434
+ [model]="_current.required === true ? 'true' : 'false'"
2435
+ (sdChange)="setRequired($event)"></sd-select>
2436
+
2437
+ <sd-input
2438
+ size="sm"
2439
+ hideInlineError
2440
+ [autoId]="_autoId ? _autoId + '-label' : undefined"
2441
+ [label]="'core.component.api-contract-builder.node.label' | sdTranslate"
2442
+ [model]="_current.label ?? ''"
2443
+ (sdChange)="setText('label', $event)"></sd-input>
2444
+
2445
+ <sd-input
2446
+ size="sm"
2447
+ hideInlineError
2448
+ [autoId]="_autoId ? _autoId + '-description' : undefined"
2449
+ [label]="'core.component.api-contract-builder.node.description' | sdTranslate"
2450
+ [model]="_current.description ?? ''"
2451
+ (sdChange)="setText('description', $event)"></sd-input>
2452
+
2453
+ @if (allowTransform() && (_current.type === 'date' || _current.type === 'datetime')) {
2454
+ <sd-select
2455
+ size="sm"
2456
+ hideInlineError
2457
+ [clearable]="false"
2458
+ [autoId]="_autoId ? _autoId + '-transform' : undefined"
2459
+ [label]="'core.component.api-contract-builder.node.transform' | sdTranslate"
2460
+ [items]="transformOptions"
2461
+ valueField="value"
2462
+ displayField="label"
2463
+ [model]="_current.transform ?? ''"
2464
+ (sdChange)="setTransform($event)"></sd-select>
2465
+ }
2466
+
2467
+ @if (layer() === 'mapping') {
2468
+ <sd-api-contract-source-editor
2469
+ [node]="_current"
2470
+ [suggestions]="suggestions()"
2471
+ [autoId]="_autoId ? _autoId + '-value' : undefined"
2472
+ (nodeChange)="applyCurrent($event)"></sd-api-contract-source-editor>
2473
+ }
2474
+
2475
+ @if (_current.type === 'object') {
2476
+ @let _children = children();
2477
+ <div class="sd-acb-drawer__children">
2478
+ <h5 class="sd-acb-drawer__children-title">
2479
+ {{ 'core.component.api-contract-builder.node.add-property' | sdTranslate }}
2480
+ </h5>
2481
+
2482
+ @if (!_children.length) {
2483
+ <p class="sd-acb-drawer__empty">{{ 'core.component.api-contract-builder.node.empty' | sdTranslate }}</p>
2484
+ }
2485
+
2486
+ @for (child of _children; track child.key) {
2487
+ <sd-api-contract-node-summary
2488
+ [name]="child.key"
2489
+ [node]="child.node"
2490
+ [autoId]="_autoId ? _autoId + '-child-' + child.key : undefined"
2491
+ (edit)="enter(child.key)"
2492
+ (remove)="removeChild(child.key)"></sd-api-contract-node-summary>
2493
+ }
2494
+
2495
+ <sd-button
2496
+ type="light"
2497
+ size="sm"
2498
+ prefixIcon="add"
2499
+ [autoId]="_autoId ? _autoId + '-add-child' : undefined"
2500
+ [title]="'core.component.api-contract-builder.node.add-property' | sdTranslate"
2501
+ (click)="addChild()"></sd-button>
2502
+ </div>
2503
+ }
2504
+
2505
+ @if (discardPrompt()) {
2506
+ <div class="sd-acb-drawer__confirm" role="alertdialog">
2507
+ <span>{{ 'core.component.api-contract-builder.drawer.discard-confirm' | sdTranslate }}</span>
2508
+ <sd-button
2509
+ type="text"
2510
+ size="sm"
2511
+ color="error"
2512
+ [autoId]="_autoId ? _autoId + '-discard' : undefined"
2513
+ [title]="'core.component.api-contract-builder.confirm.yes' | sdTranslate"
2514
+ (click)="confirmDiscard()"></sd-button>
2515
+ <sd-button
2516
+ type="text"
2517
+ size="sm"
2518
+ [autoId]="_autoId ? _autoId + '-keep' : undefined"
2519
+ [title]="'core.component.api-contract-builder.confirm.no' | sdTranslate"
2520
+ (click)="cancelDiscard()"></sd-button>
2521
+ </div>
2522
+ }
2523
+ </div>
2524
+ }
2525
+
2526
+ <sd-button
2527
+ sdFooterRight
2528
+ type="text"
2529
+ [autoId]="_autoId ? _autoId + '-cancel' : undefined"
2530
+ [title]="'core.component.api-contract-builder.drawer.cancel' | sdTranslate"
2531
+ (click)="requestCancel()"></sd-button>
2532
+ <sd-button
2533
+ sdFooterRight
2534
+ type="fill"
2535
+ color="primary"
2536
+ [autoId]="_autoId ? _autoId + '-save' : undefined"
2537
+ [disabled]="!canSave()"
2538
+ [title]="'core.component.api-contract-builder.drawer.save' | sdTranslate"
2539
+ (click)="save()"></sd-button>
2540
+ </sd-side-drawer>
2541
+ `, isInline: true, styles: [".sd-acb-drawer{display:flex;flex-direction:column;gap:12px;padding:16px}.sd-acb-drawer__crumbs{display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px}.sd-acb-drawer__crumb{padding:2px 4px;border:0;border-radius:4px;background:transparent;color:var(--sd-primary, #005cbb);font:inherit;cursor:pointer}.sd-acb-drawer__crumb:hover{background:color-mix(in srgb,var(--sd-primary, #005cbb) 10%,transparent)}.sd-acb-drawer__crumb--current{color:var(--sd-text, #1a1b1f);font-weight:600;cursor:default}.sd-acb-drawer__crumb-sep{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__children{display:flex;flex-direction:column;gap:6px;padding-top:4px;border-top:1px solid var(--sd-border-color, #e6e6e6)}.sd-acb-drawer__children-title{margin:0;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__confirm{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:8px 10px;border:1px dashed var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface-muted, #f3f5f8);font-size:12px}\n"], dependencies: [{ kind: "component", type: SdSideDrawer, selector: "sd-side-drawer", inputs: ["title", "width", "hideClose", "disableBackdropClose", "beforeClose", "drawerClass", "autoId"], outputs: ["sdClosed", "sdCloseError"] }, { kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "mask", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "showSelectAll", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "component", type: SdApiContractNodeSummary, selector: "sd-api-contract-node-summary", inputs: ["name", "node", "readonly", "autoId"], outputs: ["edit", "remove"] }, { kind: "component", type: SdApiContractSourceEditor, selector: "sd-api-contract-source-editor", inputs: ["node", "suggestions", "disabled", "autoId"], outputs: ["nodeChange"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2542
+ }
2543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeDrawer, decorators: [{
2544
+ type: Component,
2545
+ args: [{ selector: 'sd-api-contract-node-drawer', standalone: true, imports: [SdSideDrawer, SdButton, SdInput, SdSelect, SdApiContractNodeSummary, SdApiContractSourceEditor, SdTranslatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2546
+ @let _autoId = autoId();
2547
+ @let _current = current();
2548
+ @let _nameError = nameError();
2549
+
2550
+ <sd-side-drawer
2551
+ #drawer
2552
+ width="560px"
2553
+ disableBackdropClose
2554
+ [title]="title()"
2555
+ [autoId]="_autoId"
2556
+ [beforeClose]="closeGuard"
2557
+ (sdClosed)="onClosed()">
2558
+ @if (_current) {
2559
+ <div class="sd-acb-drawer">
2560
+ @let _breadcrumb = breadcrumb();
2561
+ @if (_breadcrumb.length > 1) {
2562
+ <nav class="sd-acb-drawer__crumbs" [attr.aria-label]="title()">
2563
+ @for (crumb of _breadcrumb; track $index) {
2564
+ @if ($index < _breadcrumb.length - 1) {
2565
+ <button
2566
+ type="button"
2567
+ class="sd-acb-drawer__crumb"
2568
+ [attr.data-autoid]="_autoId ? _autoId + '-crumb-' + $index : null"
2569
+ (click)="backTo($index)">
2570
+ {{ crumb }}
2571
+ </button>
2572
+ <span class="sd-acb-drawer__crumb-sep">›</span>
2573
+ } @else {
2574
+ <span class="sd-acb-drawer__crumb sd-acb-drawer__crumb--current">{{ crumb }}</span>
2575
+ }
2576
+ }
2577
+ </nav>
2578
+ }
2579
+
2580
+ <sd-input
2581
+ size="sm"
2582
+ [autoId]="_autoId ? _autoId + '-name' : undefined"
2583
+ [label]="'core.component.api-contract-builder.node.name' | sdTranslate"
2584
+ [model]="currentName()"
2585
+ [inlineError]="_nameError ?? undefined"
2586
+ (sdChange)="setName($event)"></sd-input>
2587
+
2588
+ <sd-select
2589
+ size="sm"
2590
+ hideInlineError
2591
+ [clearable]="false"
2592
+ [autoId]="_autoId ? _autoId + '-type' : undefined"
2593
+ [label]="'core.component.api-contract-builder.node.type' | sdTranslate"
2594
+ [items]="typeOptions"
2595
+ valueField="value"
2596
+ displayField="label"
2597
+ [model]="_current.type"
2598
+ (sdChange)="setType($event)"></sd-select>
2599
+
2600
+ <sd-select
2601
+ size="sm"
2602
+ hideInlineError
2603
+ [clearable]="false"
2604
+ [autoId]="_autoId ? _autoId + '-required' : undefined"
2605
+ [label]="'core.component.api-contract-builder.node.required' | sdTranslate"
2606
+ [items]="requiredOptions"
2607
+ valueField="value"
2608
+ displayField="label"
2609
+ [model]="_current.required === true ? 'true' : 'false'"
2610
+ (sdChange)="setRequired($event)"></sd-select>
2611
+
2612
+ <sd-input
2613
+ size="sm"
2614
+ hideInlineError
2615
+ [autoId]="_autoId ? _autoId + '-label' : undefined"
2616
+ [label]="'core.component.api-contract-builder.node.label' | sdTranslate"
2617
+ [model]="_current.label ?? ''"
2618
+ (sdChange)="setText('label', $event)"></sd-input>
2619
+
2620
+ <sd-input
2621
+ size="sm"
2622
+ hideInlineError
2623
+ [autoId]="_autoId ? _autoId + '-description' : undefined"
2624
+ [label]="'core.component.api-contract-builder.node.description' | sdTranslate"
2625
+ [model]="_current.description ?? ''"
2626
+ (sdChange)="setText('description', $event)"></sd-input>
2627
+
2628
+ @if (allowTransform() && (_current.type === 'date' || _current.type === 'datetime')) {
2629
+ <sd-select
2630
+ size="sm"
2631
+ hideInlineError
2632
+ [clearable]="false"
2633
+ [autoId]="_autoId ? _autoId + '-transform' : undefined"
2634
+ [label]="'core.component.api-contract-builder.node.transform' | sdTranslate"
2635
+ [items]="transformOptions"
2636
+ valueField="value"
2637
+ displayField="label"
2638
+ [model]="_current.transform ?? ''"
2639
+ (sdChange)="setTransform($event)"></sd-select>
2640
+ }
2641
+
2642
+ @if (layer() === 'mapping') {
2643
+ <sd-api-contract-source-editor
2644
+ [node]="_current"
2645
+ [suggestions]="suggestions()"
2646
+ [autoId]="_autoId ? _autoId + '-value' : undefined"
2647
+ (nodeChange)="applyCurrent($event)"></sd-api-contract-source-editor>
2648
+ }
2649
+
2650
+ @if (_current.type === 'object') {
2651
+ @let _children = children();
2652
+ <div class="sd-acb-drawer__children">
2653
+ <h5 class="sd-acb-drawer__children-title">
2654
+ {{ 'core.component.api-contract-builder.node.add-property' | sdTranslate }}
2655
+ </h5>
2656
+
2657
+ @if (!_children.length) {
2658
+ <p class="sd-acb-drawer__empty">{{ 'core.component.api-contract-builder.node.empty' | sdTranslate }}</p>
2659
+ }
2660
+
2661
+ @for (child of _children; track child.key) {
2662
+ <sd-api-contract-node-summary
2663
+ [name]="child.key"
2664
+ [node]="child.node"
2665
+ [autoId]="_autoId ? _autoId + '-child-' + child.key : undefined"
2666
+ (edit)="enter(child.key)"
2667
+ (remove)="removeChild(child.key)"></sd-api-contract-node-summary>
2668
+ }
2669
+
2670
+ <sd-button
2671
+ type="light"
2672
+ size="sm"
2673
+ prefixIcon="add"
2674
+ [autoId]="_autoId ? _autoId + '-add-child' : undefined"
2675
+ [title]="'core.component.api-contract-builder.node.add-property' | sdTranslate"
2676
+ (click)="addChild()"></sd-button>
2677
+ </div>
2678
+ }
2679
+
2680
+ @if (discardPrompt()) {
2681
+ <div class="sd-acb-drawer__confirm" role="alertdialog">
2682
+ <span>{{ 'core.component.api-contract-builder.drawer.discard-confirm' | sdTranslate }}</span>
2683
+ <sd-button
2684
+ type="text"
2685
+ size="sm"
2686
+ color="error"
2687
+ [autoId]="_autoId ? _autoId + '-discard' : undefined"
2688
+ [title]="'core.component.api-contract-builder.confirm.yes' | sdTranslate"
2689
+ (click)="confirmDiscard()"></sd-button>
2690
+ <sd-button
2691
+ type="text"
2692
+ size="sm"
2693
+ [autoId]="_autoId ? _autoId + '-keep' : undefined"
2694
+ [title]="'core.component.api-contract-builder.confirm.no' | sdTranslate"
2695
+ (click)="cancelDiscard()"></sd-button>
2696
+ </div>
2697
+ }
2698
+ </div>
2699
+ }
2700
+
2701
+ <sd-button
2702
+ sdFooterRight
2703
+ type="text"
2704
+ [autoId]="_autoId ? _autoId + '-cancel' : undefined"
2705
+ [title]="'core.component.api-contract-builder.drawer.cancel' | sdTranslate"
2706
+ (click)="requestCancel()"></sd-button>
2707
+ <sd-button
2708
+ sdFooterRight
2709
+ type="fill"
2710
+ color="primary"
2711
+ [autoId]="_autoId ? _autoId + '-save' : undefined"
2712
+ [disabled]="!canSave()"
2713
+ [title]="'core.component.api-contract-builder.drawer.save' | sdTranslate"
2714
+ (click)="save()"></sd-button>
2715
+ </sd-side-drawer>
2716
+ `, styles: [".sd-acb-drawer{display:flex;flex-direction:column;gap:12px;padding:16px}.sd-acb-drawer__crumbs{display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px}.sd-acb-drawer__crumb{padding:2px 4px;border:0;border-radius:4px;background:transparent;color:var(--sd-primary, #005cbb);font:inherit;cursor:pointer}.sd-acb-drawer__crumb:hover{background:color-mix(in srgb,var(--sd-primary, #005cbb) 10%,transparent)}.sd-acb-drawer__crumb--current{color:var(--sd-text, #1a1b1f);font-weight:600;cursor:default}.sd-acb-drawer__crumb-sep{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__children{display:flex;flex-direction:column;gap:6px;padding-top:4px;border-top:1px solid var(--sd-border-color, #e6e6e6)}.sd-acb-drawer__children-title{margin:0;font-size:12px;text-transform:uppercase;letter-spacing:.04em;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-drawer__confirm{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:8px 10px;border:1px dashed var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface-muted, #f3f5f8);font-size:12px}\n"] }]
2717
+ }] });
2718
+ /** First free `truong`, `truong2`, … so adding twice never collides. */
2719
+ function uniqueChildKey(properties) {
2720
+ const base = 'truong';
2721
+ if (!Object.prototype.hasOwnProperty.call(properties, base))
2722
+ return base;
2723
+ for (let index = 2;; index += 1) {
2724
+ const candidate = `${base}${index}`;
2725
+ if (!Object.prototype.hasOwnProperty.call(properties, candidate))
2726
+ return candidate;
2727
+ }
2728
+ }
2729
+
2730
+ /**
2731
+ * One schema layer — `input.schema`, `req.body`, `res.body`, `output.schema` — rendered as a flat list
2732
+ * of the fields it declares.
2733
+ *
2734
+ * It used to be a recursive tree of editable seven-column rows. It is now a list you read: adding and
2735
+ * editing ask the builder to open its drawer, and only removal is applied straight away. The list is
2736
+ * deliberately FLAT — an `object` field shows how many fields it holds and the author drills into it
2737
+ * inside the drawer, so a deeply nested node is edited with a drawer's worth of room instead of a
2738
+ * 100px cell.
2739
+ */
2740
+ class SdApiContractNodeEditor {
2741
+ /** The layer's root node. */
2742
+ node = input.required();
2743
+ /** Diagnostic path of the layer itself, e.g. `input.schema`. */
2744
+ basePath = input.required();
2745
+ disabled = input(false, { transform: booleanAttribute });
2746
+ autoId = input();
2747
+ nodeChange = output();
2748
+ editRequest = output();
2749
+ /**
2750
+ * Which node actually holds the fields.
2751
+ *
2752
+ * why: một tầng `array` (`output.schema` sau khi nhận `${res.body.items}`) khai field ở `items`, chứ
2753
+ * bản thân nó không có `properties`. Đọc thẳng `node.properties` sẽ ra danh sách rỗng và tầng output
2754
+ * của contract mẫu thành không sửa được.
2755
+ */
2756
+ #container = computed(() => {
2757
+ const node = this.node();
2758
+ return node.type === 'array' && node.items ? node.items : node;
2759
+ });
2760
+ /** `['items']` for an array layer, `[]` otherwise — the prefix a commit has to be written under. */
2761
+ #containerPointer = computed(() => {
2762
+ const node = this.node();
2763
+ return node.type === 'array' && node.items ? ['items'] : [];
2764
+ });
2765
+ entries = computed(() => {
2766
+ const properties = this.#container().properties;
2767
+ if (!properties)
2768
+ return [];
2769
+ return Object.keys(properties).map(key => ({ key, node: properties[key] }));
2770
+ });
2771
+ #keys = computed(() => this.entries().map(entry => entry.key));
2772
+ requestAdd() {
2773
+ this.editRequest.emit({ name: null, node: null, siblingNames: this.#keys(), pointer: this.#containerPointer() });
2774
+ }
2775
+ requestEdit(key, node) {
2776
+ this.editRequest.emit({ name: key, node, siblingNames: this.#keys(), pointer: this.#containerPointer() });
2777
+ }
2778
+ removeProperty(key) {
2779
+ const stripped = removeSdApiContractProperty(this.#container(), key);
2780
+ this.nodeChange.emit(setSdApiContractNodeAt(this.node(), this.#containerPointer(), stripped));
2781
+ }
2782
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2783
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractNodeEditor, isStandalone: true, selector: "sd-api-contract-node-editor", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeChange: "nodeChange", editRequest: "editRequest" }, ngImport: i0, template: "@let _entries = entries();\n@let _disabled = disabled();\n@let _autoId = autoId();\n\n<div class=\"sd-acb-list\">\n @if (!_entries.length) {\n <p class=\"sd-acb-list__empty\">{{ 'core.component.api-contract-builder.node.empty' | sdTranslate }}</p>\n }\n\n @for (entry of _entries; track entry.key) {\n <sd-api-contract-node-summary\n [name]=\"entry.key\"\n [node]=\"entry.node\"\n [readonly]=\"_disabled\"\n [autoId]=\"_autoId ? _autoId + '-' + entry.key : undefined\"\n (edit)=\"requestEdit(entry.key, entry.node)\"\n (remove)=\"removeProperty(entry.key)\"></sd-api-contract-node-summary>\n }\n\n @if (!_disabled) {\n <sd-button\n class=\"sd-acb-list__add\"\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add' : undefined\"\n [title]=\"'core.component.api-contract-builder.node.add-property' | sdTranslate\"\n (click)=\"requestAdd()\"></sd-button>\n }\n</div>\n", styles: [":host{display:block;width:100%}.sd-acb-list{display:flex;flex-direction:column;gap:6px}.sd-acb-list__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-list__add{align-self:flex-start}\n"], dependencies: [{ kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }, { kind: "component", type: SdApiContractNodeSummary, selector: "sd-api-contract-node-summary", inputs: ["name", "node", "readonly", "autoId"], outputs: ["edit", "remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2784
+ }
2785
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractNodeEditor, decorators: [{
2786
+ type: Component,
2787
+ args: [{ selector: 'sd-api-contract-node-editor', standalone: true, imports: [SdButton, SdTranslatePipe, SdApiContractNodeSummary], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let _entries = entries();\n@let _disabled = disabled();\n@let _autoId = autoId();\n\n<div class=\"sd-acb-list\">\n @if (!_entries.length) {\n <p class=\"sd-acb-list__empty\">{{ 'core.component.api-contract-builder.node.empty' | sdTranslate }}</p>\n }\n\n @for (entry of _entries; track entry.key) {\n <sd-api-contract-node-summary\n [name]=\"entry.key\"\n [node]=\"entry.node\"\n [readonly]=\"_disabled\"\n [autoId]=\"_autoId ? _autoId + '-' + entry.key : undefined\"\n (edit)=\"requestEdit(entry.key, entry.node)\"\n (remove)=\"removeProperty(entry.key)\"></sd-api-contract-node-summary>\n }\n\n @if (!_disabled) {\n <sd-button\n class=\"sd-acb-list__add\"\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add' : undefined\"\n [title]=\"'core.component.api-contract-builder.node.add-property' | sdTranslate\"\n (click)=\"requestAdd()\"></sd-button>\n }\n</div>\n", styles: [":host{display:block;width:100%}.sd-acb-list{display:flex;flex-direction:column;gap:6px}.sd-acb-list__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-list__add{align-self:flex-start}\n"] }]
2788
+ }] });
2789
+
2790
+ /**
2791
+ * One keyed collection of nodes — `req.path` / `req.query` / `req.headers` / `res.headers` — rendered
2792
+ * as a list you read, not a list you type into.
2793
+ *
2794
+ * Every entry is a collapsed row. Adding and editing both ask the builder to open its drawer; only
2795
+ * removal is applied straight away, because there is nothing to stage about deleting a row. That is
2796
+ * why `add()` emits an edit request rather than an entry: a half-declared field must never reach the
2797
+ * contract just because someone clicked the add button and changed their mind.
2798
+ */
2799
+ class SdApiContractRecordEditor {
2800
+ record = input();
2801
+ /** Diagnostic path of the collection itself, e.g. `req.query`. */
2802
+ basePath = input.required();
2803
+ disabled = input(false, { transform: booleanAttribute });
2804
+ emptyLabel = input('');
2805
+ autoId = input();
2806
+ recordChange = output();
2807
+ editRequest = output();
2808
+ entries = computed(() => {
2809
+ const record = this.record();
2810
+ if (!record)
2811
+ return [];
2812
+ const base = this.basePath();
2813
+ return Object.keys(record).map(key => ({ key, node: record[key], path: `${base}.${key}` }));
2814
+ });
2815
+ #keys = computed(() => this.entries().map(entry => entry.key));
2816
+ requestAdd() {
2817
+ this.editRequest.emit({ name: null, node: null, siblingNames: this.#keys() });
2818
+ }
2819
+ requestEdit(entry) {
2820
+ this.editRequest.emit({ name: entry.key, node: entry.node, siblingNames: this.#keys() });
2821
+ }
2822
+ remove(key) {
2823
+ const record = this.record();
2824
+ if (!record)
2825
+ return;
2826
+ this.recordChange.emit(sdApiContractRecordRemove(record, key));
2827
+ }
2828
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractRecordEditor, deps: [], target: i0.ɵɵFactoryTarget.Component });
2829
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractRecordEditor, isStandalone: true, selector: "sd-api-contract-record-editor", inputs: { record: { classPropertyName: "record", publicName: "record", isSignal: true, isRequired: false, transformFunction: null }, basePath: { classPropertyName: "basePath", publicName: "basePath", isSignal: true, isRequired: true, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, emptyLabel: { classPropertyName: "emptyLabel", publicName: "emptyLabel", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { recordChange: "recordChange", editRequest: "editRequest" }, ngImport: i0, template: `
2830
+ @let _entries = entries();
2831
+ @let _disabled = disabled();
2832
+ @let _autoId = autoId();
2833
+
2834
+ <div class="sd-acb-record">
2835
+ @if (!_entries.length) {
2836
+ <p class="sd-acb-record__empty">{{ emptyLabel() }}</p>
2837
+ }
2838
+
2839
+ @for (entry of _entries; track entry.key) {
2840
+ <sd-api-contract-node-summary
2841
+ [name]="entry.key"
2842
+ [node]="entry.node"
2843
+ [readonly]="_disabled"
2844
+ [autoId]="_autoId ? _autoId + '-' + entry.key : undefined"
2845
+ (edit)="requestEdit(entry)"
2846
+ (remove)="remove(entry.key)"></sd-api-contract-node-summary>
2847
+ }
2848
+
2849
+ @if (!_disabled) {
2850
+ <sd-button
2851
+ class="sd-acb-record__add"
2852
+ type="light"
2853
+ size="sm"
2854
+ prefixIcon="add"
2855
+ [autoId]="_autoId ? _autoId + '-add' : undefined"
2856
+ [title]="'core.component.api-contract-builder.request.add-entry' | sdTranslate"
2857
+ (click)="requestAdd()"></sd-button>
2858
+ }
2859
+ </div>
2860
+ `, isInline: true, styles: [":host{display:block;width:100%}.sd-acb-record{display:flex;flex-direction:column;gap:6px}.sd-acb-record__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-record__add{align-self:flex-start}\n"], dependencies: [{ kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }, { kind: "component", type: SdApiContractNodeSummary, selector: "sd-api-contract-node-summary", inputs: ["name", "node", "readonly", "autoId"], outputs: ["edit", "remove"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
2861
+ }
2862
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractRecordEditor, decorators: [{
2863
+ type: Component,
2864
+ args: [{ selector: 'sd-api-contract-record-editor', standalone: true, imports: [SdButton, SdTranslatePipe, SdApiContractNodeSummary], changeDetection: ChangeDetectionStrategy.OnPush, template: `
2865
+ @let _entries = entries();
2866
+ @let _disabled = disabled();
2867
+ @let _autoId = autoId();
2868
+
2869
+ <div class="sd-acb-record">
2870
+ @if (!_entries.length) {
2871
+ <p class="sd-acb-record__empty">{{ emptyLabel() }}</p>
2872
+ }
2873
+
2874
+ @for (entry of _entries; track entry.key) {
2875
+ <sd-api-contract-node-summary
2876
+ [name]="entry.key"
2877
+ [node]="entry.node"
2878
+ [readonly]="_disabled"
2879
+ [autoId]="_autoId ? _autoId + '-' + entry.key : undefined"
2880
+ (edit)="requestEdit(entry)"
2881
+ (remove)="remove(entry.key)"></sd-api-contract-node-summary>
2882
+ }
2883
+
2884
+ @if (!_disabled) {
2885
+ <sd-button
2886
+ class="sd-acb-record__add"
2887
+ type="light"
2888
+ size="sm"
2889
+ prefixIcon="add"
2890
+ [autoId]="_autoId ? _autoId + '-add' : undefined"
2891
+ [title]="'core.component.api-contract-builder.request.add-entry' | sdTranslate"
2892
+ (click)="requestAdd()"></sd-button>
2893
+ }
2894
+ </div>
2895
+ `, styles: [":host{display:block;width:100%}.sd-acb-record{display:flex;flex-direction:column;gap:6px}.sd-acb-record__empty{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb-record__add{align-self:flex-start}\n"] }]
2896
+ }] });
2897
+
2898
+ const STEP_GENERAL = 0;
2899
+ const STEP_INPUT = 1;
2900
+ const STEP_REQUEST = 2;
2901
+ const STEP_RESPONSE = 3;
2902
+ const STEP_OUTPUT = 4;
2903
+ const STEP_REVIEW = 5;
2904
+ /**
2905
+ * Visual builder for an `SdApiContract`.
2906
+ *
2907
+ * The component is a **design-time** tool: it never performs a request, never resolves an
2908
+ * expression and never holds a secret. It edits, validates and serializes the contract; executing
2909
+ * it is a separate concern for a future `form-builder` / `form-render` integration.
2910
+ *
2911
+ * @example
2912
+ * ```html
2913
+ * <sd-api-contract-builder [(model)]="contract" autoId="product-search"></sd-api-contract-builder>
2914
+ * ```
2915
+ */
2916
+ class SdApiContractBuilder {
2917
+ #i18n = inject(I18nService);
2918
+ #configuration = resolveSdApiContractConfiguration(inject(SD_API_CONTRACT_CONFIGURATION, { optional: true }));
2919
+ model = model(null);
2920
+ mode = input('edit');
2921
+ disabled = input(false, { transform: booleanAttribute });
2922
+ autoId = input();
2923
+ /** Fires whenever the diagnostics change, including once for the initially seeded contract. */
2924
+ diagnosticsChange = output();
2925
+ /** Fires only when validity flips, so a consumer can gate a Save button without debouncing. */
2926
+ validChange = output();
2927
+ // why: bản nháp nội bộ là BẢN SAO SÂU của model cha. Component không bao giờ ghi vào object cha —
2928
+ // mọi thao tác dựng contract mới rồi mới `model.set`.
2929
+ #draft = signal(null);
2930
+ // why: mốc so sánh để phân biệt "cha đưa giá trị mới vào" với "chính ta vừa bắn ra" — thiếu nó,
2931
+ // effect seed sẽ clone lại contract ta vừa emit và tạo vòng lặp phản hồi.
2932
+ #lastEmitted = null;
2933
+ #lastValid = null;
2934
+ activeStep = signal(STEP_GENERAL);
2935
+ contractVersion = SD_API_CONTRACT_VERSION;
2936
+ responseFieldToken = signal(null);
2937
+ urlPlaceholder = '${env.baseUrl}/products/{id}';
2938
+ draft = this.#draft.asReadonly();
2939
+ readonly = computed(() => this.disabled() || this.mode() === 'view');
2940
+ isView = computed(() => this.mode() === 'view');
2941
+ steps = [
2942
+ { index: STEP_GENERAL, key: 'general', label: this.#i18n.t('core.component.api-contract-builder.step.general') },
2943
+ { index: STEP_INPUT, key: 'input', label: this.#i18n.t('core.component.api-contract-builder.step.input') },
2944
+ { index: STEP_REQUEST, key: 'request', label: this.#i18n.t('core.component.api-contract-builder.step.request') },
2945
+ { index: STEP_RESPONSE, key: 'response', label: this.#i18n.t('core.component.api-contract-builder.step.response') },
2946
+ { index: STEP_OUTPUT, key: 'output', label: this.#i18n.t('core.component.api-contract-builder.step.output') },
2947
+ { index: STEP_REVIEW, key: 'review', label: this.#i18n.t('core.component.api-contract-builder.step.review') },
2948
+ ];
2949
+ methodOptions = SD_API_CONTRACT_HTTP_METHODS.map(method => ({ value: method, label: method }));
2950
+ allTypes = SD_API_CONTRACT_DATA_TYPES;
2951
+ scalarTypes = SD_API_CONTRACT_SCALAR_DATA_TYPES;
2952
+ queryTypes = [...SD_API_CONTRACT_SCALAR_DATA_TYPES, 'array'];
2953
+ #envSuggestions = Object.keys(this.#configuration.env).map(key => {
2954
+ const variable = this.#configuration.env[key];
2955
+ const sensitiveMark = variable.sensitive ? ` · ${this.#i18n.t('core.component.api-contract-builder.mapping.sensitive')}` : '';
2956
+ return {
2957
+ expression: formatSdApiContractExpression('env', [key]),
2958
+ path: `env.${key}`,
2959
+ root: 'env',
2960
+ type: variable.type,
2961
+ display: `env.${key} — ${variable.type}${sensitiveMark}`,
2962
+ };
2963
+ });
2964
+ // why: builder sở hữu ĐÚNG MỘT drawer, nên phải tự nhớ danh sách nào vừa yêu cầu mở nó. Không có
2965
+ // mốc này thì `applyNodeCommit` không biết ghi kết quả vào `req.query` hay `res.headers`.
2966
+ #drawerTarget = signal(null);
2967
+ nodeDrawer = viewChild('nodeDrawer');
2968
+ drawerLayer = computed(() => this.#drawerTarget()?.section === 'input.schema' ? 'schema' : 'mapping');
2969
+ // why chỉ input/output schema: `transform` là thuộc tính của schema frontend. `req`/`res` mô tả HTTP,
2970
+ // ở đó một giá trị thời gian đã là chuỗi trên đường truyền rồi.
2971
+ drawerAllowsTransform = computed(() => {
2972
+ const section = this.#drawerTarget()?.section;
2973
+ return section === 'input.schema' || section === 'output.schema';
2974
+ });
2975
+ drawerSuggestions = computed(() => {
2976
+ const section = this.#drawerTarget()?.section;
2977
+ if (section === undefined)
2978
+ return [];
2979
+ if (section === 'output.schema')
2980
+ return this.outputSuggestions();
2981
+ if (section === 'res.headers' || section === 'res.body')
2982
+ return this.outputSuggestions();
2983
+ return this.requestSuggestions();
2984
+ });
2985
+ // why: JSON dán vào có thể chưa parse được (đang gõ dở, thiếu ngoặc). Lỗi đó KHÔNG đến từ
2986
+ // `validateSdApiContract` — nó chưa bao giờ thấy contract — nên phải mang riêng rồi trộn vào
2987
+ // danh sách chẩn đoán, để người dán thấy lý do thay vì thấy contract im lặng không đổi.
2988
+ #pasteError = signal(null);
2989
+ diagnostics = computed(() => {
2990
+ const draft = this.#draft();
2991
+ const base = draft ? validateSdApiContract(draft, this.#configuration) : [];
2992
+ const pasteError = this.#pasteError();
2993
+ return pasteError ? [pasteError, ...base] : base;
2994
+ });
2995
+ json = computed(() => serializeSdApiContract(this.#draft()));
2996
+ // why: contract từ ngoài có thể thiếu hẳn một tầng. Mọi computed đọc draft phải chịu được điều đó —
2997
+ // builder có nhiệm vụ HIỂN THỊ contract sai kèm chẩn đoán, không được nổ và cũng không được tự vá.
2998
+ #inputSuggestions = computed(() => {
2999
+ const schema = this.#draft()?.input?.schema;
3000
+ if (!schema)
3001
+ return [];
3002
+ return listSdApiContractSchemaFields(schema, { arrays: 'stop' }).map(field => ({
3003
+ expression: formatSdApiContractExpression('input', field.segments),
3004
+ path: `input.${field.path}`,
3005
+ root: 'input',
3006
+ type: field.type,
3007
+ display: `input.${field.path} — ${field.type}${field.label ? ` (${field.label})` : ''}`,
3008
+ }));
3009
+ });
3010
+ #responseSuggestions = computed(() => {
3011
+ const response = this.#draft()?.res;
3012
+ if (!response)
3013
+ return [];
3014
+ return listSdApiContractResponseFields(response).map(field => ({
3015
+ expression: formatSdApiContractExpression('res', field.segments),
3016
+ path: `res.${field.path}`,
3017
+ root: 'res',
3018
+ type: field.type,
3019
+ display: `res.${field.path} — ${field.type}`,
3020
+ }));
3021
+ });
3022
+ requestSuggestions = computed(() => [...this.#inputSuggestions(), ...this.#envSuggestions]);
3023
+ outputSuggestions = computed(() => [
3024
+ ...this.#responseSuggestions(),
3025
+ ...this.#inputSuggestions(),
3026
+ ...this.#envSuggestions,
3027
+ ]);
3028
+ /** Response paths offered by the "use a response field as the output" action. */
3029
+ responseFieldOptions = computed(() => {
3030
+ const response = this.#draft()?.res;
3031
+ if (!response)
3032
+ return [];
3033
+ return listSdApiContractResponseFields(response).map(field => ({ value: field.path, label: `${field.path} — ${field.type}` }));
3034
+ });
3035
+ /** Leaf fields a dropdown / table consumer will see once this contract runs. */
3036
+ outputFields = computed(() => {
3037
+ const schema = this.#draft()?.output?.schema;
3038
+ if (!schema)
3039
+ return [];
3040
+ return listSdApiContractSchemaFields(schema).filter(field => field.leaf);
3041
+ });
3042
+ statusText = computed(() => {
3043
+ const status = this.#draft()?.res?.status;
3044
+ if (status === undefined)
3045
+ return '';
3046
+ return Array.isArray(status) ? status.join(', ') : String(status);
3047
+ });
3048
+ get autoIdAttr() {
3049
+ return this.autoId() ?? null;
3050
+ }
3051
+ constructor() {
3052
+ effect(() => {
3053
+ const external = this.model();
3054
+ untracked(() => {
3055
+ if (external === this.#lastEmitted)
3056
+ return;
3057
+ // why: clone sâu — cha giữ nguyên object của mình, mọi chỉnh sửa chỉ chạm bản nháp.
3058
+ // Contract sai cấu trúc vẫn được nạp nguyên trạng để hiển thị + chẩn đoán, KHÔNG tự sửa.
3059
+ this.#draft.set(external ? cloneSdApiContract(external) : null);
3060
+ });
3061
+ });
3062
+ effect(() => {
3063
+ const diagnostics = this.diagnostics();
3064
+ untracked(() => {
3065
+ this.diagnosticsChange.emit(diagnostics);
3066
+ const valid = !diagnostics.some(diagnostic => diagnostic.severity === 'error');
3067
+ if (valid !== this.#lastValid) {
3068
+ this.#lastValid = valid;
3069
+ this.validChange.emit(valid);
3070
+ }
3071
+ });
3072
+ });
3073
+ }
3074
+ // -------------------------------------------------------------------------
3075
+ // Navigation
3076
+ // -------------------------------------------------------------------------
3077
+ goToStep(index) {
3078
+ this.activeStep.set(index);
3079
+ }
3080
+ goToDiagnostic(diagnostic) {
3081
+ this.activeStep.set(stepForPath(diagnostic.path));
3082
+ }
3083
+ // -------------------------------------------------------------------------
3084
+ // Contract-level edits
3085
+ // -------------------------------------------------------------------------
3086
+ createContract() {
3087
+ this.#commit({
3088
+ contractVersion: SD_API_CONTRACT_VERSION,
3089
+ code: '',
3090
+ name: '',
3091
+ input: { schema: { type: 'object', properties: {} } },
3092
+ req: { method: 'GET', url: '' },
3093
+ res: { status: 200 },
3094
+ output: { schema: { type: 'object', properties: {} } },
3095
+ });
3096
+ }
3097
+ setText(key, value) {
3098
+ const draft = this.#draft();
3099
+ if (!draft)
3100
+ return;
3101
+ const text = typeof value === 'string' ? value : '';
3102
+ if (key === 'description' && !text) {
3103
+ const { description: _dropped, ...rest } = draft;
3104
+ this.#commit(rest);
3105
+ return;
3106
+ }
3107
+ this.#commit({ ...draft, [key]: text });
3108
+ }
3109
+ setInputSchema(node) {
3110
+ const draft = this.#draft();
3111
+ if (!draft)
3112
+ return;
3113
+ this.#commit({ ...draft, input: { schema: node } });
3114
+ }
3115
+ setMethod(value) {
3116
+ const draft = this.#draft();
3117
+ if (!draft || typeof value !== 'string')
3118
+ return;
3119
+ this.#commit({ ...draft, req: { ...draft.req, method: value } });
3120
+ }
3121
+ setUrl(value) {
3122
+ const draft = this.#draft();
3123
+ if (!draft)
3124
+ return;
3125
+ this.#commit({ ...draft, req: { ...draft.req, url: typeof value === 'string' ? value : '' } });
3126
+ }
3127
+ // -------------------------------------------------------------------------
3128
+ // Drawer
3129
+ // -------------------------------------------------------------------------
3130
+ /**
3131
+ * Opens the one drawer for whichever list asked.
3132
+ *
3133
+ * why gác `readonly` ở đây nữa: hàng thu gọn đã tự chặn khi read-only, nhưng builder là nơi duy nhất
3134
+ * biết `mode`/`disabled` thật. Hai lớp gác rẻ hơn một đường mở drawer lọt trong chế độ xem.
3135
+ */
3136
+ openNodeDrawer(section, request) {
3137
+ if (this.readonly())
3138
+ return;
3139
+ this.#drawerTarget.set({ section, request });
3140
+ const drawer = this.nodeDrawer();
3141
+ if (!drawer)
3142
+ return;
3143
+ if (request.name === null || request.node === null)
3144
+ drawer.openForAdd(request.siblingNames);
3145
+ else
3146
+ drawer.openForEdit(request.name, request.node, request.siblingNames);
3147
+ }
3148
+ /**
3149
+ * Writes a committed node back where it came from — one `modelChange`, at Save time.
3150
+ *
3151
+ * A rename goes through `sdApiContractRecordRename` first so the entry keeps its position in the
3152
+ * JSON instead of jumping to the end. The drawer has already refused a duplicate name, so the
3153
+ * rename cannot collide by the time it reaches here.
3154
+ */
3155
+ applyNodeCommit(commit) {
3156
+ const target = this.#drawerTarget();
3157
+ const draft = this.#draft();
3158
+ this.#drawerTarget.set(null);
3159
+ if (!target || !draft)
3160
+ return;
3161
+ const previous = target.request.name;
3162
+ const pointer = target.request.pointer ?? [];
3163
+ switch (target.section) {
3164
+ case 'req.path':
3165
+ case 'req.query':
3166
+ case 'req.headers': {
3167
+ const key = target.section.slice('req.'.length);
3168
+ const record = (draft.req?.[key] ?? {});
3169
+ this.setRequestRecord(key, commitIntoRecord(record, previous, commit));
3170
+ return;
3171
+ }
3172
+ case 'res.headers': {
3173
+ const record = (draft.res?.headers ?? {});
3174
+ this.setResponseHeaders(commitIntoRecord(record, previous, commit));
3175
+ return;
3176
+ }
3177
+ case 'input.schema': {
3178
+ const root = draft.input?.schema;
3179
+ if (!root)
3180
+ return;
3181
+ this.setInputSchema(commitIntoProperties(root, pointer, previous, commit));
3182
+ return;
3183
+ }
3184
+ case 'req.body': {
3185
+ const root = draft.req?.body;
3186
+ if (!root)
3187
+ return;
3188
+ this.setRequestBody(commitIntoProperties(root, pointer, previous, commit));
3189
+ return;
3190
+ }
3191
+ case 'res.body': {
3192
+ const root = draft.res?.body;
3193
+ if (!root)
3194
+ return;
3195
+ this.setResponseBody(commitIntoProperties(root, pointer, previous, commit));
3196
+ return;
3197
+ }
3198
+ case 'output.schema': {
3199
+ const root = draft.output?.schema;
3200
+ if (!root)
3201
+ return;
3202
+ this.setOutputSchema(commitIntoProperties(root, pointer, previous, commit));
3203
+ return;
3204
+ }
3205
+ default:
3206
+ return;
3207
+ }
3208
+ }
3209
+ setRequestRecord(section, record) {
3210
+ const draft = this.#draft();
3211
+ if (!draft)
3212
+ return;
3213
+ this.#commit({ ...draft, req: { ...draft.req, [section]: record } });
3214
+ }
3215
+ setRequestBody(node) {
3216
+ const draft = this.#draft();
3217
+ if (!draft)
3218
+ return;
3219
+ this.#commit({ ...draft, req: { ...draft.req, body: node } });
3220
+ }
3221
+ addRequestBody() {
3222
+ this.setRequestBody(createSdApiContractNode('object'));
3223
+ }
3224
+ removeRequestBody() {
3225
+ const draft = this.#draft();
3226
+ if (!draft)
3227
+ return;
3228
+ const { body: _dropped, ...req } = draft.req;
3229
+ this.#commit({ ...draft, req });
3230
+ }
3231
+ setStatus(value) {
3232
+ const draft = this.#draft();
3233
+ if (!draft)
3234
+ return;
3235
+ const parts = String(value ?? '')
3236
+ .split(',')
3237
+ .map(part => part.trim())
3238
+ .filter(part => part.length > 0)
3239
+ .map(part => Number(part));
3240
+ // why: KHÔNG bỏ giá trị không phải số — validator phải báo `res.status.invalid` cho người dùng
3241
+ // thấy, im lặng nuốt đi là "âm thầm sửa contract sai".
3242
+ const status = parts.length === 1 ? parts[0] : parts;
3243
+ this.#commit({ ...draft, res: { ...draft.res, status } });
3244
+ }
3245
+ setResponseHeaders(record) {
3246
+ const draft = this.#draft();
3247
+ if (!draft)
3248
+ return;
3249
+ this.#commit({ ...draft, res: { ...draft.res, headers: record } });
3250
+ }
3251
+ setResponseBody(node) {
3252
+ const draft = this.#draft();
3253
+ if (!draft)
3254
+ return;
3255
+ this.#commit({ ...draft, res: { ...draft.res, body: node } });
3256
+ }
3257
+ addResponseBody() {
3258
+ this.setResponseBody(createSdApiContractNode('object'));
3259
+ }
3260
+ removeResponseBody() {
3261
+ const draft = this.#draft();
3262
+ if (!draft)
3263
+ return;
3264
+ const { body: _dropped, ...res } = draft.res;
3265
+ this.#commit({ ...draft, res });
3266
+ }
3267
+ setOutputSchema(node) {
3268
+ const draft = this.#draft();
3269
+ if (!draft)
3270
+ return;
3271
+ this.#commit({ ...draft, output: { schema: node } });
3272
+ }
3273
+ /**
3274
+ * Adopts a response subtree as the output schema.
3275
+ *
3276
+ * The subtree is **deep-copied**, never referenced: editing the output afterwards must not reach
3277
+ * back into the response declaration. An array or scalar target takes a whole-node `source`; an
3278
+ * object target keeps its shape and each branch gets its own `source`, because an object with both
3279
+ * a whole-node source and child mappings is invalid by design.
3280
+ */
3281
+ useResponseFieldAsOutput(value) {
3282
+ const draft = this.#draft();
3283
+ if (!draft || typeof value !== 'string' || !value)
3284
+ return;
3285
+ const segments = value.split('.');
3286
+ const resolved = resolveSdApiContractResponsePath(draft.res, segments);
3287
+ this.responseFieldToken.set(null);
3288
+ if (!resolved)
3289
+ return;
3290
+ const schema = resolved.node
3291
+ ? adoptResponseNode(resolved.node, segments)
3292
+ : { type: resolved.type, source: formatSdApiContractExpression('res', segments) };
3293
+ this.setOutputSchema(schema);
3294
+ }
3295
+ /**
3296
+ * Adopts a contract pasted into the review editor.
3297
+ *
3298
+ * `<sd-code-editor language="json">` emits the PARSED value when the text is valid JSON and the
3299
+ * raw STRING while it is still half-typed. A string therefore means "not parseable yet": keep the
3300
+ * current draft and report it, because replacing a contract with a fragment of text would destroy
3301
+ * the author's work on a keystroke.
3302
+ *
3303
+ * A parseable object is adopted VERBATIM — no field is added, removed or repaired. Whatever is
3304
+ * wrong with it surfaces through `validateSdApiContract`, which is the whole point of pasting.
3305
+ */
3306
+ applyPastedJson(value) {
3307
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
3308
+ this.#pasteError.set({
3309
+ code: 'contract.invalid',
3310
+ severity: 'error',
3311
+ path: '',
3312
+ message: 'The pasted text is not valid JSON. The contract was left unchanged.',
3313
+ });
3314
+ return;
3315
+ }
3316
+ this.#pasteError.set(null);
3317
+ this.#commit(cloneSdApiContract(value));
3318
+ }
3319
+ #commit(next) {
3320
+ this.#lastEmitted = next;
3321
+ this.#draft.set(next);
3322
+ this.model.set(next);
3323
+ }
3324
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractBuilder, deps: [], target: i0.ɵɵFactoryTarget.Component });
3325
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.21", type: SdApiContractBuilder, isStandalone: true, selector: "sd-api-contract-builder", inputs: { model: { classPropertyName: "model", publicName: "model", isSignal: true, isRequired: false, transformFunction: null }, mode: { classPropertyName: "mode", publicName: "mode", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, autoId: { classPropertyName: "autoId", publicName: "autoId", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { model: "modelChange", diagnosticsChange: "diagnosticsChange", validChange: "validChange" }, host: { properties: { "attr.data-autoId": "this.autoIdAttr" } }, viewQueries: [{ propertyName: "nodeDrawer", first: true, predicate: ["nodeDrawer"], descendants: true, isSignal: true }], ngImport: i0, template: "@let _draft = draft();\n@let _readonly = readonly();\n@let _autoId = autoId();\n@let _diagnostics = diagnostics();\n@let _activeStep = activeStep();\n@let _requestSuggestions = requestSuggestions();\n@let _responseFieldOptions = responseFieldOptions();\n\n<!-- why: contract t\u1EEB ngo\u00E0i c\u00F3 th\u1EC3 thi\u1EBFu h\u1EB3n m\u1ED9t t\u1EA7ng. M\u1ECDi nh\u00E1nh \u0111\u1ECDc `_draft.<section>` \u0111\u1EC1u ph\u1EA3i\n g\u00E1c \u2014 builder hi\u1EC3n th\u1ECB contract sai k\u00E8m ch\u1EA9n \u0111o\u00E1n ch\u1EE9 kh\u00F4ng t\u1EF1 v\u00E1, v\u00E0 c\u0169ng kh\u00F4ng \u0111\u01B0\u1EE3c n\u1ED5. -->\n@if (!_draft) {\n <div class=\"sd-acb__empty\">\n <sd-icon name=\"description\" size=\"lg\"></sd-icon>\n <p>{{ 'core.component.api-contract-builder.empty.contract' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"fill\"\n color=\"primary\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-create' : undefined\"\n [title]=\"'core.component.api-contract-builder.create-contract' | sdTranslate\"\n (click)=\"createContract()\"></sd-button>\n }\n </div>\n} @else if (isView()) {\n <section class=\"sd-acb__view\">\n <dl class=\"sd-acb__summary\">\n <dt>{{ 'core.component.api-contract-builder.contract.code' | sdTranslate }}</dt>\n <dd data-field=\"code\">{{ _draft.code }}</dd>\n <dt>{{ 'core.component.api-contract-builder.contract.name' | sdTranslate }}</dt>\n <dd data-field=\"name\">{{ _draft.name }}</dd>\n <dt>{{ 'core.component.api-contract-builder.request.method' | sdTranslate }}</dt>\n <dd data-field=\"method\">{{ _draft.req?.method }}</dd>\n <dt>{{ 'core.component.api-contract-builder.request.url' | sdTranslate }}</dt>\n <dd data-field=\"url\">{{ _draft.req?.url }}</dd>\n </dl>\n\n <sd-api-contract-diagnostic-list\n [diagnostics]=\"_diagnostics\"\n [autoId]=\"_autoId ? _autoId + '-diagnostics' : undefined\"></sd-api-contract-diagnostic-list>\n\n <sd-code-editor language=\"json\" [viewed]=\"true\" maxHeight=\"420px\" [model]=\"json()\"></sd-code-editor>\n </section>\n} @else {\n <nav class=\"sd-acb__steps\" [attr.aria-label]=\"'core.component.api-contract-builder.steps' | sdTranslate\">\n @for (step of steps; track step.index) {\n <button\n type=\"button\"\n class=\"sd-acb__step\"\n [class.sd-acb__step--active]=\"_activeStep === step.index\"\n [attr.aria-current]=\"_activeStep === step.index ? 'step' : null\"\n [attr.data-autoId]=\"_autoId ? _autoId + '-step-' + step.key : null\"\n (click)=\"goToStep(step.index)\">\n <span class=\"sd-acb__step-index\">{{ step.index + 1 }}</span>\n <span class=\"sd-acb__step-label\">{{ step.label }}</span>\n </button>\n }\n </nav>\n\n <div class=\"sd-acb__body\">\n @switch (_activeStep) {\n @case (0) {\n <section class=\"sd-acb__section\" data-step=\"general\">\n <div class=\"sd-acb__grid\">\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-code' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.code' | sdTranslate\"\n [model]=\"_draft.code\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('code', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-name' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.name' | sdTranslate\"\n [model]=\"_draft.name\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('name', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-description' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.description' | sdTranslate\"\n [model]=\"_draft.description ?? ''\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('description', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-version' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.version' | sdTranslate\"\n [model]=\"contractVersion\"\n [readonly]=\"true\"\n [disabled]=\"true\"></sd-input>\n </div>\n </section>\n }\n\n @case (1) {\n <section class=\"sd-acb__section\" data-step=\"input\">\n @if (_draft.input?.schema; as _inputSchema) {\n <sd-api-contract-node-editor\n [node]=\"$any(_inputSchema)\"\n basePath=\"input.schema\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-input' : undefined\"\n (nodeChange)=\"setInputSchema($event)\"\n (editRequest)=\"openNodeDrawer('input.schema', $event)\"></sd-api-contract-node-editor>\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (2) {\n <section class=\"sd-acb__section\" data-step=\"request\">\n @if (_draft.req; as _req) {\n <div class=\"sd-acb__grid\">\n <sd-select\n size=\"sm\"\n hideInlineError\n [clearable]=\"false\"\n [autoId]=\"_autoId ? _autoId + '-method' : undefined\"\n [label]=\"'core.component.api-contract-builder.request.method' | sdTranslate\"\n [items]=\"methodOptions\"\n valueField=\"value\"\n displayField=\"label\"\n [model]=\"_req.method\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setMethod($event)\"></sd-select>\n <sd-input\n class=\"sd-acb__url\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-url' : undefined\"\n [label]=\"'core.component.api-contract-builder.request.url' | sdTranslate\"\n [placeholder]=\"urlPlaceholder\"\n [model]=\"_req.url\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setUrl($event)\"></sd-input>\n </div>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.path' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.path)\"\n basePath=\"req.path\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.path' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-path' : undefined\"\n (recordChange)=\"setRequestRecord('path', $event)\"\n (editRequest)=\"openNodeDrawer('req.path', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.query' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.query)\"\n basePath=\"req.query\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.query' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-query' : undefined\"\n (recordChange)=\"setRequestRecord('query', $event)\"\n (editRequest)=\"openNodeDrawer('req.query', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.headers' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.headers)\"\n basePath=\"req.headers\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.headers' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-headers' : undefined\"\n (recordChange)=\"setRequestRecord('headers', $event)\"\n (editRequest)=\"openNodeDrawer('req.headers', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.body' | sdTranslate }}</h4>\n @if (_req.body) {\n <sd-api-contract-node-editor\n [node]=\"$any(_req.body)\"\n basePath=\"req.body\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-body' : undefined\"\n (nodeChange)=\"setRequestBody($event)\"\n (editRequest)=\"openNodeDrawer('req.body', $event)\"></sd-api-contract-node-editor>\n @if (!_readonly) {\n <sd-button\n type=\"text\"\n color=\"error\"\n size=\"sm\"\n prefixIcon=\"delete\"\n [autoId]=\"_autoId ? _autoId + '-remove-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.request.remove-body' | sdTranslate\"\n (click)=\"removeRequestBody()\"></sd-button>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.body' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.request.add-body' | sdTranslate\"\n (click)=\"addRequestBody()\"></sd-button>\n }\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (3) {\n <section class=\"sd-acb__section\" data-step=\"response\">\n @if (_draft.res; as _res) {\n <sd-input\n class=\"sd-acb__status\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-status' : undefined\"\n [label]=\"'core.component.api-contract-builder.response.status' | sdTranslate\"\n [helperText]=\"'core.component.api-contract-builder.response.status-hint' | sdTranslate\"\n [model]=\"statusText()\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setStatus($event)\"></sd-input>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.response.headers' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_res.headers)\"\n basePath=\"res.headers\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.headers' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-res-headers' : undefined\"\n (recordChange)=\"setResponseHeaders($event)\"\n (editRequest)=\"openNodeDrawer('res.headers', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.response.body' | sdTranslate }}</h4>\n @if (_res.body) {\n <sd-api-contract-node-editor\n [node]=\"$any(_res.body)\"\n basePath=\"res.body\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-res-body' : undefined\"\n (nodeChange)=\"setResponseBody($event)\"\n (editRequest)=\"openNodeDrawer('res.body', $event)\"></sd-api-contract-node-editor>\n @if (!_readonly) {\n <sd-button\n type=\"text\"\n color=\"error\"\n size=\"sm\"\n prefixIcon=\"delete\"\n [autoId]=\"_autoId ? _autoId + '-remove-res-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.response.remove-body' | sdTranslate\"\n (click)=\"removeResponseBody()\"></sd-button>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.body' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add-res-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.response.add-body' | sdTranslate\"\n (click)=\"addResponseBody()\"></sd-button>\n }\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (4) {\n <section class=\"sd-acb__section\" data-step=\"output\">\n @if (_draft.output?.schema; as _outputSchema) {\n @if (!_readonly && _responseFieldOptions.length) {\n <sd-select\n class=\"sd-acb__adopt\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-use-response' : undefined\"\n [label]=\"'core.component.api-contract-builder.output.use-response' | sdTranslate\"\n [items]=\"_responseFieldOptions\"\n valueField=\"value\"\n displayField=\"label\"\n [(model)]=\"responseFieldToken\"\n (sdChange)=\"useResponseFieldAsOutput($event)\"></sd-select>\n }\n\n <sd-api-contract-node-editor\n [node]=\"$any(_outputSchema)\"\n basePath=\"output.schema\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-output' : undefined\"\n (nodeChange)=\"setOutputSchema($event)\"\n (editRequest)=\"openNodeDrawer('output.schema', $event)\"></sd-api-contract-node-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.output.fields' | sdTranslate }}</h4>\n @let _outputFields = outputFields();\n @if (_outputFields.length) {\n <ul class=\"sd-acb__fields\" [attr.data-autoId]=\"_autoId ? _autoId + '-output-fields' : null\">\n @for (field of _outputFields; track field.path) {\n <li class=\"sd-acb__field\">\n <code>{{ field.path }}</code>\n <span class=\"sd-acb__field-type\">{{ field.type }}</span>\n @if (field.required === true) {\n <span class=\"sd-acb__field-flag\">{{ 'core.component.api-contract-builder.field.required' | sdTranslate }}</span>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.output.no-fields' | sdTranslate }}</p>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @default {\n <section class=\"sd-acb__section\" data-step=\"review\">\n <sd-api-contract-diagnostic-list\n [diagnostics]=\"_diagnostics\"\n [autoId]=\"_autoId ? _autoId + '-diagnostics' : undefined\"\n (navigate)=\"goToDiagnostic($event)\"></sd-api-contract-diagnostic-list>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.review.json' | sdTranslate }}</h4>\n <sd-code-editor\n language=\"json\"\n maxHeight=\"480px\"\n [viewed]=\"disabled() || mode() === 'view'\"\n [model]=\"json()\"\n (modelChange)=\"applyPastedJson($event)\"></sd-code-editor>\n </section>\n }\n }\n </div>\n\n <!-- why M\u1ED8T instance duy nh\u1EA5t, \u0111\u1EB7t ngo\u00E0i @switch: m\u1ED7i layer m\u1ED9t drawer s\u1EBD l\u00E0 m\u1ED7i layer m\u1ED9t draft, v\u00E0\n \u0111\u1ED5i step gi\u1EEFa l\u00FAc \u0111ang s\u1EEDa s\u1EBD destroy drawer c\u00F9ng v\u1EDBi thay \u0111\u1ED5i ch\u01B0a l\u01B0u. -->\n <sd-api-contract-node-drawer\n #nodeDrawer\n [layer]=\"drawerLayer()\"\n [allowTransform]=\"drawerAllowsTransform()\"\n [suggestions]=\"drawerSuggestions()\"\n [autoId]=\"_autoId ? _autoId + '-node-drawer' : undefined\"\n (nodeCommit)=\"applyNodeCommit($event)\"></sd-api-contract-node-drawer>\n}\n", styles: [":host{display:block;width:100%}.sd-acb__empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:32px 16px;border:1px dashed var(--sd-border-color, #e6e6e6);border-radius:8px;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__empty p{margin:0;font-size:13px}.sd-acb__steps{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--sd-border-color, #e6e6e6);padding-bottom:8px}.sd-acb__step{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border:1px solid transparent;border-radius:999px;background:transparent;color:var(--sd-text-secondary, #6b6b6b);font:inherit;font-size:13px;cursor:pointer}.sd-acb__step:hover,.sd-acb__step:focus-visible{background:var(--sd-surface-muted, #f3f5f8)}.sd-acb__step--active{border-color:var(--sd-primary, #005cbb);background:var(--sd-primary-light, #e5efff);color:var(--sd-primary-dark, #00419e);font-weight:600}.sd-acb__step-index{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:var(--sd-surface-muted, #f3f5f8);font-size:11px;font-weight:700}.sd-acb__step--active .sd-acb__step-index{background:var(--sd-primary, #005cbb);color:#fff}.sd-acb__body{display:block}.sd-acb__section{display:flex;flex-direction:column;gap:12px}.sd-acb__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;align-items:start}.sd-acb__url,.sd-acb__status{grid-column:span 2}.sd-acb__adopt{max-width:420px}.sd-acb__heading{margin:8px 0 0;font-size:13px;font-weight:600;color:var(--sd-text, #1f2937)}.sd-acb__empty-text{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__fields{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}.sd-acb__field{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border:1px solid var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface-muted, #f3f5f8);font-size:12px}.sd-acb__field-type{color:var(--sd-primary, #005cbb);font-weight:600}.sd-acb__field-flag{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__view{display:flex;flex-direction:column;gap:16px}.sd-acb__summary{display:grid;grid-template-columns:minmax(120px,max-content) 1fr;gap:6px 16px;margin:0;font-size:13px}.sd-acb__summary dt{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__summary dd{margin:0;font-weight:600;overflow-wrap:anywhere}@media(max-width:768px){.sd-acb__url,.sd-acb__status{grid-column:span 1}.sd-acb__step-label{display:none}}\n"], dependencies: [{ kind: "component", type: SdButton, selector: "sd-button", inputs: ["autoId", "type", "color", "size", "fontSet", "title", "width", "tooltip", "prefixIcon", "suffixIcon", "disabled", "loading", "block", "htmlType"], outputs: ["click"] }, { kind: "component", type: SdCodeEditor, selector: "sd-code-editor", inputs: ["model", "language", "maxHeight", "viewed"], outputs: ["modelChange"] }, { kind: "component", type: SdIcon, selector: "sd-icon", inputs: ["name", "fontIcon", "color", "set", "fontSet", "size", "strokeWidth", "absoluteStrokeWidth", "ariaLabel"] }, { kind: "component", type: SdInput, selector: "sd-input", inputs: ["autoId", "name", "appearance", "floatLabel", "size", "form", "label", "helperText", "placeholder", "type", "mask", "hideInlineError", "blurOnEnter", "clearable", "required", "readonly", "disabled", "viewed", "minlength", "maxlength", "pattern", "patternErrorMessage", "validator", "inlineError", "hyperlink", "model"], outputs: ["modelChange", "sdChange", "sdFocus", "sdBlur", "keyupEnter", "cleared", "sdFocusForceBlur"] }, { kind: "component", type: SdSelect, selector: "sd-select", inputs: ["autoId", "name", "size", "form", "label", "helperText", "placeholder", "valueField", "displayField", "disabledField", "cacheChecksum", "limit", "hyperlink", "minWidthPanel", "hideInlineError", "required", "disabled", "viewed", "multiple", "showSelectAll", "clearable", "validator", "inlineError", "appearance", "floatLabel", "items", "model"], outputs: ["modelChange", "sdChange", "sdSelection"] }, { kind: "pipe", type: SdTranslatePipe, name: "sdTranslate" }, { kind: "component", type: SdApiContractDiagnosticList, selector: "sd-api-contract-diagnostic-list", inputs: ["diagnostics", "autoId"], outputs: ["navigate"] }, { kind: "component", type: SdApiContractNodeEditor, selector: "sd-api-contract-node-editor", inputs: ["node", "basePath", "disabled", "autoId"], outputs: ["nodeChange", "editRequest"] }, { kind: "component", type: SdApiContractNodeDrawer, selector: "sd-api-contract-node-drawer", inputs: ["layer", "allowTransform", "suggestions", "autoId"], outputs: ["nodeCommit"] }, { kind: "component", type: SdApiContractRecordEditor, selector: "sd-api-contract-record-editor", inputs: ["record", "basePath", "disabled", "emptyLabel", "autoId"], outputs: ["recordChange", "editRequest"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3326
+ }
3327
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.21", ngImport: i0, type: SdApiContractBuilder, decorators: [{
3328
+ type: Component,
3329
+ args: [{ selector: 'sd-api-contract-builder', standalone: true, imports: [
3330
+ SdButton,
3331
+ SdCodeEditor,
3332
+ SdIcon,
3333
+ SdInput,
3334
+ SdSelect,
3335
+ SdTranslatePipe,
3336
+ SdApiContractDiagnosticList,
3337
+ SdApiContractNodeEditor,
3338
+ SdApiContractNodeDrawer,
3339
+ SdApiContractRecordEditor,
3340
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: "@let _draft = draft();\n@let _readonly = readonly();\n@let _autoId = autoId();\n@let _diagnostics = diagnostics();\n@let _activeStep = activeStep();\n@let _requestSuggestions = requestSuggestions();\n@let _responseFieldOptions = responseFieldOptions();\n\n<!-- why: contract t\u1EEB ngo\u00E0i c\u00F3 th\u1EC3 thi\u1EBFu h\u1EB3n m\u1ED9t t\u1EA7ng. M\u1ECDi nh\u00E1nh \u0111\u1ECDc `_draft.<section>` \u0111\u1EC1u ph\u1EA3i\n g\u00E1c \u2014 builder hi\u1EC3n th\u1ECB contract sai k\u00E8m ch\u1EA9n \u0111o\u00E1n ch\u1EE9 kh\u00F4ng t\u1EF1 v\u00E1, v\u00E0 c\u0169ng kh\u00F4ng \u0111\u01B0\u1EE3c n\u1ED5. -->\n@if (!_draft) {\n <div class=\"sd-acb__empty\">\n <sd-icon name=\"description\" size=\"lg\"></sd-icon>\n <p>{{ 'core.component.api-contract-builder.empty.contract' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"fill\"\n color=\"primary\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-create' : undefined\"\n [title]=\"'core.component.api-contract-builder.create-contract' | sdTranslate\"\n (click)=\"createContract()\"></sd-button>\n }\n </div>\n} @else if (isView()) {\n <section class=\"sd-acb__view\">\n <dl class=\"sd-acb__summary\">\n <dt>{{ 'core.component.api-contract-builder.contract.code' | sdTranslate }}</dt>\n <dd data-field=\"code\">{{ _draft.code }}</dd>\n <dt>{{ 'core.component.api-contract-builder.contract.name' | sdTranslate }}</dt>\n <dd data-field=\"name\">{{ _draft.name }}</dd>\n <dt>{{ 'core.component.api-contract-builder.request.method' | sdTranslate }}</dt>\n <dd data-field=\"method\">{{ _draft.req?.method }}</dd>\n <dt>{{ 'core.component.api-contract-builder.request.url' | sdTranslate }}</dt>\n <dd data-field=\"url\">{{ _draft.req?.url }}</dd>\n </dl>\n\n <sd-api-contract-diagnostic-list\n [diagnostics]=\"_diagnostics\"\n [autoId]=\"_autoId ? _autoId + '-diagnostics' : undefined\"></sd-api-contract-diagnostic-list>\n\n <sd-code-editor language=\"json\" [viewed]=\"true\" maxHeight=\"420px\" [model]=\"json()\"></sd-code-editor>\n </section>\n} @else {\n <nav class=\"sd-acb__steps\" [attr.aria-label]=\"'core.component.api-contract-builder.steps' | sdTranslate\">\n @for (step of steps; track step.index) {\n <button\n type=\"button\"\n class=\"sd-acb__step\"\n [class.sd-acb__step--active]=\"_activeStep === step.index\"\n [attr.aria-current]=\"_activeStep === step.index ? 'step' : null\"\n [attr.data-autoId]=\"_autoId ? _autoId + '-step-' + step.key : null\"\n (click)=\"goToStep(step.index)\">\n <span class=\"sd-acb__step-index\">{{ step.index + 1 }}</span>\n <span class=\"sd-acb__step-label\">{{ step.label }}</span>\n </button>\n }\n </nav>\n\n <div class=\"sd-acb__body\">\n @switch (_activeStep) {\n @case (0) {\n <section class=\"sd-acb__section\" data-step=\"general\">\n <div class=\"sd-acb__grid\">\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-code' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.code' | sdTranslate\"\n [model]=\"_draft.code\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('code', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-name' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.name' | sdTranslate\"\n [model]=\"_draft.name\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('name', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-description' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.description' | sdTranslate\"\n [model]=\"_draft.description ?? ''\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setText('description', $event)\"></sd-input>\n <sd-input\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-version' : undefined\"\n [label]=\"'core.component.api-contract-builder.contract.version' | sdTranslate\"\n [model]=\"contractVersion\"\n [readonly]=\"true\"\n [disabled]=\"true\"></sd-input>\n </div>\n </section>\n }\n\n @case (1) {\n <section class=\"sd-acb__section\" data-step=\"input\">\n @if (_draft.input?.schema; as _inputSchema) {\n <sd-api-contract-node-editor\n [node]=\"$any(_inputSchema)\"\n basePath=\"input.schema\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-input' : undefined\"\n (nodeChange)=\"setInputSchema($event)\"\n (editRequest)=\"openNodeDrawer('input.schema', $event)\"></sd-api-contract-node-editor>\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (2) {\n <section class=\"sd-acb__section\" data-step=\"request\">\n @if (_draft.req; as _req) {\n <div class=\"sd-acb__grid\">\n <sd-select\n size=\"sm\"\n hideInlineError\n [clearable]=\"false\"\n [autoId]=\"_autoId ? _autoId + '-method' : undefined\"\n [label]=\"'core.component.api-contract-builder.request.method' | sdTranslate\"\n [items]=\"methodOptions\"\n valueField=\"value\"\n displayField=\"label\"\n [model]=\"_req.method\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setMethod($event)\"></sd-select>\n <sd-input\n class=\"sd-acb__url\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-url' : undefined\"\n [label]=\"'core.component.api-contract-builder.request.url' | sdTranslate\"\n [placeholder]=\"urlPlaceholder\"\n [model]=\"_req.url\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setUrl($event)\"></sd-input>\n </div>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.path' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.path)\"\n basePath=\"req.path\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.path' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-path' : undefined\"\n (recordChange)=\"setRequestRecord('path', $event)\"\n (editRequest)=\"openNodeDrawer('req.path', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.query' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.query)\"\n basePath=\"req.query\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.query' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-query' : undefined\"\n (recordChange)=\"setRequestRecord('query', $event)\"\n (editRequest)=\"openNodeDrawer('req.query', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.headers' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_req.headers)\"\n basePath=\"req.headers\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.headers' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-headers' : undefined\"\n (recordChange)=\"setRequestRecord('headers', $event)\"\n (editRequest)=\"openNodeDrawer('req.headers', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.request.body' | sdTranslate }}</h4>\n @if (_req.body) {\n <sd-api-contract-node-editor\n [node]=\"$any(_req.body)\"\n basePath=\"req.body\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-body' : undefined\"\n (nodeChange)=\"setRequestBody($event)\"\n (editRequest)=\"openNodeDrawer('req.body', $event)\"></sd-api-contract-node-editor>\n @if (!_readonly) {\n <sd-button\n type=\"text\"\n color=\"error\"\n size=\"sm\"\n prefixIcon=\"delete\"\n [autoId]=\"_autoId ? _autoId + '-remove-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.request.remove-body' | sdTranslate\"\n (click)=\"removeRequestBody()\"></sd-button>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.body' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.request.add-body' | sdTranslate\"\n (click)=\"addRequestBody()\"></sd-button>\n }\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (3) {\n <section class=\"sd-acb__section\" data-step=\"response\">\n @if (_draft.res; as _res) {\n <sd-input\n class=\"sd-acb__status\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-status' : undefined\"\n [label]=\"'core.component.api-contract-builder.response.status' | sdTranslate\"\n [helperText]=\"'core.component.api-contract-builder.response.status-hint' | sdTranslate\"\n [model]=\"statusText()\"\n [disabled]=\"_readonly\"\n (sdChange)=\"setStatus($event)\"></sd-input>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.response.headers' | sdTranslate }}</h4>\n <sd-api-contract-record-editor\n [record]=\"$any(_res.headers)\"\n basePath=\"res.headers\"\n [disabled]=\"_readonly\"\n [emptyLabel]=\"'core.component.api-contract-builder.empty.headers' | sdTranslate\"\n [autoId]=\"_autoId ? _autoId + '-res-headers' : undefined\"\n (recordChange)=\"setResponseHeaders($event)\"\n (editRequest)=\"openNodeDrawer('res.headers', $event)\"></sd-api-contract-record-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.response.body' | sdTranslate }}</h4>\n @if (_res.body) {\n <sd-api-contract-node-editor\n [node]=\"$any(_res.body)\"\n basePath=\"res.body\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-res-body' : undefined\"\n (nodeChange)=\"setResponseBody($event)\"\n (editRequest)=\"openNodeDrawer('res.body', $event)\"></sd-api-contract-node-editor>\n @if (!_readonly) {\n <sd-button\n type=\"text\"\n color=\"error\"\n size=\"sm\"\n prefixIcon=\"delete\"\n [autoId]=\"_autoId ? _autoId + '-remove-res-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.response.remove-body' | sdTranslate\"\n (click)=\"removeResponseBody()\"></sd-button>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.body' | sdTranslate }}</p>\n @if (!_readonly) {\n <sd-button\n type=\"light\"\n size=\"sm\"\n prefixIcon=\"add\"\n [autoId]=\"_autoId ? _autoId + '-add-res-body' : undefined\"\n [title]=\"'core.component.api-contract-builder.response.add-body' | sdTranslate\"\n (click)=\"addResponseBody()\"></sd-button>\n }\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @case (4) {\n <section class=\"sd-acb__section\" data-step=\"output\">\n @if (_draft.output?.schema; as _outputSchema) {\n @if (!_readonly && _responseFieldOptions.length) {\n <sd-select\n class=\"sd-acb__adopt\"\n size=\"sm\"\n hideInlineError\n [autoId]=\"_autoId ? _autoId + '-use-response' : undefined\"\n [label]=\"'core.component.api-contract-builder.output.use-response' | sdTranslate\"\n [items]=\"_responseFieldOptions\"\n valueField=\"value\"\n displayField=\"label\"\n [(model)]=\"responseFieldToken\"\n (sdChange)=\"useResponseFieldAsOutput($event)\"></sd-select>\n }\n\n <sd-api-contract-node-editor\n [node]=\"$any(_outputSchema)\"\n basePath=\"output.schema\"\n [disabled]=\"_readonly\"\n [autoId]=\"_autoId ? _autoId + '-output' : undefined\"\n (nodeChange)=\"setOutputSchema($event)\"\n (editRequest)=\"openNodeDrawer('output.schema', $event)\"></sd-api-contract-node-editor>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.output.fields' | sdTranslate }}</h4>\n @let _outputFields = outputFields();\n @if (_outputFields.length) {\n <ul class=\"sd-acb__fields\" [attr.data-autoId]=\"_autoId ? _autoId + '-output-fields' : null\">\n @for (field of _outputFields; track field.path) {\n <li class=\"sd-acb__field\">\n <code>{{ field.path }}</code>\n <span class=\"sd-acb__field-type\">{{ field.type }}</span>\n @if (field.required === true) {\n <span class=\"sd-acb__field-flag\">{{ 'core.component.api-contract-builder.field.required' | sdTranslate }}</span>\n }\n </li>\n }\n </ul>\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.output.no-fields' | sdTranslate }}</p>\n }\n } @else {\n <p class=\"sd-acb__empty-text\">{{ 'core.component.api-contract-builder.empty.section' | sdTranslate }}</p>\n }\n </section>\n }\n\n @default {\n <section class=\"sd-acb__section\" data-step=\"review\">\n <sd-api-contract-diagnostic-list\n [diagnostics]=\"_diagnostics\"\n [autoId]=\"_autoId ? _autoId + '-diagnostics' : undefined\"\n (navigate)=\"goToDiagnostic($event)\"></sd-api-contract-diagnostic-list>\n\n <h4 class=\"sd-acb__heading\">{{ 'core.component.api-contract-builder.review.json' | sdTranslate }}</h4>\n <sd-code-editor\n language=\"json\"\n maxHeight=\"480px\"\n [viewed]=\"disabled() || mode() === 'view'\"\n [model]=\"json()\"\n (modelChange)=\"applyPastedJson($event)\"></sd-code-editor>\n </section>\n }\n }\n </div>\n\n <!-- why M\u1ED8T instance duy nh\u1EA5t, \u0111\u1EB7t ngo\u00E0i @switch: m\u1ED7i layer m\u1ED9t drawer s\u1EBD l\u00E0 m\u1ED7i layer m\u1ED9t draft, v\u00E0\n \u0111\u1ED5i step gi\u1EEFa l\u00FAc \u0111ang s\u1EEDa s\u1EBD destroy drawer c\u00F9ng v\u1EDBi thay \u0111\u1ED5i ch\u01B0a l\u01B0u. -->\n <sd-api-contract-node-drawer\n #nodeDrawer\n [layer]=\"drawerLayer()\"\n [allowTransform]=\"drawerAllowsTransform()\"\n [suggestions]=\"drawerSuggestions()\"\n [autoId]=\"_autoId ? _autoId + '-node-drawer' : undefined\"\n (nodeCommit)=\"applyNodeCommit($event)\"></sd-api-contract-node-drawer>\n}\n", styles: [":host{display:block;width:100%}.sd-acb__empty{display:flex;flex-direction:column;align-items:center;gap:10px;padding:32px 16px;border:1px dashed var(--sd-border-color, #e6e6e6);border-radius:8px;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__empty p{margin:0;font-size:13px}.sd-acb__steps{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px;border-bottom:1px solid var(--sd-border-color, #e6e6e6);padding-bottom:8px}.sd-acb__step{display:inline-flex;align-items:center;gap:8px;padding:6px 12px;border:1px solid transparent;border-radius:999px;background:transparent;color:var(--sd-text-secondary, #6b6b6b);font:inherit;font-size:13px;cursor:pointer}.sd-acb__step:hover,.sd-acb__step:focus-visible{background:var(--sd-surface-muted, #f3f5f8)}.sd-acb__step--active{border-color:var(--sd-primary, #005cbb);background:var(--sd-primary-light, #e5efff);color:var(--sd-primary-dark, #00419e);font-weight:600}.sd-acb__step-index{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;background:var(--sd-surface-muted, #f3f5f8);font-size:11px;font-weight:700}.sd-acb__step--active .sd-acb__step-index{background:var(--sd-primary, #005cbb);color:#fff}.sd-acb__body{display:block}.sd-acb__section{display:flex;flex-direction:column;gap:12px}.sd-acb__grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;align-items:start}.sd-acb__url,.sd-acb__status{grid-column:span 2}.sd-acb__adopt{max-width:420px}.sd-acb__heading{margin:8px 0 0;font-size:13px;font-weight:600;color:var(--sd-text, #1f2937)}.sd-acb__empty-text{margin:0;font-size:12px;font-style:italic;color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__fields{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:8px}.sd-acb__field{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border:1px solid var(--sd-border-color, #e6e6e6);border-radius:6px;background:var(--sd-surface-muted, #f3f5f8);font-size:12px}.sd-acb__field-type{color:var(--sd-primary, #005cbb);font-weight:600}.sd-acb__field-flag{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__view{display:flex;flex-direction:column;gap:16px}.sd-acb__summary{display:grid;grid-template-columns:minmax(120px,max-content) 1fr;gap:6px 16px;margin:0;font-size:13px}.sd-acb__summary dt{color:var(--sd-text-secondary, #6b6b6b)}.sd-acb__summary dd{margin:0;font-weight:600;overflow-wrap:anywhere}@media(max-width:768px){.sd-acb__url,.sd-acb__status{grid-column:span 1}.sd-acb__step-label{display:none}}\n"] }]
3341
+ }], ctorParameters: () => [], propDecorators: { autoIdAttr: [{
3342
+ type: HostBinding,
3343
+ args: ['attr.data-autoId']
3344
+ }] } });
3345
+ /**
3346
+ * Places a committed node back under a schema root's `properties`.
3347
+ *
3348
+ * why phân biệt add với overwrite: `addSdApiContractProperty` CỐ Ý không ghi đè key đã tồn tại (nó bảo
3349
+ * caller phải tự dedupe). Sửa một field đang có thì phải đi qua `setSdApiContractNodeAt`, nếu không
3350
+ * mọi lần Lưu trên field cũ sẽ im lặng không có tác dụng.
3351
+ */
3352
+ function commitIntoProperties(root, pointer, previous, commit) {
3353
+ // why đi qua pointer: danh sách nói cho ta biết nó đang đọc `properties` hay `items.properties`.
3354
+ // Ghi thẳng vào root sẽ tạo ra một nhánh `properties` song song mà tầng `array` không bao giờ đọc.
3355
+ const container = getSdApiContractNodeAt(root, pointer);
3356
+ if (!container)
3357
+ return root;
3358
+ let next = container;
3359
+ if (previous !== null && previous !== commit.name) {
3360
+ next = renameSdApiContractProperty(next, previous, commit.name);
3361
+ }
3362
+ const exists = next.properties && Object.prototype.hasOwnProperty.call(next.properties, commit.name);
3363
+ next = exists
3364
+ ? setSdApiContractNodeAt(next, ['properties', commit.name], commit.node)
3365
+ : addSdApiContractProperty(next, commit.name, commit.node);
3366
+ return setSdApiContractNodeAt(root, pointer, next);
3367
+ }
3368
+ /** Places a committed node back into a keyed record, keeping the author's key order. */
3369
+ function commitIntoRecord(record, previous, commit) {
3370
+ if (previous !== null && previous !== commit.name) {
3371
+ const renamed = sdApiContractRecordRename(record, previous, commit.name);
3372
+ return sdApiContractRecordSet(renamed, commit.name, commit.node);
3373
+ }
3374
+ return sdApiContractRecordSet(record, commit.name, commit.node);
3375
+ }
3376
+ /** Recursively rebuilds a response subtree as a mapped output subtree. */
3377
+ function adoptResponseNode(node, segments) {
3378
+ if (node.type === 'object' && node.properties) {
3379
+ const properties = {};
3380
+ for (const key of Object.keys(node.properties)) {
3381
+ properties[key] = adoptResponseNode(node.properties[key], [...segments, key]);
3382
+ }
3383
+ const next = { type: 'object', properties };
3384
+ if (node.required !== undefined)
3385
+ next.required = node.required;
3386
+ if (node.label !== undefined)
3387
+ next.label = node.label;
3388
+ if (node.description !== undefined)
3389
+ next.description = node.description;
3390
+ return next;
3391
+ }
3392
+ return { ...cloneSdApiContractNode(node), source: formatSdApiContractExpression('res', segments) };
3393
+ }
3394
+ function stepForPath(path) {
3395
+ if (path.startsWith('input.'))
3396
+ return STEP_INPUT;
3397
+ if (path.startsWith('req'))
3398
+ return STEP_REQUEST;
3399
+ if (path.startsWith('res'))
3400
+ return STEP_RESPONSE;
3401
+ if (path.startsWith('output'))
3402
+ return STEP_OUTPUT;
3403
+ return STEP_GENERAL;
3404
+ }
3405
+
3406
+ /**
3407
+ * Generated bundle index. Do not edit.
3408
+ */
3409
+
3410
+ export { SD_API_CONTRACT_ALLOWED_ROOTS, SD_API_CONTRACT_CONFIGURATION, SD_API_CONTRACT_DATA_TYPES, SD_API_CONTRACT_EMPTY_CONFIGURATION, SD_API_CONTRACT_EXPRESSION_ROOTS, SD_API_CONTRACT_HTTP_METHODS, SD_API_CONTRACT_SAMPLE_ENVIRONMENT, SD_API_CONTRACT_SCALAR_DATA_TYPES, SD_API_CONTRACT_VERSION, SdApiContractBuilder, addSdApiContractProperty, changeSdApiContractNodeType, cloneSdApiContract, cloneSdApiContractNode, createSdApiContractNode, extractSdApiContractReferences, formatSdApiContractExpression, formatSdApiContractPointer, getSdApiContractNodeAt, listSdApiContractResponseFields, listSdApiContractSchemaFields, parseSdApiContractTemplate, parseSdApiContractUrlPlaceholders, provideSdApiContract, removeSdApiContractProperty, renameSdApiContractProperty, resolveSdApiContractConfiguration, resolveSdApiContractResponsePath, resolveSdApiContractSchemaPath, sdApiContractCreateSample, sdApiContractInvalidSample, sdApiContractRecordRemove, sdApiContractRecordRename, sdApiContractRecordSet, sdApiContractSearchSample, sdIsApiContractDataType, sdIsApiContractHttpMethod, sdIsApiContractScalarDataType, sdIsApiContractTemporalDataType, serializeSdApiContract, setSdApiContractNodeAt, validateSdApiContract };
3411
+ //# sourceMappingURL=sdcorejs-angular-components-api-contract-builder.mjs.map