Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
00c8c2f
Add inspector configuration schema and migration for map view
ev-sc Jan 15, 2026
107c0f2
Add complete database schema documentation and interfaces for area, u…
ev-sc Jan 15, 2026
9105bd9
Refactor inspector panel into separate files for tabs
ev-sc Jan 15, 2026
dcfdd26
Add inspectorConfig to new map view and default view creation
ev-sc Jan 15, 2026
6735019
Linting
ev-sc Jan 15, 2026
fd44ca3
Add TogglePanel component for collapsible content
ev-sc Jan 15, 2026
bfcc0db
Restyle data source item in line with designs
ev-sc Jan 15, 2026
ec6d344
Enhance InspectorConfigTab with data source configuration options and…
ev-sc Jan 15, 2026
4f898ae
Add shadcn multi select
ev-sc Jan 15, 2026
7dc79fa
Refactor inspector configuration to use structured boundaries and upd…
ev-sc Jan 15, 2026
58c3bf8
Add BoundaryConfigItem component and integrate it into InspectorConfi…
ev-sc Jan 15, 2026
ed30a86
Refactor TogglePanel icon prop to accept React nodes and implement Bo…
ev-sc Jan 15, 2026
a4de620
Update src/server/services/database/schema.ts
ev-sc Jan 15, 2026
5c79e61
Update src/app/map/[id]/components/inspector/InspectorPanel.tsx
ev-sc Jan 15, 2026
f66d9b5
Update src/app/map/[id]/components/inspector/InspectorDataTab.tsx
ev-sc Jan 15, 2026
7920dee
Update src/app/map/[id]/page.tsx
ev-sc Jan 15, 2026
89cbaad
Refactor InspectorConfigTab to use useCallback for updateView and get…
ev-sc Jan 15, 2026
02419ef
Add aria-label to remove button in MultiSelect for improved accessibi…
ev-sc Jan 15, 2026
66edd19
Merge branch 'main' into feature/CONSULT-5055
ev-sc Jan 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions migrations/1764611637231_map_view_inspector_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Kysely } from "kysely";

export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable("map_view")
.addColumn("inspector_config", "jsonb")
.execute();
}

export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable("map_view")
.dropColumn("inspector_config")
.execute();
}
1 change: 1 addition & 0 deletions src/app/map/[id]/components/MapViews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ export default function MapViews() {
name: newViewName.trim(),
config: createNewViewConfig(),
dataSourceViews: [],
inspectorConfig: { boundaries: [] },
mapId,
createdAt: new Date(),
};
Expand Down
67 changes: 67 additions & 0 deletions src/app/map/[id]/components/TogglePanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { ChevronDown } from "lucide-react";
import { useState } from "react";
import { cn } from "@/shadcn/utils";
import type { LucideIcon } from "lucide-react";

interface TogglePanelProps {
label: string;
icon?: React.ReactNode;
defaultExpanded?: boolean;
children?: React.ReactNode;
headerRight?: React.ReactNode;
rightIconButton?: LucideIcon;
onRightIconButtonClick?: () => void;
}

export default function TogglePanel({
label,
icon: Icon,
defaultExpanded = false,
children,
headerRight,
rightIconButton: RightIconButton,
onRightIconButtonClick,
}: TogglePanelProps) {
const [expanded, setExpanded] = useState(defaultExpanded);

return (
<div>
<div className="flex items-center justify-between relative">
<button
onClick={() => setExpanded(!expanded)}
className="flex items-center gap-2 hover:bg-neutral-100 rounded px-1 py-2 -mx-1 / text-sm font-medium cursor-pointer"
>
<ChevronDown
size={16}
className={cn(
"transition-transform",
expanded ? "rotate-0" : "-rotate-90",
)}
/>

{Icon}
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The icon parameter is typed as React.ReactNode but is being conditionally rendered directly in JSX. When undefined, it will render nothing, which is fine. However, for consistency with the TogglePanelProps interface where rightIconButton is typed as LucideIcon, consider typing icon more specifically or adding a comment explaining that undefined icons are expected and handled correctly.

Copilot uses AI. Check for mistakes.

{label}
</button>

{headerRight && (
<div className="shrink-0 ml-auto flex flex-row items-center">
{headerRight}
</div>
)}

{RightIconButton && (
<button
onClick={onRightIconButtonClick}
className="p-2 hover:bg-neutral-100 rounded text-muted-foreground hover:text-foreground cursor-pointer ml-2"
aria-label="Action button"
>
<RightIconButton size={16} />
</button>
)}
</div>

{expanded && <div>{children}</div>}
</div>
);
}
162 changes: 162 additions & 0 deletions src/app/map/[id]/components/inspector/BoundaryConfigItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { Database } from "lucide-react";
import { useMemo, useState } from "react";
import DataSourceIcon from "@/components/DataSourceIcon";
import { DataSourceItem, getDataSourceType } from "@/components/DataSourceItem";
import {
type InspectorBoundaryConfig,
InspectorBoundaryConfigType,
inspectorBoundaryTypes,
} from "@/server/models/MapView";
import { Input } from "@/shadcn/ui/input";
import { Label } from "@/shadcn/ui/label";
import { MultiSelect } from "@/shadcn/ui/multi-select";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shadcn/ui/select";
import { useDataSources } from "../../hooks/useDataSources";
import TogglePanel from "../TogglePanel";

