From 0fe3fbe947c1d0d031bdaa93d9e1661fe55e5622 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Fri, 6 Sep 2024 09:07:50 +0300 Subject: [PATCH 1/6] 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 2/6] 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: Wed, 11 Sep 2024 19:57:12 +0300 Subject: [PATCH 3/6] Squashed commit of the following: commit 742f25001369978fc4b9d03a851c2f1ef72024a7 Author: imdeaconu Date: Wed Sep 11 19:54:55 2024 +0300 add read notification checkmark commit ea11fa0f637ad2e00cc7a8601c6c6f51fcb64a3f Author: imdeaconu Date: Wed Sep 11 19:54:30 2024 +0300 add read notification column --- .../PushMessageDetails/PushMessageDetails.tsx | 18 ++++-- .../components/PushMessages/PushMessages.tsx | 63 +++++-------------- 2 files changed, 28 insertions(+), 53 deletions(-) diff --git a/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx b/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx index 8872e6583..49489700a 100644 --- a/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx +++ b/web/src/features/monitoring-observers/components/PushMessageDetails/PushMessageDetails.tsx @@ -7,12 +7,13 @@ import { DateTimeFormat } from '@/common/formats'; import type { FunctionComponent } from '@/common/types'; import { NavigateBack } from '@/components/NavigateBack/NavigateBack'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; -import { pushMessageDetailsQueryOptions ,Route} from '@/routes/monitoring-observers/push-messages.$id_.view'; +import { pushMessageDetailsQueryOptions, Route } from '@/routes/monitoring-observers/push-messages.$id_.view'; +import { CheckIcon } from '@heroicons/react/24/outline'; import { useSuspenseQuery } from '@tanstack/react-query'; export default function PushMessageDetails(): FunctionComponent { - const { id } = Route.useParams() - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const { id } = Route.useParams(); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const { data: pushMessage } = useSuspenseQuery(pushMessageDetailsQueryOptions(currentElectionRoundId, id)); return ( @@ -47,9 +48,14 @@ export default function PushMessageDetails(): FunctionComponent {

Total targeted observers {pushMessage?.receivers?.length ?? 0}

{pushMessage?.receivers?.map((receiver) => ( -

- {receiver.name} -

+
+

+ {receiver.name} +

+ {receiver.hasReadNotification && ( + + )} +
))}
diff --git a/web/src/features/monitoring-observers/components/PushMessages/PushMessages.tsx b/web/src/features/monitoring-observers/components/PushMessages/PushMessages.tsx index 4df312a30..0cc9372fc 100644 --- a/web/src/features/monitoring-observers/components/PushMessages/PushMessages.tsx +++ b/web/src/features/monitoring-observers/components/PushMessages/PushMessages.tsx @@ -3,19 +3,19 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DataTableColumnHeader } from '@/components/ui/DataTable/DataTableColumnHeader'; import { QueryParamsDataTable } from '@/components/ui/DataTable/QueryParamsDataTable'; import { Separator } from '@/components/ui/separator'; +import { ChevronRightIcon } from '@heroicons/react/24/outline'; import { Link, useNavigate } from '@tanstack/react-router'; import type { CellContext, ColumnDef } from '@tanstack/react-table'; import { Plus } from 'lucide-react'; -import { ChevronRightIcon } from '@heroicons/react/24/outline'; -import { usePushMessages } from '../../hooks/push-messages-queries'; -import { format } from 'date-fns'; -import type { PushMessageModel } from '../../models/push-message'; -import { useCallback } from 'react'; import { DateTimeFormat } from '@/common/formats'; -import type { TableCellProps } from '@/components/ui/DataTable/DataTable'; import type { FunctionComponent } from '@/common/types'; +import type { TableCellProps } from '@/components/ui/DataTable/DataTable'; import { useCurrentElectionRoundStore } from '@/context/election-round.store'; +import { format } from 'date-fns'; +import { useCallback } from 'react'; +import { usePushMessages } from '../../hooks/push-messages-queries'; +import type { PushMessageModel } from '../../models/push-message'; function PushMessages(): FunctionComponent { const pushMessagesColDefs: ColumnDef[] = [ @@ -24,67 +24,37 @@ function PushMessages(): FunctionComponent { accessorKey: 'sentAt', enableSorting: false, enableGlobalFilter: false, - cell: ({ row }) =>
{format(row.original.sentAt, DateTimeFormat)}
+ cell: ({ row }) =>
{format(row.original.sentAt, DateTimeFormat)}
, }, { header: ({ column }) => , accessorKey: 'sender', enableSorting: false, enableGlobalFilter: false, - cell: ({ - row: { - original: { sender }, - }, - }) => ( -

- {sender} -

- ), }, { header: ({ column }) => , accessorKey: 'numberOfTargetedObservers', enableSorting: false, enableGlobalFilter: false, - cell: ({ - row: { - original: { numberOfTargetedObservers }, - }, - }) => ( -

- {numberOfTargetedObservers} -

- ), + }, + { + header: ({ column }) => , + accessorKey: 'numberOfReadNotifications', + enableSorting: false, + enableGlobalFilter: false, }, { header: ({ column }) => , accessorKey: 'title', enableSorting: false, enableGlobalFilter: false, - cell: ({ - row: { - original: { title }, - }, - }) => ( -

- {title} -

- ), }, { header: ({ column }) => , accessorKey: 'body', enableSorting: false, enableGlobalFilter: false, - cell: ({ - row: { - original: { body }, - }, - }) => ( -

- {body} -

- ), }, { header: '', @@ -105,15 +75,14 @@ function PushMessages(): FunctionComponent { const getCellProps = (context: CellContext): TableCellProps | void => { if (context.column.id === 'body' || context.column.id === 'title') { - return { className: 'truncate hover:text-clip', - } + }; } - } + }; const navigate = useNavigate(); - const currentElectionRoundId = useCurrentElectionRoundStore(s => s.currentElectionRoundId); + const currentElectionRoundId = useCurrentElectionRoundStore((s) => s.currentElectionRoundId); const navigateToPushMessage = useCallback( (id: string) => { From 0facf6510d14b3946c23807c299655248c810335 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Fri, 13 Sep 2024 13:38:18 +0300 Subject: [PATCH 4/6] Squashed commit of the following: commit d8833dcf5669c257a28ed0bd58f5085385f2b53f Author: imdeaconu Date: Fri Sep 13 13:29:31 2024 +0300 WIP: add selector functionality commit 3608c0e7d3d79a26037f8b7961d50f019b924406 Author: imdeaconu Date: Fri Sep 13 10:00:05 2024 +0300 WIP: create new tags input --- web/src/components/ui/tag-selector.tsx | 304 +++++++++---------------- 1 file changed, 107 insertions(+), 197 deletions(-) diff --git a/web/src/components/ui/tag-selector.tsx b/web/src/components/ui/tag-selector.tsx index c65e07302..33c3a9296 100644 --- a/web/src/components/ui/tag-selector.tsx +++ b/web/src/components/ui/tag-selector.tsx @@ -1,32 +1,13 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { - ChevronDown, - XIcon -} from "lucide-react"; -import * as React from "react"; +import { cn, getTagColor } from '@/lib/utils'; +import { Combobox, Popover } from '@headlessui/react'; +import { ChevronDown, Search, XIcon } from 'lucide-react'; +import { FC, useEffect, useRef, useState } from 'react'; +import { Badge } from './badge'; +import { Input } from './input'; +import { Separator } from './separator'; +import { CommandItem } from './command'; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, - CommandSeparator, -} from "@/components/ui/command"; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover"; -import { Separator } from "@/components/ui/separator"; -import { cn, getTagColor } from "@/lib/utils"; - - -interface TagsSelectFormFieldProps - extends React.ButtonHTMLAttributes { +interface TagsSelectFormFieldProps extends React.ButtonHTMLAttributes { asChild?: boolean; options: string[]; defaultValue?: string[]; @@ -36,79 +17,56 @@ interface TagsSelectFormFieldProps onValueChange: (value: string[]) => void; } -const TagsSelectFormField = React.forwardRef< - HTMLButtonElement, - TagsSelectFormFieldProps ->( - ( - { - className, - asChild = false, - options, - defaultValue, - onValueChange, - disabled, - placeholder, - ...props - }, - ref - ) => { - const [selectedValues, setSelectedValues] = React.useState( - defaultValue || [] - ); - const selectedValuesSet = React.useRef(new Set(selectedValues)); - const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); - const [search, setSearch] = React.useState('') +const TagsSelectFormField: FC = (props) => { + const { options, defaultValue, placeholder, onValueChange } = props; + const [selectedValues, setSelectedValues] = useState(defaultValue || []); + const [query, setQuery] = useState(''); + const searchRef = useRef(null); + const hasSelectedValues = selectedValues.length > 0; + + useEffect(() => { + const valuesSet = new Set(selectedValues); + onValueChange(Array.from(valuesSet)); + }, [selectedValues]); - React.useEffect(() => { - setSelectedValues(defaultValue || []); - selectedValuesSet.current = new Set(defaultValue); - }, [defaultValue]); + const handleInputKeyDown = (event: any) => { + if (event.key !== 'Enter') return; + setQuery(''); + }; + const toggleOption = (value: string) => { + const currentTag = selectedValues.find((t) => t.toLocaleLowerCase() === value.trim().toLocaleLowerCase()); - const handleInputKeyDown = (event: any) => { - if (event.key === "Enter") { - if(search){ - toggleOption(search) - } - } - }; + if (currentTag) setSelectedValues(selectedValues.filter((v) => v !== value.trim())); + else setSelectedValues([...selectedValues, value.trim()]); + }; - const toggleOption = (value: string) => { - const currentTag = selectedValues.find(t => t.toLocaleLowerCase() === value.trim().toLocaleLowerCase()); + const filteredOptions = + query === '' + ? options + : options.filter((option) => { + return option.toLowerCase().includes(query.toLowerCase()); + }); - if (currentTag) { - selectedValuesSet.current.delete(currentTag); - setSelectedValues(selectedValues.filter((v) => v !== value.trim())); - } else { - selectedValuesSet.current.add(value.trim()); - setSelectedValues([...selectedValues, value.trim()]); - } - - onValueChange(Array.from(selectedValuesSet.current)); - }; + const comboboxClasses = cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none hover:bg-accent hover:text-accent-foreground data-[focus]:bg-accent data-[focus]:text-accent-foreground data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50 cursor-pointer" + ); - return ( - - - - - setIsPopoverOpen(false)} - > - - - - {/* Press enter to create this tag. */} - - {options.map((option) => { - return ( - toggleOption(option)} - style={{ - pointerEvents: "auto", - opacity: 1, + +
+ {hasSelectedValues && ( + <> + { + setSelectedValues([]); + onValueChange([]); + event.stopPropagation(); }} - className="cursor-pointer" - > - {option} - - ); - })} - - - -
- {selectedValues.length > 0 && ( - <> - { - setSelectedValues([]); - selectedValuesSet.current.clear(); - onValueChange([]); - }} - style={{ - pointerEvents: "auto", - opacity: 1, - }} - className="flex-1 justify-center cursor-pointer" - > - Clear - - - - )} - - setIsPopoverOpen(false)} - style={{ - pointerEvents: "auto", - opacity: 1, - }} - className="flex-1 justify-center cursor-pointer" - > - Close - -
-
- - - - - ); - } -); + /> + + + )} + + + +
+ + + +
+ + + setQuery(event.target.value)} + onKeyDown={handleInputKeyDown} + /> +
+ + + {query.length > 0 && ( + + Create "{query}" + + )} -TagsSelectFormField.displayName = "TagsSelectFormField"; + {filteredOptions.map((option) => ( + + {option} + + ))} + + +
+ + )} +
+ + ); +}; export default TagsSelectFormField; From 67f681d128e767e5d9cd5e2d43a7db7fa1cff59e Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Fri, 13 Sep 2024 14:00:35 +0300 Subject: [PATCH 5/6] chore: remove unused import --- web/src/components/ui/tag-selector.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/src/components/ui/tag-selector.tsx b/web/src/components/ui/tag-selector.tsx index 33c3a9296..aff394da6 100644 --- a/web/src/components/ui/tag-selector.tsx +++ b/web/src/components/ui/tag-selector.tsx @@ -5,7 +5,6 @@ import { FC, useEffect, useRef, useState } from 'react'; import { Badge } from './badge'; import { Input } from './input'; import { Separator } from './separator'; -import { CommandItem } from './command'; interface TagsSelectFormFieldProps extends React.ButtonHTMLAttributes { asChild?: boolean; @@ -125,7 +124,6 @@ const TagsSelectFormField: FC = (props) => { ))} - )} From 8d73252c776795f71dea15ba1382c3a67da30223 Mon Sep 17 00:00:00 2001 From: imdeaconu Date: Mon, 16 Sep 2024 18:55:29 +0300 Subject: [PATCH 6/6] chore: delete duplicated / unused classes --- web/src/components/ui/tag-selector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/components/ui/tag-selector.tsx b/web/src/components/ui/tag-selector.tsx index aff394da6..37e6c48ca 100644 --- a/web/src/components/ui/tag-selector.tsx +++ b/web/src/components/ui/tag-selector.tsx @@ -97,7 +97,7 @@ const TagsSelectFormField: FC = (props) => { - +