From 0fe3fbe947c1d0d031bdaa93d9e1661fe55e5622 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Fri, 6 Sep 2024 09:07:50 +0300 Subject: [PATCH 01/12] fix: make dropdown menus scrollable --- web/src/components/ui/dropdown-menu.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/ui/dropdown-menu.tsx b/web/src/components/ui/dropdown-menu.tsx index 04c77e929..8743a25e0 100644 --- a/web/src/components/ui/dropdown-menu.tsx +++ b/web/src/components/ui/dropdown-menu.tsx @@ -60,7 +60,7 @@ const DropdownMenuContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - 'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', + 'z-50 min-w-[8rem] overflow-y-auto max-h-[12rem] rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', className )} {...props} From 4ca1a44ad5f2dedc56dc11cbc18bed953f35d047 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Fri, 6 Sep 2024 13:01:07 +0300 Subject: [PATCH 02/12] fix: truncate overflowing table columns --- web/src/components/ui/DataTable/DataTable.tsx | 19 ++++++++++--------- web/src/components/ui/table.tsx | 6 ++++-- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/web/src/components/ui/DataTable/DataTable.tsx b/web/src/components/ui/DataTable/DataTable.tsx index 8c2e4b953..b2efeb507 100644 --- a/web/src/components/ui/DataTable/DataTable.tsx +++ b/web/src/components/ui/DataTable/DataTable.tsx @@ -1,23 +1,23 @@ +import { SortOrder, type DataTableParameters, type PageResponse } from '@/common/types'; +import { EmptyCollectionIcon } from '@/components/icons/EmptyCollectionIcon'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import type { UseQueryResult } from '@tanstack/react-query'; import { flexRender, getCoreRowModel, + getExpandedRowModel, getSortedRowModel, + useReactTable, + type CellContext, type ColumnDef, type PaginationState, + type Row, type SortingState, - useReactTable, type VisibilityState, - getExpandedRowModel, - type Row, - type CellContext, } from '@tanstack/react-table'; -import { EmptyCollectionIcon } from '@/components/icons/EmptyCollectionIcon'; -import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { useEffect, useState, type ReactElement } from 'react'; -import { SortOrder, type DataTableParameters, type PageResponse } from '@/common/types'; -import { DataTablePagination } from './DataTablePagination'; -import type { UseQueryResult } from '@tanstack/react-query'; import { Skeleton } from '../skeleton'; +import { DataTablePagination } from './DataTablePagination'; export interface RowData { id: string; @@ -237,6 +237,7 @@ export function DataTable( style={{ cursor: onRowClick ? 'pointer' : undefined }}> {row.getVisibleCells().map((cell) => ( diff --git a/web/src/components/ui/table.tsx b/web/src/components/ui/table.tsx index d7832fa43..4490fc994 100644 --- a/web/src/components/ui/table.tsx +++ b/web/src/components/ui/table.tsx @@ -64,7 +64,9 @@ TableHead.displayName = 'TableHead'; const TableCell = React.forwardRef>( ({ className, ...props }, ref) => ( - + + {props.children} + ) ); TableCell.displayName = 'TableCell'; @@ -76,4 +78,4 @@ const TableCaption = React.forwardRef Date: Tue, 10 Sep 2024 10:18:11 +0300 Subject: [PATCH 03/12] create reusable filtering containers --- .../components/FilteringContainer.tsx | 19 ++ .../components/GenericSelectFilter.tsx | 33 +++ .../components/ObserverStatusSelect.tsx | 37 ++++ .../filtering/hooks/useFilteringContainer.ts | 34 +++ .../MonitoringObserversList.tsx | 199 ++++++++++-------- .../MonitoringObserversListFilters.tsx | 11 + .../ResetFiltersButton/ResetFiltersButton.tsx | 4 +- 7 files changed, 252 insertions(+), 85 deletions(-) create mode 100644 web/src/features/filtering/components/FilteringContainer.tsx create mode 100644 web/src/features/filtering/components/GenericSelectFilter.tsx create mode 100644 web/src/features/filtering/components/ObserverStatusSelect.tsx create mode 100644 web/src/features/filtering/hooks/useFilteringContainer.ts create mode 100644 web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversListFilters.tsx diff --git a/web/src/features/filtering/components/FilteringContainer.tsx b/web/src/features/filtering/components/FilteringContainer.tsx new file mode 100644 index 000000000..00832a29f --- /dev/null +++ b/web/src/features/filtering/components/FilteringContainer.tsx @@ -0,0 +1,19 @@ +import { Button } from '@/components/ui/button'; +import { FC, ReactNode } from 'react'; +import { useFilteringContainer } from '../hooks/useFilteringContainer'; + +interface FilteringContainerProps { + children?: ReactNode; +} + +export const FilteringContainer: FC = ({ children }) => { + const { filteringIsActive, resetFilters } = useFilteringContainer(); + return ( +
+ {children} + +
+ ); +}; diff --git a/web/src/features/filtering/components/GenericSelectFilter.tsx b/web/src/features/filtering/components/GenericSelectFilter.tsx new file mode 100644 index 000000000..4a536dcde --- /dev/null +++ b/web/src/features/filtering/components/GenericSelectFilter.tsx @@ -0,0 +1,33 @@ +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { FC } from 'react'; + +export type GenericSelectFilterOption = { + value: string; + label: string; +}; + +interface GenericSelectFilterProps { + placeholder: string; + value: string; + options: GenericSelectFilterOption[]; + onChange: (value: string) => void; +} + +export const GenericSelectFilter: FC = (props) => { + const { placeholder, value, options, onChange } = props; + + return ( + + ); +}; diff --git a/web/src/features/filtering/components/ObserverStatusSelect.tsx b/web/src/features/filtering/components/ObserverStatusSelect.tsx new file mode 100644 index 000000000..4ac3868cf --- /dev/null +++ b/web/src/features/filtering/components/ObserverStatusSelect.tsx @@ -0,0 +1,37 @@ +import { FC } from 'react'; +import { useFilteringContainer } from '../hooks/useFilteringContainer'; +import { GenericSelectFilter, GenericSelectFilterOption } from './GenericSelectFilter'; + +const observerStatusOptions: GenericSelectFilterOption[] = [ + { + value: 'Active', + label: 'Active', + }, + + { + value: 'Pending', + label: 'Pending', + }, + + { + value: 'Suspended', + label: 'Suspended', + }, +]; + +export const ObserverStatusSelect: FC = () => { + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onStatusChange = (value: string) => { + navigateHandler({ status: value }); + }; + + return ( + + ); +}; diff --git a/web/src/features/filtering/hooks/useFilteringContainer.ts b/web/src/features/filtering/hooks/useFilteringContainer.ts new file mode 100644 index 000000000..6da8d4194 --- /dev/null +++ b/web/src/features/filtering/hooks/useFilteringContainer.ts @@ -0,0 +1,34 @@ +import { useSetPrevSearch } from '@/common/prev-search-store'; +import { useNavigate, useSearch } from '@tanstack/react-router'; +import { useCallback } from 'react'; + +export function useFilteringContainer() { + const navigate = useNavigate(); + const queryParams = useSearch({ strict: false }); + const setPrevSearch = useSetPrevSearch(); + const filteringIsActive = Object.keys(queryParams).some((key) => key !== 'tab' && key !== 'viewBy'); + + const navigateHandler = useCallback( + (search: Record) => { + void navigate({ + // @ts-ignore + search: (prev) => { + const newSearch: Record = { + ...prev, + ...search, + }; + setPrevSearch(newSearch); + return newSearch; + }, + }); + }, + [navigate, setPrevSearch] + ); + + const resetFilters = () => { + navigate({}); + setPrevSearch({}); + }; + + return { queryParams, filteringIsActive, navigate, navigateHandler, resetFilters }; +} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index 52e9095be..c254714e1 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -19,23 +19,24 @@ import { Separator } from '@/components/ui/separator'; import { useDialog } from '@/components/ui/use-dialog'; import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline'; import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; -import { useNavigate, useRouter } from '@tanstack/react-router'; +import { useNavigate, useRouter, useSearch } from '@tanstack/react-router'; import { CellContext, ColumnDef } from '@tanstack/react-table'; import { X } from 'lucide-react'; import { useCallback, useState } from 'react'; import { DateTimeFormat } from '@/common/formats'; import { TableCellProps } from '@/components/ui/DataTable/DataTable'; +import { toast } from '@/components/ui/use-toast'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { isQueryFiltered } from '@/lib/utils'; +import { queryClient } from '@/main'; import { format } from 'date-fns'; import { useMonitoringObserversTags } from '../../../../hooks/tags-queries'; import { MonitoringObserver, MonitoringObserverStatus } from '../../models/monitoring-observer'; import ImportMonitoringObserversDialog from '../MonitoringObserversList/ImportMonitoringObserversDialog'; import ImportMonitoringObserversErrorsDialog from '../MonitoringObserversList/ImportMonitoringObserversErrorsDialog'; import ConfirmResendInvitationDialog from './ConfirmResendInvitationDialog'; -import { queryClient } from '@/main'; -import { toast } from '@/components/ui/use-toast'; -import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { MonitoringObserversListFilters } from './MonitoringObserversListFilters'; type ListMonitoringObserverResponse = PageResponse; @@ -44,6 +45,7 @@ type UseMonitoringObserversResult = UseQueryResult[] = [ { @@ -114,7 +116,9 @@ function MonitoringObserversList() { navigateToEdit(row.original.id)}>Edit handleResendInviteToObserver(row.original.id)}>Resend invitation email + onClick={() => handleResendInviteToObserver(row.original.id)}> + Resend invitation email + ), @@ -136,13 +140,16 @@ function MonitoringObserversList() { }; const navigateToObserver = (monitoringObserverId: string) => { - navigate({ to: '/monitoring-observers/view/$monitoringObserverId/$tab', params: { monitoringObserverId, tab: 'details' } }); + navigate({ + to: '/monitoring-observers/view/$monitoringObserverId/$tab', + params: { monitoringObserverId, tab: 'details' }, + }); }; const navigateToEdit = (monitoringObserverId: string) => { navigate({ to: '/monitoring-observers/edit/$monitoringObserverId', params: { monitoringObserverId } }); }; - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: tags } = useMonitoringObserversTags(currentElectionRoundId); const useMonitoringObservers = (params: DataTableParameters): UseMonitoringObserversResult => { @@ -154,7 +161,7 @@ function MonitoringObserversList() { params.sortColumnName, params.sortOrder, searchText, - statusFilter, + (queryParams as any).status, tagsFilter, ], queryFn: async () => { @@ -164,10 +171,9 @@ function MonitoringObserversList() { SortColumnName: params.sortColumnName, SortOrder: params.sortOrder, searchText: searchText, - status: statusFilter, + status: (queryParams as any).status, }; - const response = await authApi.get>( `/election-rounds/${currentElectionRoundId}/monitoring-observers?${tagsFilter .map((n) => `tags=${n}`) @@ -185,14 +191,20 @@ function MonitoringObserversList() { return { ...response.data, isEmpty: !isQueryFiltered(paramsObject) && response.data.items.length === 0 }; }, - enabled: !!currentElectionRoundId + enabled: !!currentElectionRoundId, }); }; const resendInvitationsMutation = useMutation({ - mutationFn: ({ electionRoundId, monitoringObserverId }: { electionRoundId: string; monitoringObserverId: string | undefined }) => { + mutationFn: ({ + electionRoundId, + monitoringObserverId, + }: { + electionRoundId: string; + monitoringObserverId: string | undefined; + }) => { return authApi.put(`/election-rounds/${electionRoundId}/monitoring-observers:resend-invites`, { - ids: [monitoringObserverId].filter(id => !!id) + ids: [monitoringObserverId].filter((id) => !!id), }); }, @@ -212,9 +224,9 @@ function MonitoringObserversList() { toast({ title: 'Error resending invitation', description: 'Please contact Platform admins', - variant: 'destructive' + variant: 'destructive', }); - } + }, }); const changeIsFiltering = () => { @@ -255,7 +267,9 @@ function MonitoringObserversList() { ); const exportMonitoringObservers = async () => { - const res = await authApi.get(`/election-rounds/${currentElectionRoundId}/monitoring-observers:export`, { responseType: "blob" }); + const res = await authApi.get(`/election-rounds/${currentElectionRoundId}/monitoring-observers:export`, { + responseType: 'blob', + }); const csvData = res.data; const blob = new Blob([csvData], { type: 'text/csv' }); @@ -275,12 +289,11 @@ function MonitoringObserversList() { // Func to provide props to table cell const getCellProps = (context: CellContext): TableCellProps | void => { if (context.column.id === 'tags') { - return { className: 'flex-wrap', - } + }; } - } + }; return ( @@ -288,9 +301,22 @@ function MonitoringObserversList() {
Monitoring observers list
- {!!importErrorsFileId && } - { setImportErrorsFileId(fileId); importMonitoringObserverErrorsDialog.trigger(); }} /> - - resendInvitationsMutation.mutate({ electionRoundId: currentElectionRoundId, monitoringObserverId })} - {...confirmResendInvitesDialog.dialogProps} /> + onConfirm={() => + resendInvitationsMutation.mutate({ electionRoundId: currentElectionRoundId, monitoringObserverId }) + } + {...confirmResendInvitesDialog.dialogProps} + />
@@ -357,62 +387,65 @@ function MonitoringObserversList() { {isFiltering ? ( -
- - - -
- Tags -
-
- - {tags?.map((tag) => ( - toggleTagsFilter(tag)} - key={tag}> - {tag} - - ))} - -
- -
- {statusFilter && ( - handleStatusFilter('')} - className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> - Observer status: {statusFilter} - - - )} - - {tagsFilter.map((tag) => ( - toggleTagsFilter(tag)} - className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> - Tags: {tag} - + <> + +
+ + + +
+ Tags +
+
+ + {tags?.map((tag) => ( + toggleTagsFilter(tag)} + key={tag}> + {tag} + + ))} + +
+ +
+ {statusFilter && ( + handleStatusFilter('')} + className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> + Observer status: {statusFilter} + + + )} + + {tagsFilter.map((tag) => ( + toggleTagsFilter(tag)} + className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> + Tags: {tag} + + + ))} +
-
+ ) : ( '' )} diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversListFilters.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversListFilters.tsx new file mode 100644 index 000000000..2c22b3cfb --- /dev/null +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversListFilters.tsx @@ -0,0 +1,11 @@ +import { FilteringContainer } from '@/features/filtering/components/FilteringContainer'; +import { ObserverStatusSelect } from '@/features/filtering/components/ObserverStatusSelect'; +import { FC } from 'react'; + + + +export const MonitoringObserversListFilters: FC = () => { + return + + +}; diff --git a/web/src/features/responses/components/ResetFiltersButton/ResetFiltersButton.tsx b/web/src/features/responses/components/ResetFiltersButton/ResetFiltersButton.tsx index 5c5ccf4f5..d886db5e7 100644 --- a/web/src/features/responses/components/ResetFiltersButton/ResetFiltersButton.tsx +++ b/web/src/features/responses/components/ResetFiltersButton/ResetFiltersButton.tsx @@ -1,7 +1,7 @@ -import { useNavigate } from '@tanstack/react-router'; +import { useSetPrevSearch } from '@/common/prev-search-store'; import type { FunctionComponent } from '@/common/types'; import { Button } from '@/components/ui/button'; -import { useSetPrevSearch } from '@/common/prev-search-store'; +import { useNavigate } from '@tanstack/react-router'; type ResetFiltersButtonProps = { disabled: boolean; From eab319981b70a78711e47b251211262d5ffb3111 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Tue, 10 Sep 2024 12:48:28 +0300 Subject: [PATCH 04/12] add ObserverTags component --- .../components/GenericSelectFilter.tsx | 2 +- .../components/ObserverTagsSelect.tsx | 54 +++++++++++++++++++ .../MonitoringObserversList.tsx | 54 +++++-------------- .../MonitoringObserversListFilters.tsx | 12 +++-- 4 files changed, 76 insertions(+), 46 deletions(-) create mode 100644 web/src/features/filtering/components/ObserverTagsSelect.tsx diff --git a/web/src/features/filtering/components/GenericSelectFilter.tsx b/web/src/features/filtering/components/GenericSelectFilter.tsx index 4a536dcde..7957d0ace 100644 --- a/web/src/features/filtering/components/GenericSelectFilter.tsx +++ b/web/src/features/filtering/components/GenericSelectFilter.tsx @@ -17,7 +17,7 @@ export const GenericSelectFilter: FC = (props) => { const { placeholder, value, options, onChange } = props; return ( - diff --git a/web/src/features/filtering/components/ObserverTagsSelect.tsx b/web/src/features/filtering/components/ObserverTagsSelect.tsx new file mode 100644 index 000000000..25edabcf2 --- /dev/null +++ b/web/src/features/filtering/components/ObserverTagsSelect.tsx @@ -0,0 +1,54 @@ +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useMonitoringObserversTags } from '@/hooks/tags-queries'; +import { FC, useEffect, useState } from 'react'; +import { useFilteringContainer } from '../hooks/useFilteringContainer'; + +export const ObserverTagsSelect: FC = () => { + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); + const { data: tags } = useMonitoringObserversTags(currentElectionRoundId); + const [tagsFilter, setTagsFilter] = useState([]); + + const { queryParams, navigateHandler } = useFilteringContainer(); + + const onStatusChange = (value: string) => {}; + + useEffect(() => { + navigateHandler({ tags: tagsFilter as any }); + }, [tagsFilter]); + + const toggleTagsFilter = (tag: string) => { + setTagsFilter((prevTags: any) => { + if (!prevTags.includes(tag)) { + return [...prevTags, tag]; + } else { + return prevTags.filter((tagText: string) => tagText !== tag); + } + }); + }; + + return ( + + +
+ Observer tags +
+
+ + {tags?.map((tag) => ( + toggleTagsFilter(tag)} + key={tag}> + {tag} + + ))} + +
+ ); +}; diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index c254714e1..7c68233ac 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -8,13 +8,11 @@ import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumn import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { DropdownMenu, - DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { Input } from '@/components/ui/input'; -import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Separator } from '@/components/ui/separator'; import { useDialog } from '@/components/ui/use-dialog'; import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } from '@heroicons/react/24/outline'; @@ -162,9 +160,13 @@ function MonitoringObserversList() { params.sortOrder, searchText, (queryParams as any).status, - tagsFilter, + (queryParams as any).tags, ], queryFn: async () => { + const tags = new URLSearchParams(); + tags.append('tags', 'west'); + tags.append('tags', 'ios'); + const paramsObject: any = { PageNumber: params.pageNumber, PageSize: params.pageSize, @@ -172,12 +174,18 @@ function MonitoringObserversList() { SortOrder: params.sortOrder, searchText: searchText, status: (queryParams as any).status, + tags: (queryParams as any).tags, }; + const tagString = + (queryParams as any).tags == undefined + ? '' + : (queryParams as any).tags?.map((n: string) => `tags=${n}`).join('&'); + + //const tagString = (queryParams as any)?.tags?.length===0?. + const response = await authApi.get>( - `/election-rounds/${currentElectionRoundId}/monitoring-observers?${tagsFilter - .map((n) => `tags=${n}`) - .join('&')}`, + `/election-rounds/${currentElectionRoundId}/monitoring-observers?${tagString ?? ''}`, { params: Object.keys(paramsObject) .filter((k) => paramsObject[k] !== null && paramsObject[k] !== '') @@ -390,40 +398,6 @@ function MonitoringObserversList() { <>
- - - -
- Tags -
-
- - {tags?.map((tag) => ( - toggleTagsFilter(tag)} - key={tag}> - {tag} - - ))} - -
-
{statusFilter && ( { - return - - + return ( + + + + + ); }; From 1cd8c18c839527704ebc00bfde9b565c34aa783b Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Tue, 10 Sep 2024 17:02:23 +0300 Subject: [PATCH 05/12] fix ObserversTagSelect --- .../filtering/components/ActiveFilters.tsx | 36 +++++++++++++++++++ .../components/FilteringContainer.tsx | 4 ++- .../components/ObserverTagsSelect.tsx | 23 ++++-------- .../filtering/hooks/useFilteringContainer.ts | 2 +- 4 files changed, 47 insertions(+), 18 deletions(-) create mode 100644 web/src/features/filtering/components/ActiveFilters.tsx diff --git a/web/src/features/filtering/components/ActiveFilters.tsx b/web/src/features/filtering/components/ActiveFilters.tsx new file mode 100644 index 000000000..ec8347030 --- /dev/null +++ b/web/src/features/filtering/components/ActiveFilters.tsx @@ -0,0 +1,36 @@ +import { FilterBadge } from '@/components/ui/badge'; +import { useNavigate } from '@tanstack/react-router'; +import { FC, useCallback } from 'react'; + +interface ActiveFilterProps { + filterId: string; + value: string; +} + +const ActiveFilter: FC = ({ filterId, value }) => { + const navigate = useNavigate(); + const onClearFilter = useCallback( + (filter: keyof any) => () => { + void navigate({ search: (prev) => ({ ...prev, [filter]: undefined }) }); + }, + [navigate] + ); + return ; +}; + +interface ActiveFiltersProps { + queryParams: any; +} + +export const ActiveFilters: FC = ({ queryParams }) => { + return ( +
+ {Object.keys(queryParams).map((key) => { + const value = queryParams[key]; + + if (!Array.isArray(value)) return ; + return value.map((item) => ); + })} +
+ ); +}; diff --git a/web/src/features/filtering/components/FilteringContainer.tsx b/web/src/features/filtering/components/FilteringContainer.tsx index 00832a29f..d6487c581 100644 --- a/web/src/features/filtering/components/FilteringContainer.tsx +++ b/web/src/features/filtering/components/FilteringContainer.tsx @@ -1,19 +1,21 @@ import { Button } from '@/components/ui/button'; import { FC, ReactNode } from 'react'; import { useFilteringContainer } from '../hooks/useFilteringContainer'; +import { ActiveFilters } from './ActiveFilters'; interface FilteringContainerProps { children?: ReactNode; } export const FilteringContainer: FC = ({ children }) => { - const { filteringIsActive, resetFilters } = useFilteringContainer(); + const { filteringIsActive, queryParams, resetFilters } = useFilteringContainer(); return (
{children} + {filteringIsActive && }
); }; diff --git a/web/src/features/filtering/components/ObserverTagsSelect.tsx b/web/src/features/filtering/components/ObserverTagsSelect.tsx index 25edabcf2..f1029a5b3 100644 --- a/web/src/features/filtering/components/ObserverTagsSelect.tsx +++ b/web/src/features/filtering/components/ObserverTagsSelect.tsx @@ -6,30 +6,21 @@ import { } from '@/components/ui/dropdown-menu'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; import { useMonitoringObserversTags } from '@/hooks/tags-queries'; -import { FC, useEffect, useState } from 'react'; +import { FC } from 'react'; import { useFilteringContainer } from '../hooks/useFilteringContainer'; export const ObserverTagsSelect: FC = () => { const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: tags } = useMonitoringObserversTags(currentElectionRoundId); - const [tagsFilter, setTagsFilter] = useState([]); - const { queryParams, navigateHandler } = useFilteringContainer(); + const currentTags = (queryParams as any)?.tags ?? []; - const onStatusChange = (value: string) => {}; + const toggleTagsFilter = (tag: string) => { + if (!currentTags.includes(tag)) return navigateHandler({ tags: [...currentTags, tag] }); - useEffect(() => { - navigateHandler({ tags: tagsFilter as any }); - }, [tagsFilter]); + const filteredTags = currentTags.filter((tagText: string) => tagText !== tag); - const toggleTagsFilter = (tag: string) => { - setTagsFilter((prevTags: any) => { - if (!prevTags.includes(tag)) { - return [...prevTags, tag]; - } else { - return prevTags.filter((tagText: string) => tagText !== tag); - } - }); + return navigateHandler({ tags: filteredTags }); }; return ( @@ -42,7 +33,7 @@ export const ObserverTagsSelect: FC = () => { {tags?.map((tag) => ( toggleTagsFilter(tag)} key={tag}> {tag} diff --git a/web/src/features/filtering/hooks/useFilteringContainer.ts b/web/src/features/filtering/hooks/useFilteringContainer.ts index 6da8d4194..bd7c5fbfa 100644 --- a/web/src/features/filtering/hooks/useFilteringContainer.ts +++ b/web/src/features/filtering/hooks/useFilteringContainer.ts @@ -9,7 +9,7 @@ export function useFilteringContainer() { const filteringIsActive = Object.keys(queryParams).some((key) => key !== 'tab' && key !== 'viewBy'); const navigateHandler = useCallback( - (search: Record) => { + (search: Record) => { void navigate({ // @ts-ignore search: (prev) => { From d5d9ecc854bd828d565440170e6148716bbf0103 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Tue, 10 Sep 2024 17:26:03 +0300 Subject: [PATCH 06/12] remove old filtering system from observers list --- .../MonitoringObserversList.tsx | 67 ++----------------- 1 file changed, 6 insertions(+), 61 deletions(-) diff --git a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx index 7c68233ac..25a387f3e 100644 --- a/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx +++ b/web/src/features/monitoring-observers/components/MonitoringObserversList/MonitoringObserversList.tsx @@ -19,13 +19,13 @@ import { Cog8ToothIcon, EllipsisVerticalIcon, FunnelIcon, PaperAirplaneIcon } fr import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query'; import { useNavigate, useRouter, useSearch } from '@tanstack/react-router'; import { CellContext, ColumnDef } from '@tanstack/react-table'; -import { X } from 'lucide-react'; -import { useCallback, useState } from 'react'; +import { useState } from 'react'; import { DateTimeFormat } from '@/common/formats'; import { TableCellProps } from '@/components/ui/DataTable/DataTable'; import { toast } from '@/components/ui/use-toast'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { useFilteringContainer } from '@/features/filtering/hooks/useFilteringContainer'; import { isQueryFiltered } from '@/lib/utils'; import { queryClient } from '@/main'; import { format } from 'date-fns'; @@ -123,15 +123,14 @@ function MonitoringObserversList() { }, ]; - const [tagsFilter, setTagsFilter] = useState([]); - const [statusFilter, setStatusFilter] = useState(''); const [searchText, setSearchText] = useState(''); - const [isFiltering, setFiltering] = useState(false); const [importErrorsFileId, setImportErrorsFileId] = useState(); const [monitoringObserverId, setMonitoringObserverId] = useState(); const importMonitoringObserversDialog = useDialog(); const importMonitoringObserverErrorsDialog = useDialog(); const confirmResendInvitesDialog = useDialog(); + const { filteringIsActive } = useFilteringContainer(); + const [isFiltering, setIsFiltering] = useState(filteringIsActive); const handleSearchInput = (ev: React.FormEvent) => { setSearchText(ev.currentTarget.value); @@ -238,42 +237,16 @@ function MonitoringObserversList() { }); const changeIsFiltering = () => { - setFiltering((prev) => { + setIsFiltering((prev) => { return !prev; }); }; - const handleStatusFilter = (status: string) => { - setStatusFilter(status); - }; - - const resetFilters = () => { - setStatusFilter(''); - setTagsFilter([]); - }; - function handleResendInviteToObserver(id?: string): void { setMonitoringObserverId(id); confirmResendInvitesDialog.trigger(); } - const toggleTagsFilter = (tag: string) => { - setTagsFilter((prevTags: any) => { - if (!prevTags.includes(tag)) { - return [...prevTags, tag]; - } else { - return prevTags.filter((tagText: string) => tagText !== tag); - } - }); - }; - - const rowClickHandler = useCallback( - (monitoringObserverId: string) => { - navigateToObserver(monitoringObserverId); - }, - [navigateToObserver] - ); - const exportMonitoringObservers = async () => { const res = await authApi.get(`/election-rounds/${currentElectionRoundId}/monitoring-observers:export`, { responseType: 'blob', @@ -394,35 +367,7 @@ function MonitoringObserversList() {
- {isFiltering ? ( - <> - -
-
- {statusFilter && ( - handleStatusFilter('')} - className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> - Observer status: {statusFilter} - - - )} - - {tagsFilter.map((tag) => ( - toggleTagsFilter(tag)} - className='rounded-full cursor-pointer py-1 px-4 bg-purple-100 text-sm text-purple-900 font-medium flex items-center gap-2'> - Tags: {tag} - - - ))} -
-
- - ) : ( - '' - )} + {isFiltering && } Date: Wed, 11 Sep 2024 10:21:01 +0300 Subject: [PATCH 07/12] feature: remove just one item from a filter array --- .../filtering/components/ActiveFilters.tsx | 39 +++++++++++++++---- .../components/GenericSelectFilter.tsx | 8 +++- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/web/src/features/filtering/components/ActiveFilters.tsx b/web/src/features/filtering/components/ActiveFilters.tsx index ec8347030..dec14596b 100644 --- a/web/src/features/filtering/components/ActiveFilters.tsx +++ b/web/src/features/filtering/components/ActiveFilters.tsx @@ -5,17 +5,31 @@ import { FC, useCallback } from 'react'; interface ActiveFilterProps { filterId: string; value: string; + isArray?: boolean; } -const ActiveFilter: FC = ({ filterId, value }) => { +type SearchParams = { + [key: string]: any; +}; + + +const ActiveFilter: FC = ({ filterId, value, isArray }) => { const navigate = useNavigate(); const onClearFilter = useCallback( - (filter: keyof any) => () => { - void navigate({ search: (prev) => ({ ...prev, [filter]: undefined }) }); + (filter: string, value?: string) => () => { + if (!isArray) { + return navigate({ search: (prev) => ({ ...prev, [filter]: undefined }) }); + } + return navigate({ + search: (prev: SearchParams) => ({ + ...prev, + [filter]: prev[filter]?.filter((item: string) => item !== value), // Remove the value from the array + }), + }); }, [navigate] ); - return ; + return ; }; interface ActiveFiltersProps { @@ -25,11 +39,20 @@ interface ActiveFiltersProps { export const ActiveFilters: FC = ({ queryParams }) => { return (
- {Object.keys(queryParams).map((key) => { - const value = queryParams[key]; + {Object.keys(queryParams).map((filterId) => { + let key = ''; + const value = queryParams[filterId]; + const isArray = Array.isArray(value); + + if (!isArray) { + key = `active-filter-${filterId}`; + return ; + } + return value.map((item) => { + key = `active-filter-${filterId}-${item}`; - if (!Array.isArray(value)) return ; - return value.map((item) => ); + return ; + }); })}
); diff --git a/web/src/features/filtering/components/GenericSelectFilter.tsx b/web/src/features/filtering/components/GenericSelectFilter.tsx index 7957d0ace..3642a63be 100644 --- a/web/src/features/filtering/components/GenericSelectFilter.tsx +++ b/web/src/features/filtering/components/GenericSelectFilter.tsx @@ -16,6 +16,8 @@ interface GenericSelectFilterProps { export const GenericSelectFilter: FC = (props) => { const { placeholder, value, options, onChange } = props; + const selectId = crypto.randomUUID(); + return (