Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
84 changes: 83 additions & 1 deletion apps/geolibre-desktop/src/components/panels/DashboardPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,7 @@ function WidgetCard({
const data = useLayerChartData(widget.layerId);
const result = useMemo(
() =>
widget.type === "indicator"
widget.type === "indicator" || widget.type === "selector"
? null
: computeChart(data.rows, widgetToSpec(widget, widget.type)),
[data.rows, widget],
Expand Down Expand Up @@ -407,6 +407,8 @@ function WidgetCard({
const aggLabel = t(`dashboard.indicatorAggregation.${agg}`);
return widget.field ? `${aggLabel} · ${widget.field}` : aggLabel;
}
case "selector":
return `${t("dashboard.chartType.selector")} · ${widget.category ?? ""}`;
}
};
const title = widget.title?.trim() || defaultWidgetTitle();
Expand Down Expand Up @@ -496,6 +498,42 @@ function WidgetCard({
);
})()}
</div>
) : widget.type === "selector" ? (
<div className="flex min-h-0 flex-1 flex-col gap-1.5 overflow-auto">
{(() => {
if (!widget.category || !data.hasData) {
return (
<p className="text-center text-xs text-muted-foreground">{t("dashboard.noData")}</p>
);
}
// Extract distinct values from the category field, sorted.
const cat = widget.category!;
const values = Array.from(
new Set(
data.rows
.map((row) => String((row as unknown as Record<string, unknown>)[cat] ?? ""))
.filter((v) => v !== ""),
),
).sort((a, b) => a.localeCompare(b));

if (values.length === 0) {
return (
<p className="text-center text-xs text-muted-foreground">{t("dashboard.noData")}</p>
);
}

// Keyed on the selector config so switching field or single/multi
// mode remounts with an empty selection instead of carrying over
// values that no longer apply.
return (
<SelectorValues
key={`${cat}:${widget.multiple ?? false}`}
values={values}
multiple={widget.multiple ?? false}
/>
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
})()}
</div>
) : (
<div className="flex min-h-0 flex-1 flex-col [&>svg]:min-h-0 [&>svg]:flex-1">
{data.hasData && result ? (
Expand All @@ -510,3 +548,47 @@ function WidgetCard({
</div>
);
}

/** Renders the selector widget body: a scrollable list of clickable value
* chips. In single mode clicking a value toggles it as the only selected value.
* In multi mode each chip toggles independently. Cross-filtering is not yet
* wired; this prepares the UI and selection state for it. */
function SelectorValues({ values, multiple }: { values: string[]; multiple: boolean }) {
const [selected, setSelected] = useState<Set<string>>(new Set());

const toggle = (value: string) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(value)) {
next.delete(value);
} else {
if (!multiple) next.clear();
next.add(value);
}
return next;
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
};

return (
<div className="flex flex-wrap gap-1.5">
{values.map((value) => {
const isSelected = selected.has(value);
return (
<button
key={value}
type="button"
aria-pressed={isSelected}
onClick={() => toggle(value)}
className={`rounded-full border px-2.5 py-0.5 text-xs transition-colors ${
isSelected
? "border-primary bg-primary text-primary-foreground"
: "border-border bg-background text-muted-foreground hover:border-primary/50"
}`}
>
{value}
</button>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
})}
</div>
);
}
43 changes: 38 additions & 5 deletions apps/geolibre-desktop/src/components/panels/WidgetEditorDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export function WidgetEditorDialog({
const [indicatorAggregation, setIndicatorAggregation] = useState<IndicatorAggregation>("count");
const [prefix, setPrefix] = useState("");
const [suffix, setSuffix] = useState("");
// "selector" widget fields (issue #1381).
const [multiple, setMultiple] = useState(false);
// "" means no custom color: fall back to the theme primary / palette.
const [color, setColor] = useState("");

Expand All @@ -88,6 +90,7 @@ export function WidgetEditorDialog({
setIndicatorAggregation(widget?.indicatorAggregation ?? "count");
setPrefix(widget?.prefix ?? "");
setSuffix(widget?.suffix ?? "");
setMultiple(widget?.multiple ?? false);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open, widget]);

Expand All @@ -104,15 +107,18 @@ export function WidgetEditorDialog({
// aren't forced to the same column; the rest need one numeric field.
const isCategorical = type === "bar" || type === "pie";
const isIndicator = type === "indicator";
const isSelector = type === "selector";
const canSave =
layerId !== "" &&
(isIndicator
? indicatorAggregation === "count" || hasNumeric
: isCategorical
? hasCategory && (aggregation === "count" || hasNumeric)
: type === "scatter"
? numericCols.length >= 2
: hasNumeric);
: isSelector
? hasCategory
: isCategorical
? hasCategory && (aggregation === "count" || hasNumeric)
: type === "scatter"
? numericCols.length >= 2
: hasNumeric);
const save = () => {
if (!canSave) return;
const next: DashboardWidget = {
Expand Down Expand Up @@ -151,6 +157,10 @@ export function WidgetEditorDialog({
if (prefix) next.prefix = prefix;
if (suffix) next.suffix = suffix;
}
if (type === "selector") {
next.category = pick(category, categoryCols);
if (multiple) next.multiple = true;
}
onSave(next);
onOpenChange(false);
};
Expand Down Expand Up @@ -229,6 +239,9 @@ export function WidgetEditorDialog({
{t("dashboard.chartType.pie")}
</option>
<option value="indicator">{t("dashboard.chartType.indicator")}</option>
<option value="selector" disabled={!hasCategory}>
{t("dashboard.chartType.selector")}
</option>
</Select>
</div>

Expand Down Expand Up @@ -335,6 +348,26 @@ export function WidgetEditorDialog({
</>
)}

{type === "selector" && (
<>
<FieldSelect
id="widget-selector-category"
label={t("dashboard.editor.category")}
value={pick(category, categoryCols)}
options={categoryCols}
onChange={setCategory}
/>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={multiple}
onChange={(event) => setMultiple(event.target.checked)}
/>
{t("dashboard.editor.multiSelect")}
</label>
</>
)}

{type === "indicator" && (
<>
<div className="grid gap-1.5">
Expand Down
4 changes: 3 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3040,7 +3040,8 @@
"line": "Line",
"box": "Box plot",
"pie": "Pie",
"indicator": "Indicator"
"indicator": "Indicator",
"selector": "Selector"
},
"aggregate": {
"count": "Count",
Expand Down Expand Up @@ -3082,6 +3083,7 @@
"value": "Value",
"prefix": "Prefix",
"suffix": "Suffix",
"multiSelect": "Allow multiple selection",
"titleLabel": "Title",
"titlePlaceholder": "Optional title",
"color": "Color",
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1520,7 +1520,8 @@ export type DashboardWidgetType =
| "line"
| "box"
| "pie"
| "indicator";
| "indicator"
| "selector";

/** How a bar widget reduces its category groups. */
export type DashboardWidgetAggregation = "count" | "sum" | "mean";
Expand Down Expand Up @@ -1567,6 +1568,8 @@ export interface DashboardWidget {
prefix?: string;
/** Indicator widget: optional suffix (e.g. " kg", " ha"). */
suffix?: string;
/** Selector widget: whether multiple values can be picked (default false). */
multiple?: boolean;
}

/** Aggregation functions for indicator widgets (issue #1381). Extends the bar
Expand Down
Loading