-
-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/consult 5055 #271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/consult 5055 #271
Changes from all commits
00c8c2f
107c0f2
9105bd9
dcfdd26
6735019
fd44ca3
bfcc0db
ec6d344
4f898ae
7dc79fa
58c3bf8
ed30a86
a4de620
5c79e61
f66d9b5
7920dee
89cbaad
02419ef
66edd19
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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(); | ||
| } |
| 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} | ||
|
|
||
| {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> | ||
| ); | ||
| } | ||
| 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> | ||
| ); | ||
| } |
| 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
|
||
| 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> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
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.