@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.
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Models/DatabaseModels/RunnerJob.ts +17 -2
- package/Server/API/LlmProviderAPI.ts +8 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLabelRuleEngineService.ts +240 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Services/NetworkDeviceService.ts +265 -12
- package/Server/Services/RunnerJobService.ts +24 -1
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/LLM/LLMService.ts +108 -19
- package/Server/Utils/Monitor/MonitorAlert.ts +22 -115
- package/Server/Utils/Monitor/MonitorIncident.ts +7 -337
- package/Server/Utils/Monitor/MonitorMaintenanceSuppression.ts +27 -2
- package/Server/Utils/Monitor/SeriesResourceLabels.ts +32 -0
- package/Server/Utils/Monitor/SeriesResourceLinker.ts +469 -0
- package/Server/Utils/SSRFProtection.ts +5 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/AdminDashboard/AdminUsersBulkAddToProject.test.tsx +955 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProject.test.ts +778 -0
- package/Tests/App/AdminDashboard/BulkAddUsersToProjectModal.test.tsx +1051 -0
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/OnCallReadinessSurfaces.test.tsx +81 -6
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/App/Dashboard/UserElementEmail.test.tsx +447 -0
- package/Tests/App/Dashboard/UserEmailCallSites.test.tsx +284 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLabelRuleRun.test.ts +618 -0
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Services/NetworkSiteAssignmentRuleRun.test.ts +706 -0
- package/Tests/Server/Services/RunnerJobEnqueue.test.ts +412 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/AI/LLMServiceRequestPolicy.test.ts +2 -2
- package/Tests/Server/Utils/AI/LLMServiceRetryPolicy.test.ts +450 -0
- package/Tests/Server/Utils/Monitor/MonitorAlertResourceLinking.test.ts +556 -0
- package/Tests/Server/Utils/Monitor/MonitorIncidentResourceLinking.test.ts +547 -0
- package/Tests/Server/Utils/Monitor/MonitorMaintenanceSuppression.test.ts +30 -0
- package/Tests/Server/Utils/Monitor/SeriesResourceLinker.test.ts +699 -0
- package/Tests/Server/Utils/SeriesResourceLabels.test.ts +55 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/API/URLMalformedTolerance.test.ts +301 -0
- package/Tests/Types/API/URLTelScheme.test.ts +261 -0
- package/Tests/Types/API/URLUnsupportedScheme.test.ts +148 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/Types/NetworkAutomation/RuleRunResult.test.ts +184 -0
- package/Tests/Types/Runbook/RunbookStepType.test.ts +124 -0
- package/Tests/Types/Workflow/BaseModelComponents.test.ts +17 -0
- package/Tests/UI/Components/Button.test.tsx +279 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/DictionaryValuePlaceholderAndSuggestions.test.tsx +595 -0
- package/Tests/UI/Components/Icon.test.tsx +57 -0
- package/Tests/UI/Components/List.test.tsx +20 -3
- package/Tests/UI/Components/LogsSavedViewsDropdown.test.tsx +304 -0
- package/Tests/UI/Components/LogsViewerClearSavedView.test.tsx +179 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/UI/Components/Pagination.test.tsx +721 -146
- package/Tests/UI/Components/PaginationIntegration.test.tsx +386 -0
- package/Tests/UI/Components/PaginationUtil.test.ts +417 -0
- package/Tests/UI/Components/TelemetrySavedViewsDropdown.test.tsx +343 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnControl.test.ts +289 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnOperators.test.ts +271 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRow.test.ts +119 -0
- package/Tests/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.test.ts +968 -0
- package/Tests/UI/Components/Workflow/ModelColumnEditor.render.test.tsx +766 -0
- package/Tests/UI/Utils/RunNetworkRule.test.ts +320 -0
- package/Tests/Utils/APIRetryPolicy.test.ts +823 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/NetworkAutomation/RuleRunSummary.test.ts +286 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/API/Protocol.ts +9 -0
- package/Types/API/URL.ts +286 -20
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +121 -0
- package/Types/Runbook/RunbookStepType.ts +23 -0
- package/UI/Components/AutocompleteTextInput/AutocompleteTextInput.tsx +7 -0
- package/UI/Components/Button/Button.tsx +14 -22
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/Dictionary/Dictionary.tsx +4 -39
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/UI/Components/LogsViewer/LogsViewer.tsx +5 -1
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +8 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +29 -91
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +2 -0
- package/UI/Components/LogsViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/UI/Components/Pagination/Pagination.tsx +373 -328
- package/UI/Components/Pagination/PaginationUtil.ts +263 -0
- package/UI/Components/Pagination/Screenshots/README.md +15 -0
- package/UI/Components/Pagination/Screenshots/pagination-before-after.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-go-to-page-modal.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-long-list.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-mobile.png +0 -0
- package/UI/Components/Pagination/Screenshots/pagination-telemetry.png +0 -0
- package/UI/Components/TelemetryViewer/components/SavedViewsDropdown.tsx +66 -1
- package/UI/Components/TelemetryViewer/components/TelemetryPagination.tsx +38 -92
- package/UI/Components/TextArea/TextArea.tsx +3 -0
- package/UI/Components/Workflow/ArgumentsForm.tsx +18 -1
- package/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.tsx +177 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.tsx +243 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnControl.ts +255 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.tsx +178 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnOperators.ts +206 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRow.ts +157 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.ts +517 -0
- package/UI/Components/Workflow/ColumnEditor/ColumnValueInput.tsx +482 -0
- package/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.tsx +131 -0
- package/UI/Components/Workflow/ColumnEditor/ModelRecordForm.tsx +163 -0
- package/UI/Components/Workflow/ModelColumnEditor.tsx +284 -100
- package/UI/Utils/NetworkAutomation/RunNetworkRule.ts +122 -0
- package/Utils/API.ts +243 -36
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/Utils/NetworkAutomation/RuleRunSummary.ts +147 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Models/DatabaseModels/RunnerJob.js +17 -2
- package/build/dist/Models/DatabaseModels/RunnerJob.js.map +1 -1
- package/build/dist/Server/API/LlmProviderAPI.js +9 -1
- package/build/dist/Server/API/LlmProviderAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js +202 -0
- package/build/dist/Server/Services/NetworkDeviceLabelRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceService.js +223 -12
- package/build/dist/Server/Services/NetworkDeviceService.js.map +1 -1
- package/build/dist/Server/Services/RunnerJobService.js +20 -2
- package/build/dist/Server/Services/RunnerJobService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/LLM/LLMService.js +78 -22
- package/build/dist/Server/Utils/LLM/LLMService.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +22 -98
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +7 -254
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js +14 -2
- package/build/dist/Server/Utils/Monitor/MonitorMaintenanceSuppression.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js +27 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLabels.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js +285 -0
- package/build/dist/Server/Utils/Monitor/SeriesResourceLinker.js.map +1 -0
- package/build/dist/Server/Utils/SSRFProtection.js +5 -4
- package/build/dist/Server/Utils/SSRFProtection.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/API/Protocol.js +9 -0
- package/build/dist/Types/API/Protocol.js.map +1 -1
- package/build/dist/Types/API/URL.js +241 -18
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +39 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -0
- package/build/dist/Types/Runbook/RunbookStepType.js +21 -0
- package/build/dist/Types/Runbook/RunbookStepType.js.map +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js +1 -1
- package/build/dist/UI/Components/AutocompleteTextInput/AutocompleteTextInput.js.map +1 -1
- package/build/dist/UI/Components/Button/Button.js +10 -16
- package/build/dist/UI/Components/Button/Button.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +4 -18
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +3 -3
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +5 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +14 -44
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/LogsViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/UI/Components/Pagination/Pagination.js +174 -199
- package/build/dist/UI/Components/Pagination/Pagination.js.map +1 -1
- package/build/dist/UI/Components/Pagination/PaginationUtil.js +160 -0
- package/build/dist/UI/Components/Pagination/PaginationUtil.js.map +1 -0
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js +20 -2
- package/build/dist/UI/Components/TelemetryViewer/components/SavedViewsDropdown.js.map +1 -1
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js +23 -47
- package/build/dist/UI/Components/TelemetryViewer/components/TelemetryPagination.js.map +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js +1 -1
- package/build/dist/UI/Components/TextArea/TextArea.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js +14 -2
- package/build/dist/UI/Components/Workflow/ArgumentsForm.js.map +1 -1
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js +86 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/AddColumnDropdown.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js +119 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnConditionRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js +206 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnControl.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js +57 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnFieldRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js +150 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnOperators.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js +67 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRow.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js +309 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnRowSerialization.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js +204 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ColumnValueInput.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js +66 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelQueryBuilder.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js +83 -0
- package/build/dist/UI/Components/Workflow/ColumnEditor/ModelRecordForm.js.map +1 -0
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js +187 -68
- package/build/dist/UI/Components/Workflow/ModelColumnEditor.js.map +1 -1
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js +75 -0
- package/build/dist/UI/Utils/NetworkAutomation/RunNetworkRule.js.map +1 -0
- package/build/dist/Utils/API.js +175 -34
- package/build/dist/Utils/API.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js +78 -0
- package/build/dist/Utils/NetworkAutomation/RuleRunSummary.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* One value cell.
|
|
3
|
+
*
|
|
4
|
+
* The control comes from the column's type in the model schema, so nobody is
|
|
5
|
+
* asked whether the thing they are typing is Text, Number or Boolean - the
|
|
6
|
+
* model already said. What is left for the builder to choose is the one thing
|
|
7
|
+
* the schema genuinely cannot know: whether this value is typed in, or read
|
|
8
|
+
* from an earlier step at run time.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import Color from "../../../../Types/Color";
|
|
12
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
13
|
+
import AutocompleteTextInput from "../../AutocompleteTextInput/AutocompleteTextInput";
|
|
14
|
+
import { DictionaryFilterOperatorOption } from "../../Dictionary/DictionaryFilterOperator";
|
|
15
|
+
import ColorPicker from "../../Forms/Fields/ColorPicker";
|
|
16
|
+
import Icon from "../../Icon/Icon";
|
|
17
|
+
import Input, { InputType } from "../../Input/Input";
|
|
18
|
+
import TextArea from "../../TextArea/TextArea";
|
|
19
|
+
import { ColumnValueMode, ModelColumnControl } from "./ColumnRow";
|
|
20
|
+
import { containsTemplateExpression } from "./ColumnRowSerialization";
|
|
21
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
22
|
+
|
|
23
|
+
export interface ComponentProps {
|
|
24
|
+
control: ModelColumnControl;
|
|
25
|
+
valueMode: ColumnValueMode;
|
|
26
|
+
text: string;
|
|
27
|
+
values: Array<string>;
|
|
28
|
+
/** Absent in record mode, where every row is an equality. */
|
|
29
|
+
operatorOption?: DictionaryFilterOperatorOption | undefined;
|
|
30
|
+
placeholder?: string | undefined;
|
|
31
|
+
/** The other steps' return values and the workflow's variables. */
|
|
32
|
+
suggestions?: Array<string> | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Every edit reports as one change.
|
|
35
|
+
*
|
|
36
|
+
* Deliberately not three callbacks: two of them fired from a single event
|
|
37
|
+
* both close over the same render's row, so the second silently discards the
|
|
38
|
+
* first - which is what made a raw cell impossible to type into and the
|
|
39
|
+
* "typing {{ turns this into a reference" behaviour dead.
|
|
40
|
+
*/
|
|
41
|
+
onChange: (
|
|
42
|
+
change: Partial<{
|
|
43
|
+
text: string;
|
|
44
|
+
values: Array<string>;
|
|
45
|
+
valueMode: ColumnValueMode;
|
|
46
|
+
}>,
|
|
47
|
+
) => void;
|
|
48
|
+
tabIndex?: number | undefined;
|
|
49
|
+
ariaLabelledby?: string | undefined;
|
|
50
|
+
/** Focused on mount, so picking a field lands the caret in its value box. */
|
|
51
|
+
autoFocus?: boolean | undefined;
|
|
52
|
+
dataTestId?: string | undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const INPUT_WRAPPER_CLASS: string = "relative w-full";
|
|
56
|
+
|
|
57
|
+
const MONO_INPUT_CLASS: string =
|
|
58
|
+
"block w-full rounded-md border-0 bg-transparent py-1 pl-0 pr-0 font-mono text-xs text-indigo-900 placeholder-indigo-300 focus:outline-none focus:ring-0";
|
|
59
|
+
|
|
60
|
+
const ColumnValueInput: FunctionComponent<ComponentProps> = (
|
|
61
|
+
props: ComponentProps,
|
|
62
|
+
): ReactElement => {
|
|
63
|
+
const isReference: boolean = props.valueMode === ColumnValueMode.Reference;
|
|
64
|
+
|
|
65
|
+
type SetTextFunction = (value: string) => void;
|
|
66
|
+
|
|
67
|
+
/*
|
|
68
|
+
* Typing "{{" anywhere in a text control means the builder is reaching for
|
|
69
|
+
* another step's output, so the cell becomes a reference there and then and
|
|
70
|
+
* keeps what has been typed. Without this the autocomplete would never open
|
|
71
|
+
* unless the reference button had been found first.
|
|
72
|
+
*/
|
|
73
|
+
const setTextAndDetectReference: SetTextFunction = (value: string): void => {
|
|
74
|
+
if (!isReference && value.includes("{{")) {
|
|
75
|
+
props.onChange({ text: value, valueMode: ColumnValueMode.Reference });
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
props.onChange({ text: value });
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/*
|
|
83
|
+
* Spelled "{ }" rather than drawn as an icon. The shared variable glyph is a
|
|
84
|
+
* dashed square that reads as a placeholder or a spinner, and this button has
|
|
85
|
+
* to say what it does at a glance - it is the only route from a typed value to
|
|
86
|
+
* an earlier step's output, which is how most of these payloads are built.
|
|
87
|
+
*/
|
|
88
|
+
const referenceButton: ReactElement = (
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
aria-pressed={isReference}
|
|
92
|
+
title={
|
|
93
|
+
isReference
|
|
94
|
+
? "Type a value instead"
|
|
95
|
+
: "Use a value from an earlier step"
|
|
96
|
+
}
|
|
97
|
+
aria-label={
|
|
98
|
+
isReference
|
|
99
|
+
? "Type a value instead"
|
|
100
|
+
: "Use a value from an earlier step"
|
|
101
|
+
}
|
|
102
|
+
data-testid={`${props.dataTestId || "model-column"}-reference-toggle`}
|
|
103
|
+
className={`shrink-0 rounded-md border px-1.5 py-1 font-mono text-[11px] leading-none transition-colors focus:outline-none focus:ring-1 focus:ring-indigo-500 ${
|
|
104
|
+
isReference
|
|
105
|
+
? "border-indigo-200 bg-white text-indigo-500 hover:bg-indigo-50"
|
|
106
|
+
: "border-transparent text-gray-400 hover:border-gray-200 hover:bg-gray-50 hover:text-gray-600"
|
|
107
|
+
}`}
|
|
108
|
+
onClick={() => {
|
|
109
|
+
props.onChange({
|
|
110
|
+
valueMode: isReference
|
|
111
|
+
? ColumnValueMode.Literal
|
|
112
|
+
: ColumnValueMode.Reference,
|
|
113
|
+
});
|
|
114
|
+
}}
|
|
115
|
+
>
|
|
116
|
+
{isReference ? "abc" : "{ }"}
|
|
117
|
+
</button>
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// An operator like "is not set" takes no value at all.
|
|
121
|
+
if (props.operatorOption?.hidesValueInput) {
|
|
122
|
+
return (
|
|
123
|
+
<div className="flex h-9 items-center rounded-md border border-dashed border-gray-200 px-3 text-xs text-gray-400">
|
|
124
|
+
No value needed
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (props.operatorOption?.expectsMultiValue) {
|
|
130
|
+
return (
|
|
131
|
+
<MultiValueInput
|
|
132
|
+
values={props.values}
|
|
133
|
+
placeholder={props.placeholder}
|
|
134
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
135
|
+
dataTestId={props.dataTestId}
|
|
136
|
+
onChange={(values: Array<string>) => {
|
|
137
|
+
props.onChange({ values: values });
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (isReference) {
|
|
144
|
+
return (
|
|
145
|
+
<div className="flex w-full items-center gap-1.5 rounded-md border border-indigo-200 bg-indigo-50/40 px-2 py-1">
|
|
146
|
+
<span className="shrink-0 font-mono text-[11px] text-indigo-400">
|
|
147
|
+
{"{ }"}
|
|
148
|
+
</span>
|
|
149
|
+
<div className="min-w-0 flex-1">
|
|
150
|
+
<AutocompleteTextInput
|
|
151
|
+
value={props.text}
|
|
152
|
+
placeholder="{{local.components.step.returnValues.value}}"
|
|
153
|
+
suggestions={props.suggestions}
|
|
154
|
+
className={MONO_INPUT_CLASS}
|
|
155
|
+
outerDivClassName={INPUT_WRAPPER_CLASS}
|
|
156
|
+
disableSpellCheck={true}
|
|
157
|
+
autoFocus={props.autoFocus}
|
|
158
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
159
|
+
dataTestId={props.dataTestId}
|
|
160
|
+
onChange={(value: string) => {
|
|
161
|
+
props.onChange({ text: value });
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
</div>
|
|
165
|
+
{referenceButton}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type RenderControlFunction = () => ReactElement;
|
|
171
|
+
|
|
172
|
+
const renderControl: RenderControlFunction = (): ReactElement => {
|
|
173
|
+
/*
|
|
174
|
+
* A value the typed control cannot hold - text where the column wants a
|
|
175
|
+
* number, most often - is edited as text and written back exactly as it
|
|
176
|
+
* was read. Forcing it through the typed control would rewrite it.
|
|
177
|
+
*/
|
|
178
|
+
if (props.valueMode === ColumnValueMode.Raw) {
|
|
179
|
+
return (
|
|
180
|
+
<Input
|
|
181
|
+
value={props.text}
|
|
182
|
+
placeholder={props.placeholder}
|
|
183
|
+
outerDivClassName={INPUT_WRAPPER_CLASS}
|
|
184
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
185
|
+
dataTestId={props.dataTestId}
|
|
186
|
+
tabIndex={props.tabIndex}
|
|
187
|
+
autoFocus={props.autoFocus}
|
|
188
|
+
onChange={(value: string) => {
|
|
189
|
+
/*
|
|
190
|
+
* The first keystroke ends raw mode: what is in the box is now what
|
|
191
|
+
* the builder means, so it is written in the column's own type. The
|
|
192
|
+
* mode and the text move together in one change - reported
|
|
193
|
+
* separately, the second report would overwrite the first and the
|
|
194
|
+
* cell could never be typed into at all.
|
|
195
|
+
*/
|
|
196
|
+
props.onChange({
|
|
197
|
+
text: value,
|
|
198
|
+
valueMode: value.includes("{{")
|
|
199
|
+
? ColumnValueMode.Reference
|
|
200
|
+
: ColumnValueMode.Literal,
|
|
201
|
+
});
|
|
202
|
+
}}
|
|
203
|
+
/>
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
switch (props.control) {
|
|
208
|
+
case ModelColumnControl.Boolean:
|
|
209
|
+
return (
|
|
210
|
+
<BooleanSegments
|
|
211
|
+
value={props.text}
|
|
212
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
213
|
+
autoFocus={props.autoFocus}
|
|
214
|
+
dataTestId={props.dataTestId}
|
|
215
|
+
onChange={(value: string) => {
|
|
216
|
+
props.onChange({ text: value });
|
|
217
|
+
}}
|
|
218
|
+
/>
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
case ModelColumnControl.Number:
|
|
222
|
+
return (
|
|
223
|
+
<Input
|
|
224
|
+
type={InputType.NUMBER}
|
|
225
|
+
value={props.text}
|
|
226
|
+
placeholder={props.placeholder || "0"}
|
|
227
|
+
outerDivClassName={INPUT_WRAPPER_CLASS}
|
|
228
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
229
|
+
dataTestId={props.dataTestId}
|
|
230
|
+
tabIndex={props.tabIndex}
|
|
231
|
+
autoFocus={props.autoFocus}
|
|
232
|
+
onChange={(value: string) => {
|
|
233
|
+
props.onChange({ text: value });
|
|
234
|
+
}}
|
|
235
|
+
/>
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
case ModelColumnControl.Date:
|
|
239
|
+
return (
|
|
240
|
+
<Input
|
|
241
|
+
type={InputType.DATETIME_LOCAL}
|
|
242
|
+
value={props.text}
|
|
243
|
+
outerDivClassName={INPUT_WRAPPER_CLASS}
|
|
244
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
245
|
+
dataTestId={props.dataTestId}
|
|
246
|
+
tabIndex={props.tabIndex}
|
|
247
|
+
autoFocus={props.autoFocus}
|
|
248
|
+
onChange={(value: string) => {
|
|
249
|
+
props.onChange({ text: value });
|
|
250
|
+
}}
|
|
251
|
+
/>
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
case ModelColumnControl.Color:
|
|
255
|
+
return (
|
|
256
|
+
<ColorPicker
|
|
257
|
+
value={props.text}
|
|
258
|
+
placeholder={props.placeholder || "#000000"}
|
|
259
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
260
|
+
dataTestId={props.dataTestId}
|
|
261
|
+
tabIndex={props.tabIndex}
|
|
262
|
+
onChange={(value: Color | null) => {
|
|
263
|
+
props.onChange({ text: value ? value.toString() : "" });
|
|
264
|
+
}}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
case ModelColumnControl.LongText:
|
|
269
|
+
return (
|
|
270
|
+
<TextArea
|
|
271
|
+
value={props.text}
|
|
272
|
+
placeholder={props.placeholder}
|
|
273
|
+
className="block w-full rounded-md border border-gray-300 bg-white py-2 px-3 text-sm placeholder-gray-500 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500 resize-y min-h-16"
|
|
274
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
275
|
+
dataTestId={props.dataTestId}
|
|
276
|
+
tabIndex={props.tabIndex}
|
|
277
|
+
autoFocus={props.autoFocus}
|
|
278
|
+
onChange={setTextAndDetectReference}
|
|
279
|
+
/>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
default:
|
|
283
|
+
return (
|
|
284
|
+
<AutocompleteTextInput
|
|
285
|
+
value={props.text}
|
|
286
|
+
placeholder={props.placeholder}
|
|
287
|
+
suggestions={props.suggestions}
|
|
288
|
+
outerDivClassName={INPUT_WRAPPER_CLASS}
|
|
289
|
+
className={
|
|
290
|
+
props.control === ModelColumnControl.ObjectId
|
|
291
|
+
? "block w-full rounded-md border border-gray-300 bg-white py-2 pl-3 pr-3 font-mono text-xs text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
|
|
292
|
+
: undefined
|
|
293
|
+
}
|
|
294
|
+
ariaLabelledby={props.ariaLabelledby}
|
|
295
|
+
autoFocus={props.autoFocus}
|
|
296
|
+
dataTestId={props.dataTestId}
|
|
297
|
+
onChange={setTextAndDetectReference}
|
|
298
|
+
/>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className="flex w-full items-center gap-1">
|
|
305
|
+
<div className="min-w-0 flex-1">{renderControl()}</div>
|
|
306
|
+
{referenceButton}
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
interface BooleanSegmentsProps {
|
|
312
|
+
value: string;
|
|
313
|
+
ariaLabelledby?: string | undefined;
|
|
314
|
+
autoFocus?: boolean | undefined;
|
|
315
|
+
dataTestId?: string | undefined;
|
|
316
|
+
onChange: (value: string) => void;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/*
|
|
320
|
+
* Three states, not two. A boolean column with nothing typed into it is not
|
|
321
|
+
* "false" - it is a field the record does not set, and a toggle cannot say
|
|
322
|
+
* that. (The shared Toggle component also fires its onChange twice per click.)
|
|
323
|
+
*/
|
|
324
|
+
const BooleanSegments: FunctionComponent<BooleanSegmentsProps> = (
|
|
325
|
+
props: BooleanSegmentsProps,
|
|
326
|
+
): ReactElement => {
|
|
327
|
+
const segments: Array<{ label: string; value: string }> = [
|
|
328
|
+
{ label: "True", value: "true" },
|
|
329
|
+
{ label: "False", value: "false" },
|
|
330
|
+
{ label: "Not set", value: "" },
|
|
331
|
+
];
|
|
332
|
+
|
|
333
|
+
return (
|
|
334
|
+
<div
|
|
335
|
+
className="inline-flex rounded-md border border-gray-300 bg-white p-0.5"
|
|
336
|
+
role="group"
|
|
337
|
+
aria-labelledby={props.ariaLabelledby}
|
|
338
|
+
data-testid={props.dataTestId}
|
|
339
|
+
>
|
|
340
|
+
{segments.map((segment: { label: string; value: string }) => {
|
|
341
|
+
const isSelected: boolean = props.value === segment.value;
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<button
|
|
345
|
+
key={segment.label}
|
|
346
|
+
type="button"
|
|
347
|
+
autoFocus={props.autoFocus && segment.value === "true"}
|
|
348
|
+
aria-pressed={isSelected}
|
|
349
|
+
className={`rounded px-2.5 py-1 text-xs font-medium transition-colors ${
|
|
350
|
+
isSelected
|
|
351
|
+
? "bg-indigo-50 text-indigo-700"
|
|
352
|
+
: "text-gray-500 hover:text-gray-700"
|
|
353
|
+
}`}
|
|
354
|
+
onClick={() => {
|
|
355
|
+
props.onChange(segment.value);
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
{segment.label}
|
|
359
|
+
</button>
|
|
360
|
+
);
|
|
361
|
+
})}
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
interface MultiValueInputProps {
|
|
367
|
+
values: Array<string>;
|
|
368
|
+
placeholder?: string | undefined;
|
|
369
|
+
ariaLabelledby?: string | undefined;
|
|
370
|
+
dataTestId?: string | undefined;
|
|
371
|
+
onChange: (values: Array<string>) => void;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/*
|
|
375
|
+
* "is any of" holds a list. The previous editor rendered a multi-select whose
|
|
376
|
+
* options were only ever the suggestions it had been handed, so a value nobody
|
|
377
|
+
* had suggested could not be entered at all - which for a list of IDs is every
|
|
378
|
+
* value. This is a chip input: type, press Enter or comma, and it is in.
|
|
379
|
+
*/
|
|
380
|
+
const MultiValueInput: FunctionComponent<MultiValueInputProps> = (
|
|
381
|
+
props: MultiValueInputProps,
|
|
382
|
+
): ReactElement => {
|
|
383
|
+
const [draft, setDraft] = React.useState<string>("");
|
|
384
|
+
|
|
385
|
+
type CommitDraftFunction = () => void;
|
|
386
|
+
|
|
387
|
+
const commitDraft: CommitDraftFunction = (): void => {
|
|
388
|
+
const entry: string = draft.trim();
|
|
389
|
+
|
|
390
|
+
if (entry === "" || props.values.includes(entry)) {
|
|
391
|
+
setDraft("");
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
props.onChange([...props.values, entry]);
|
|
396
|
+
setDraft("");
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return (
|
|
400
|
+
<div className="w-full rounded-md border border-gray-300 bg-white px-2 py-1.5">
|
|
401
|
+
{props.values.length > 0 && (
|
|
402
|
+
<div className="mb-1 flex flex-wrap gap-1">
|
|
403
|
+
{props.values.map((value: string, index: number) => {
|
|
404
|
+
return (
|
|
405
|
+
<span
|
|
406
|
+
key={`${value}-${index}`}
|
|
407
|
+
className="inline-flex items-center gap-1 rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-700"
|
|
408
|
+
>
|
|
409
|
+
{containsTemplateExpression(value) ? (
|
|
410
|
+
<span className="font-mono text-indigo-600">{value}</span>
|
|
411
|
+
) : (
|
|
412
|
+
value
|
|
413
|
+
)}
|
|
414
|
+
<button
|
|
415
|
+
type="button"
|
|
416
|
+
aria-label={`Remove ${value}`}
|
|
417
|
+
className="text-gray-400 hover:text-gray-600"
|
|
418
|
+
onClick={() => {
|
|
419
|
+
props.onChange(
|
|
420
|
+
props.values.filter((_entry: string, i: number) => {
|
|
421
|
+
return i !== index;
|
|
422
|
+
}),
|
|
423
|
+
);
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
<Icon icon={IconProp.Close} className="h-3 w-3" />
|
|
427
|
+
</button>
|
|
428
|
+
</span>
|
|
429
|
+
);
|
|
430
|
+
})}
|
|
431
|
+
</div>
|
|
432
|
+
)}
|
|
433
|
+
<input
|
|
434
|
+
type="text"
|
|
435
|
+
value={draft}
|
|
436
|
+
aria-labelledby={props.ariaLabelledby}
|
|
437
|
+
data-testid={props.dataTestId}
|
|
438
|
+
placeholder={props.placeholder || "Type a value and press Enter"}
|
|
439
|
+
className="block w-full border-0 p-0 text-sm placeholder-gray-400 focus:outline-none focus:ring-0"
|
|
440
|
+
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
441
|
+
const value: string = event.target.value;
|
|
442
|
+
|
|
443
|
+
if (value.endsWith(",")) {
|
|
444
|
+
setDraft(value.slice(0, -1));
|
|
445
|
+
/*
|
|
446
|
+
* setDraft is async, so commit from the typed text rather than from
|
|
447
|
+
* state, which still holds the previous keystroke here.
|
|
448
|
+
*/
|
|
449
|
+
const entry: string = value.slice(0, -1).trim();
|
|
450
|
+
|
|
451
|
+
if (entry !== "" && !props.values.includes(entry)) {
|
|
452
|
+
props.onChange([...props.values, entry]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
setDraft("");
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
setDraft(value);
|
|
460
|
+
}}
|
|
461
|
+
onKeyDown={(event: React.KeyboardEvent<HTMLInputElement>) => {
|
|
462
|
+
if (event.key === "Enter") {
|
|
463
|
+
event.preventDefault();
|
|
464
|
+
commitDraft();
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
event.key === "Backspace" &&
|
|
470
|
+
draft === "" &&
|
|
471
|
+
props.values.length > 0
|
|
472
|
+
) {
|
|
473
|
+
props.onChange(props.values.slice(0, -1));
|
|
474
|
+
}
|
|
475
|
+
}}
|
|
476
|
+
onBlur={commitDraft}
|
|
477
|
+
/>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
export default ColumnValueInput;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The query body: the conditions that decide which records a step acts on.
|
|
3
|
+
*
|
|
4
|
+
* Fully controlled, like the record form. One column header for the whole list
|
|
5
|
+
* rather than a Key/Operator/Value label repeated above every row, which is
|
|
6
|
+
* what made the old editor read as a spreadsheet.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
10
|
+
import Icon from "../../Icon/Icon";
|
|
11
|
+
import { ModelSchemaColumn, findColumn } from "../ModelSchema";
|
|
12
|
+
import AddColumnDropdown from "./AddColumnDropdown";
|
|
13
|
+
import { isOfferableColumn } from "./ColumnControl";
|
|
14
|
+
import ColumnConditionRow from "./ColumnConditionRow";
|
|
15
|
+
import { ModelColumnRow, makeColumnRow } from "./ColumnRow";
|
|
16
|
+
import React, { FunctionComponent, ReactElement, useState } from "react";
|
|
17
|
+
|
|
18
|
+
export interface ComponentProps {
|
|
19
|
+
rows: Array<ModelColumnRow>;
|
|
20
|
+
columns: Array<ModelSchemaColumn>;
|
|
21
|
+
suggestions?: Array<string> | undefined;
|
|
22
|
+
onChange: (rows: Array<ModelColumnRow>) => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ModelQueryBuilder: FunctionComponent<ComponentProps> = (
|
|
26
|
+
props: ComponentProps,
|
|
27
|
+
): ReactElement => {
|
|
28
|
+
/*
|
|
29
|
+
* The row just added from the picker, so its value control can take focus.
|
|
30
|
+
* Without it the picker remounts to clear its own selection and focus falls
|
|
31
|
+
* to the document body, which for a keyboard user means starting again from
|
|
32
|
+
* the top of the panel.
|
|
33
|
+
*/
|
|
34
|
+
const [justAddedKey, setJustAddedKey] = useState<string>("");
|
|
35
|
+
|
|
36
|
+
const offerableColumns: Array<ModelSchemaColumn> = props.columns.filter(
|
|
37
|
+
(column: ModelSchemaColumn) => {
|
|
38
|
+
return isOfferableColumn(column);
|
|
39
|
+
},
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
type ReplaceRowFunction = (index: number, row: ModelColumnRow) => void;
|
|
43
|
+
|
|
44
|
+
const replaceRow: ReplaceRowFunction = (
|
|
45
|
+
index: number,
|
|
46
|
+
row: ModelColumnRow,
|
|
47
|
+
): void => {
|
|
48
|
+
const next: Array<ModelColumnRow> = [...props.rows];
|
|
49
|
+
next[index] = row;
|
|
50
|
+
props.onChange(next);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div>
|
|
55
|
+
<div className="overflow-hidden rounded-md border border-gray-200 bg-white">
|
|
56
|
+
{props.rows.length === 0 ? (
|
|
57
|
+
<div className="px-6 py-10 text-center">
|
|
58
|
+
<Icon
|
|
59
|
+
icon={IconProp.Filter}
|
|
60
|
+
className="mx-auto h-6 w-6 text-gray-300"
|
|
61
|
+
/>
|
|
62
|
+
<p className="mt-2 text-sm font-medium text-gray-700">
|
|
63
|
+
No conditions yet
|
|
64
|
+
</p>
|
|
65
|
+
<p className="mt-1 text-xs text-gray-500">
|
|
66
|
+
With no conditions this matches every record in the project.
|
|
67
|
+
</p>
|
|
68
|
+
</div>
|
|
69
|
+
) : (
|
|
70
|
+
<>
|
|
71
|
+
{/* Column widths must stay in step with ColumnConditionRow's grid. */}
|
|
72
|
+
<div className="hidden border-b border-gray-100 bg-gray-50/60 px-3 py-1.5 text-[10px] font-semibold uppercase tracking-[0.08em] text-gray-400 sm:grid sm:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)_minmax(0,1.3fr)_auto] sm:gap-2">
|
|
73
|
+
<span>Column</span>
|
|
74
|
+
<span>Comparison</span>
|
|
75
|
+
<span>Value</span>
|
|
76
|
+
<span className="w-8" />
|
|
77
|
+
</div>
|
|
78
|
+
<div className="divide-y divide-gray-100">
|
|
79
|
+
{props.rows.map((row: ModelColumnRow, index: number) => {
|
|
80
|
+
return (
|
|
81
|
+
<ColumnConditionRow
|
|
82
|
+
key={row.key}
|
|
83
|
+
row={row}
|
|
84
|
+
column={findColumn(props.columns, row.columnId)}
|
|
85
|
+
columns={offerableColumns}
|
|
86
|
+
suggestions={props.suggestions}
|
|
87
|
+
autoFocus={row.key === justAddedKey}
|
|
88
|
+
onChange={(nextRow: ModelColumnRow) => {
|
|
89
|
+
replaceRow(index, nextRow);
|
|
90
|
+
}}
|
|
91
|
+
onRemove={() => {
|
|
92
|
+
props.onChange(
|
|
93
|
+
props.rows.filter((_row: ModelColumnRow, i: number) => {
|
|
94
|
+
return i !== index;
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
}}
|
|
98
|
+
/>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</div>
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<div className="border-t border-gray-100 bg-gray-50/40 px-3 py-2.5">
|
|
106
|
+
<AddColumnDropdown
|
|
107
|
+
columns={offerableColumns.filter((column: ModelSchemaColumn) => {
|
|
108
|
+
return !props.rows.some((row: ModelColumnRow) => {
|
|
109
|
+
return row.columnId === column.id;
|
|
110
|
+
});
|
|
111
|
+
})}
|
|
112
|
+
requiredColumnIds={[]}
|
|
113
|
+
placeholder="Add a condition..."
|
|
114
|
+
allowCustomColumn={true}
|
|
115
|
+
dataTestId="model-column-add"
|
|
116
|
+
onAdd={(columnId: string) => {
|
|
117
|
+
const added: ModelColumnRow = makeColumnRow({
|
|
118
|
+
columnId: columnId,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
setJustAddedKey(added.key);
|
|
122
|
+
props.onChange([...props.rows, added]);
|
|
123
|
+
}}
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export default ModelQueryBuilder;
|