Skip to content

Commit

Permalink
Merge branch 'feature/search-with-data-source'
Browse files Browse the repository at this point in the history
  • Loading branch information
jhf committed Oct 31, 2024
2 parents 8e7f210 + 684fd9f commit d07e1dc
Show file tree
Hide file tree
Showing 25 changed files with 1,532 additions and 458 deletions.
22 changes: 14 additions & 8 deletions app/src/app/search/SearchResults.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Tables } from "@/lib/database.types";
import { toURLSearchParams, URLSearchParamsDict } from "@/lib/url-search-params-dict";
import { createSupabaseBrowserClientAsync } from "@/utils/supabase/client";
import { getStatisticalUnits } from "./search-requests";
import { activityCategoryDeriveStateUpdateFromSearchParams, externalIdentDeriveStateUpdateFromSearchParams, fullTextSearchDeriveStateUpdateFromSearchParams, invalidCodesDeriveStateUpdateFromSearchParams, legalFormDeriveStateUpdateFromSearchParams, regionDeriveStateUpdateFromSearchParams, sectorDeriveStateUpdateFromSearchParams, statisticalVariablesDeriveStateUpdateFromSearchParams, unitTypeDeriveStateUpdateFromSearchParams } from "./filters/url-search-params";
import { activityCategoryDeriveStateUpdateFromSearchParams, dataSourceDeriveStateUpdateFromSearchParams, externalIdentDeriveStateUpdateFromSearchParams, fullTextSearchDeriveStateUpdateFromSearchParams, invalidCodesDeriveStateUpdateFromSearchParams, legalFormDeriveStateUpdateFromSearchParams, regionDeriveStateUpdateFromSearchParams, sectorDeriveStateUpdateFromSearchParams, statisticalVariablesDeriveStateUpdateFromSearchParams, unitTypeDeriveStateUpdateFromSearchParams } from "./filters/url-search-params";

