Skip to content

Commit

Permalink
Merge pull request #277 from uselagoon/organization-filtering
Browse files Browse the repository at this point in the history
Change: add friendlyname and description debounced filtering to organizations page
  • Loading branch information
tobybellwood authored Jul 4, 2024
2 parents a07070b + 9b820e5 commit f648b5c
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 39 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import Box from 'components/Box';

import { Organization, OrganizationsPage, OrgsHeader, SearchInput } from './StyledOrganizations';

const OrganizationsSkeleton = () => {
interface Props {
setSearch: React.Dispatch<React.SetStateAction<string>>;
}

const OrganizationsSkeleton = ({ setSearch }: Props) => {
const RenderSkeletonBox = (index: number) => {
return (
<Box className="box" key={index}>
Expand All @@ -27,7 +31,13 @@ const OrganizationsSkeleton = () => {
<Skeleton width={'20%'} />
</label>
<label></label>
<SearchInput aria-labelledby="search" className="searchInput" type="text" placeholder="Type to search" />
<SearchInput
onChange={e => setSearch(e.target.value)}
aria-labelledby="search"
className="searchInput"
type="text"
placeholder="Type to search"
/>
</OrgsHeader>
<>{[...Array<undefined>(numberOfItems)].map((_, idx) => RenderSkeletonBox(idx))}</>
</OrganizationsPage>
Expand Down
133 changes: 98 additions & 35 deletions src/components/Organizations/Organizations/index.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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<OrganizationProps> = ({ organizations = [], initialSearch }) => {
const [searchInput, setSearchInput] = useState(initialSearch || '');

const [isFiltering, setIsFiltering] = useState(false);
const [filteredOrgs, setFilteredOrgs] = useState(organizations);

const searchInputRef = useRef<HTMLInputElement>(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<IOrganization[]> => {
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 => (
<OrganizationLink
organizationSlug={organization.name}
organizationId={organization.id}
key={organization.id}
orgFriendlyName={organization.friendlyName}
>
<Box className="box">
<Organization>
<h4>
<Highlighter
searchWords={[searchInput]}
autoEscape={true}
textToHighlight={organization.friendlyName || organization.name}
/>
</h4>
<div className="description">
<Highlighter searchWords={[searchInput]} autoEscape={true} textToHighlight={organization.description} />
</div>
</Organization>
<div className="customer"></div>
</Box>
</OrganizationLink>
));
}, [filteredOrgs]);

return (
<OrganizationsPage>
<OrgsHeader>
<label>Organizations</label>
<label>
Organizations
{isFiltering && <Spin indicator={<LoadingOutlined />} />}
</label>
<label></label>
<SearchInput
ref={searchInputRef}
aria-labelledby="search"
className="searchInput"
type="text"
value={searchInput}
onChange={e => setSearchInput(e.target.value)}
onChange={e => handleSearch(e.target.value)}
placeholder="Type to search"
disabled={organizations.length === 0}
/>
Expand All @@ -48,37 +134,14 @@ const Organizations = ({ organizations = [] }: { organizations: IOrganization[]
</Organization>
</Box>
)}
{searchInput && !filteredOrganizations.length && (
{searchInput && !filteredMappedOrgs.length && (
<Box className="box">
<Organization>
<h4>No organizations matching &quot;{searchInput}&quot;</h4>
</Organization>
</Box>
)}
{filteredOrganizations.map(organization => (
<OrganizationLink
organizationSlug={organization.name}
organizationId={organization.id}
key={organization.id}
orgFriendlyName={organization.friendlyName}
>
<Box className="box">
<Organization>
<h4>
<Highlighter
searchWords={[searchInput]}
autoEscape={true}
textToHighlight={organization.friendlyName || organization.name}
/>
</h4>
<div className="description">
<Highlighter searchWords={[searchInput]} autoEscape={true} textToHighlight={organization.description} />
</div>
</Organization>
<div className="customer"></div>
</Box>
</OrganizationLink>
))}
{filteredMappedOrgs}
</OrganizationsPage>
);
};
Expand Down
9 changes: 7 additions & 2 deletions src/pages/organizations/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useState } from 'react';

import Head from 'next/head';

Expand All @@ -18,6 +18,7 @@ const OrganizationsPage = () => {
const { data, error, loading } = useQuery(AllOrganizationsQuery, {
displayName: 'AllOrganizationsQuery',
});
const [searchInput, setSearchInput] = useState('');

if (error) {
return <QueryError error={error} />;
Expand All @@ -31,7 +32,11 @@ const OrganizationsPage = () => {
<CommonWrapper>
<h2>Organizations</h2>
<div className="content">
{loading ? <OrganizationsSkeleton /> : <Organizations organizations={data.allOrganizations || []} />}
{loading ? (
<OrganizationsSkeleton setSearch={setSearchInput} />
) : (
<Organizations organizations={data.allOrganizations || []} initialSearch={searchInput} />
)}
</div>
</CommonWrapper>
</MainLayout>
Expand Down

0 comments on commit f648b5c

Please sign in to comment.