@oneuptime/common 12.0.9 → 12.0.11

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 (262) hide show
  1. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  2. package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
  3. package/Models/DatabaseModels/RunnerJob.ts +17 -2
  4. package/Server/API/LlmProviderAPI.ts +8 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  7. package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
  8. package/Server/Services/NetworkDeviceLinkService.ts +338 -2
  9. package/Server/Services/NetworkDeviceService.ts +265 -12
  10. package/Server/Services/RunnerJobService.ts +24 -1
  11. package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
  12. package/Server/Types/Database/JSONColumnQuery.ts +42 -4
  13. package/Server/Utils/LLM/LLMService.ts +108 -19
  14. package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
  15. package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
  16. package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
  17. package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
  18. package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
  19. package/Server/Utils/SSRFProtection.ts +5 -4
  20. package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
  21. package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
  22. package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
  23. package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
  24. package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
  25. package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
  26. package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
  27. package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
  28. package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
  29. package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
  30. package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
  31. package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
  32. package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
  33. package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
  34. package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
  35. package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
  36. package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
  37. package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
  38. package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
  39. package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
  40. package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
  41. package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
  42. package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
  43. package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
  44. package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
  45. package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
  46. package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
  47. package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
  48. package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
  49. package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
  50. package/Tests/Types/API/URLTelScheme.test.ts +261 -0
  51. package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
  52. package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
  53. package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
  54. package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
  55. package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
  56. package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
  57. package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
  58. package/Tests/UI/Components/Button.test.tsx +279 -0
  59. package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
  60. package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
  61. package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
  62. package/Tests/UI/Components/Icon.test.tsx +57 -0
  63. package/Tests/UI/Components/List.test.tsx +20 -3
  64. package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
  65. package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
  66. package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
  67. package/Tests/UI/Components/Pagination.test.tsx +721 -146
  68. package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
  69. package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
  70. package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
  71. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
  72. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
  73. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
  74. package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
  75. package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
  76. package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
  77. package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
  78. package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
  79. package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
  80. package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
  81. package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
  82. package/Types/API/Protocol.ts +9 -0
  83. package/Types/API/URL.ts +286 -20
  84. package/Types/Billing/PayAsYouGoPricing.ts +47 -0
  85. package/Types/CustomField/CustomFieldType.ts +28 -0
  86. package/Types/Monitor/MonitorType.ts +11 -0
  87. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
  88. package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
  89. package/Types/Runbook/RunbookStepType.ts +23 -0
  90. package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
  91. package/UI/Components/Button/Button.tsx +14 -22
  92. package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
  93. package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
  94. package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
  95. package/UI/Components/Charts/Line/LineChart.tsx +4 -2
  96. package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
  97. package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
  98. package/UI/Components/Dictionary/Dictionary.tsx +4 -39
  99. package/UI/Components/Icon/Icon.tsx +1 -1
  100. package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
  101. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
  102. package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
  103. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
  104. package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
  105. package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
  106. package/UI/Components/Pagination/Pagination.tsx +373 -328
  107. package/UI/Components/Pagination/PaginationUtil.ts +263 -0
  108. package/UI/Components/Pagination/Screenshots/README.md +15 -0
  109. package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
  110. package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
  111. package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
  112. package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
  113. package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
  114. package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
  115. package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
  116. package/UI/Components/TextArea/TextArea.tsx +3 -0
  117. package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
  118. package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
  119. package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
  120. package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
  121. package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
  122. package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
  123. package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
  124. package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
  125. package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
  126. package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
  127. package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
  128. package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
  129. package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
  130. package/Utils/API.ts +243 -36
  131. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
  132. package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
  133. package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
  134. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  135. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  136. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
  137. package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
  138. package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
  139. package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
  140. package/build/dist/Server/API/LlmProviderAPI.js +9 -1
  141. package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
  142. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
  143. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
  144. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  145. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  146. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
  147. package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
  148. package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
  149. package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
  150. package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
  151. package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
  152. package/build/dist/Server/Services/RunnerJobService.js +20 -2
  153. package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
  154. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
  155. package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
  156. package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
  157. package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
  158. package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
  159. package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
  160. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
  161. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  162. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
  163. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  164. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
  165. package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
  166. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
  167. package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
  168. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
  169. package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
  170. package/build/dist/Server/Utils/SSRFProtection.js +5 -4
  171. package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
  172. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
  173. package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
  174. package/build/dist/Types/API/Protocol.js +9 -0
  175. package/build/dist/Types/API/Protocol.js.map +1 -1
  176. package/build/dist/Types/API/URL.js +241 -18
  177. package/build/dist/Types/API/URL.js.map +1 -1
  178. package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
  179. package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
  180. package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
  181. package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
  182. package/build/dist/Types/Monitor/MonitorType.js +10 -0
  183. package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
  184. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
  185. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
  186. package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
  187. package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
  188. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
  189. package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
  190. package/build/dist/UI/Components/Button/Button.js +10 -16
  191. package/build/dist/UI/Components/Button/Button.js.map +1 -1
  192. package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
  193. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  194. package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
  195. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  196. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
  197. package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
  198. package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
  199. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  200. package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
  201. package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
  202. package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
  203. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  204. package/build/dist/UI/Components/Icon/Icon.js +1 -1
  205. package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
  206. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
  207. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  208. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
  209. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
  210. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
  211. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  212. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
  213. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  214. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
  215. package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
  216. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
  217. package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
  218. package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
  219. package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
  220. package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
  221. package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
  222. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
  223. package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
  224. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
  225. package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
  226. package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
  227. package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
  228. package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
  229. package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
  230. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
  231. package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
  232. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
  233. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
  234. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
  235. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
  236. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
  237. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
  238. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
  239. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
  240. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
  241. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
  242. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
  243. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
  244. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
  245. package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
  246. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
  247. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
  248. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
  249. package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
  250. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
  251. package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
  252. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
  253. package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
  254. package/build/dist/Utils/API.js +175 -34
  255. package/build/dist/Utils/API.js.map +1 -1
  256. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
  257. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  258. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
  259. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  260. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
  261. package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
  262. package/package.json +1 -1
