Skip to content

Commit a3ebb8e

Browse files
authored
Merge pull request plan4better#3638 from EPajares/main
Improvements dashboard builder
2 parents 649db5e + 2a318ab commit a3ebb8e

30 files changed

+3425
-372
lines changed

apps/web/components/builder/PanelContainer.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,11 @@ export const Container: React.FC<ContainerProps> = ({
313313
...(panel.config?.options?.style === "default" && {
314314
justifyContent: panel.config?.position?.alignItems,
315315
}),
316+
// Add minimal padding for clickable area to select panel (only in edit mode with widgets)
317+
...(!viewOnly &&
318+
visibleWidgets.length > 0 && {
319+
p: 0.5,
320+
}),
316321
}}>
317322
{/* Show empty message if widgets array is empty */}
318323
{visibleWidgets.length === 0 ? (

apps/web/components/builder/WidgetConfiguration.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
WidgetInfo,
1515
WidgetOptions,
1616
WidgetSetup,
17+
WidgetStyle,
1718
} from "@/components/builder/widgets/common/WidgetCommonConfigs";
1819
import TabsWidgetConfig from "@/components/builder/widgets/elements/TabsWidgetConfig";
1920

@@ -79,6 +80,7 @@ const WidgetConfiguration = ({ onChange, samePanelWidgets }: WidgetConfiguration
7980
<WidgetInfo config={selectedBuilderItem.config} onChange={handleConfigChange} />
8081
{hasDataConfig && <WidgetData config={selectedBuilderItem.config} onChange={handleConfigChange} />}
8182
<WidgetSetup config={selectedBuilderItem.config} onChange={handleConfigChange} />
83+
<WidgetStyle config={selectedBuilderItem.config} onChange={handleConfigChange} />
8284
<WidgetOptions config={selectedBuilderItem.config} onChange={handleConfigChange} />
8385
</Stack>
8486
);

apps/web/components/builder/widgets/WidgetWrapper.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const WidgetWrapper: React.FC<WidgetWrapperProps> = ({
125125
);
126126

127127
return viewOnly ? (
128-
<Box sx={{ width: "100%", p: 2, pointerEvents: "all" }}>{widgetContent}</Box>
128+
<Box sx={{ width: "100%", p: 1, pointerEvents: "all" }}>{widgetContent}</Box>
129129
) : (
130130
<DraggableWidgetContainer widget={widget} onWidgetDelete={onWidgetDelete}>
131131
{widgetContent}

apps/web/components/builder/widgets/chart/Categories.tsx

Lines changed: 135 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,109 @@ import { StaleDataLoader } from "@/components/builder/widgets/common/StaleDataLo
1616
import { WidgetStatusContainer } from "@/components/builder/widgets/common/WidgetStatusContainer";
1717

1818
const DEFAULT_COLOR = "#0e58ff";
19+
const DEFAULT_HOVER_COLOR = "#3b82f6";
20+
const DEFAULT_SELECTED_COLOR = "#f5b704";
1921
const OPACITY_MODIFIER = "33";
2022

2123
export const CategoriesChartWidget = ({ config: rawConfig }: { config: CategoriesChartSchema }) => {
2224
const { t, i18n } = useTranslation("common");
2325
const theme = useTheme();
24-
const { config, queryParams, layerId } = useChartWidget(
26+
const { config, queryParams, baseQueryParams, layerId } = useChartWidget(
2527
rawConfig,
2628
categoriesChartConfigSchema,
2729
aggregationStatsQueryParams
2830
);
2931

32+
// Determine if we're in highlight mode
33+
const isHighlightMode = config?.options?.selection_response === "highlight";
34+
35+
// In highlight mode: always fetch full data for main display
36+
// In filter mode: use filtered data
37+
const mainQueryParams = isHighlightMode ? baseQueryParams : queryParams;
38+
3039
const { aggregationStats, isLoading, isError } = useProjectLayerAggregationStats(
3140
layerId,
41+
mainQueryParams as AggregationStatsQueryParams
42+
);
43+
44+
// Fetch selected/filtered data (only in highlight mode)
45+
const { aggregationStats: selectedStats, isLoading: isSelectedLoading } = useProjectLayerAggregationStats(
46+
isHighlightMode ? layerId : undefined,
3247
queryParams as AggregationStatsQueryParams
3348
);
3449

3550
// Data handling
3651
const originalData = useMemo(() => aggregationStats?.items || [], [aggregationStats]);
3752

53+
// Only show highlight visualization when there's actually filtered data
54+
const showHighlight = useMemo(() => {
55+
if (!isHighlightMode || !selectedStats || !aggregationStats) return false;
56+
// Calculate total counts to check if there's any filtering
57+
const totalMain = originalData.reduce((sum, item) => sum + item.operation_value, 0);
58+
const totalSelected = (selectedStats.items || []).reduce((sum, item) => sum + item.operation_value, 0);
59+
return totalSelected < totalMain;
60+
}, [isHighlightMode, selectedStats, aggregationStats, originalData]);
61+
62+
// Create a map of selected counts by category
63+
const selectedCountMap = useMemo(() => {
64+
if (!showHighlight || !selectedStats?.items) return new Map<string, number>();
65+
return new Map(selectedStats.items.map((item) => [item.grouped_value, item.operation_value]));
66+
}, [showHighlight, selectedStats]);
67+
68+
// Apply custom order if defined
69+
const orderedData = useMemo(() => {
70+
if (!originalData.length) return originalData;
71+
72+
const customOrder = config?.setup?.custom_order;
73+
if (!customOrder || customOrder.length === 0) {
74+
return originalData;
75+
}
76+
77+
// Sort by custom order - items in customOrder come first in that order,
78+
// items not in customOrder are excluded
79+
const orderMap = new Map(customOrder.map((val, idx) => [val, idx]));
80+
return originalData
81+
.filter((item) => orderMap.has(item.grouped_value))
82+
.sort((a, b) => {
83+
const aIdx = orderMap.get(a.grouped_value) ?? Infinity;
84+
const bIdx = orderMap.get(b.grouped_value) ?? Infinity;
85+
return aIdx - bIdx;
86+
});
87+
}, [originalData, config?.setup?.custom_order]);
88+
3889
const displayData = useMemo(() => {
39-
if (originalData.length > 0) return originalData;
90+
if (orderedData.length > 0) return orderedData;
4091
return [{ grouped_value: t("no_data"), operation_value: 0 }];
41-
}, [originalData, t]);
92+
}, [orderedData, t]);
4293

4394
// Calculate max value for progress scaling
4495
const maxValue = useMemo(() => {
45-
return originalData.length > 0 ? Math.max(...originalData.map((item) => item.operation_value)) : 1; // For "No data" state
46-
}, [originalData]);
96+
return orderedData.length > 0 ? Math.max(...orderedData.map((item) => item.operation_value)) : 1; // For "No data" state
97+
}, [orderedData]);
4798

4899
const [activeCategory, setActiveCategory] = useState<string | undefined>();
49100
const [isHovering, setIsHovering] = useState(false);
50101

51-
const getColor = (category: (typeof displayData)[number]) => {
52-
const baseColor = originalData.length > 0 ? config?.options?.color || DEFAULT_COLOR : "#e0e0e0";
53-
const shouldDim = originalData.length === 0 || (isHovering && activeCategory !== category.grouped_value);
102+
// Colors
103+
const baseColor = config?.options?.color || DEFAULT_COLOR;
104+
const hoverColor = config?.options?.highlight_color || DEFAULT_HOVER_COLOR;
105+
const selectedColor = config?.options?.selected_color || DEFAULT_SELECTED_COLOR;
106+
107+
const getColor = (category: (typeof displayData)[number], isSelected: boolean) => {
108+
const isActive = activeCategory === category.grouped_value;
109+
const hasData = orderedData.length > 0;
110+
111+
if (!hasData) return "#e0e0e0";
112+
113+
// In highlight mode with selections shown
114+
if (showHighlight && isSelected) {
115+
if (isActive) return hoverColor;
116+
return selectedColor;
117+
}
54118

55-
return shouldDim ? `${baseColor}${OPACITY_MODIFIER}` : baseColor;
119+
if (isActive) return hoverColor;
120+
if (isHovering && !isActive) return `${baseColor}${OPACITY_MODIFIER}`;
121+
return baseColor;
56122
};
57123

58124
const isChartConfigured = useMemo(() => {
@@ -62,7 +128,7 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
62128
return (
63129
<>
64130
<WidgetStatusContainer
65-
isLoading={isLoading && !aggregationStats && !isError}
131+
isLoading={(isLoading || isSelectedLoading) && !aggregationStats && !isError}
66132
isNotConfigured={!isChartConfigured}
67133
isError={isError}
68134
height={150}
@@ -84,7 +150,10 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
84150
p: 2,
85151
}}>
86152
{displayData.map((category) => {
87-
const percentage = (category.operation_value / maxValue) * 100;
153+
const totalValue = category.operation_value;
154+
const selectedValue = showHighlight ? selectedCountMap.get(category.grouped_value) || 0 : 0;
155+
const percentage = (totalValue / maxValue) * 100;
156+
const selectedPercentage = showHighlight ? (selectedValue / maxValue) * 100 : 0;
88157
const displayValue = formatNumber(
89158
category.operation_value,
90159
config.options?.format,
@@ -110,28 +179,69 @@ export const CategoriesChartWidget = ({ config: rawConfig }: { config: Categorie
110179
{category.grouped_value}
111180
</Typography>
112181
<Typography variant="caption" fontWeight={500}>
113-
{displayValue}
182+
{showHighlight && selectedValue > 0 ? `${selectedValue} / ${displayValue}` : displayValue}
114183
</Typography>
115184
</Box>
116-
<LinearProgress
117-
variant="determinate"
118-
value={percentage}
119-
sx={{
120-
height: 8,
121-
borderRadius: 4,
122-
backgroundColor: theme.palette.grey[200],
123-
"& .MuiLinearProgress-bar": {
185+
{showHighlight ? (
186+
// Stacked progress bar for highlight mode
187+
<Box sx={{ position: "relative", height: 8 }}>
188+
{/* Base bar (full width for total) */}
189+
<LinearProgress
190+
variant="determinate"
191+
value={percentage}
192+
sx={{
193+
position: "absolute",
194+
width: "100%",
195+
height: 8,
196+
borderRadius: 4,
197+
backgroundColor: theme.palette.grey[200],
198+
"& .MuiLinearProgress-bar": {
199+
borderRadius: 4,
200+
backgroundColor: getColor(category, false),
201+
},
202+
}}
203+
/>
204+
{/* Selected bar (overlay showing selected portion) */}
205+
{selectedValue > 0 && (
206+
<LinearProgress
207+
variant="determinate"
208+
value={selectedPercentage}
209+
sx={{
210+
position: "absolute",
211+
width: "100%",
212+
height: 8,
213+
borderRadius: 4,
214+
backgroundColor: "transparent",
215+
"& .MuiLinearProgress-bar": {
216+
borderRadius: 4,
217+
backgroundColor: getColor(category, true),
218+
},
219+
}}
220+
/>
221+
)}
222+
</Box>
223+
) : (
224+
// Normal single progress bar
225+
<LinearProgress
226+
variant="determinate"
227+
value={percentage}
228+
sx={{
229+
height: 8,
124230
borderRadius: 4,
125-
backgroundColor: getColor(category),
126-
},
127-
}}
128-
/>
231+
backgroundColor: theme.palette.grey[200],
232+
"& .MuiLinearProgress-bar": {
233+
borderRadius: 4,
234+
backgroundColor: getColor(category, false),
235+
},
236+
}}
237+
/>
238+
)}
129239
</Box>
130240
);
131241
})}
132242
</Box>
133243
)}
134-
<StaleDataLoader isLoading={isLoading} hasData={!!originalData.length} />
244+
<StaleDataLoader isLoading={isLoading || isSelectedLoading} hasData={!!orderedData.length} />
135245
</>
136246
);
137247
};

0 commit comments

Comments
 (0)