Skip to content

Commit

Permalink
Merge pull request #52 from Vizzuality/SKY30-91-fe-allow-filtering-th…
Browse files Browse the repository at this point in the history
…e-table

[SKY30-91] Allow Filtering of the Global/Regional Data Tool table
  • Loading branch information
SARodrigues authored Nov 13, 2023
2 parents 848f79b + 35fe5b6 commit cde6399
Show file tree
Hide file tree
Showing 10 changed files with 436 additions and 150 deletions.
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@loaders.gl/zip": "^3.4.14",
"@mapbox/mapbox-gl-draw": "^1.4.2",
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-label": "^2.0.2",
Expand Down Expand Up @@ -59,6 +60,7 @@
"postcss": "8.4.21",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.48.2",
"react-icons": "4.11.0",
"react-map-gl": "7.1.6",
"recharts": "^2.9.0",
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';

import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { cva, type VariantProps } from 'class-variance-authority';
import { CheckIcon } from 'lucide-react';

import { cn } from '@/lib/classnames';

const checkboxVariants = cva('');

const Checkbox = React.forwardRef<
React.ElementRef<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root> &
VariantProps<typeof checkboxVariants>
>(({ className, name, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(checkboxVariants(), className, 'h-3.5 w-3.5 border-2 border-black')}
defaultChecked
id={name}
name={name}
{...props}
>
<CheckboxPrimitive.Indicator className="text-black">
<CheckIcon className="h-2.5 w-2.5" strokeWidth={4} />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
));

Checkbox.displayName = CheckboxPrimitive.Root.displayName;

export { Checkbox };
19 changes: 19 additions & 0 deletions frontend/src/containers/data-tool/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// ? This should be a Collection Type on Strapi, relation with location, which we would then pull from the API
export const LOCATION_TYPES_FILTER_OPTIONS = [
{
name: 'Country',
value: 'country',
},
{
name: 'Worldwide',
value: 'worldwide',
},
{
name: 'HighSeas',
value: 'highseas',
},
{
name: 'Region',
value: 'region',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { useEffect, useMemo, useState } from 'react';

import { useForm } from 'react-hook-form';

import { xor } from 'lodash-es';
import { Filter } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';

const ICON_CLASSNAMES = 'h-4 w-4 fill-black';

type FiltersButtonProps = {
field: string;
options: {
name: string;
value: string;
}[];
values: string[];
onChange: (field: string, values: string[]) => void;
};

type FormValues = {
filters: string[];
};

const FiltersButton: React.FC<FiltersButtonProps> = ({ field, options, values, onChange }) => {
const allFilterValues = useMemo(() => options.map(({ value }) => value), [options]);

const [isFiltersOpen, setIsFiltersOpen] = useState<boolean>(false);

const { watch, setValue } = useForm<FormValues>({
mode: 'onChange',
defaultValues: {
filters: values,
},
});

const filters = watch('filters');

useEffect(() => {
const filtersChanged = xor(filters, values).length > 0;
const allFiltersSelected = filters.length === allFilterValues.length;

if (filtersChanged || allFiltersSelected) {
onChange(field, filters);
}
}, [field, filters, values, onChange, allFilterValues.length]);

const handleSelectAll = () => {
setValue('filters', allFilterValues);
};

const handleClearAll = () => {
setValue('filters', []);
};

const handleOnCheckedChange = (type, checked) => {
if (checked) {
setValue('filters', [...filters, type]);
} else {
setValue(
'filters',
filters.filter((entry) => entry !== type)
);
}
};

const noFiltersSelected = filters.length === 0;

return (
<div>
<Popover open={isFiltersOpen} onOpenChange={setIsFiltersOpen}>
<PopoverTrigger>
<Button className="-ml-4" size="icon" variant="ghost">
<span className="sr-only">Filter</span>
<Filter className={ICON_CLASSNAMES} aria-hidden />
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="flex flex-col gap-6 font-mono text-xs">
<div className="space-between flex gap-6">
<Button
className="p-0 font-bold normal-case underline hover:bg-transparent"
variant="ghost"
size="sm"
onClick={handleSelectAll}
>
Select all
</Button>
<Button
className="p-0 font-bold normal-case text-slate-400 hover:bg-transparent hover:text-slate-400"
variant="ghost"
size="sm"
onClick={handleClearAll}
>
Clear all (None selected)
</Button>
</div>
<div className="mb-1 flex flex-col gap-5 py-2">
<form className="flex flex-col gap-5">
{options.map(({ name, value }) => {
return (
<div key={value} className="flex items-center">
<Checkbox
name={value}
value={value}
checked={filters.includes(value)}
onCheckedChange={(v) => handleOnCheckedChange(value, v)}
/>
<label
className="flex-grow cursor-pointer pl-2 pt-px text-xs leading-none text-black"
htmlFor={value}
>
{name}
</label>
</div>
);
})}
</form>
</div>
{noFiltersSelected && (
<div className="text-orange">Please, select at least one option</div>
)}
</PopoverContent>
</Popover>
</div>
);
};

export default FiltersButton;
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const DataToolTable = ({ columns, data }) => {
getSortedRowModel: getSortedRowModel(),
});

const hasData = table.getRowModel().rows?.length;
const hasData = table.getRowModel().rows?.length > 0;

const firstColumn = columns[0];
const lastColumn = columns[columns.length - 1];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { Column } from '@tanstack/react-table';
import { ArrowDownNarrowWide, ArrowUpNarrowWide, ArrowUpDown } from 'lucide-react';

import { Button } from '@/components/ui/button';
import { GlobalRegionalTableColumns } from '@/containers/data-tool/content/details/tables/global-regional/columns';
import { GlobalRegionalTableColumns } from '@/containers/data-tool/content/details/tables/global-regional/useColumns';

const BUTTON_CLASSNAMES = '-ml-4';
const ICON_CLASSNAMES = 'h-4 w-4';

type SortingButtonProps = {
Expand All @@ -16,19 +17,34 @@ const SortingButton: React.FC<SortingButtonProps> = ({ column }) => {
return (
<>
{!isSorted && (
<Button size="icon" variant="ghost" onClick={() => column.toggleSorting(false)}>
<Button
className={BUTTON_CLASSNAMES}
size="icon"
variant="ghost"
onClick={() => column.toggleSorting(false)}
>
<span className="sr-only">Sort ascending</span>
<ArrowUpDown className={ICON_CLASSNAMES} aria-hidden />
</Button>
)}
{isSorted === 'asc' && (
<Button size="icon" variant="ghost" onClick={() => column.toggleSorting(true)}>
<Button
className={BUTTON_CLASSNAMES}
size="icon"
variant="ghost"
onClick={() => column.toggleSorting(true)}
>
<span className="sr-only">Sort descending</span>
<ArrowDownNarrowWide className={ICON_CLASSNAMES} aria-hidden />
</Button>
)}
{isSorted === 'desc' && (
<Button size="icon" variant="ghost" onClick={() => column.clearSorting()}>
<Button
className={BUTTON_CLASSNAMES}
size="icon"
variant="ghost"
onClick={() => column.clearSorting()}
>
<span className="sr-only">Clear sorting</span>
<ArrowUpNarrowWide className={ICON_CLASSNAMES} aria-hidden />
</Button>
Expand Down
Loading

0 comments on commit cde6399

Please sign in to comment.