const fetcher = async (derivedApiSearchParams: URLSearchParams) => {
// Notice that the createSupabaseBrowserClientAsync must be inside the fetcher
Expand Down Expand Up @@ -40,6 +40,7 @@ const fetcher = async (derivedApiSearchParams: URLSearchParams) => {
initialUrlSearchParams: URLSearchParams,
maybeDefaultExternalIdentType: Tables<"external_ident_type_ordered">,
statDefinitions: Tables<"stat_definition_ordered">[],
allDataSources: Tables<"data_source">[],
) : SearchState {
let actions = [
fullTextSearchDeriveStateUpdateFromSearchParams(initialUrlSearchParams),
Expand All @@ -49,6 +50,7 @@ const fetcher = async (derivedApiSearchParams: URLSearchParams) => {
regionDeriveStateUpdateFromSearchParams(initialUrlSearchParams),
sectorDeriveStateUpdateFromSearchParams(initialUrlSearchParams),
activityCategoryDeriveStateUpdateFromSearchParams(initialUrlSearchParams),
dataSourceDeriveStateUpdateFromSearchParams(initialUrlSearchParams, allDataSources),
externalIdentDeriveStateUpdateFromSearchParams(maybeDefaultExternalIdentType, initialUrlSearchParams),
].concat(
statisticalVariablesDeriveStateUpdateFromSearchParams(statDefinitions, initialUrlSearchParams)
Expand All @@ -62,8 +64,9 @@ interface SearchResultsProps {
readonly children: ReactNode;
readonly initialOrder: SearchOrder;
readonly initialPagination: SearchPagination;
readonly regions: Tables<"region_used">[];
readonly activityCategories: Tables<"activity_category_used">[];
readonly allRegions: Tables<"region_used">[];
readonly allActivityCategories: Tables<"activity_category_used">[];
readonly allDataSources: Tables<"data_source">[];
readonly initialUrlSearchParamsDict: URLSearchParamsDict;
}

Expand All @@ -72,8 +75,9 @@ export function SearchResults({
children,
initialOrder,
initialPagination,
regions,
activityCategories,
allRegions,
allActivityCategories,
allDataSources,
initialUrlSearchParamsDict,
}: SearchResultsProps) {
const { selectedTimeContext } = useTimeContext();
Expand All @@ -94,6 +98,7 @@ export function SearchResults({
initialUrlSearchParams,
externalIdentTypes?.[0],
statDefinitions,
allDataSources,
);

const [searchState, modifySearchState] = useReducer(modifySearchStateReducer, initialSearchState);
Expand Down Expand Up @@ -145,13 +150,14 @@ export function SearchResults({
modifySearchState,
searchResult,
derivedApiSearchParams,
regions: regions ?? [],
activityCategories: activityCategories ?? [],
allRegions: allRegions ?? [],
allActivityCategories: allActivityCategories ?? [],
allDataSources: allDataSources ?? [],
selectedTimeContext,
isLoading,
error
} as SearchContextState),
[searchState, searchResult, derivedApiSearchParams, regions, activityCategories, selectedTimeContext, isLoading, error]
[searchState, searchResult, derivedApiSearchParams, allRegions, allActivityCategories, allDataSources, selectedTimeContext, isLoading, error]
);

useDerivedUrlSearchParams(ctx);
Expand Down
2 changes: 1 addition & 1 deletion app/src/app/search/components/search-result-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { SearchResultTableBodySkeleton } from "@/app/search/components/search-re
import { useRegionLevel } from "@/app/search/hooks/useRegionLevel";

export default function SearchResultTable() {
const { searchResult, error, isLoading, regions } = useSearchContext();
const { searchResult, error, isLoading, allRegions: regions } = useSearchContext();
const { regionLevel, setRegionLevel } = useRegionLevel();
const maxRegionLevel = Math.max(
...(regions?.map((region) => region.level ?? 0) ?? [])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export const StatisticalUnitTableHeader = ({
name="primary_activity_category_path"
label="Activity Category"
/>
<TableHead
className="text-left hidden lg:table-cell"
>Data Sources
</TableHead>
<TableHead />
</TableRow>
</TableHeader>
Expand Down
42 changes: 30 additions & 12 deletions app/src/app/search/components/statistical-unit-table-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { thousandSeparator } from "@/lib/number-utils";
import { useBaseData } from "@/app/BaseDataClient";
import { StatisticalUnit } from "@/app/types";
import { InvalidCodes } from "./invalid-codes";
import { Popover, PopoverContent } from "@/components/ui/popover";
import { PopoverTrigger } from "@radix-ui/react-popover";

interface SearchResultTableRowProps {
unit: Tables<"statistical_unit">;
Expand All @@ -23,7 +25,7 @@ export const StatisticalUnitTableRow = ({
className,
regionLevel,
}: SearchResultTableRowProps) => {
const { regions, activityCategories } = useSearchContext();
const { allRegions, allActivityCategories, allDataSources } = useSearchContext();
const { statDefinitions, externalIdentTypes } = useBaseData();
const { selected } = useSelectionContext();

Expand All @@ -42,33 +44,33 @@ export const StatisticalUnitTableRow = ({
sector_name,
sector_code,
invalid_codes,
data_source_ids,
} = unit as StatisticalUnit;

const getRegionByPath = (physical_region_path: unknown) => {
if (typeof physical_region_path !== "string") return undefined;
const regionParts = physical_region_path.split(".");
const selectedRegionPath = regionParts.slice(0, regionLevel).join(".");
return regions.find(({ path }) => path === selectedRegionPath);
return allRegions.find(({ path }) => path === selectedRegionPath);
};

const getActivityCategoryByPath = (primary_activity_category_path: unknown) =>
activityCategories.find(
allActivityCategories.find(
({ path }) => path === primary_activity_category_path
);

/* TODO - Remove this once the search results include the activity category and region names
* Until activity category and region names are included in the search results,
* we need to provide activity categories and regions via the search provider
* so that the names can be displayed in the search results.
*
* A better solution would be to include the names in the search results
* so that we do not need any blocking calls to supabase here.
*/

const activityCategory = getActivityCategoryByPath(
primary_activity_category_path
);

const getDataSourcesByIds = (data_source_ids: number[] | null) => {
if (!data_source_ids) return [];
return data_source_ids
.map((id) => allDataSources.find((ds) => ds.id === id));
};

const dataSources = getDataSourcesByIds(data_source_ids ?? []);

const region = getRegionByPath(physical_region_path);

const prettifyUnitType = (type: UnitType | null): string => {
Expand Down Expand Up @@ -160,6 +162,22 @@ export const StatisticalUnitTableRow = ({
</small>
</div>
</TableCell>
<TableCell className="py-2 text-left hidden lg:table-cell">
<div className="flex flex-col space-y-0.5 leading-tight">
{dataSources.map((ds) => (
<Popover key={`dataSource-${ds?.id}`}>
<PopoverTrigger asChild>
<span className="cursor-pointer" title={ds?.name}>
{ds?.code}
</span>
</PopoverTrigger>
<PopoverContent className="p-1.5 w-full">
<p className="text-xs">{ds?.name}</p>
</PopoverContent>
</Popover>
))}
</div>
</TableCell>
<TableCell className="p-1 text-right">
<SearchResultTableRowDropdownMenu unit={unit} />
</TableCell>
Expand Down
4 changes: 4 additions & 0 deletions app/src/app/search/components/table-toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import InvalidCodesFilter from "@/app/search/filters/invalid-codes-filter";
import { ResetFilterButton } from "@/app/search/components/reset-filter-button";

import { IURLSearchParamsDict } from "@/lib/url-search-params-dict";
import DataSourceFilter from "../filters/data-source/data-source-filter";

export default function TableToolbar({ initialUrlSearchParamsDict }: IURLSearchParamsDict) {
return (
Expand All @@ -31,6 +32,9 @@ export default function TableToolbar({ initialUrlSearchParamsDict }: IURLSearchP
<Suspense fallback={<FilterSkeleton title="Activity Category" />}>
<ActivityCategoryFilter/>
</Suspense>
<Suspense fallback={<FilterSkeleton title="Data Source" />}>
<DataSourceFilter/>
</Suspense>
<StatisticalVariablesFilter/>
<InvalidCodesFilter/>
<ResetFilterButton />
Expand Down
20 changes: 20 additions & 0 deletions app/src/app/search/filters/data-source/data-source-filter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createSupabaseSSRClient } from "@/utils/supabase/server";
import DataSourceOptions from "@/app/search/filters/data-source/data-source-options";

export default async function DataSourceFilter() {
const client = await createSupabaseSSRClient();
const {data: dataSources} = await client.from("data_source").select().filter('active','eq',true);

return (
<DataSourceOptions
dataSources={dataSources ?? []}
options={
dataSources?.map(({ code, name }) => ({
label: name,
value: code,
humanReadableValue: name,
})) ?? []
}
/>
);
}
49 changes: 49 additions & 0 deletions app/src/app/search/filters/data-source/data-source-options.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"use client";
import { OptionsFilter } from "@/app/search/components/options-filter";
import { useSearchContext } from "@/app/search/use-search-context";
import { useCallback } from "react";
import { DATA_SOURCE, dataSourceDeriveStateUpdateFromValues } from "@/app/search/filters/url-search-params";
import { SearchFilterOption } from "../../search";
import { Tables } from "@/lib/database.types";


export default function DataSourceOptions({
options,
dataSources,
}: {
readonly options: SearchFilterOption[];
readonly dataSources: Tables<"data_source">[];
}) {
const {
modifySearchState,
searchState: {
appSearchParams: { [DATA_SOURCE]: selected = [] },
},
} = useSearchContext();

const toggle = useCallback(
({ value }: SearchFilterOption) => {
const values = selected.includes(value)
? selected.filter((v) => v !== value)
: [...selected, value];

modifySearchState(dataSourceDeriveStateUpdateFromValues(values, dataSources));
},
[selected, modifySearchState, dataSources]
);

const reset = useCallback(() => {
modifySearchState(dataSourceDeriveStateUpdateFromValues([], []));
}, [modifySearchState]);

return (
<OptionsFilter
className="p-2 h-9"
title="Data Source"
options={options}
selectedValues={selected}
onToggle={toggle}
onReset={reset}
/>
);
}
28 changes: 28 additions & 0 deletions app/src/app/search/filters/url-search-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,34 @@ export function activityCategoryDeriveStateUpdateFromValues(values: (string | nu
}


export const DATA_SOURCE = "data_source";

export function dataSourceDeriveStateUpdateFromSearchParams(urlSearchParams: URLSearchParams, dataSources: Tables<"data_source">[]): SearchAction {
const initialValue = urlSearchParams.get(DATA_SOURCE);
const initialValues = parseInitialValues(initialValue);
return dataSourceDeriveStateUpdateFromValues(initialValues, dataSources);
}

export function dataSourceDeriveStateUpdateFromValues(values: (string | null)[], dataSources: Tables<"data_source">[]): SearchAction {
const codeToIdMap = new Map(dataSources.map(ds => [ds.code, ds.id]));
const ids = values
.filter(value => value !== null) // Remove null values
.map(value => codeToIdMap.get(value))
.filter(id => id !== undefined); // Remove undefined ids (in case of unmatched codet)

const searchAction = {
type: "set_query",
payload: {
app_param_name: DATA_SOURCE,
api_param_name: "data_source_ids",
api_param_value: ids.length ? `ov.{${ids.join(",")}}` : null,
app_param_values: values,
},
} as SearchAction;
return searchAction;
}


export function externalIdentDeriveStateUpdateFromSearchParams(
maybeDefaultExternalIdentType: Tables<"external_ident_type_ordered">,
urlSearchParams: URLSearchParams
Expand Down
8 changes: 5 additions & 3 deletions app/src/app/search/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,10 @@ export default async function SearchPage({ searchParams: initialUrlSearchParamsD
* so that we do not need any blocking calls to supabase here.
*/
const client = await createSupabaseSSRClient();
const [{data: activityCategories}, {data: regions}] = await Promise.all([
const [{data: activityCategories}, {data: regions}, {data: dataSources}] = await Promise.all([
client.from("activity_category_used").select(),
client.from("region_used").select(),
client.from("data_source").select().filter('active','eq',true),
]);

let order = defaultOrder;
Expand All @@ -50,8 +51,9 @@ export default async function SearchPage({ searchParams: initialUrlSearchParamsD
<SearchResults
initialOrder={order}
initialPagination={{ pageNumber: currentPage, pageSize: defaultPageSize }}
regions={regions ?? []}
activityCategories={activityCategories ?? []}
allRegions={regions ?? []}
allActivityCategories={activityCategories ?? []}
allDataSources={dataSources ?? []}
initialUrlSearchParamsDict={initialUrlSearchParamsDict}
>
<main className="mx-auto flex w-full flex-col py-8 md:py-12 px-4">
Expand Down
5 changes: 3 additions & 2 deletions app/src/app/search/search-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ export interface SearchContextState {
readonly modifySearchState: Dispatch<SearchAction>;
readonly searchResult?: SearchResult;
readonly derivedApiSearchParams: URLSearchParams;
readonly regions: Tables<"region_used">[];
readonly activityCategories: Tables<"activity_category_available">[];
readonly allRegions: Tables<"region_used">[];
readonly allActivityCategories: Tables<"activity_category_available">[];
readonly allDataSources: Tables<"data_source">[];
readonly selectedTimeContext: TimeContextRow;
/**
* Indicates whether the search is currently loading new data.
Expand Down
Loading

0 comments on commit d07e1dc

Please sign in to comment.