diff --git a/packages/lib-components/src/components/actions/Button/style.ts b/packages/lib-components/src/components/actions/Button/style.ts index e009961df6..ffe4703d67 100644 --- a/packages/lib-components/src/components/actions/Button/style.ts +++ b/packages/lib-components/src/components/actions/Button/style.ts @@ -34,6 +34,7 @@ export const Default = styled.button<{ span { font-size: 1.25rem; font-weight: 600; + white-space: nowrap; color: ${({ theme, color }) => { switch (color) { case 'white': diff --git a/packages/lib-components/src/components/data/Table/index.tsx b/packages/lib-components/src/components/data/Table/index.tsx index 2dd25b9c46..c8e25e056d 100644 --- a/packages/lib-components/src/components/data/Table/index.tsx +++ b/packages/lib-components/src/components/data/Table/index.tsx @@ -28,6 +28,7 @@ import { AiOutlinePicRight as TightDensityIcon, AiOutlineRight as ExpandIcon, AiOutlineUp as CollapseIcon, + AiOutlineSearch as SearchIcon, } from 'react-icons/ai'; import { ColumnHeader } from './subcomponents/ColumnHeader'; import { ColumnVisibility } from './subcomponents/ColumnVisibility'; @@ -37,7 +38,8 @@ import { PageSizeSelect } from './subcomponents/Pagination/PageSizeSelect'; import { ColumnFilter, PageOptions } from '../../../hooks/useTableActions'; import { GenericCheckBox as CheckBox } from '../../inputs/CheckBox/Generic'; -import { useLocalStorage } from '../../../hooks'; +import { useDebounce, useLocalStorage } from '../../../hooks'; +import { UnControlledTextField } from '../../../components'; export interface TableProps { id: string; @@ -58,6 +60,10 @@ export interface TableProps { /// Renders actions that are only visible when one or more rows are selected. renderRowSelectionActions?: () => JSX.Element; + /// When callback is assigned, an input field will appear in the toolbar. + onSearchInputChanged?: (input: string) => void; + searchInputPlaceholder?: string; + title?: string; rowSelection?: { @@ -95,6 +101,8 @@ export function Table({ rowSelection, columnSearch, renderDetailPanel, + onSearchInputChanged, + searchInputPlaceholder = 'Search...', renderToolbar, canExpand = () => false, renderRowSelectionActions, @@ -127,6 +135,15 @@ export function Table({ const [columnPinning, setColumnPinning] = useState({}); const { storedValue: density, setValue: setDensity } = useLocalStorage(`table-density-${id}`, 'tight'); + const [searchInput, setSearchInput] = useState(''); + const debouncedValue = useDebounce(searchInput, 350); + + useEffect(() => { + if (onSearchInputChanged) { + onSearchInputChanged(debouncedValue); + table.resetPagination(); + } + }, [debouncedValue]); // Might because potentially none fullfil the canExpand condtion. const rowsMightExpand = renderDetailPanel ? true : false; @@ -246,6 +263,18 @@ export function Table({ {renderToolbar && renderToolbar()} + {onSearchInputChanged && ( + } + onChange={(e) => setSearchInput(e.currentTarget.value)} + /> + )} {!isLoading && } ({ pageSize: 25 }); + const [quickSearchInput, setQuickSearchInput] = useState(''); const { data, isLoading } = useQuery( playersQueryOptions({ page: pagination.paginationState.pageIndex, @@ -73,7 +74,10 @@ function Component() { xboxLiveId: columnFilters.columnFiltersState.find((filter) => filter.id === 'xboxLiveId')?.value, }, search: { - name: columnSearch.columnSearchState.find((search) => search.id === 'name')?.value, + name: [ + ...(columnSearch.columnSearchState.find((search) => search.id === 'name')?.value ?? []), + quickSearchInput, + ], steamId: columnSearch.columnSearchState.find((search) => search.id === 'steamId')?.value, epicOnlineServicesId: columnSearch.columnSearchState.find((search) => search.id === 'epicOnlineServicesId') ?.value, @@ -271,6 +275,8 @@ function Component() { columnSearch={columnSearch} sorting={sorting} isLoading={isLoading} + onSearchInputChanged={setQuickSearchInput} + searchInputPlaceholder="Search player by name..." /> ); diff --git a/packages/web-main/src/routes/_auth/_global/users.tsx b/packages/web-main/src/routes/_auth/_global/users.tsx index c1cba6dc8d..9e00cdc1e2 100644 --- a/packages/web-main/src/routes/_auth/_global/users.tsx +++ b/packages/web-main/src/routes/_auth/_global/users.tsx @@ -19,7 +19,7 @@ import { useUserRemove, useInviteUser, usersQueryOptions, userMeQueryOptions } f import { UserOutputWithRolesDTO, UserSearchInputDTOSortDirectionEnum, PERMISSIONS } from '@takaro/apiclient'; import { createColumnHelper } from '@tanstack/react-table'; import { - AiOutlinePlus as PlusIcon, + AiOutlineSolution as InviteUserIcon, AiOutlineDelete as DeleteIcon, AiOutlineUser as ProfileIcon, AiOutlineEdit as EditIcon, @@ -45,6 +45,7 @@ export const Route = createFileRoute('/_auth/_global/users')({ function Component() { useDocumentTitle('Users'); const { pagination, columnFilters, sorting, columnSearch } = useTableActions(); + const [quickSearchInput, setQuickSearchInput] = useState(''); const { data, isLoading } = useQuery({ ...usersQueryOptions({ @@ -60,7 +61,10 @@ function Component() { playerId: columnFilters.columnFiltersState.find((filter) => filter.id === 'playerId')?.value, }, search: { - name: columnSearch.columnSearchState.find((search) => search.id === 'name')?.value, + name: [ + ...(columnSearch.columnSearchState.find((search) => search.id === 'name')?.value ?? []), + quickSearchInput, + ], discordId: columnSearch.columnSearchState.find((search) => search.id === 'discordId')?.value, playerId: columnSearch.columnSearchState.find((search) => search.id === 'playerId')?.value, }, @@ -152,6 +156,8 @@ function Component() { columnFiltering={columnFilters} columnSearch={columnSearch} sorting={sorting} + onSearchInputChanged={setQuickSearchInput} + searchInputPlaceholder="Search by user name..." /> ); } @@ -193,7 +199,7 @@ const InviteUser: FC = () => {