diff --git a/src/app/(private)/(dashboards)/superadmin/data-sources/[id]/components/InspectorConfigSection.tsx b/src/app/(private)/(dashboards)/superadmin/data-sources/[id]/components/InspectorConfigSection.tsx index 8041b00f..6b5c8e8f 100644 --- a/src/app/(private)/(dashboards)/superadmin/data-sources/[id]/components/InspectorConfigSection.tsx +++ b/src/app/(private)/(dashboards)/superadmin/data-sources/[id]/components/InspectorConfigSection.tsx @@ -13,7 +13,11 @@ import { GripVertical, LayoutGrid, LayoutList } from "lucide-react"; import { useCallback, useMemo } from "react"; import { v4 as uuidv4 } from "uuid"; import ColumnMetadataIcons from "@/app/(private)/map/[id]/components/ColumnMetadataIcons"; -import { INSPECTOR_COLOR_OPTIONS } from "@/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions"; +import { + DEFAULT_BAR_COLOR_VALUE, + INSPECTOR_BAR_COLOR_OPTIONS, + INSPECTOR_COLOR_OPTIONS, +} from "@/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions"; import { useDataSourceColumn } from "@/app/(private)/map/[id]/hooks/useDataSourceColumn"; import { useInspectorColumn } from "@/app/(private)/map/[id]/hooks/useInspectorColumn"; import { NULL_UUID } from "@/constants"; @@ -580,6 +584,26 @@ function ColumnItemRow({ )} + {inspectorColumn && + (inspectorColumn.displayFormat === ColumnDisplayFormat.Percentage || + inspectorColumn.displayFormat === ColumnDisplayFormat.Scale) && ( + + )} + {inspectorColumn?.displayFormat === ColumnDisplayFormat.Scale && ( ) { const num = parseColumnNumber(value, { isCount: false, columnMetadata, }); const fill = barFill( - getBarColorForLabel( - inspectorColumn.name, - columnMetadata?.displayName, - inspectorColumn.barColor, - ), + getBarColorForLabel({ + columnName: inspectorColumn.name, + displayName: columnMetadata?.displayName, + barColor: inspectorColumn.barColor, + inspectorColor, + }), ); if (num === null) { @@ -229,17 +233,19 @@ function ScaleValue({ value, inspectorColumn, columnMetadata, + inspectorColor, }: Omit) { const num = parseColumnNumber(value, { isCount: false, columnMetadata, }); const fill = barFill( - getBarColorForLabel( - inspectorColumn.name, - columnMetadata?.displayName, - inspectorColumn.barColor, - ), + getBarColorForLabel({ + columnName: inspectorColumn.name, + displayName: columnMetadata?.displayName, + barColor: inspectorColumn.barColor, + inspectorColor, + }), ); if (num === null) { @@ -275,6 +281,7 @@ function DataRecordPropertyValue({ }) { const { columnMetadata, columnDef } = useDataSourceColumn(dataSourceId, name); const inspectorColumn = useInspectorColumn(dataSourceId, name); + const inspectorConfig = useInspectorDataSourceConfig(dataSourceId); const format = inspectorColumn?.displayFormat; @@ -310,6 +317,7 @@ function DataRecordPropertyValue({ value={value} inspectorColumn={inspectorColumn} columnMetadata={columnMetadata} + inspectorColor={inspectorConfig?.color} /> ); } @@ -320,6 +328,7 @@ function DataRecordPropertyValue({ value={value} inspectorColumn={inspectorColumn} columnMetadata={columnMetadata} + inspectorColor={inspectorConfig?.color} /> ); } diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableColumnRow.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableColumnRow.tsx deleted file mode 100644 index b4499f34..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableColumnRow.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { Checkbox } from "@/shadcn/ui/checkbox"; -import { cn } from "@/shadcn/utils"; - -/** Simple row for the Available list: checkbox to add, no drag. */ -export function AvailableColumnRow({ - columnName, - onAdd, -}: { - columnName: string; - onAdd: () => void; -}) { - return ( -
- checked === true && onAdd()} - aria-label={`Add ${columnName} to columns to show`} - /> - {columnName} -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableListWithDividers.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableListWithDividers.tsx deleted file mode 100644 index 4a053b7d..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/AvailableListWithDividers.tsx +++ /dev/null @@ -1,127 +0,0 @@ -"use client"; - -import { useDroppable } from "@dnd-kit/core"; -import { - SortableContext, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { cn } from "@/shadcn/utils"; -import { AvailableColumnRow } from "./AvailableColumnRow"; -import { SELECTED_LEFT_DROPPABLE_ID } from "./constants"; -import { SortableAvailableRow } from "./SortableAvailableRow"; -import { SortableDividerRow } from "./SortableDividerRow"; -import type { InspectorItem } from "@/models/MapView"; - -export function AvailableListWithDividers({ - selectedItemsInOrder, - selectedSectionIds, - availableColumns, - onAddColumn, - onRemoveColumn, - onAddDivider, - onDividerLabelChange, - onRemoveDivider, - activeId, - mode = "both", -}: { - selectedItemsInOrder: InspectorItem[]; - selectedSectionIds: string[]; - availableColumns: string[]; - onAddColumn: (columnName: string) => void; - onRemoveColumn: (columnName: string) => void; - onAddDivider: () => void; - onDividerLabelChange: (id: string, label: string) => void; - onRemoveDivider: (id: string) => void; - activeId: string | null; - mode?: "both" | "selected" | "available"; -}) { - const { setNodeRef, isOver } = useDroppable({ - id: SELECTED_LEFT_DROPPABLE_ID, - }); - - return ( -
- {mode !== "available" && ( - <> -
- -
-
-

- Selected -

-
- - {selectedItemsInOrder.length === 0 ? ( -

- Tick columns below to add -

- ) : ( - selectedItemsInOrder.map((item, i) => - item.type === "column" ? ( - onRemoveColumn(item.name)} - isDragging={activeId === selectedSectionIds[i]} - /> - ) : ( - - onDividerLabelChange(item.id, value) - } - onRemove={() => onRemoveDivider(item.id)} - isDragging={activeId === selectedSectionIds[i]} - /> - ), - ) - )} -
-
-
- - )} - - {mode !== "selected" && ( -
-
- {availableColumns.length === 0 ? ( -

- All columns selected -

- ) : ( - availableColumns.map((col) => ( - onAddColumn(col)} - /> - )) - )} -
-
- )} -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/ColumnsSection.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/ColumnsSection.tsx deleted file mode 100644 index d6fddf49..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/ColumnsSection.tsx +++ /dev/null @@ -1,333 +0,0 @@ -"use client"; - -import { - DndContext, - DragOverlay, - KeyboardSensor, - PointerSensor, - closestCenter, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { sortableKeyboardCoordinates } from "@dnd-kit/sortable"; -import { useCallback, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; -import { v4 as uuidv4 } from "uuid"; -import { Label } from "@/shadcn/ui/label"; -import { AvailableListWithDividers } from "./AvailableListWithDividers"; -import { SELECTED_DROPPABLE_ID, SELECTED_LEFT_DROPPABLE_ID } from "./constants"; -import { - AvailableDragPreview, - ColumnDragPreview, - DividerDragPreview, -} from "./DragPreviews"; -import { DroppableSelectedColumns } from "./DroppableSelectedColumns"; -import type { InspectorDataSourceConfig } from "@/models/InspectorDataSourceConfig"; -import type { InspectorItem } from "@/models/MapView"; -import type { DragEndEvent } from "@dnd-kit/core"; - -function isDivider( - item: InspectorItem, -): item is { type: "divider"; id: string; label: string } { - return item.type === "divider"; -} - -function isColumn( - item: InspectorItem, -): item is Extract { - return item.type === "column"; -} - -export function ColumnsSection({ - selectedColumnsInOrder, - selectedItemsInOrder, - availableColumns, - columnIds, - updateConfig, - handleAddColumn, - handleRemoveColumn, - handleRemoveColumnFromRight, -}: { - config: InspectorDataSourceConfig; - selectedColumnsInOrder: string[]; - selectedItemsInOrder: InspectorItem[]; - availableColumns: string[]; - columnIds: string[]; - updateConfig: ( - updater: (prev: InspectorDataSourceConfig) => InspectorDataSourceConfig, - ) => void; - handleAddColumn: (colName: string) => void; - handleRemoveColumn: (colName: string) => void; - handleRemoveColumnFromRight: (colName: string) => void; -}) { - const [activeId, setActiveId] = useState(null); - - const sensors = useSensors( - useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }), - ); - - const selectedSectionIds = useMemo( - () => - selectedItemsInOrder.map((item, i) => - isColumn(item) - ? `left-selected-${i}-${item.name}` - : `divider-${item.id}`, - ), - [selectedItemsInOrder], - ); - - const columnItems = useMemo( - () => selectedItemsInOrder.filter(isColumn), - [selectedItemsInOrder], - ); - - const isLeftSelectedItem = (s: string) => - s.startsWith("left-selected-") || s.startsWith("divider-"); - - const handleDragEnd = useCallback( - (event: DragEndEvent) => { - const { active, over } = event; - setActiveId(null); - const activeStr = String(active.id); - const overStr = over ? String(over.id) : null; - - if (isLeftSelectedItem(activeStr) && overStr) { - const oldIndex = selectedSectionIds.indexOf(activeStr); - if (oldIndex === -1) return; - const newIndex = - overStr === SELECTED_LEFT_DROPPABLE_ID - ? selectedSectionIds.length - : selectedSectionIds.indexOf(overStr); - if ( - !isLeftSelectedItem(overStr) && - overStr !== SELECTED_LEFT_DROPPABLE_ID - ) - return; - if (newIndex === -1 && overStr !== SELECTED_LEFT_DROPPABLE_ID) return; - const next = [...selectedItemsInOrder]; - const [removed] = next.splice(oldIndex, 1); - next.splice(newIndex, 0, removed); - updateConfig((prev) => ({ - ...prev, - items: next, - })); - return; - } - - const isSelectedItem = (s: string) => s.startsWith("col-"); - if (isSelectedItem(activeStr) && overStr) { - const oldIndex = columnIds.indexOf(activeStr); - if (oldIndex === -1) return; - const newIndex = - overStr === SELECTED_DROPPABLE_ID - ? columnIds.length - : columnIds.indexOf(overStr); - if (!isSelectedItem(overStr) && overStr !== SELECTED_DROPPABLE_ID) - return; - if (newIndex === -1 && overStr !== SELECTED_DROPPABLE_ID) return; - const nextColOrder = [...selectedColumnsInOrder]; - const [removed] = nextColOrder.splice(oldIndex, 1); - nextColOrder.splice(newIndex, 0, removed); - updateConfig((prev) => { - const colItemsMap = new Map( - (prev.items ?? []).filter(isColumn).map((ci) => [ci.name, ci]), - ); - const newColItems = nextColOrder - .map((name) => colItemsMap.get(name)) - .filter((c) => c !== undefined); - let colIdx = 0; - return { - ...prev, - items: (prev.items ?? []).map((item) => - isColumn(item) ? newColItems[colIdx++] : item, - ), - }; - }); - return; - } - }, - [ - columnIds, - selectedSectionIds, - selectedItemsInOrder, - updateConfig, - selectedColumnsInOrder, - ], - ); - - return ( -
-
-
- -

- Tick columns in Available to add. Reorder with the handle. -

-
-
- - -
-
- setActiveId(active.id as string)} - onDragEnd={handleDragEnd} - > -
- {/* Available columns */} -
-

- Available columns -

- - updateConfig((prev) => ({ - ...prev, - items: [ - ...(prev.items ?? []), - { type: "divider" as const, id: uuidv4(), label: "" }, - ], - })) - } - onDividerLabelChange={(id, label) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((i) => - isDivider(i) && i.id === id ? { ...i, label } : i, - ), - })) - } - onRemoveDivider={(id) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).filter( - (i) => !(isDivider(i) && i.id === id), - ), - })) - } - activeId={activeId} - mode="available" - /> -
- - {/* Selected columns */} -
-