export function BoundaryConfigItem({
boundaryConfig,
index,
onUpdate,
}: {
boundaryConfig: InspectorBoundaryConfig;
index: number;
onUpdate: (config: InspectorBoundaryConfig) => void;
}) {
const { getDataSourceById } = useDataSources();
const dataSource = getDataSourceById(boundaryConfig.dataSourceId);
const [configName, setConfigName] = useState(boundaryConfig.name || "");
const [selectedColumns, setSelectedColumns] = useState<string[]>(
boundaryConfig.columns || [],
);

const dataSourceType = dataSource ? getDataSourceType(dataSource) : null;

const columnOptions = useMemo(() => {
if (!dataSource) return [];
return dataSource.columnDefs.map((col) => ({
value: col.name,
label: col.name,
}));
}, [dataSource]);

if (!dataSource) {
return (
<div className="py-8 text-center text-muted-foreground">
<p className="text-sm">Data source not found</p>
</div>
);
}

const handleNameChange = (newName: string) => {
setConfigName(newName);
onUpdate({
...boundaryConfig,
name: newName,
});
};

const handleTypeChange = (newType: InspectorBoundaryConfigType) => {
onUpdate({
...boundaryConfig,
type: newType,
});
};

const handleColumnsChange = (newColumns: string[]) => {
setSelectedColumns(newColumns);
onUpdate({
...boundaryConfig,
columns: newColumns,
});
};

return (
<div className="border rounded-lg p-3">
<TogglePanel
label={dataSource.name.toUpperCase()}
icon={
dataSourceType ? <DataSourceIcon type={dataSourceType} /> : undefined
}
defaultExpanded={true}
>
<div className="pt-4 pb-2 flex flex-col gap-4">
<h3 className="text-sm font-medium flex items-center gap-2 text-muted-foreground">
<Database size={16} />
Data source
</h3>

{/* Data source info */}
<DataSourceItem
className="shadow-xs"
dataSource={{
...dataSource,
}}
/>

{/* Name field */}
<div className="space-y-2">
<Label
htmlFor={`config-name-${index}`}
className="text-muted-foreground"
>
Name
</Label>
<Input
id={`config-name-${index}`}
value={configName}
onChange={(e) => handleNameChange(e.target.value)}
placeholder="e.g. Main Data"
/>
</div>

{/* Type field */}
<div className="space-y-2">
<Label
htmlFor={`config-type-${index}`}
className="text-muted-foreground"
>
Type
</Label>
<Select
defaultValue={
boundaryConfig.type || InspectorBoundaryConfigType.Simple
}
onValueChange={(value) =>
handleTypeChange(value as InspectorBoundaryConfigType)
}
>
<SelectTrigger id={`config-type-${index}`}>
<SelectValue />
</SelectTrigger>
<SelectContent>
{inspectorBoundaryTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

{/* Columns field */}
<div className="space-y-2">
<Label className="text-muted-foreground">Columns</Label>
<MultiSelect
options={columnOptions}
selected={selectedColumns}
onChange={handleColumnsChange}
placeholder="Select columns..."
/>
</div>
</div>
</TogglePanel>
</div>
);
}
80 changes: 80 additions & 0 deletions src/app/map/[id]/components/inspector/BoundaryDataPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useQuery } from "@tanstack/react-query";
import { useMemo } from "react";
import TogglePanel from "@/app/map/[id]/components/TogglePanel";
import { useInspector } from "@/app/map/[id]/hooks/useInspector";
import DataSourceIcon from "@/components/DataSourceIcon";
import { getDataSourceType } from "@/components/DataSourceItem";
import { AreaSetCode } from "@/server/models/AreaSet";
import { useTRPC } from "@/services/trpc/react";
import { useDataSources } from "../../hooks/useDataSources";
import PropertiesList from "./PropertiesList";

export function BoundaryDataPanel({
config,
dataSourceId,
areaCode,
columns,
defaultExpanded,
}: {
config: { name: string; dataSourceId: string };
dataSourceId: string;
areaCode: string;
columns: string[];
defaultExpanded: boolean;
}) {
Comment on lines +12 to +24
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The config parameter type is loosely defined as an object with name and dataSourceId, but the actual InspectorBoundaryConfig type has additional required fields (type and columns). This creates a type mismatch - the function should accept the full InspectorBoundaryConfig type to ensure type safety.

Copilot uses AI. Check for mistakes.
const trpc = useTRPC();
const { selectedBoundary } = useInspector();
const { getDataSourceById } = useDataSources();
const dataSource = getDataSourceById(dataSourceId);

const dataSourceType = dataSource ? getDataSourceType(dataSource) : null;

const { data, isLoading } = useQuery(
trpc.dataRecord.byAreaCode.queryOptions(
{
dataSourceId,
areaCode,
areaSetCode:
(selectedBoundary?.areaSetCode as AreaSetCode) || AreaSetCode.WMC24,
},
{
enabled: Boolean(selectedBoundary?.areaSetCode),
},
),
);

const filteredProperties = useMemo(() => {
if (!data?.json) return {};
const filtered: Record<string, unknown> = {};
columns.forEach((columnName) => {
if (data.json[columnName] !== undefined) {
filtered[columnName] = data.json[columnName];
}
});
return filtered;
}, [data?.json, columns]);

return (
<TogglePanel
label={config.name}
icon={
dataSourceType ? <DataSourceIcon type={dataSourceType} /> : undefined
}
defaultExpanded={defaultExpanded}
>
<div className="flex flex-col gap-4 pt-4">
{isLoading ? (
<div className="py-4 text-center text-muted-foreground">
<p className="text-sm">Loading...</p>
</div>
) : Object.keys(filteredProperties).length > 0 ? (
<PropertiesList properties={filteredProperties} />
) : (
<div className="py-4 text-center text-muted-foreground">
<p className="text-sm">No data available</p>
</div>
)}
</div>
</TogglePanel>
);
}
Loading