@@ -0,0 +1,163 @@
1
+ /*
2
+ * The record body: every field this step writes, as a form.
3
+ *
4
+ * Fully controlled - the rows live in ModelColumnEditor, so what is on screen
5
+ * and what is stored on the argument can never drift apart.
6
+ */
7
+
8
+ import IconProp from "../../../../Types/Icon/IconProp";
9
+ import Icon from "../../Icon/Icon";
10
+ import {
11
+ ModelSchemaColumn,
12
+ findColumn,
13
+ requiredWritableColumns,
14
+ } from "../ModelSchema";
15
+ import AddColumnDropdown from "./AddColumnDropdown";
16
+ import { isOfferableColumn } from "./ColumnControl";
17
+ import ColumnFieldRow from "./ColumnFieldRow";
18
+ import { ModelColumnRow, makeColumnRow } from "./ColumnRow";
19
+ import React, { FunctionComponent, ReactElement, useState } from "react";
20
+
21
+ export interface ComponentProps {
22
+ rows: Array<ModelColumnRow>;
23
+ columns: Array<ModelSchemaColumn>;
24
+ suggestions?: Array<string> | undefined;
25
+ onChange: (rows: Array<ModelColumnRow>) => void;
26
+ }
27
+
28
+ const ModelRecordForm: FunctionComponent<ComponentProps> = (
29
+ props: ComponentProps,
30
+ ): ReactElement => {
31
+ /*
32
+ * The row just added from the picker, so its value control can take focus.
33
+ * Without it the picker remounts to clear its own selection and focus falls
34
+ * to the document body, which for a keyboard user means starting again from
35
+ * the top of the panel.
36
+ */
37
+ const [justAddedKey, setJustAddedKey] = useState<string>("");
38
+
39
+ const requiredColumnIds: Array<string> = requiredWritableColumns(
40
+ props.columns,
41
+ ).map((column: ModelSchemaColumn) => {
42
+ return column.id;
43
+ });
44
+
45
+ const usedColumnIds: Array<string> = props.rows.map((row: ModelColumnRow) => {
46
+ return row.columnId;
47
+ });
48
+
49
+ const offerableColumns: Array<ModelSchemaColumn> = props.columns.filter(
50
+ (column: ModelSchemaColumn) => {
51
+ return isOfferableColumn(column) && !usedColumnIds.includes(column.id);
52
+ },
53
+ );
54
+
55
+ /*
56
+ * Named so the builder is not left wondering where a column went. Relations
57
+ * and blobs cannot be a single box, and saying which ones is kinder than
58
+ * silently offering a shorter list than the model has.
59
+ */
60
+ const unofferableColumnTitles: Array<string> = props.columns
61
+ .filter((column: ModelSchemaColumn) => {
62
+ return !isOfferableColumn(column) && !column.isTenantColumn;
63
+ })
64
+ .map((column: ModelSchemaColumn) => {
65
+ return column.title;
66
+ });
67
+
68
+ const knownColumnIds: Array<string> = props.columns.map(
69
+ (column: ModelSchemaColumn) => {
70
+ return column.id;
71
+ },
72
+ );
73
+
74
+ type ReplaceRowFunction = (index: number, row: ModelColumnRow) => void;
75
+
76
+ const replaceRow: ReplaceRowFunction = (
77
+ index: number,
78
+ row: ModelColumnRow,
79
+ ): void => {
80
+ const next: Array<ModelColumnRow> = [...props.rows];
81
+ next[index] = row;
82
+ props.onChange(next);
83
+ };
84
+
85
+ return (
86
+ <div>
87
+ <div className="overflow-hidden rounded-md border border-gray-200 bg-white">
88
+ {props.rows.length === 0 ? (
89
+ <div className="px-6 py-10 text-center">
90
+ <Icon
91
+ icon={IconProp.Database}
92
+ className="mx-auto h-6 w-6 text-gray-300"
93
+ />
94
+ <p className="mt-2 text-sm font-medium text-gray-700">
95
+ No fields set yet
96
+ </p>
97
+ <p className="mt-1 text-xs text-gray-500">
98
+ Pick a field below to start building this record.
99
+ </p>
100
+ </div>
101
+ ) : (
102
+ <div className="divide-y divide-gray-100">
103
+ {props.rows.map((row: ModelColumnRow, index: number) => {
104
+ return (
105
+ <ColumnFieldRow
106
+ key={row.key}
107
+ row={row}
108
+ column={findColumn(props.columns, row.columnId)}
109
+ knownColumnIds={knownColumnIds}
110
+ isRequired={requiredColumnIds.includes(row.columnId)}
111
+ suggestions={props.suggestions}
112
+ autoFocus={row.key === justAddedKey}
113
+ onChange={(nextRow: ModelColumnRow) => {
114
+ replaceRow(index, nextRow);
115
+ }}
116
+ onRemove={() => {
117
+ props.onChange(
118
+ props.rows.filter((_row: ModelColumnRow, i: number) => {
119
+ return i !== index;
120
+ }),
121
+ );
122
+ }}
123
+ />
124
+ );
125
+ })}
126
+ </div>
127
+ )}
128
+
129
+ <div className="border-t border-gray-100 bg-gray-50/40 px-3 py-2.5">
130
+ <AddColumnDropdown
131
+ columns={offerableColumns}
132
+ requiredColumnIds={requiredColumnIds}
133
+ placeholder="Add a field..."
134
+ allowCustomColumn={true}
135
+ dataTestId="model-column-add"
136
+ onAdd={(columnId: string) => {
137
+ const added: ModelColumnRow = makeColumnRow({
138
+ columnId: columnId,
139
+ });
140
+
141
+ setJustAddedKey(added.key);
142
+ props.onChange([...props.rows, added]);
143
+ }}
144
+ />
145
+ </div>
146
+ </div>
147
+
148
+ {unofferableColumnTitles.length > 0 && (
149
+ <p className="mt-1.5 text-xs text-gray-400">
150
+ {unofferableColumnTitles.slice(0, 3).join(", ")}
151
+ {unofferableColumnTitles.length > 3
152
+ ? ` and ${unofferableColumnTitles.length - 3} other field${
153
+ unofferableColumnTitles.length - 3 === 1 ? "" : "s"
154
+ }`
155
+ : ""}{" "}
156
+ can only be set with Edit as JSON.
157
+ </p>
158
+ )}
159
+ </div>
160
+ );
161
+ };
162
+
163
+ export default ModelRecordForm;
@@ -1,24 +1,29 @@
1
1
  /*
2
- * A row-based editor for the two workflow arguments that are keyed on a
2
+ * A schema-driven editor for the two workflow arguments that are keyed on a
3
3
  * model's columns: the Query that picks which records a component acts on,
4
4
  * and the record of values a create/update writes.
5
5
  *
6
6
  * Both were bare JSON editors, with the column names — and, for a query, the
7
- * operator syntax — left entirely to memory. The description on those
8
- * arguments had to carry a hint about "_id" versus "id" precisely because
9
- * nothing else told the builder what the columns were called.
7
+ * operator syntax — left entirely to memory. The first attempt at fixing that
8
+ * borrowed the generic key/value row editor, which asked for a column name as
9
+ * free text and then asked, in a dropdown next to it, whether the value was
10
+ * Text, Number or Boolean. Both questions were already answered:
11
+ * /model-schema/:tableName says that `color` is a Color and `name` is a Name,
12
+ * so the builder could only agree with the schema or be wrong.
10
13
  *
11
- * Everything needed to do better already existed: /model-schema/:tableName
12
- * knows the columns, and DictionaryForm's operator mode emits exactly the
13
- * {_type, value} shape that JSONFunctions.deserialize hydrates on the server
14
- * (FindManyBaseModel and friends). This wires the two together, and keeps a
15
- * JSON escape hatch for the values rows cannot express.
14
+ * So the rows are generated from the schema instead. A record is a form of the
15
+ * model's own fields, with their real titles and descriptions; a query is a
16
+ * list of conditions whose comparisons are filtered by the column's type. The
17
+ * JSON escape hatch stays for the values rows cannot express, and now it
18
+ * re-reads what was typed in it rather than discarding it.
16
19
  */
