Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
48 changes: 48 additions & 0 deletions src/app/map/[id]/components/inspector/ClusterMarkersList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { useInspector } from "@/app/map/[id]/hooks/useInspector";
import { DataSourceRecordType } from "@/server/models/DataSource";
import { MarkersList, MembersList } from "./MarkersLists";
import type { MarkerFeature } from "@/types";

export default function ClusterMarkersList() {
const { selectedRecords, inspectorContent } = useInspector();
const recordType = inspectorContent?.dataSource?.recordType;
const markerFeatures = selectedRecords
.map((r): MarkerFeature | null => {
if (!r.dataSourceId || !r.geocodePoint) {
// Should never happen for markers in a cluster
return null;
}
return {
type: "Feature",
geometry: {
// [0, 0] should never happen because these records are in a cluster on the map
coordinates: [r.geocodePoint?.lng || 0, r.geocodePoint?.lat || 0],
type: "Point",
},
properties: {
id: r.id,
name: r.name,
dataSourceId: r.dataSourceId,
matched: true,
},
};
Comment on lines +15 to +28
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

Records without valid geocodePoints should be filtered out rather than creating markers with coordinates [0, 0]. Consider adding a null check for r.geocodePoint and returning null for records that don't have valid coordinates, or check if both lng and lat exist and are not 0 before creating the feature.

Copilot uses AI. Check for mistakes.
})
.filter((r) => r !== null);
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

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

The filter method doesn't properly narrow the TypeScript type from (MarkerFeature | null)[] to MarkerFeature[]. TypeScript's built-in filter doesn't use a type predicate, so markerFeatures will still have type (MarkerFeature | null)[] instead of MarkerFeature[]. This could cause type errors when passing markerFeatures to MembersList or MarkersList components. Consider using a type predicate or type assertion: .filter((r): r is MarkerFeature => r !== null)

Suggested change
.filter((r) => r !== null);
.filter((r): r is MarkerFeature => r !== null);

Copilot uses AI. Check for mistakes.

return (
<div className="flex flex-col gap-6">
{recordType === DataSourceRecordType.Members ? (
<MembersList
dataSource={inspectorContent?.dataSource}
markers={markerFeatures}
areaType="cluster"
/>
) : (
<MarkersList
dataSource={inspectorContent?.dataSource}
markers={markerFeatures}
/>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions src/app/map/[id]/components/inspector/InspectorMarkersTab.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { LayerType } from "@/types";
import BoundaryMarkersList from "./BoundaryMarkersList";
import ClusterMarkersList from "./ClusterMarkersList";
import TurfMarkersList from "./TurfMarkersList";

interface InspectorMarkersTabProps {
Expand All @@ -11,6 +12,7 @@ export default function InspectorMarkersTab({
}: InspectorMarkersTabProps) {
return (
<div className="flex flex-col gap-4">
{type === LayerType.Cluster && <ClusterMarkersList />}
{type === LayerType.Turf && <TurfMarkersList />}
{type === LayerType.Boundary && <BoundaryMarkersList />}
</div>
Expand Down
75 changes: 50 additions & 25 deletions src/app/map/[id]/components/inspector/InspectorPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ArrowLeftIcon, SettingsIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { useMemo, useState } from "react";

import { useInspector } from "@/app/map/[id]/hooks/useInspector";
import { cn } from "@/shadcn/utils";
Expand All @@ -26,12 +26,26 @@ export default function InspectorPanel() {
setFocusedRecord,
selectedRecords,
} = useInspector();
const { dataSource, properties, type } = inspectorContent ?? {};

const safeActiveTab = useMemo(() => {
if (activeTab === "data" && type === LayerType.Cluster) {
return "markers";
}
const isMarkers = type === LayerType.Marker || type === LayerType.Member;
if (activeTab === "markers" && isMarkers) {
return "data";
}
if (activeTab === "config" && type !== LayerType.Boundary) {
return type === LayerType.Cluster ? "markers" : "data";
}
return activeTab;
}, [activeTab, type]);

if (!Boolean(inspectorContent)) {
return <></>;
}

const { dataSource, properties, type } = inspectorContent ?? {};
const isDetailsView = Boolean(
(selectedTurf && type !== LayerType.Turf) ||
(selectedBoundary && type !== LayerType.Boundary),
Expand All @@ -49,17 +63,17 @@ export default function InspectorPanel() {
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
activeTab === "config" ? "h-full" : "h-fit max-h-full",
safeActiveTab === "config" ? "h-full" : "h-fit max-h-full",
)}
style={{
minWidth: activeTab === "config" ? "400px" : "250px",
minWidth: safeActiveTab === "config" ? "400px" : "250px",
maxWidth: "450px",
}}
>
<div
className={cn(
"relative z-50 w-full overflow-auto / flex flex-col / rounded shadow-lg bg-white / text-sm font-sans",
activeTab === "config" ? "h-full" : "max-h-full",
safeActiveTab === "config" ? "h-full" : "max-h-full",
)}
>
<div className="flex justify-between items-center gap-4 p-3">
Expand Down Expand Up @@ -98,12 +112,14 @@ export default function InspectorPanel() {

<UnderlineTabs
defaultValue="data"
value={activeTab}
value={safeActiveTab}
onValueChange={setActiveTab}
className="flex flex-col overflow-hidden h-full"
>
<UnderlineTabsList className="w-full flex gap-6 border-t px-3">
<UnderlineTabsTrigger value="data">Data</UnderlineTabsTrigger>
{type !== LayerType.Cluster && (
<UnderlineTabsTrigger value="data">Data</UnderlineTabsTrigger>
)}
<UnderlineTabsTrigger
value="markers"
className={cn(
Expand All @@ -116,20 +132,27 @@ export default function InspectorPanel() {
<UnderlineTabsTrigger value="notes" className="hidden">
Notes 0
</UnderlineTabsTrigger>
<UnderlineTabsTrigger value="config" className="px-2">
<SettingsIcon size={16} />
</UnderlineTabsTrigger>
{type === LayerType.Boundary && (
<UnderlineTabsTrigger value="config" className="px-2">
<SettingsIcon size={16} />
</UnderlineTabsTrigger>
)}
</UnderlineTabsList>

<UnderlineTabsContent value="data" className="grow overflow-auto p-3">
<InspectorDataTab
dataSource={dataSource}
properties={properties}
isDetailsView={isDetailsView}
focusedRecord={focusedRecord}
type={type}
/>
</UnderlineTabsContent>
{type !== LayerType.Cluster && (
<UnderlineTabsContent
value="data"
className="grow overflow-auto p-3"
>
<InspectorDataTab
dataSource={dataSource}
properties={properties}
isDetailsView={isDetailsView}
focusedRecord={focusedRecord}
type={type}
/>
</UnderlineTabsContent>
)}

<UnderlineTabsContent
value="markers"
Expand All @@ -145,12 +168,14 @@ export default function InspectorPanel() {
<InspectorNotesTab />
</UnderlineTabsContent>

<UnderlineTabsContent
value="config"
className="grow overflow-auto p-3 h-full"
>
<InspectorConfigTab />
</UnderlineTabsContent>
{type === LayerType.Boundary && (
<UnderlineTabsContent
value="config"
className="grow overflow-auto p-3 h-full"
>
<InspectorConfigTab />
</UnderlineTabsContent>
)}
</UnderlineTabs>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions src/app/map/[id]/components/inspector/MarkersLists.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export const MembersList = ({
areaType,
}: {
markers: MarkerFeature[];
dataSource: DataSource | null;
areaType: "area" | "boundary";
dataSource: DataSource | undefined | null;
areaType: "area" | "boundary" | "cluster";
}) => {
const { setSelectedRecords } = useInspector();

Expand Down Expand Up @@ -68,7 +68,7 @@ export const MarkersList = ({
dataSource,
}: {
markers: MarkerFeature[];
dataSource: DataSource | null;
dataSource: DataSource | undefined | null;
}) => {
const { setSelectedRecords } = useInspector();
const total = markers.length;
Expand Down
18 changes: 13 additions & 5 deletions src/app/map/[id]/hooks/useInspector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,26 +71,34 @@ export function useInspector() {
}

const dataSourceId = focusedRecord.dataSourceId;

const dataSource = dataSourceId ? getDataSourceById(dataSourceId) : null;
if (selectedRecords.length > 1) {
return {
type: LayerType.Cluster,
name: "Cluster",
properties: null,
dataSource,
};
}

const type =
dataSourceId === mapConfig.membersDataSourceId
? LayerType.Member
: LayerType.Marker;

return {
type: type,
type,
name: focusedRecord.name,
properties: null,
dataSource: dataSource,
dataSource,
};
}, [
focusedRecord,
getDataSourceById,
mapConfig.membersDataSourceId,
selectedBoundary,
selectedTurf?.id,
selectedTurf?.name,
selectedRecords.length,
selectedTurf,
]);

const resetInspector = useCallback(() => {
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export interface FilterField {

export enum LayerType {
Boundary = "Boundary",
Cluster = "Cluster",
Member = "Member",
Marker = "Marker",
Turf = "Turf",
Expand Down
Loading