From 0be2482e6d52315a3fd28a14ffc0ece50af23dcc Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Thu, 22 Jan 2026 17:17:06 +0100 Subject: [PATCH 1/5] feat: inspector data: change and add data sources --- .../components/DataSourceSelectButton.tsx | 237 ++++++++++++++++++ .../VisualisationPanel/VisualisationPanel.tsx | 214 +++------------- .../inspector/BoundaryConfigItem.tsx | 15 +- .../inspector/BoundaryDataPanel.tsx | 2 +- .../inspector/InspectorConfigTab.tsx | 63 ++++- .../components/inspector/InspectorDataTab.tsx | 4 +- .../components/inspector/InspectorPanel.tsx | 29 +-- src/components/DataSourceItem.tsx | 8 +- 8 files changed, 347 insertions(+), 225 deletions(-) create mode 100644 src/app/map/[id]/components/DataSourceSelectButton.tsx diff --git a/src/app/map/[id]/components/DataSourceSelectButton.tsx b/src/app/map/[id]/components/DataSourceSelectButton.tsx new file mode 100644 index 00000000..e1cb6eb5 --- /dev/null +++ b/src/app/map/[id]/components/DataSourceSelectButton.tsx @@ -0,0 +1,237 @@ +import { PlusIcon, RotateCwIcon, X } from "lucide-react"; +import { useMemo, useState } from "react"; +import { DataSourceItem } from "@/components/DataSourceItem"; +import { Button } from "@/shadcn/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/shadcn/ui/dialog"; +import { Input } from "@/shadcn/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shadcn/ui/select"; +import { cn } from "@/shadcn/utils"; +import { useDataSources } from "../hooks/useDataSources"; +import { useMapViews } from "../hooks/useMapViews"; +import type { DataSourceWithImportInfo } from "@/components/DataSourceItem"; +import type { AreaSetCode } from "@/server/models/AreaSet"; + +export default function DataSourceSelectButton({ + areaSetCode, + className, + dataSource, + onClickRemove, + onSelect, + selectButtonText, +}: { + areaSetCode?: AreaSetCode | null | undefined; + className?: string | null | undefined; + dataSource?: DataSourceWithImportInfo | null | undefined; + onClickRemove?: () => void; + onSelect: (dataSourceId: string) => void; + selectButtonText?: string | null | undefined; +}) { + const [isModalOpen, setIsModalOpen] = useState(false); + + return ( + <> + + + + ); +} + +function DataSourceSelectButtonModalTrigger({ + className, + dataSource, + setIsModalOpen, + onClickRemove, + selectButtonText, +}: { + className?: string | null | undefined; + dataSource?: DataSourceWithImportInfo | null | undefined; + setIsModalOpen: (o: boolean) => void; + onClickRemove?: () => void; + selectButtonText?: string | null | undefined; +}) { + if (!dataSource) { + return ( + + ); + } + return ( +
+ +
+ + {onClickRemove && ( + + )} +
+
+ ); +} + +function DataSourceSelectModal({ + areaSetCode, + isModalOpen, + setIsModalOpen, + onSelect, +}: { + areaSetCode?: AreaSetCode | null | undefined; + isModalOpen: boolean; + setIsModalOpen: (o: boolean) => void; + onSelect: (dataSourceId: string) => void; +}) { + const [activeTab, setActiveTab] = useState<"all" | "public" | "user">("all"); + const [searchQuery, setSearchQuery] = useState(""); + const { data: dataSources } = useDataSources(); + const { viewConfig } = useMapViews(); + + // Update the filtering logic to include search + const filteredAndSearchedDataSources = useMemo(() => { + let sources = dataSources || []; + + if (searchQuery) { + sources = sources.filter( + (ds) => + ds.name.toLowerCase().includes(searchQuery.toLowerCase()) || + ds.columnDefs.some((col) => + col.name.toLowerCase().includes(searchQuery.toLowerCase()), + ), + ); + } + + if (activeTab === "public") { + // Include only public data sources + sources = sources.filter((ds) => ds.public); + } else if (activeTab === "user") { + // Include only user data sources + sources = sources.filter((ds) => !ds.public); + } + + if (areaSetCode) { + sources = sources.filter((ds) => { + if (!("areaSetCode" in ds.geocodingConfig)) { + return false; + } + return ds.geocodingConfig.areaSetCode === areaSetCode; + }); + } + + return sources; + }, [activeTab, areaSetCode, dataSources, searchQuery]); + return ( + + + + Select data source for visualisation + + +
+ {/* Search and Filter Bar */} +
+ setSearchQuery(e.target.value)} + /> + +
+ + {/* Data Source Grid */} +
+
+ {filteredAndSearchedDataSources.map((ds) => ( + + ))} +
+
+
+
+
+ ); +} diff --git a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx index 8bf9e670..0aac925c 100644 --- a/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx +++ b/src/app/map/[id]/components/controls/VisualisationPanel/VisualisationPanel.tsx @@ -1,13 +1,5 @@ -import { - CircleAlert, - Database, - Palette, - PieChart, - PlusIcon, - RotateCwIcon, - X, -} from "lucide-react"; -import { useMemo, useState } from "react"; +import { CircleAlert, Database, Palette, PieChart, X } from "lucide-react"; +import { useState } from "react"; import { useChoropleth } from "@/app/map/[id]/hooks/useChoropleth"; import { useChoroplethDataSource, @@ -50,8 +42,8 @@ import { dataRecordsWillAggregate, getValidAreaSetGroupCodes, } from "../../Choropleth/areas"; +import DataSourceSelectButton from "../../DataSourceSelectButton"; import CategoryColorEditor from "./CategoryColorEditor"; -import { DataSourceItem } from "./DataSourceItem"; import SteppedColorEditor from "./SteppedColorEditor"; import type { AreaSetGroupCode } from "@/server/models/AreaSet"; import type { DataSource } from "@/server/models/DataSource"; @@ -147,38 +139,10 @@ export default function VisualisationPanel({ const { data: dataSources, getDataSourceById } = useDataSources(); const dataSource = useChoroplethDataSource(); - const [activeTab, setActiveTab] = useState<"all" | "public" | "user">("all"); - const [searchQuery, setSearchQuery] = useState(""); - const [isModalOpen, setIsModalOpen] = useState(false); const [invalidDataSourceId, setInvalidDataSourceId] = useState( null, ); - // Update the filtering logic to include search - const filteredAndSearchedDataSources = useMemo(() => { - let sources = dataSources || []; - - if (searchQuery) { - sources = sources.filter( - (ds) => - ds.name.toLowerCase().includes(searchQuery.toLowerCase()) || - ds.columnDefs.some((col) => - col.name.toLowerCase().includes(searchQuery.toLowerCase()), - ), - ); - } - - if (activeTab === "public") { - // Include only public data sources - sources = sources.filter((ds) => ds.public); - } else if (activeTab === "user") { - // Include only user data sources - sources = sources.filter((ds) => !ds.public); - } - - return sources; - }, [activeTab, dataSources, searchQuery]); - if (!boundariesPanelOpen) return null; const columnOneIsNumber = @@ -213,61 +177,38 @@ export default function VisualisationPanel({ - - {viewConfig.areaDataSourceId && dataSource ? ( - // Show selected data source as a card -
- -
- - -
-
- ) : ( - // Show button to open modal when no data source selected - - - )} + + updateViewConfig({ + areaDataSourceId: "", + areaDataColumn: "", + calculationType: undefined, + }) + } + onSelect={(dataSourceId) => { + const selectedAreaSetGroup = viewConfig.areaSetGroupCode; + if (!selectedAreaSetGroup) { + updateViewConfig({ + areaDataSourceId: dataSourceId, + areaDataSecondaryColumn: undefined, + }); + return; + } + const dataSource = getDataSourceById(dataSourceId); + const validAreaSetGroups = getValidAreaSetGroupCodes( + dataSource?.geocodingConfig, + ); + if (validAreaSetGroups.includes(selectedAreaSetGroup)) { + updateViewConfig({ + areaDataSourceId: dataSourceId, + areaDataSecondaryColumn: undefined, + }); + return; + } + setInvalidDataSourceId(dataSourceId); + }} + />
@@ -889,91 +830,6 @@ export default function VisualisationPanel({ )}
- {/* Modal for data source selection */} - - - - Select data source for visualisation - - -
- {/* Search and Filter Bar */} -
- setSearchQuery(e.target.value)} - /> - -
- - {/* Data Source Grid */} -
-
- {filteredAndSearchedDataSources.map((ds) => ( - - ))} -
-
-
-
-
- {/* Modal for handling invalid data source / boundary combination */} void; onUpdate: (config: InspectorBoundaryConfig) => void; }) { const { getDataSourceById } = useDataSources(); @@ -93,11 +96,11 @@ export function BoundaryConfigItem({ {/* Data source info */} - null} /> {/* Name field */} diff --git a/src/app/map/[id]/components/inspector/BoundaryDataPanel.tsx b/src/app/map/[id]/components/inspector/BoundaryDataPanel.tsx index 55e4a0ff..ca6047b6 100644 --- a/src/app/map/[id]/components/inspector/BoundaryDataPanel.tsx +++ b/src/app/map/[id]/components/inspector/BoundaryDataPanel.tsx @@ -38,7 +38,7 @@ export function BoundaryDataPanel({ (selectedBoundary?.areaSetCode as AreaSetCode) || AreaSetCode.WMC24, }, { - enabled: Boolean(selectedBoundary?.areaSetCode), + enabled: Boolean(selectedBoundary?.areaSetCode && dataSourceId), }, ), ); diff --git a/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx b/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx index 657ad874..be8812e8 100644 --- a/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx +++ b/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx @@ -1,44 +1,60 @@ -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef } from "react"; import { type InspectorBoundaryConfig, InspectorBoundaryConfigType, } from "@/server/models/MapView"; import { useDataSources } from "../../hooks/useDataSources"; +import { useInspector } from "../../hooks/useInspector"; import { useMapViews } from "../../hooks/useMapViews"; +import DataSourceSelectButton from "../DataSourceSelectButton"; import TogglePanel from "../TogglePanel"; import { BoundaryConfigItem } from "./BoundaryConfigItem"; export default function InspectorConfigTab() { const { view, viewConfig, updateView } = useMapViews(); const { getDataSourceById } = useDataSources(); + const { selectedBoundary } = useInspector(); const boundaryStatsConfig = view?.inspectorConfig?.boundaries || []; const initializationAttemptedRef = useRef(false); - // Initialize boundaries with areaDataSourceId if empty - useEffect(() => { - if (!view || initializationAttemptedRef.current) return; - - const hasBoundaries = boundaryStatsConfig.length > 0; - const hasAreaDataSource = viewConfig.areaDataSourceId; + const addDataSourceToConfig = useCallback( + (dataSourceId: string) => { + if (!view) { + return; + } - if (!hasBoundaries && hasAreaDataSource) { - initializationAttemptedRef.current = true; - const dataSource = getDataSourceById(viewConfig.areaDataSourceId); + const dataSource = getDataSourceById(dataSourceId); const newBoundaryConfig: InspectorBoundaryConfig = { - dataSourceId: viewConfig.areaDataSourceId, + dataSourceId, name: dataSource?.name || "Boundary Data", type: InspectorBoundaryConfigType.Simple, columns: [], }; + const prevBoundaries = view.inspectorConfig?.boundaries || []; + updateView({ ...view, inspectorConfig: { ...view.inspectorConfig, - boundaries: [newBoundaryConfig], + boundaries: [...prevBoundaries, newBoundaryConfig], }, }); + }, + [getDataSourceById, updateView, view], + ); + + // Initialize boundaries with areaDataSourceId if empty + useEffect(() => { + if (!view || initializationAttemptedRef.current) return; + + const hasBoundaries = boundaryStatsConfig.length > 0; + const hasAreaDataSource = viewConfig.areaDataSourceId; + + if (!hasBoundaries && hasAreaDataSource) { + initializationAttemptedRef.current = true; + addDataSourceToConfig(viewConfig.areaDataSourceId); } }, [ view, @@ -46,17 +62,32 @@ export default function InspectorConfigTab() { boundaryStatsConfig.length, getDataSourceById, updateView, + addDataSourceToConfig, ]); return (
-
+
{boundaryStatsConfig.map((boundaryConfig, index) => ( { + if (!view) return; + const updatedBoundaries = + view.inspectorConfig?.boundaries?.filter( + (_, i) => i !== index, + ); + updateView({ + ...view, + inspectorConfig: { + ...view.inspectorConfig, + boundaries: updatedBoundaries, + }, + }); + }} onUpdate={(updatedConfig) => { if (!view) return; const updatedBoundaries = [...boundaryStatsConfig]; @@ -71,6 +102,12 @@ export default function InspectorConfigTab() { }} /> ))} + addDataSourceToConfig(dataSourceId)} + selectButtonText={"Add a data source"} + />
diff --git a/src/app/map/[id]/components/inspector/InspectorDataTab.tsx b/src/app/map/[id]/components/inspector/InspectorDataTab.tsx index cbb3b8d1..16271808 100644 --- a/src/app/map/[id]/components/inspector/InspectorDataTab.tsx +++ b/src/app/map/[id]/components/inspector/InspectorDataTab.tsx @@ -54,7 +54,9 @@ export default function InspectorDataTab({ [view?.inspectorConfig?.boundaries], ); const shouldUseInspectorConfig = - boundaryConfigs.length > 0 && type === LayerType.Boundary; + boundaryConfigs.length > 0 && + type === LayerType.Boundary && + boundaryConfigs.some((c) => c.columns.length); const boundaryData = useMemo(() => { if ( diff --git a/src/app/map/[id]/components/inspector/InspectorPanel.tsx b/src/app/map/[id]/components/inspector/InspectorPanel.tsx index 5f6318a1..1ca8a169 100644 --- a/src/app/map/[id]/components/inspector/InspectorPanel.tsx +++ b/src/app/map/[id]/components/inspector/InspectorPanel.tsx @@ -62,18 +62,17 @@ export default function InspectorPanel() { id="inspector-panel" className={cn( "absolute top-0 bottom-0 right-4 / flex flex-col gap-6 py-5", - "bottom-24", // to avoid clash with bug report button - safeActiveTab === "config" ? "h-full" : "h-fit max-h-full", )} style={{ minWidth: safeActiveTab === "config" ? "400px" : "250px", maxWidth: "450px", + maxHeight: "calc(100% - 80px)", // Avoid clash with buttons }} >
@@ -114,7 +113,7 @@ export default function InspectorPanel() { defaultValue="data" value={safeActiveTab} onValueChange={setActiveTab} - className="flex flex-col overflow-hidden h-full" + className="flex flex-col min-h-0" > {type !== LayerType.Cluster && ( @@ -140,10 +139,7 @@ export default function InspectorPanel() { {type !== LayerType.Cluster && ( - + )} - + {type && } - + {type === LayerType.Boundary && ( - + )} diff --git a/src/components/DataSourceItem.tsx b/src/components/DataSourceItem.tsx index 710d663e..4a84f4ff 100644 --- a/src/components/DataSourceItem.tsx +++ b/src/components/DataSourceItem.tsx @@ -5,13 +5,13 @@ import { DataSourceType } from "@/server/models/DataSource"; import { cn } from "@/shadcn/utils"; import type { RouterOutputs } from "@/services/trpc/react"; -type DataSourceItemType = NonNullable< +export type DataSourceWithImportInfo = NonNullable< RouterOutputs["dataSource"]["byOrganisation"] >[0]; // Helper function to get data source type from config export const getDataSourceType = ( - dataSource: DataSourceItemType, + dataSource: DataSourceWithImportInfo, ): DataSourceType | "unknown" => { try { const config = dataSource.config; @@ -64,7 +64,7 @@ const getDataSourceStyle = (type: DataSourceType | "unknown") => { }; */ // Helper function to get geocoding status -const getGeocodingStatus = (dataSource: DataSourceItemType) => { +const getGeocodingStatus = (dataSource: DataSourceWithImportInfo) => { const geocodingConfig = dataSource.geocodingConfig; if (geocodingConfig.type === "None") { return { status: "No geocoding", color: "text-neutral-500" }; @@ -82,7 +82,7 @@ export function DataSourceItem({ dataSource, className, }: { - dataSource: DataSourceItemType; + dataSource: DataSourceWithImportInfo; className?: string; }) { const dataSourceType = getDataSourceType(dataSource); From 67009cab7ae34d385fd97121a8dc3e51c5be2ed9 Mon Sep 17 00:00:00 2001 From: Joaquim d'Souza Date: Thu, 22 Jan 2026 17:40:27 +0100 Subject: [PATCH 2/5] fix: changing the data source in boundary config --- .../1769099632447_boundary_config_add_id.ts | 41 +++++++++++++++++++ .../inspector/BoundaryConfigItem.tsx | 27 ++++++++++-- .../inspector/InspectorConfigTab.tsx | 8 +++- .../components/inspector/InspectorDataTab.tsx | 2 +- src/server/models/MapView.ts | 1 + 5 files changed, 73 insertions(+), 6 deletions(-) create mode 100644 migrations/1769099632447_boundary_config_add_id.ts diff --git a/migrations/1769099632447_boundary_config_add_id.ts b/migrations/1769099632447_boundary_config_add_id.ts new file mode 100644 index 00000000..8ee71595 --- /dev/null +++ b/migrations/1769099632447_boundary_config_add_id.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { sql } from "kysely"; +import type { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql` + UPDATE map_view + SET inspector_config = jsonb_set( + inspector_config, + '{boundaries}', + ( + SELECT jsonb_agg( + jsonb_set( + boundary, + '{id}', + to_jsonb(gen_random_uuid()::text) + ) + ) + FROM jsonb_array_elements(inspector_config->'boundaries') AS boundary + ) + ) + WHERE inspector_config ? 'boundaries' + AND jsonb_typeof(inspector_config->'boundaries') = 'array' + `.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql` + UPDATE map_view + SET inspector_config = jsonb_set( + inspector_config, + '{boundaries}', + ( + SELECT jsonb_agg(boundary - 'id') + FROM jsonb_array_elements(inspector_config->'boundaries') AS boundary + ) + ) + WHERE inspector_config ? 'boundaries' + AND jsonb_typeof(inspector_config->'boundaries') = 'array' + `.execute(db); +} diff --git a/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx b/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx index fd6f8b34..0583682c 100644 --- a/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx +++ b/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx @@ -1,4 +1,4 @@ -import { Database } from "lucide-react"; +import { Database, X } from "lucide-react"; import { useMemo, useState } from "react"; import DataSourceIcon from "@/components/DataSourceIcon"; import { getDataSourceType } from "@/components/DataSourceItem"; @@ -7,6 +7,7 @@ import { InspectorBoundaryConfigType, inspectorBoundaryTypes, } from "@/server/models/MapView"; +import { Button } from "@/shadcn/ui/button"; import { Input } from "@/shadcn/ui/input"; import { Label } from "@/shadcn/ui/label"; import { MultiSelect } from "@/shadcn/ui/multi-select"; @@ -20,13 +21,16 @@ import { import { useDataSources } from "../../hooks/useDataSources"; import DataSourceSelectButton from "../DataSourceSelectButton"; import TogglePanel from "../TogglePanel"; +import type { AreaSetCode } from "@/server/models/AreaSet"; export function BoundaryConfigItem({ + areaSetCode, boundaryConfig, index, onClickRemove, onUpdate, }: { + areaSetCode: AreaSetCode | null | undefined; boundaryConfig: InspectorBoundaryConfig; index: number; onClickRemove: () => void; @@ -51,8 +55,15 @@ export function BoundaryConfigItem({ if (!dataSource) { return ( -
+

Data source not found

+
); } @@ -80,6 +91,15 @@ export function BoundaryConfigItem({ }); }; + const handleDataSourceIdChange = (dataSourceId: string) => { + setSelectedColumns([]); + onUpdate({ + ...boundaryConfig, + dataSourceId, + columns: [], + }); + }; + return (
null} + onSelect={(dataSourceId) => handleDataSourceIdChange(dataSourceId)} /> {/* Name field */} diff --git a/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx b/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx index be8812e8..45926233 100644 --- a/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx +++ b/src/app/map/[id]/components/inspector/InspectorConfigTab.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef } from "react"; +import { v4 as uuidv4 } from "uuid"; import { type InspectorBoundaryConfig, InspectorBoundaryConfigType, @@ -17,6 +18,7 @@ export default function InspectorConfigTab() { const boundaryStatsConfig = view?.inspectorConfig?.boundaries || []; const initializationAttemptedRef = useRef(false); + const areaSetCode = selectedBoundary?.areaSetCode; const addDataSourceToConfig = useCallback( (dataSourceId: string) => { @@ -26,6 +28,7 @@ export default function InspectorConfigTab() { const dataSource = getDataSourceById(dataSourceId); const newBoundaryConfig: InspectorBoundaryConfig = { + id: uuidv4(), dataSourceId, name: dataSource?.name || "Boundary Data", type: InspectorBoundaryConfigType.Simple, @@ -71,7 +74,8 @@ export default function InspectorConfigTab() {
{boundaryStatsConfig.map((boundaryConfig, index) => ( { @@ -104,7 +108,7 @@ export default function InspectorConfigTab() { ))} addDataSourceToConfig(dataSourceId)} selectButtonText={"Add a data source"} /> diff --git a/src/app/map/[id]/components/inspector/InspectorDataTab.tsx b/src/app/map/[id]/components/inspector/InspectorDataTab.tsx index 16271808..aa464c6c 100644 --- a/src/app/map/[id]/components/inspector/InspectorDataTab.tsx +++ b/src/app/map/[id]/components/inspector/InspectorDataTab.tsx @@ -100,7 +100,7 @@ export default function InspectorDataTab({ boundaryData.length > 0 ? ( boundaryData.map((item, index) => ( Date: Thu, 22 Jan 2026 17:58:35 +0100 Subject: [PATCH 3/5] Update src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx b/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx index 0583682c..701a3b9c 100644 --- a/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx +++ b/src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx @@ -92,11 +92,16 @@ export function BoundaryConfigItem({ }; const handleDataSourceIdChange = (dataSourceId: string) => { + const newDataSource = getDataSourceById(dataSourceId); + const newName = newDataSource?.name ?? configName; + + setConfigName(newName || ""); setSelectedColumns([]); onUpdate({ ...boundaryConfig, dataSourceId, columns: [], + name: newName, }); }; From d90bda3b2765181c266bfeb664e56509edd47fae Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:59:08 +0000 Subject: [PATCH 4/5] Initial plan From e7937fee1cb82ef78fad4af0f09e9d92e82d72fa Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:00:52 +0000 Subject: [PATCH 5/5] Add COALESCE to migrations to prevent null arrays Co-authored-by: joaquimds <12935136+joaquimds@users.noreply.github.com> --- .../1769099632447_boundary_config_add_id.ts | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/migrations/1769099632447_boundary_config_add_id.ts b/migrations/1769099632447_boundary_config_add_id.ts index 8ee71595..706581e5 100644 --- a/migrations/1769099632447_boundary_config_add_id.ts +++ b/migrations/1769099632447_boundary_config_add_id.ts @@ -9,12 +9,15 @@ export async function up(db: Kysely): Promise { inspector_config, '{boundaries}', ( - SELECT jsonb_agg( - jsonb_set( - boundary, - '{id}', - to_jsonb(gen_random_uuid()::text) - ) + SELECT COALESCE( + jsonb_agg( + jsonb_set( + boundary, + '{id}', + to_jsonb(gen_random_uuid()::text) + ) + ), + '[]'::jsonb ) FROM jsonb_array_elements(inspector_config->'boundaries') AS boundary ) @@ -31,7 +34,10 @@ export async function down(db: Kysely): Promise { inspector_config, '{boundaries}', ( - SELECT jsonb_agg(boundary - 'id') + SELECT COALESCE( + jsonb_agg(boundary - 'id'), + '[]'::jsonb + ) FROM jsonb_array_elements(inspector_config->'boundaries') AS boundary ) )