diff --git a/client/src/containers/analysis-visualization/analysis-filters/component.tsx b/client/src/containers/analysis-visualization/analysis-filters/component.tsx
index 57ca300c2..c6aca4034 100644
--- a/client/src/containers/analysis-visualization/analysis-filters/component.tsx
+++ b/client/src/containers/analysis-visualization/analysis-filters/component.tsx
@@ -1,4 +1,5 @@
import IndicatorsFilter from './indicators';
+import IndicatorsMapFilter from './indicators/map';
import GroupByFilter from './group-by';
import YearsRangeFilter from './years-range';
import MoreFilters from './more-filters';
@@ -12,10 +13,19 @@ const AnalysisFilters: React.FC = () => {
return (
-
- {visualizationMode !== 'map' && }
- {visualizationMode === 'map' && }
- {visualizationMode !== 'map' && }
+ {visualizationMode === 'map' && (
+ <>
+
+
+ >
+ )}
+ {visualizationMode !== 'map' && (
+ <>
+
+
+
+ >
+ )}
);
diff --git a/client/src/containers/analysis-visualization/analysis-filters/indicators/index.tsx b/client/src/containers/analysis-visualization/analysis-filters/indicators/index.tsx
index e7dbb4b08..ed883dd0e 100644
--- a/client/src/containers/analysis-visualization/analysis-filters/indicators/index.tsx
+++ b/client/src/containers/analysis-visualization/analysis-filters/indicators/index.tsx
@@ -1,151 +1,123 @@
-import { useCallback, useMemo, useEffect, ComponentProps, Fragment, useState } from 'react';
+import { useCallback, useMemo, useEffect, useState } from 'react';
import { useRouter } from 'next/router';
+import queryString from 'query-string';
+import { useSearchParams } from 'next/navigation';
-import { useAppDispatch, useAppSelector } from 'store/hooks';
-import { analysisUI } from 'store/features/analysis/ui';
-import { setFilter } from 'store/features/analysis/filters';
import { useIndicators } from 'hooks/indicators';
-import {
- Select,
- SelectContent,
- SelectGroup,
- SelectItem,
- SelectTrigger,
- SelectValue,
- SelectLabel,
- SelectSeparator,
-} from '@/components/ui/select';
+import TreeSelect from '@/components/tree-select';
+import { TreeSelectOption } from '@/components/tree-select/types';
+import { flattenTree } from '@/components/tree-select/utils';
+
+type HasParentProperty = T & { isParent?: boolean };
const IndicatorsFilter = () => {
const { query = {}, replace } = useRouter();
- const { indicator } = query;
- const { visualizationMode } = useAppSelector(analysisUI);
- const dispatch = useAppDispatch();
- const [value, setValue] = useState(undefined);
+ const searchParams = useSearchParams();
+ const [selectedOptions, setSelectedOptions] = useState(undefined);
- const { data, isFetching, isFetched } = useIndicators(
- { sort: 'name' },
+ const { data, isFetching } = useIndicators(
+ { sort: 'category' },
{
select: (data) => data?.data,
},
);
- const options = useMemo(() => {
- const categories = Array.from(
- new Set(data?.map(({ category }) => category).filter(Boolean)),
- ).sort((a, b) => a.localeCompare(b));
+ const options = useMemo(() => {
+ const categories = Array.from(new Set(data?.map(({ category }) => category).filter(Boolean)));
const categoryGroups = categories.map((category) => {
const indicators = data?.filter((indicator) => indicator.category === category);
- const categoryOptions = indicators.map((indicator) => ({
- label: indicator.name,
- value: indicator.id,
- disabled: indicator.status === 'inactive',
- }));
- return { label: category, value: category, options: categoryOptions };
+ const categoryOptions = indicators.map(
+ (indicator) =>
+ ({
+ label: indicator.name,
+ value: indicator.id,
+ // disabled: indicator.status === 'inactive',
+ }) satisfies TreeSelectOption<(typeof indicator)['id']>,
+ );
+ return {
+ label: category,
+ value: category,
+ isParent: true,
+ children: categoryOptions,
+ } satisfies HasParentProperty>;
});
- return [
- ...(visualizationMode !== 'map'
- ? [
- {
- label: 'All indicators',
- value: 'all',
- options: [],
- },
- ]
- : []),
- ...categoryGroups,
- ];
- }, [data, visualizationMode]);
+ return categoryGroups;
+ }, [data]);
- const indicatorName = useMemo(() => {
- const indicator = data?.find((indicator) => indicator.id === value);
- return indicator?.name;
- }, [data, value]);
+ const allNodes = useMemo(() => options?.flatMap((opt) => flattenTree(opt)), [options]);
const handleChange = useCallback(
- (value: Parameters['onValueChange']>[0]) => {
- replace({ query: { ...query, indicator: value } }, undefined, {
- shallow: true,
- });
-
- dispatch(
- setFilter({
- id: 'indicator',
- value: {
- label: indicatorName,
- value,
- },
- }),
+ (options: HasParentProperty[]) => {
+ const _v = options
+ .map(({ value, isParent }) => {
+ if (isParent) {
+ return allNodes
+ .filter(({ value: v }) => v === value)
+ .map(({ children }) => children.map(({ value }) => value))
+ .flat(1);
+ }
+ return value;
+ })
+ .flat(1);
+
+ const { indicators, ...restQuery } = query;
+
+ setSelectedOptions(options);
+
+ replace(
+ {
+ query: queryString.stringify(
+ { ...restQuery, indicators: _v },
+ {
+ arrayFormat: 'comma',
+ arrayFormatSeparator: ',',
+ },
+ ),
+ },
+ undefined,
+ {
+ shallow: true,
+ },
);
},
- [query, replace, indicatorName, dispatch],
+ [query, replace, allNodes],
);
- useEffect(() => {
- if (indicator) {
- return setValue(indicator as string);
- }
-
- if (visualizationMode === 'map') {
- if (options?.at(0)?.options?.length) {
- return setValue(options.at(0).options.at(0).value);
- }
- }
+ const parsedQueryParams = useMemo(() => {
+ return queryString.parse(searchParams.toString(), {
+ arrayFormat: 'comma',
+ arrayFormatSeparator: ',',
+ });
+ }, [searchParams]);
- if (options?.length && !options?.at(0)?.options?.length) {
- return setValue(options.at(0).value);
+ const initialSelectedOptions = useMemo(() => {
+ if (parsedQueryParams.indicators && options?.length) {
+ const selectedOptions = allNodes.filter(({ value }) =>
+ parsedQueryParams.indicators.includes(value),
+ );
+ return selectedOptions;
}
- }, [visualizationMode, options, indicator, indicatorName, dispatch]);
+ return undefined;
+ }, [parsedQueryParams, options, allNodes]);
useEffect(() => {
- if (indicator && indicatorName) {
- dispatch(
- setFilter({
- id: 'indicator',
- value: {
- label: indicatorName,
- value: indicator,
- },
- }),
- );
- }
- }, [dispatch, indicator, indicatorName]);
+ setSelectedOptions(initialSelectedOptions);
+ }, [initialSelectedOptions]);
return (
-
+
+
+
);
};
diff --git a/client/src/containers/analysis-visualization/analysis-filters/indicators/map/index.tsx b/client/src/containers/analysis-visualization/analysis-filters/indicators/map/index.tsx
new file mode 100644
index 000000000..92026db45
--- /dev/null
+++ b/client/src/containers/analysis-visualization/analysis-filters/indicators/map/index.tsx
@@ -0,0 +1,143 @@
+import { useCallback, useMemo, useEffect, ComponentProps, Fragment, useState } from 'react';
+import { useRouter } from 'next/router';
+import { ChevronDownIcon, ChevronRightIcon } from '@heroicons/react/solid';
+
+import { useAppDispatch } from 'store/hooks';
+import { setFilter } from 'store/features/analysis/filters';
+import { useIndicators } from 'hooks/indicators';
+import {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+ SelectLabel,
+ SelectSeparator,
+} from '@/components/ui/select';
+import { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@/components/ui/collapsible';
+
+const IndicatorsMapFilter = () => {
+ const { query = {}, replace } = useRouter();
+ const { indicator } = query;
+ const dispatch = useAppDispatch();
+ const [value, setValue] = useState(undefined);
+
+ const { data, isFetching, isFetched } = useIndicators(
+ { sort: 'name' },
+ {
+ select: (data) => data?.data,
+ },
+ );
+
+ const options = useMemo(() => {
+ const categories = Array.from(
+ new Set(data?.map(({ category }) => category).filter(Boolean)),
+ ).sort((a, b) => a.localeCompare(b));
+
+ const categoryGroups = categories.map((category) => {
+ const indicators = data?.filter((indicator) => indicator.category === category);
+ const categoryOptions = indicators.map((indicator) => ({
+ label: indicator.name,
+ value: indicator.id,
+ disabled: indicator.status === 'inactive',
+ }));
+ return { label: category, value: category, options: categoryOptions };
+ });
+
+ return categoryGroups;
+ }, [data]);
+
+ const indicatorName = useMemo(() => {
+ const indicator = data?.find((indicator) => indicator.id === value);
+ return indicator?.name;
+ }, [data, value]);
+
+ const handleChange = useCallback(
+ (value: Parameters['onValueChange']>[0]) => {
+ replace({ query: { ...query, indicator: value } }, undefined, {
+ shallow: true,
+ });
+
+ dispatch(
+ setFilter({
+ id: 'indicator',
+ value: {
+ label: indicatorName,
+ value,
+ },
+ }),
+ );
+ },
+ [query, replace, indicatorName, dispatch],
+ );
+
+ useEffect(() => {
+ if (indicator) {
+ return setValue(indicator as string);
+ }
+
+ return setValue(options?.at(0)?.options?.at(0)?.value);
+ }, [options, indicator, indicatorName, dispatch]);
+
+ useEffect(() => {
+ if (indicator && indicatorName) {
+ dispatch(
+ setFilter({
+ id: 'indicator',
+ value: {
+ label: indicatorName,
+ value: indicator,
+ },
+ }),
+ );
+ }
+ }, [dispatch, indicator, indicatorName]);
+
+ return (
+
+ );
+};
+
+export default IndicatorsMapFilter;
diff --git a/client/src/containers/analysis-visualization/analysis-table/component.tsx b/client/src/containers/analysis-visualization/analysis-table/component.tsx
index e097ab612..c48a1c0f3 100644
--- a/client/src/containers/analysis-visualization/analysis-table/component.tsx
+++ b/client/src/containers/analysis-visualization/analysis-table/component.tsx
@@ -3,6 +3,8 @@ import { DownloadIcon, InformationCircleIcon } from '@heroicons/react/outline';
import { uniq, omit } from 'lodash-es';
import toast from 'react-hot-toast';
import { ArrowLeftIcon } from '@heroicons/react/solid';
+import queryString from 'query-string';
+import { useSearchParams } from 'next/navigation';
import ComparisonCell from './comparison-cell/component';
import ChartCell from './chart-cell';
@@ -62,10 +64,20 @@ const AnalysisTable = () => {
};
}, [expandedState, paginationState, rowSelectionState, sortingState]);
+ const searchParams = useSearchParams();
+ const parsedQueryParams = useMemo(() => {
+ return queryString.parse(searchParams.toString(), {
+ arrayFormat: 'comma',
+ arrayFormatSeparator: ',',
+ });
+ }, [searchParams]);
+
+ const selectedIndicators = parsedQueryParams?.indicators;
+
const { scenarioToCompare, isComparisonEnabled, currentScenario } = useAppSelector(scenarios);
const { data: indicators } = useIndicators(
{ 'filter[status]': 'active' },
- { select: (data) => data.data },
+ { select: (data) => data?.data },
);
const downloadImpactData = useDownloadImpactData({
onSuccess: () => {
@@ -104,22 +116,25 @@ const AnalysisTable = () => {
[isComparisonEnabled, currentScenario],
);
- const { indicatorId, ...restFilters } = filters;
+ const restFilters = filters;
const isEnable =
- !!indicatorId &&
!!indicators?.length &&
!!filters.startYear &&
!!filters.endYear &&
filters.endYear !== filters.startYear;
const indicatorIds = useMemo(() => {
- if (indicatorId === 'all') {
- return indicators.map((indicator) => indicator.id);
+ if (Array.isArray(selectedIndicators)) {
+ return selectedIndicators.map((id) => id);
+ }
+
+ if (selectedIndicators && !Array.isArray(selectedIndicators)) {
+ return [selectedIndicators];
}
- if (indicatorId) return [indicatorId];
- return [];
- }, [indicators, indicatorId]);
+
+ return indicators.map((indicator) => indicator.id);
+ }, [indicators, selectedIndicators]);
const sortingParams = useMemo(() => {
if (!!sortingState.length) {
@@ -293,7 +308,7 @@ const AnalysisTable = () => {
const [tableData, setTableData] = useState[]>([]);
useEffect(() => {
- if (indicatorId !== 'all') {
+ if (selectedIndicators) {
// A single indicator is selected so we force its expansion
setTableData(initialTableData[0]?.children);
} else {
@@ -302,13 +317,13 @@ const AnalysisTable = () => {
}
setExpandedState(null);
setRowSelectionState({});
- }, [indicatorId, initialTableData]);
+ }, [selectedIndicators, initialTableData]);
const setIndicatorParam = useIndicatorParam();
const handleExitExpanded = useCallback(() => {
setExpandedState({});
- setIndicatorParam('all');
+ setIndicatorParam(undefined);
}, [setIndicatorParam]);
const handleExpandRow = useCallback(
@@ -329,10 +344,14 @@ const AnalysisTable = () => {
[isComparison, isScenarioComparison],
);
- const expanded = useMemo(
- () => (indicatorId === 'all' ? null : indicators.find((i) => i.id === indicatorId)),
- [indicatorId, indicators],
- );
+ const expanded = useMemo(() => {
+ const indicatorIds = Array.isArray(selectedIndicators)
+ ? selectedIndicators
+ : [selectedIndicators];
+ return !Boolean(selectedIndicators)
+ ? null
+ : indicators.find((i) => indicatorIds.includes(i.id));
+ }, [selectedIndicators, indicators]);
const comparisonColumn = useCallback(
(year: number): ColumnDefinition> => {
@@ -490,7 +509,7 @@ const AnalysisTable = () => {
(): TableProps> & {
firstProjectedYear: number;
} => ({
- showPagination: indicatorId !== 'all',
+ showPagination: Boolean(selectedIndicators),
paginationProps: {
totalItems: metadata.totalItems,
totalPages: metadata.totalPages,
@@ -503,7 +522,7 @@ const AnalysisTable = () => {
onPaginationChange: setPaginationState,
onExpandedChange: setExpandedState,
isLoading: isFetching,
- enableExpanding: indicatorId !== 'all',
+ enableExpanding: Boolean(selectedIndicators),
data: (tableData as ImpactRowType[]) || [],
columns: baseColumns as ColumnDefinition>[],
handleExpandedChange,
@@ -513,11 +532,11 @@ const AnalysisTable = () => {
metadata,
tableState,
isFetching,
- indicatorId,
tableData,
baseColumns,
handleExpandedChange,
firstProjectedYear,
+ selectedIndicators,
],
);
diff --git a/client/src/utils/indicator-param.ts b/client/src/utils/indicator-param.ts
index d78084d8d..cabe9db17 100644
--- a/client/src/utils/indicator-param.ts
+++ b/client/src/utils/indicator-param.ts
@@ -1,10 +1,25 @@
import { useRouter } from 'next/router';
+import queryString from 'query-string';
export const useIndicatorParam = () => {
const { query = {}, replace } = useRouter();
- return (indicator: string) => {
- replace({ query: { ...query, indicator } }, undefined, {
- shallow: true,
- });
+ return (indicator: string | string[] | undefined) => {
+ const { indicators, ...queryWithoutIndicators } = query;
+
+ replace(
+ {
+ query: queryString.stringify(
+ { ...queryWithoutIndicators, ...(Boolean(indicator) && { indicators: indicator }) },
+ {
+ arrayFormat: 'comma',
+ arrayFormatSeparator: ',',
+ },
+ ),
+ },
+ undefined,
+ {
+ shallow: true,
+ },
+ );
};
};