diff --git a/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx b/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx index 40191e0c..e0d9a676 100644 --- a/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx +++ b/src/components/Organizations/Organizations/OrganizationsSkeleton.tsx @@ -5,7 +5,11 @@ import Box from 'components/Box'; import { Organization, OrganizationsPage, OrgsHeader, SearchInput } from './StyledOrganizations'; -const OrganizationsSkeleton = () => { +interface Props { + setSearch: React.Dispatch>; +} + +const OrganizationsSkeleton = ({ setSearch }: Props) => { const RenderSkeletonBox = (index: number) => { return ( @@ -27,7 +31,13 @@ const OrganizationsSkeleton = () => { - + setSearch(e.target.value)} + aria-labelledby="search" + className="searchInput" + type="text" + placeholder="Type to search" + /> <>{[...Array(numberOfItems)].map((_, idx) => RenderSkeletonBox(idx))} diff --git a/src/components/Organizations/Organizations/index.tsx b/src/components/Organizations/Organizations/index.tsx index 8d95711a..00ac463c 100644 --- a/src/components/Organizations/Organizations/index.tsx +++ b/src/components/Organizations/Organizations/index.tsx @@ -1,8 +1,11 @@ -import React, { useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Highlighter from 'react-highlight-words'; +import { LoadingOutlined } from '@ant-design/icons'; +import { Spin } from 'antd'; import Box from 'components/Box'; import OrganizationLink from 'components/link/Organizations/Organization'; +import { debounce } from 'lib/util'; import { Organization, OrganizationsPage, OrgsHeader, SearchInput } from './StyledOrganizations'; @@ -14,29 +17,112 @@ export interface IOrganization { __typename: 'Organization'; } +interface OrganizationProps { + organizations: IOrganization[]; + initialSearch: string; +} /** * The primary list of organizations. */ -const Organizations = ({ organizations = [] }: { organizations: IOrganization[] }) => { - const [searchInput, setSearchInput] = useState(''); +const Organizations: FC = ({ organizations = [], initialSearch }) => { + const [searchInput, setSearchInput] = useState(initialSearch || ''); + + const [isFiltering, setIsFiltering] = useState(false); + const [filteredOrgs, setFilteredOrgs] = useState(organizations); + + const searchInputRef = useRef(null); + + useEffect(() => { + if (initialSearch && searchInputRef.current) { + searchInputRef.current.focus(); + } + }, []); + + const timerLengthPercentage = useMemo( + () => Math.min(1000, Math.max(40, Math.floor(organizations.length * 0.0725))), + [organizations.length] + ); + + const debouncedSearch = useCallback( + debounce((searchVal: string) => { + setSearchInput(searchVal); + }, timerLengthPercentage), + [] + ); + + const handleSearch = (searchVal: string) => { + setIsFiltering(true); + debouncedSearch(searchVal); + }; - const filteredOrganizations = organizations.filter(key => { - const sortByName = key.name.toLowerCase().includes(searchInput.toLowerCase()); - const sortByUrl = ''; - return ['name', 'environments', '__typename'].includes(key.name) ? false : (true && sortByName) || sortByUrl; - }); + useEffect(() => { + const filterOrgs = async (): Promise => { + return new Promise(resolve => { + const filteredOrganizations = organizations.filter(org => { + const searchStrLowerCase = searchInput.toLowerCase(); + const filterFn = (key?: string) => key?.toLowerCase().includes(searchStrLowerCase); + + const sortByName = filterFn(org.name); + const sortByDesc = filterFn(org.description); + const sortByFriendlyName = filterFn(org.friendlyName); + + if (['__typename', 'name', 'id'].includes(org.name)) { + return false; + } + return sortByName || sortByFriendlyName || sortByDesc; + }); + + resolve(filteredOrganizations); + }); + }; + + void filterOrgs() + .then(filtered => setFilteredOrgs(filtered)) + .finally(() => setIsFiltering(false)); + }, [searchInput, organizations]); + + const filteredMappedOrgs = useMemo(() => { + return filteredOrgs.map(organization => ( + + + +

+ +

+
+ +
+
+
+
+
+ )); + }, [filteredOrgs]); return ( - + setSearchInput(e.target.value)} + onChange={e => handleSearch(e.target.value)} placeholder="Type to search" disabled={organizations.length === 0} /> @@ -48,37 +134,14 @@ const Organizations = ({ organizations = [] }: { organizations: IOrganization[]
)} - {searchInput && !filteredOrganizations.length && ( + {searchInput && !filteredMappedOrgs.length && (

No organizations matching "{searchInput}"

)} - {filteredOrganizations.map(organization => ( - - - -

- -

-
- -
-
-
-
-
- ))} + {filteredMappedOrgs} ); }; diff --git a/src/pages/organizations/index.js b/src/pages/organizations/index.js index c6d341f4..b95c99dd 100644 --- a/src/pages/organizations/index.js +++ b/src/pages/organizations/index.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState } from 'react'; import Head from 'next/head'; @@ -18,6 +18,7 @@ const OrganizationsPage = () => { const { data, error, loading } = useQuery(AllOrganizationsQuery, { displayName: 'AllOrganizationsQuery', }); + const [searchInput, setSearchInput] = useState(''); if (error) { return ; @@ -31,7 +32,11 @@ const OrganizationsPage = () => {

Organizations

- {loading ? : } + {loading ? ( + + ) : ( + + )}