17
20
 
18
21
  import CodeEditor from "../CodeEditor/CodeEditor";
19
22
  import ComponentLoader from "../ComponentLoader/ComponentLoader";
20
- import DictionaryForm, { ValueType } from "../Dictionary/Dictionary";
21
- import { DictionaryEntryValue } from "../Dictionary/DictionaryFilterOperator";
23
+ import {
24
+ DictionaryEntryValue,
25
+ getOperatorOption,
26
+ } from "../Dictionary/DictionaryFilterOperator";
22
27
  import Icon from "../Icon/Icon";
23
28
  import {
24
29
  ModelSchemaAccess,
@@ -28,6 +33,14 @@ import {
28
33
  requiredWritableColumns,
29
34
  useModelSchema,
30
35
  } from "./ModelSchema";
36
+ import { ModelColumnRow } from "./ColumnEditor/ColumnRow";
37
+ import {
38
+ rowsFromParsedValue,
39
+ seedRequiredRows,
40
+ serializeColumnRows,
41
+ } from "./ColumnEditor/ColumnRowSerialization";
42
+ import ModelQueryBuilder from "./ColumnEditor/ModelQueryBuilder";
43
+ import ModelRecordForm from "./ColumnEditor/ModelRecordForm";
31
44
  import CodeType from "../../../Types/Code/CodeType";
32
45
  import Dictionary from "../../../Types/Dictionary";
33
46
  import IconProp from "../../../Types/Icon/IconProp";
@@ -36,6 +49,7 @@ import { maskTemplateExpressions } from "../../../Types/Workflow/TemplateSyntax"
36
49
  import React, {
37
50
  FunctionComponent,
38
51
  ReactElement,
52
+ useEffect,
39
53
  useMemo,
40
54
  useState,
41
55
  } from "react";
@@ -47,6 +61,18 @@ export enum ModelColumnEditorMode {
47
61
  Record = "Record",
48
62
  }
49
63
 
64
+ /**
65
+ * Whether the record being edited creates a row or updates one.
66
+ *
67
+ * It decides one thing: whether the editor opens with a blank row for every
68
+ * column a create must supply. An update legitimately writes a single column,
69
+ * so seeding those rows there presents six fields where the builder wanted one.
70
+ */
71
+ export enum RecordIntent {
72
+ Create = "Create",
73
+ Update = "Update",
74
+ }
75
+
50
76
  export interface ComponentProps {
51
77
  tableName: string;
52
78
  mode: ModelColumnEditorMode;
@@ -55,6 +81,8 @@ export interface ComponentProps {
55
81
  error?: string | undefined;
56
82
  placeholder?: string | undefined;
57
83
  tabIndex?: number | undefined;
84
+ /** Record mode only; ignored for a query. Defaults to Create. */
85
+ recordIntent?: RecordIntent | undefined;
58
86
  /*
59
87
  * Offered in any row whose column has no suggestions of its own - the other
60
88
  * steps' return values and the workflow's variables. The row editor replaced
@@ -356,55 +384,30 @@ const ModelColumnEditor: FunctionComponent<ComponentProps> = (
356
384
  const [viewMode, setViewMode] = useState<ViewMode>("builder");
357
385
 
358
386
  /*
359
- * The row editor is uncontrolled after mount (DictionaryForm seeds itself
360
- * once), so this is computed rather than held in state. It only has to be
361
- * right on the render that first mounts DictionaryForm, which is the first
362
- * render after the schema resolves — the loader below returns early until
363
- * then.
387
+ * The rows are the editor's own state, and both bodies are fully controlled
388
+ * from here. Null until the schema resolves, because a row cannot be built
389
+ * before it is known what kind of column it is for.
364
390
  *
365
- * Record mode opens with a blank row for every column a create must be given
366
- * a value for. Seeding is additive and never overwrites what was stored, and
367
- * blank rows are dropped on the way back out (buildColumnValueJson), so
368
- * opening a saved workflow and saving it again cannot change what it does.
391
+ * Nothing in the seeding path calls props.onChange. That matters more than it
392
+ * looks: this component is mounted every time someone opens a step's
393
+ * settings, and a change fired during mount would rewrite the stored bytes of
394
+ * every workflow just for being looked at.
369
395
  */
370
- const initialRows: Dictionary<DictionaryEntryValue> = useMemo(() => {
371
- const stored: Dictionary<DictionaryEntryValue> = (initial.parsed ||
372
- {}) as Dictionary<DictionaryEntryValue>;
373
-
374
- if (isQuery || !schema.columns) {
375
- return stored;
376
- }
377
-
378
- const seeded: Dictionary<DictionaryEntryValue> = { ...stored };
379
-
380
- for (const column of requiredWritableColumns(schema.columns)) {
381
- if (seeded[column.id] === undefined) {
382
- seeded[column.id] = "";
383
- }
384
- }
385
-
386
- return seeded;
387
- }, [schema.columns, isQuery]);
396
+ const [rows, setRows] = useState<Array<ModelColumnRow> | null>(null);
388
397
 
389
398
  /*
390
- * The model's own example for each column, shown in the row it belongs to.
391
- * MonitorSecret.secretValue ships example: "sk_test_1234567890abcdefghijklmnop",
392
- * which answers "what goes in this box" better than any prose in the sidebar.
399
+ * Why the switch back from JSON is refused, when it is. Empty means it is
400
+ * allowed.
393
401
  */
394
- const valuePlaceholders: Dictionary<string> = useMemo(() => {
395
- const placeholders: Dictionary<string> = {};
396
-
397
- for (const column of schema.columns || []) {
398
- const hint: string | undefined = column.example || column.placeholder;
399
-
400
- if (hint) {
401
- placeholders[column.id] = hint;
402
- }
403
- }
404
-
405
- return placeholders;
406
- }, [schema.columns]);
402
+ const [returnToRowsBlockedBy, setReturnToRowsBlockedBy] = useState<
403
+ Array<string>
404
+ >([]);
407
405
 
406
+ /*
407
+ * Whether the value can be shown as rows at all. Read from the value as it
408
+ * was loaded, deliberately: the decision has to be stable, or the editor
409
+ * would change shape under the builder's hands as they type.
410
+ */
408
411
  const compatibility: CompatibilityResult = useMemo(() => {
409
412
  return classifyColumnValueCompatibility(
410
413
  initial.parsed,
@@ -414,13 +417,46 @@ const ModelColumnEditor: FunctionComponent<ComponentProps> = (
414
417
  );
415
418
  }, [schema.columns, props.mode]);
416
419
 
417
- const columnKeys: Array<string> = useMemo(() => {
418
- return (schema.columns || []).map((column: ModelSchemaColumn) => {
419
- return column.id;
420
- });
421
- }, [schema.columns]);
420
+ const columns: Array<ModelSchemaColumn> = schema.columns || [];
421
+
422
+ type BuildRowsFunction = (parsed: JSONObject | null) => Array<ModelColumnRow>;
422
423
 
423
- if (schema.isLoading) {
424
+ /*
425
+ * A create opens with a blank row for every column it must be given, so the
426
+ * shape of the record is visible before anything is typed. Seeded rows hold
427
+ * "" and are dropped on the way out, so an untouched Create One still reads
428
+ * as empty to form validation and to the graph linter.
429
+ *
430
+ * Used by the JSON view's return trip as well as by the first load - built
431
+ * only on the way in, those rows disappeared the first time anyone looked at
432
+ * the JSON, and a create that had been read once no longer showed what it
433
+ * needed.
434
+ */
435
+ const buildRows: BuildRowsFunction = (
436
+ parsed: JSONObject | null,
437
+ ): Array<ModelColumnRow> => {
438
+ const storedRows: Array<ModelColumnRow> = rowsFromParsedValue(
439
+ parsed,
440
+ columns,
441
+ props.mode,
442
+ );
443
+
444
+ const shouldSeed: boolean =
445
+ !isQuery &&
446
+ (props.recordIntent || RecordIntent.Create) === RecordIntent.Create;
447
+
448
+ return shouldSeed ? seedRequiredRows(storedRows, columns) : storedRows;
449
+ };
450
+
451
+ useEffect(() => {
452
+ if (schema.isLoading || rows !== null) {
453
+ return;
454
+ }
455
+
456
+ setRows(buildRows(initial.parsed));
457
+ }, [schema.isLoading, schema.columns]);
458
+
459
+ if (schema.isLoading || rows === null) {
424
460
  return <ComponentLoader />;
425
461
  }
426
462
 
@@ -431,30 +467,130 @@ const ModelColumnEditor: FunctionComponent<ComponentProps> = (
431
467
 
432
468
  const onJsonChange: OnJsonChangeFunction = (value: string): void => {
433
469
  setJsonText(value);
470
+ /*
471
+ * Any block on returning to rows was about the previous text, so it is
472
+ * re-decided when the switch is next attempted rather than left standing.
473
+ */
474
+ setReturnToRowsBlockedBy([]);
434
475
  props.onChange(value);
435
476
  };
436
477
 
437
- type OnRowsChangeFunction = (value: Dictionary<DictionaryEntryValue>) => void;
478
+ type OnRowsChangeFunction = (value: Array<ModelColumnRow>) => void;
438
479
 
439
480
  const onRowsChange: OnRowsChangeFunction = (
440
- value: Dictionary<DictionaryEntryValue>,
481
+ value: Array<ModelColumnRow>,
441
482
  ): void => {
442
- const asJson: string = buildColumnValueJson(value, props.mode);
483
+ setRows(value);
484
+
485
+ const asJson: string = serializeColumnRows(value, columns, props.mode);
443
486
  setJsonText(asJson);
444
487
  props.onChange(asJson);
445
488
  };
446
489
 
490
+ type ShowJsonFunction = () => void;
491
+
492
+ const showJson: ShowJsonFunction = (): void => {
493
+ /*
494
+ * What is shown is what is stored. Handing the code editor the text the
495
+ * argument held before the rows were edited would be the same data loss as
496
+ * the other direction, one step later.
497
+ */
498
+ setJsonText(serializeColumnRows(rows, columns, props.mode));
499
+ setViewMode("json");
500
+ };
501
+
502
+ type ShowRowsFunction = () => void;
503
+
504
+ const showRows: ShowRowsFunction = (): void => {
505
+ /*
506
+ * The JSON is re-read here, which is the fix for a real bug: the rows used
507
+ * to be seeded once and never again, so editing as JSON, adding a key and
508
+ * switching back silently dropped the key, and the next keystroke in any
509
+ * row overwrote the argument with the pre-edit content.
510
+ */
511
+ const edited: ParsedQuery = normalizeInitialValue(jsonText);
512
+ const editedCompatibility: CompatibilityResult =
513
+ classifyColumnValueCompatibility(
514
+ edited.parsed,
515
+ columns,
516
+ props.mode,
517
+ edited.hasTemplateExpressions,
518
+ );
519
+
520
+ if (!editedCompatibility.compatible) {
521
+ // Refused rather than silently losing the edit.
522
+ setReturnToRowsBlockedBy(editedCompatibility.reasons);
523
+ return;
524
+ }
525
+
526
+ setReturnToRowsBlockedBy([]);
527
+ setRows(buildRows(edited.parsed));
528
+ setViewMode("builder");
529
+ };
530
+
531
+ const filledRowCount: number = rows.filter((row: ModelColumnRow) => {
532
+ if (row.columnId === "") {
533
+ return false;
534
+ }
535
+
536
+ /*
537
+ * "is set" and "is not set" carry no value by design, so counting only rows
538
+ * that hold one reported a query with three real conditions as having none.
539
+ */
540
+ if (isQuery && getOperatorOption(row.operator).hidesValueInput) {
541
+ return true;
542
+ }
543
+
544
+ return row.text !== "" || row.values.length > 0;
545
+ }).length;
546
+
547
+ /*
548
+ * Recomputed from the rows on screen rather than from the value as it was
549
+ * loaded, so fixing the column name that caused the warning also clears it.
550
+ * Read from the frozen initial value, the "id isn't a known column" note
551
+ * stayed up after the builder had corrected it to "_id", which reads as the
552
+ * editor not having noticed.
553
+ *
554
+ * Only the non-blocking half is live. Whether the value can be rows at all is
555
+ * decided once, above.
556
+ */
557
+ const unknownColumnWarnings: Array<string> = columns.length
558
+ ? rows
559
+ .filter((row: ModelColumnRow) => {
560
+ return row.columnId !== "" && !findColumn(columns, row.columnId);
561
+ })
562
+ .map((row: ModelColumnRow) => {
563
+ return `"${row.columnId}" isn't a known column on this model.`;
564
+ })
565
+ : [];
566
+
567
+ const emptyRequiredCount: number = isQuery
568
+ ? 0
569
+ : requiredWritableColumns(columns).filter((column: ModelSchemaColumn) => {
570
+ return rows.some((row: ModelColumnRow) => {
571
+ return row.columnId === column.id && row.text === "";
572
+ });
573
+ }).length;
574
+
575
+ const noun: string = isQuery ? "condition" : "field";
576
+
447
577
  return (
448
578
  <div>
449
579
  {schema.error && (
450
- <p className="text-sm text-amber-600 mb-2">
451
- Couldn&apos;t load this model&apos;s columns ({schema.error}). You can
452
- still write {isQuery ? "the query" : "these fields"} as JSON.
453
- </p>
580
+ <div className="mb-2 flex items-start gap-2 rounded-md border border-red-200 bg-red-50/60 px-3 py-2.5">
581
+ <Icon
582
+ icon={IconProp.Alert}
583
+ className="mt-0.5 h-3.5 w-3.5 shrink-0 text-red-500"
584
+ />
585
+ <p className="text-sm text-red-700">
586
+ Couldn&apos;t load this model&apos;s columns ({schema.error}). You
587
+ can still write {isQuery ? "the query" : "these fields"} as JSON.
588
+ </p>
589
+ </div>
454
590
  )}
455
591
 
456
592
  {isLockedToJson && compatibility.reasons.length > 0 && (
457
- <div className="mb-2 rounded-md border border-amber-200 bg-amber-50 p-3">
593
+ <div className="mb-2 rounded-md border border-amber-200 bg-amber-50/60 px-3 py-2.5">
458
594
  <p className="text-sm text-amber-800">Editing as JSON, because:</p>
459
595
  <ul className="mt-1 list-disc pl-5 text-sm text-amber-700">
460
596
  {compatibility.reasons.map((reason: string, i: number) => {
@@ -464,58 +600,106 @@ const ModelColumnEditor: FunctionComponent<ComponentProps> = (
464
600
  </div>
465
601
  )}
466
602
 
467
- {!isLockedToJson && compatibility.reasons.length > 0 && (
468
- <div className="mb-2 rounded-md border border-amber-200 bg-amber-50 p-3">
603
+ {!isLockedToJson && unknownColumnWarnings.length > 0 && (
604
+ <div className="mb-2 rounded-md border border-amber-200 bg-amber-50/60 px-3 py-2.5">
469
605
  <ul className="list-disc pl-5 text-sm text-amber-700">
470
- {compatibility.reasons.map((reason: string, i: number) => {
606
+ {unknownColumnWarnings.map((reason: string, i: number) => {
471
607
  return <li key={i}>{reason}</li>;
472
608
  })}
473
609
  </ul>
474
610
  </div>
475
611
  )}
476
612
 
477
- {effectiveMode === "builder" ? (
478
- <DictionaryForm
479
- keys={columnKeys}
480
- enableOperators={isQuery}
481
- valueTypes={[ValueType.Text, ValueType.Number, ValueType.Boolean]}
482
- addButtonSuffix={isQuery ? "condition" : "field"}
483
- keyPlaceholder="Column"
484
- valuePlaceholder="Value"
485
- valuePlaceholders={valuePlaceholders}
486
- defaultValueSuggestions={props.valueSuggestions}
487
- initialValue={initialRows}
488
- onChange={onRowsChange}
489
- />
490
- ) : (
491
- <CodeEditor
492
- type={CodeType.JSON}
493
- tabIndex={props.tabIndex}
494
- error={props.error}
495
- initialValue={initial.text}
496
- value={jsonText}
497
- placeholder={props.placeholder}
498
- onChange={onJsonChange}
499
- />
613
+ {returnToRowsBlockedBy.length > 0 && (
614
+ <div className="mb-2 rounded-md border border-amber-200 bg-amber-50/60 px-3 py-2.5">
615
+ <p className="text-sm text-amber-800">
616
+ This can&apos;t go back to {isQuery ? "conditions" : "fields"},
617
+ because:
618
+ </p>
619
+ <ul className="mt-1 list-disc pl-5 text-sm text-amber-700">
620
+ {returnToRowsBlockedBy.map((reason: string, i: number) => {
621
+ return <li key={i}>{reason}</li>;
622
+ })}
623
+ </ul>
624
+ </div>
500
625
  )}
501
626
 
502
- {!isLockedToJson && (
503
- <div className="mt-2">
627
+ <div className="mb-2 flex items-center justify-between gap-2">
628
+ <p className="text-xs text-gray-500">
629
+ {effectiveMode === "builder" ? (
630
+ <>
631
+ {filledRowCount} {noun}
632
+ {filledRowCount === 1 ? "" : "s"} set
633
+ {emptyRequiredCount > 0 && (
634
+ <span className="text-amber-600">
635
+ {" "}
636
+ · {emptyRequiredCount} required field
637
+ {emptyRequiredCount === 1 ? "" : "s"} still empty
638
+ </span>
639
+ )}
640
+ </>
641
+ ) : (
642
+ <>Editing the raw JSON this step will send.</>
643
+ )}
644
+ </p>
645
+
646
+ {!isLockedToJson && (
504
647
  <button
505
648
  type="button"
506
- className="text-sm underline text-blue-500 hover:text-blue-600 cursor-pointer inline-flex items-center gap-1"
649
+ data-testid="model-column-json-toggle"
650
+ className="inline-flex items-center gap-1.5 rounded-md border border-gray-200 bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 hover:border-gray-300 hover:bg-gray-50 focus:outline-none focus:ring-1 focus:ring-indigo-500"
507
651
  onClick={() => {
508
- setViewMode(effectiveMode === "builder" ? "json" : "builder");
652
+ if (effectiveMode === "builder") {
653
+ showJson();
654
+ return;
655
+ }
656
+
657
+ showRows();
509
658
  }}
510
659
  >
511
- <Icon icon={IconProp.Code} className="h-3.5 w-3.5" />
660
+ <Icon
661
+ icon={
662
+ effectiveMode === "builder"
663
+ ? IconProp.Code
664
+ : IconProp.ListBullet
665
+ }
666
+ className="h-3.5 w-3.5 text-gray-500"
667
+ />
512
668
  {effectiveMode === "builder"
513
669
  ? "Edit as JSON"
514
670
  : isQuery
515
671
  ? "Back to conditions"
516
672
  : "Back to fields"}
517
673
  </button>
518
- </div>
674
+ )}
675
+ </div>
676
+
677
+ {effectiveMode === "builder" ? (
678
+ isQuery ? (
679
+ <ModelQueryBuilder
680
+ rows={rows}
681
+ columns={columns}
682
+ suggestions={props.valueSuggestions}
683
+ onChange={onRowsChange}
684
+ />
685
+ ) : (
686
+ <ModelRecordForm
687
+ rows={rows}
688
+ columns={columns}
689
+ suggestions={props.valueSuggestions}
690
+ onChange={onRowsChange}
691
+ />
692
+ )
693
+ ) : (
694
+ <CodeEditor
695
+ type={CodeType.JSON}
696
+ tabIndex={props.tabIndex}
697
+ error={props.error}
698
+ initialValue={initial.text}
699
+ value={jsonText}
700
+ placeholder={props.placeholder}
701
+ onChange={onJsonChange}
702
+ />
519
703
  )}
520
704
 
521
705
  {props.error && effectiveMode === "builder" && (