Skip to content

Commit

Permalink
Søk i brukerliste (#987)
Browse files Browse the repository at this point in the history
* add first version of user search

* refactor search

* rename "entrees" to "entries"

* Add live region

* Update user search placeholder text and move filter logick to hook

* Move user search feedback text

* Add hideLabel prop to Search component and update user search placeholder text

* Refactor List component to use TypeScript interface for props

* chore: Update UsersList component to useFilteredRightHolders hook and fix search functionality

* Refactor user search functionality and update nn text

* fix typo

---------

Co-authored-by: Sondre Wittek <sondrewittek@gmail.com>
  • Loading branch information
mgunnerud and sonwit authored Aug 6, 2024
1 parent 03db21b commit 654c872
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 46 deletions.
18 changes: 10 additions & 8 deletions src/components/List/List.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import classNames from 'classnames';
import React, { ReactNode } from 'react';
import type { ReactNode } from 'react';
import React from 'react';
import classes from './List.module.css';

export const List = ({
children,
heading,
compact,
}: {
interface ListProps extends React.HTMLAttributes<HTMLElement> {
children: React.ReactNode[];
heading?: ReactNode;
compact?: boolean;
}) => {
}

export const List = ({ children, heading, compact, ...props }: ListProps) => {
return (
<>
{heading}
<ul className={classNames(classes.list, compact ? classes.compact : classes.spacious)}>
<ul
className={classNames(classes.list, compact ? classes.compact : classes.spacious)}
{...props}
>
{children}
</ul>
</>
Expand Down
4 changes: 4 additions & 0 deletions src/features/amUI/users/UsersList.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,7 @@
justify-content: center;
padding-top: 1rem;
}

.searchBar {
max-width: 27rem;
}
87 changes: 54 additions & 33 deletions src/features/amUI/users/UsersList.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,85 @@
import React, { useState, useMemo } from 'react';
import { Button, Heading, Tag, Pagination } from '@digdir/designsystemet-react';
import React, { useState } from 'react';
import { Button, Heading, Tag, Pagination, Search, Paragraph } from '@digdir/designsystemet-react';
import classes from './UsersList.module.css';
import { useTranslation } from 'react-i18next';
import type { RightHolder } from '@/rtk/features/userInfo/userInfoApi';
import { useGetRightHoldersQuery } from '@/rtk/features/userInfo/userInfoApi';
import { getArrayPage, getTotalNumOfPages } from '@/resources/utils';

import { ChevronDownIcon, ChevronUpIcon } from '@navikt/aksel-icons';

import { ListItem } from '@/components/List/ListItem';
import { List } from '@/components/List/List';
import { UserIcon } from '@/components/UserIcon/UserIcon';
import { useFilteredRightHolders } from './useFilteredRightHolders';
import { debounce } from '@/resources/utils';

export const UsersList = () => {
const { t } = useTranslation();

const [currentPage, setCurrentPage] = useState(1);
const [searchString, setSearchString] = useState<string>('');

const pageSize = 10;

const { data: rightHolders } = useGetRightHoldersQuery();
const { pageEntries, numOfPages, searchResultLength } = useFilteredRightHolders(
searchString,
currentPage,
pageSize,
);

const [pageEntrees, numOfPages] = useMemo(() => {
if (!rightHolders) {
return [[], 1];
}
const numPages = getTotalNumOfPages(rightHolders, pageSize);
return [getArrayPage(rightHolders, currentPage, pageSize), numPages];
}, [rightHolders, currentPage]);
const onSearch = debounce((newSearchString: string) => {
setSearchString(newSearchString);
setCurrentPage(1); // reset current page when searching
}, 300);

return (
<div className={classes.usersList}>
<Heading
level={2}
size='sm'
spacing
id='user_list_heading_id'
>
{t('users_page.user_list_heading')}
</Heading>
<Search
className={classes.searchBar}
placeholder={t('users_page.user_search_placeholder')}
onChange={(event) => onSearch(event.target.value)}
onClear={() => {
setSearchString('');
setCurrentPage(1);
}}
hideLabel
label={t('users_page.user_search_placeholder')}
/>
<List
aria-labelledby='user_list_heading_id'
compact
heading={
<Heading
level={2}
size='lg'
spacing
>
{t('users_page.user_list_heading')}
</Heading>
}
>
{pageEntrees.map((user) => (
{pageEntries.map((user) => (
<UserListItem
key={user.partyUuid}
user={user}
/>
))}
</List>
<Pagination
className={classes.pagination}
size='sm'
hideLabels={true}
currentPage={currentPage}
totalPages={numOfPages}
onChange={(newPage) => setCurrentPage(newPage)}
nextLabel='Neste'
previousLabel='Forrige'
/>
<Paragraph
role='alert'
size='lg'
>
{searchResultLength === 0 ? t('users_page.user_no_search_result') : ''}
</Paragraph>
{numOfPages > 1 && (
<Pagination
className={classes.pagination}
size='sm'
hideLabels={true}
currentPage={currentPage}
totalPages={numOfPages}
onChange={(newPage) => setCurrentPage(newPage)}
nextLabel='Neste'
previousLabel='Forrige'
/>
)}
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/amUI/users/UsersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useDocumentTitle } from '@/resources/hooks/useDocumentTitle';
import { useTranslation } from 'react-i18next';

import { PageWrapper } from '@/components';
import { Heading, Pagination } from '@digdir/designsystemet-react';
import { Heading } from '@digdir/designsystemet-react';
import { useGetReporteeQuery } from '@/rtk/features/userInfo/userInfoApi';

import { UsersList } from './UsersList';
Expand Down
76 changes: 76 additions & 0 deletions src/features/amUI/users/useFilteredRightHolders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { getTotalNumOfPages, getArrayPage } from '@/resources/utils';
import type { RightHolder } from '@/rtk/features/userInfo/userInfoApi';
import { useGetRightHoldersQuery } from '@/rtk/features/userInfo/userInfoApi';
import { useState, useEffect } from 'react';

const isSearchMatch = (searchString: string, rightHolder: RightHolder): boolean => {
const isNameMatch = rightHolder.name.toLowerCase().indexOf(searchString.toLowerCase()) > -1;
const isPersonIdMatch = rightHolder.personId === searchString;
const isOrgNrMatch = rightHolder.organizationNumber === searchString;
return isNameMatch || isPersonIdMatch || isOrgNrMatch;
};

const computePageEntries = (
searchString: string,
currentPage: number,
pageSize: number,
rightHolders?: RightHolder[],
) => {
if (!rightHolders) {
return {
pageEntries: [],
numOfPages: 1,
searchResultLength: 0,
};
}

const searchResult: RightHolder[] = [];

rightHolders.forEach((rightHolder) => {
if (isSearchMatch(searchString, rightHolder)) {
searchResult.push(rightHolder);
} else if (rightHolder.inheritingRightHolders?.length > 0) {
// check for searchString matches in inheritingRightHolders
const matchingInheritingItems = rightHolder.inheritingRightHolders.filter(
(inheritRightHolder) => isSearchMatch(searchString, inheritRightHolder),
);
if (matchingInheritingItems.length > 0) {
searchResult.push({
...rightHolder,
inheritingRightHolders: matchingInheritingItems,
});
}
}
});

return {
pageEntries: getArrayPage(searchResult, currentPage, pageSize),
numOfPages: getTotalNumOfPages(searchResult, pageSize),
searchResultLength: searchResult.length,
};
};

export const useFilteredRightHolders = (
searchString: string,
currentPage: number,
pageSize: number,
) => {
const { data: rightHolders } = useGetRightHoldersQuery();
const [pageEntries, setPageEntries] = useState<RightHolder[]>([]);
const [numOfPages, setNumOfPages] = useState<number>(1);
const [searchResultLength, setSearchResultLength] = useState<number>(0);

useEffect(() => {
const { pageEntries, numOfPages, searchResultLength } = computePageEntries(
searchString,
currentPage,
pageSize,
rightHolders,
);
setPageEntries(pageEntries);
setNumOfPages(numOfPages);
setSearchResultLength(searchResultLength);
}, [searchString, currentPage, rightHolders]);

return { pageEntries, numOfPages, searchResultLength };
};
4 changes: 3 additions & 1 deletion src/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,9 @@
"users_page": {
"page_title": "Users and groups - Altinn",
"main_page_heading": "Users with power of attorney in {{name}}",
"user_list_heading": "Other users with power of attorney"
"user_list_heading": "Other users with power of attorney",
"user_search_placeholder": "Search for users",
"user_no_search_result": "No users found"
},
"user_role": {
"SREVA": "Auditor registered in the registry of auditors",
Expand Down
4 changes: 3 additions & 1 deletion src/localizations/no_nb.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,9 @@
"users_page": {
"page_title": "Brukere og grupper - Altinn",
"main_page_heading": "Brukere med fullmakter i {{name}}",
"user_list_heading": "Andre med fullmakt"
"user_list_heading": "Andre med fullmakt",
"user_search_placeholder": "Søk etter person eller virksomhet",
"user_no_search_result": "Ingen brukere funnet"
},
"user_role": {
"SREVA": "Revisor registrert i revisorregisteret",
Expand Down
4 changes: 3 additions & 1 deletion src/localizations/no_nn.json
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,9 @@
"users_page": {
"page_title": "Brukarar og grupper - Altinn",
"main_page_heading": "Brukarar med fullmakter i {{name}}",
"user_list_heading": "Andre med fullmakt"
"user_list_heading": "Andre med fullmakt",
"user_search_placeholder": "Søk etter brukarar eller virksomheter",
"user_no_search_result": "Inga resultat for søket"
},
"user_role": {
"SREVA": "Revisor registrert i revisorregisteret",
Expand Down
9 changes: 8 additions & 1 deletion src/resources/utils/debounce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export function debounce(func: (...args: any[]) => void, timeout = 300) {
let timer: NodeJS.Timeout;
return (...args: any[]) => {

const debouncedFunction = (...args: any[]) => {
clearTimeout(timer);
timer = setTimeout(() => {
func(...args);
}, timeout);
};

debouncedFunction.cancel = () => {
clearTimeout(timer);
};

return debouncedFunction;
}

0 comments on commit 654c872

Please sign in to comment.