- Selected columns -

- - updateConfig((prev) => ({ - ...prev, - items: [ - ...(prev.items ?? []), - { type: "divider" as const, id: uuidv4(), label: "" }, - ], - })) - } - onDividerLabelChange={(id, label) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((i) => - isDivider(i) && i.id === id ? { ...i, label } : i, - ), - })) - } - onRemoveDivider={(id) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).filter( - (i) => !(isDivider(i) && i.id === id), - ), - })) - } - activeId={activeId} - mode="selected" - /> -
- - {/* Selected column settings */} -
-

- Column settings -

- -
-
- {typeof document !== "undefined" && - createPortal( - - {activeId && String(activeId).startsWith("col-") ? ( - - ) : activeId && String(activeId).startsWith("divider-") ? ( - { - const item = selectedItemsInOrder.find( - (i) => - isDivider(i) && `divider-${i.id}` === String(activeId), - ); - return (item && isDivider(item) ? item.label : "") ?? ""; - })()} - /> - ) : activeId && - (String(activeId).startsWith("available-") || - String(activeId).startsWith("left-selected-")) ? ( - - ) : null} - , - document.body, - )} -
-
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DragPreviews.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DragPreviews.tsx deleted file mode 100644 index 81adab6c..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DragPreviews.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; - -import { GripVertical, Minus } from "lucide-react"; - -export function ColumnDragPreview({ activeId }: { activeId: string }) { - const parts = activeId.startsWith("col-") ? activeId.split("-") : []; - // id format: col-{index}-{colName} - const columnName = parts.length >= 3 ? parts.slice(2).join("-") : ""; - - return ( -
-
- - - {columnName} - -
-
- ); -} - -export function DividerDragPreview({ label }: { label: string }) { - return ( -
- - - - {label || "Section label"} - -
- ); -} - -export function AvailableDragPreview({ activeId }: { activeId: string }) { - let columnName = ""; - if (activeId.startsWith("available-")) { - columnName = activeId.includes("::") - ? activeId.slice(activeId.indexOf("::") + 2) - : activeId.slice("available-".length); - } else if (activeId.startsWith("left-selected-")) { - const match = activeId.match(/^left-selected-\d+-/); - columnName = match ? activeId.slice(match[0].length) : ""; - } - return ( -
- - - {columnName} - -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DroppableSelectedColumns.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DroppableSelectedColumns.tsx deleted file mode 100644 index a9099788..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/DroppableSelectedColumns.tsx +++ /dev/null @@ -1,132 +0,0 @@ -"use client"; - -import { useDroppable } from "@dnd-kit/core"; -import { - SortableContext, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { useMemo } from "react"; -import { cn } from "@/shadcn/utils"; -import { SortableColumnRow } from "../../SortableColumnRow"; -import { SELECTED_DROPPABLE_ID } from "./constants"; -import type { InspectorDataSourceConfig } from "@/models/InspectorDataSourceConfig"; -import type { InspectorItem } from "@/models/MapView"; - -type ColumnItem = Extract; - -export function DroppableSelectedColumns({ - columns, - columnItems, - updateConfig, - onRemoveColumn, - activeId, -}: { - columns: string[]; - columnItems: ColumnItem[]; - updateConfig: ( - updater: (prev: InspectorDataSourceConfig) => InspectorDataSourceConfig, - ) => void; - onRemoveColumn?: (columnName: string) => void; - activeId: string | null; -}) { - const { setNodeRef, isOver } = useDroppable({ id: SELECTED_DROPPABLE_ID }); - const columnIds = useMemo( - () => columns.map((c, i) => `col-${i}-${c}`), - [columns], - ); - - const isEmpty = columns.length === 0; - - return ( -
- {isEmpty ? ( -

- No columns — tick Available to add -

- ) : ( - -
- {columns.map((col, i) => { - const item = columnItems.find((ci) => ci.name === col); - return ( - - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((ci) => - ci.type === "column" && ci.name === col - ? { ...ci, displayFormat: format } - : ci, - ), - })) - } - comparisonStat={item?.comparisonStat} - onComparisonStatChange={(comparisonStat) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((ci) => - ci.type === "column" && ci.name === col - ? { ...ci, comparisonStat } - : ci, - ), - })) - } - scaleMax={item?.scaleMax ?? 3} - onScaleMaxChange={(scaleMax) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((ci) => - ci.type === "column" && ci.name === col - ? { ...ci, scaleMax } - : ci, - ), - })) - } - barColor={item?.barColor} - onBarColorChange={(value) => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).map((ci) => - ci.type === "column" && ci.name === col - ? { ...ci, barColor: value || undefined } - : ci, - ), - })) - } - onRemove={ - onRemoveColumn - ? () => onRemoveColumn(col) - : () => - updateConfig((prev) => ({ - ...prev, - items: (prev.items ?? []).filter( - (ci) => - !(ci.type === "column" && ci.name === col), - ), - })) - } - isDragging={activeId === columnIds[i]} - /> - ); - })} -
-
- )} -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableAvailableRow.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableAvailableRow.tsx deleted file mode 100644 index 87689b5c..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableAvailableRow.tsx +++ /dev/null @@ -1,67 +0,0 @@ -"use client"; - -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { GripVertical } from "lucide-react"; -import { Checkbox } from "@/shadcn/ui/checkbox"; -import { cn } from "@/shadcn/utils"; - -export function SortableAvailableRow({ - id, - columnName, - selected, - onToggle, - isDragging, -}: { - id: string; - columnName: string; - selected: boolean; - onToggle: (checked: boolean) => void; - isDragging?: boolean; -}) { - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging: dndDragging, - } = useSortable({ id }); - const dragging = isDragging ?? dndDragging; - const style = dragging - ? { transition } - : { - transform: CSS.Transform.toString(transform), - transition, - }; - return ( -
- - onToggle(checked === true)} - aria-label={ - selected - ? `Remove ${columnName} from columns to show` - : `Add ${columnName} to columns to show` - } - /> - {columnName} -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableDividerRow.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableDividerRow.tsx deleted file mode 100644 index 91ca39cf..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/SortableDividerRow.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { GripVertical, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { Input } from "@/shadcn/ui/input"; -import { cn } from "@/shadcn/utils"; -import { useDebouncedCallback } from "../../../hooks/useDebouncedCallback"; - -export function SortableDividerRow({ - id, - label, - onLabelChange, - onRemove, - isDragging, -}: { - id: string; - label: string; - onLabelChange: (value: string) => void; - onRemove?: () => void; - isDragging?: boolean; -}) { - const [localLabel, setLocalLabel] = useState(label); - useEffect(() => setLocalLabel(label), [label]); - const debouncedChange = useDebouncedCallback(onLabelChange, 600); - - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging: dndDragging, - } = useSortable({ id }); - - const dragging = isDragging ?? dndDragging; - const style = dragging - ? { transition } - : { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( -
- - { - const v = e.target.value; - setLocalLabel(v); - debouncedChange(v); - }} - onClick={(e) => e.stopPropagation()} - /> - {onRemove && ( - - )} -
- ); -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/constants.ts b/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/constants.ts deleted file mode 100644 index a8eb3aad..00000000 --- a/src/app/(private)/map/[id]/components/InspectorPanel/InspectorSettingsModal/constants.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ColumnDisplayFormat } from "@/models/shared"; - -export const SELECTED_DROPPABLE_ID = "selected-columns"; -export const AVAILABLE_DROPPABLE_ID = "available-columns"; -/** Droppable id for the left column's "Selected" section (reorder selected items). */ -export const SELECTED_LEFT_DROPPABLE_ID = "selected-left-section"; - -export type InspectorLayout = "single" | "twoColumn"; - -/** Infer column format from name: Percentage if name contains % or "percentage". */ -export function inferFormat( - columnName: string, -): ColumnDisplayFormat | undefined { - const lower = columnName.toLowerCase(); - if (lower.includes("%") || lower.includes("percentage")) - return ColumnDisplayFormat.Percentage; - return undefined; -} diff --git a/src/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions.tsx b/src/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions.tsx index 8c943bae..ae1946b2 100644 --- a/src/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions.tsx +++ b/src/app/(private)/map/[id]/components/InspectorPanel/inspectorPanelOptions.tsx @@ -122,7 +122,7 @@ interface BarColorOption { } export const INSPECTOR_BAR_COLOR_OPTIONS: BarColorOption[] = [ - { value: DEFAULT_BAR_COLOR_VALUE, label: "Default (primary)", hex: "" }, + { value: DEFAULT_BAR_COLOR_VALUE, label: "Default", hex: "" }, { value: SMART_MATCH_BAR_COLOR_VALUE, label: "Smart match", hex: "" }, { value: "#3b82f6", label: "Blue", hex: "#3b82f6" }, { value: "#22c55e", label: "Green", hex: "#22c55e" }, @@ -140,7 +140,10 @@ export function getSmartMatchInfo( columnName: string, displayName?: string, ): { color: string; matchLabel: string } { - const combined = `${displayName} ${columnName}`.toLowerCase().trim(); + const combined = [columnName, displayName] + .map((s) => (s ? s.trim() : "")) + .join(" ") + .toLowerCase(); // Exact match first (e.g. column named "ruk" or "lab") if (PARTY_COLORS[combined]) { return { color: PARTY_COLORS[combined], matchLabel: combined }; @@ -154,17 +157,39 @@ export function getSmartMatchInfo( return { color: "var(--primary)", matchLabel: "default" }; } -export function getBarColorForLabel( - columnName: string, - displayName?: string | undefined, - barColor?: string | null, -): string { - if ( - !barColor || - barColor === DEFAULT_BAR_COLOR_VALUE || - barColor === SMART_MATCH_BAR_COLOR_VALUE - ) { +const INSPECTOR_COLOR_HEX_MAP: Record = { + blue: "#3b82f6", + green: "#22c55e", + yellow: "#f59e0b", + red: "#ef4444", + purple: "#8b5cf6", + pink: "#ec4899", + orange: "#f97316", + teal: "#14b8a6", + indigo: "#6366f1", + gray: "#6b7280", +}; + +export function getBarColorForLabel({ + columnName, + displayName, + barColor, + inspectorColor, +}: { + columnName: string; + displayName?: string | undefined; + barColor?: string | null; + inspectorColor?: string | null; +}): string { + if (barColor === SMART_MATCH_BAR_COLOR_VALUE) { return getSmartMatchInfo(columnName, displayName).color; } + // Default / no explicit bar color: use the inspector config color + if (!barColor || barColor === DEFAULT_BAR_COLOR_VALUE) { + if (inspectorColor && INSPECTOR_COLOR_HEX_MAP[inspectorColor]) { + return INSPECTOR_COLOR_HEX_MAP[inspectorColor]; + } + return "var(--primary)"; + } return barColor; } diff --git a/src/app/(private)/map/[id]/components/SortableColumnRow.tsx b/src/app/(private)/map/[id]/components/SortableColumnRow.tsx deleted file mode 100644 index 1b53e88c..00000000 --- a/src/app/(private)/map/[id]/components/SortableColumnRow.tsx +++ /dev/null @@ -1,384 +0,0 @@ -"use client"; - -import { useSortable } from "@dnd-kit/sortable"; -import { CSS } from "@dnd-kit/utilities"; -import { GripVertical, X } from "lucide-react"; -import { type MouseEvent, useEffect, useState } from "react"; -import { ColumnDisplayFormat, InspectorComparisonStat } from "@/models/shared"; -import { Input } from "@/shadcn/ui/input"; -import { Label } from "@/shadcn/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shadcn/ui/select"; -import { cn } from "@/shadcn/utils"; -import { useDebouncedCallback } from "../hooks/useDebouncedCallback"; -import { - DEFAULT_BAR_COLOR_VALUE, - INSPECTOR_BAR_COLOR_OPTIONS, - SMART_MATCH_BAR_COLOR_VALUE, - getSmartMatchInfo, -} from "./InspectorPanel/inspectorPanelOptions"; - -const FORMAT_OPTIONS: { value: ColumnDisplayFormat; label: string }[] = [ - { value: ColumnDisplayFormat.Text, label: "Text" }, - { value: ColumnDisplayFormat.Number, label: "Number" }, - { - value: ColumnDisplayFormat.NumberWithComparison, - label: "Number with comparison", - }, - { value: ColumnDisplayFormat.Percentage, label: "Percentage (bar)" }, - { value: ColumnDisplayFormat.Scale, label: "Scale (bars)" }, -]; - -const COMPARISON_STAT_OPTIONS: { - value: InspectorComparisonStat; - label: string; -}[] = [ - { value: InspectorComparisonStat.Average, label: "Average" }, - { value: InspectorComparisonStat.Median, label: "Median" }, - { value: InspectorComparisonStat.Min, label: "Min" }, - { value: InspectorComparisonStat.Max, label: "Max" }, -]; - -function barColorSelectValue(barColor: string | undefined): string { - if (barColor === DEFAULT_BAR_COLOR_VALUE) return DEFAULT_BAR_COLOR_VALUE; - if (!barColor || barColor === "" || barColor === SMART_MATCH_BAR_COLOR_VALUE) - return SMART_MATCH_BAR_COLOR_VALUE; - return barColor; -} - -function BarColorSelect({ - barColor, - onBarColorChange, - displayName, - columnName, - onClick, -}: { - barColor?: string; - onBarColorChange: (value: string) => void; - displayName: string | undefined; - columnName: string; - onClick: (e: MouseEvent) => void; -}) { - const value = barColorSelectValue(barColor); - const smartMatch = getSmartMatchInfo(displayName ?? columnName, columnName); - - const triggerLabel = - value === DEFAULT_BAR_COLOR_VALUE - ? "Default" - : value === SMART_MATCH_BAR_COLOR_VALUE - ? `Smart match (${smartMatch.matchLabel})` - : null; - - const triggerSwatchColor = - value === DEFAULT_BAR_COLOR_VALUE - ? "var(--primary)" - : value === SMART_MATCH_BAR_COLOR_VALUE - ? smartMatch.color - : null; - - return ( -
- - -
- ); -} - -export function SortableColumnRow({ - id, - columnName, - displayName, - onDisplayNameChange, - description, - onDescriptionChange, - format = ColumnDisplayFormat.Text, - onFormatChange, - comparisonStat, - onComparisonStatChange, - scaleMax = 3, - onScaleMaxChange, - barColor, - onBarColorChange, - onRemove, - isDragging, -}: { - id: string; - columnName: string; - displayName?: string; - onDisplayNameChange?: (value: string) => void; - description?: string; - onDescriptionChange?: (value: string) => void; - format?: ColumnDisplayFormat; - onFormatChange?: (format: ColumnDisplayFormat) => void; - comparisonStat?: InspectorComparisonStat; - onComparisonStatChange?: (value: InspectorComparisonStat) => void; - scaleMax?: number; - onScaleMaxChange?: (value: number) => void; - barColor?: string; - onBarColorChange?: (value: string) => void; - onRemove?: () => void; - isDragging?: boolean; -}) { - const [localDisplayName, setLocalDisplayName] = useState(displayName ?? ""); - useEffect(() => setLocalDisplayName(displayName ?? ""), [displayName]); - const debouncedChange = useDebouncedCallback( - (v: string) => onDisplayNameChange?.(v), - 600, - ); - - const [localDescription, setLocalDescription] = useState(description ?? ""); - useEffect(() => setLocalDescription(description ?? ""), [description]); - const debouncedDescriptionChange = useDebouncedCallback( - (v: string) => onDescriptionChange?.(v), - 600, - ); - - const [localScaleMax, setLocalScaleMax] = useState(String(scaleMax)); - useEffect(() => setLocalScaleMax(String(scaleMax)), [scaleMax]); - const debouncedScaleMax = useDebouncedCallback( - (v: number) => onScaleMaxChange?.(v), - 600, - ); - - const { - attributes, - listeners, - setNodeRef, - transform, - transition, - isDragging: dndDragging, - } = useSortable({ id }); - - const dragging = isDragging ?? dndDragging; - const style = dragging - ? { transition } - : { - transform: CSS.Transform.toString(transform), - transition, - }; - - return ( -
-
- - - {columnName} - - {onRemove && ( - - )} -
-
- - { - const v = e.target.value; - setLocalDisplayName(v); - debouncedChange(v); - }} - onClick={(e) => e.stopPropagation()} - /> -
- {onDescriptionChange && ( -
- - { - const v = e.target.value; - setLocalDescription(v); - debouncedDescriptionChange(v); - }} - onClick={(e) => e.stopPropagation()} - /> -
- )} - {onFormatChange && ( -
- - -
- )} - {format === ColumnDisplayFormat.Scale && onScaleMaxChange && ( -
- - { - setLocalScaleMax(e.target.value); - const n = parseInt(e.target.value, 10); - if (!Number.isNaN(n) && n >= 2 && n <= 10) { - debouncedScaleMax(n); - } - }} - onClick={(e) => e.stopPropagation()} - /> -
- )} - {format === ColumnDisplayFormat.NumberWithComparison && - onComparisonStatChange && ( -
- - -
- )} - {(format === ColumnDisplayFormat.Percentage || - format === ColumnDisplayFormat.Scale) && - onBarColorChange && ( - e.stopPropagation()} - /> - )} -
- ); -}