Skip to content

Commit

Permalink
feat: Add filters/sorting for lib v2 tab
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Jun 19, 2024
1 parent 09e3979 commit 4a27b49
Show file tree
Hide file tree
Showing 7 changed files with 340 additions and 16 deletions.
2 changes: 2 additions & 0 deletions src/studio-home/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export async function getStudioHomeLibrariesV2(customParams) {
page: customParams.page || 1,
pageSize: customParams.pageSize || 50,
pagination: customParams.pagination !== undefined ? customParams.pagination : true,
order: customParams.order,
textSearch: customParams.search,
};
const customParamsFormat = snakeCaseObject(customParamsDefaults);
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat });
Expand Down
68 changes: 52 additions & 16 deletions src/studio-home/tabs-section/libraries-v2-tab/index.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Icon, Row, Pagination } from '@openedx/paragon';
import {
Icon,
Row,
Pagination,
Alert,
Button,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig, getPath } from '@edx/frontend-platform';
import { Error } from '@openedx/paragon/icons';

import { constructLibraryAuthoringURL } from '../../../utils';
import useListStudioHomeV2Libraries from '../../data/apiHooks';
import { LoadingSpinner } from '../../../generic/Loading';
import AlertMessage from '../../../generic/alert-message';
import CardItem from '../../card-item';
import messages from '../messages';
import LibrariesV2Filters from './libraries-v2-filters';

const LibrariesV2Tab = ({
libraryAuthoringMfeUrl,
Expand All @@ -18,18 +26,26 @@ const LibrariesV2Tab = ({
const intl = useIntl();

const [currentPage, setCurrentPage] = useState(1);
const [isFiltered, setIsFiltered] = useState(false);
const [filterParams, setFilterParams] = useState({});

const handlePageSelect = (page) => {
setCurrentPage(page);

Check warning on line 33 in src/studio-home/tabs-section/libraries-v2-tab/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/index.jsx#L33

Added line #L33 was not covered by tests
};

const handleClearFilters = () => {
setFilterParams({});
setCurrentPage(1);
setIsFiltered(false);

Check warning on line 39 in src/studio-home/tabs-section/libraries-v2-tab/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/index.jsx#L37-L39

Added lines #L37 - L39 were not covered by tests
};

const {
data,
isLoading,
isError,
} = useListStudioHomeV2Libraries({ page: currentPage });
} = useListStudioHomeV2Libraries({ page: currentPage, ...filterParams });

if (isLoading) {
if (isLoading && !isFiltered) {
return (
<Row className="m-0 mt-4 justify-content-center">
<LoadingSpinner />
Expand All @@ -46,6 +62,8 @@ const LibrariesV2Tab = ({
: `${window.location.origin}${getPath(getConfig().PUBLIC_PATH)}library/${id}`
);

const hasV2Libraries = data?.results?.length > 0;

return (
isError ? (
<AlertMessage

Check warning on line 69 in src/studio-home/tabs-section/libraries-v2-tab/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/index.jsx#L69

Added line #L69 was not covered by tests
Expand All @@ -61,18 +79,25 @@ const LibrariesV2Tab = ({
) : (
<div className="courses-tab-container">
<div className="d-flex flex-row justify-content-between my-4">
{/* Temporary div to add spacing. This will be replaced with lib search/filters */}
<div className="d-flex" />
<p data-testid="pagination-info">
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data.results.length,
total: data.count,
})}
</p>
<LibrariesV2Filters
isLoading={isLoading}
setIsFiltered={setIsFiltered}
isFiltered={isFiltered}
setFilterParams={setFilterParams}
/>
{ !isLoading
&& (
<p data-testid="pagination-info">
{intl.formatMessage(messages.coursesPaginationInfo, {
length: data.results.length,
total: data.count,
})}
</p>
)}
</div>

{
data.results.map(({
{ hasV2Libraries
? data.results.map(({
id, org, slug, title,
}) => (
<CardItem
Expand All @@ -83,11 +108,22 @@ const LibrariesV2Tab = ({
number={slug}
url={libURL(id)}
/>
))
}
)) : isFiltered && !isLoading && (
<Alert className="mt-4">

Check warning on line 112 in src/studio-home/tabs-section/libraries-v2-tab/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/index.jsx#L112

Added line #L112 was not covered by tests
<Alert.Heading>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)}
</Alert.Heading>
<p data-testid="courses-not-found-alert">
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)}
</p>
<Button variant="primary" onClick={handleClearFilters}>
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)}
</Button>
</Alert>
)}

{
data.numPages > 1
data?.numPages > 1
&& (
<Pagination

Check warning on line 128 in src/studio-home/tabs-section/libraries-v2-tab/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/index.jsx#L128

Added line #L128 was not covered by tests
className="d-flex justify-content-center"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useState, useCallback, useEffect } from 'react';
import PropTypes from 'prop-types';
import { SearchField } from '@openedx/paragon';
import { debounce } from 'lodash';

import { LoadingSpinner } from '../../../../generic/Loading';
import LibrariesV2OrderFilterMenu from './libraries-v2-order-filter-menu';

/* regex to check if a string has only whitespace
example " "
*/
const regexOnlyWhiteSpaces = /^\s+$/;

const LibrariesV2Filters = ({
isLoading,
setIsFiltered,
setFilterParams,
isFiltered,
}) => {
const [search, setSearch] = useState('');
const [order, setOrder] = useState('title');

// Reset search & order when filters cleared
useEffect(() => {
if (!isFiltered) {
setSearch('');
setOrder('title');
}
}, [isFiltered, setSearch, setOrder]);

const getOrderFromFilterType = (filterType) => {
const orders = {

Check warning on line 32 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L32

Added line #L32 was not covered by tests
azLibrariesV2: 'title',
zaLibrariesV2: '-title',
newestLibrariesV2: '-created',
oldestLibrariesV2: 'created',
};

// Default to 'A-Z` if invalid filtertype
return orders[filterType] || 'title';
};

const getFilterTypeData = (baseFilters) => ({
azLibrariesV2: { ...baseFilters, order: 'title' },
zaLibrariesV2: { ...baseFilters, order: '-title' },
newestLibrariesV2: { ...baseFilters, order: '-created' },
oldestLibrariesV2: { ...baseFilters, order: 'created' },
});

const handleMenuFilterItemSelected = (filterType) => {
setOrder(getOrderFromFilterType(filterType));
setIsFiltered(true);

Check warning on line 52 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L51-L52

Added lines #L51 - L52 were not covered by tests

const baseFilters = {

Check warning on line 54 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L54

Added line #L54 was not covered by tests
search,
order,
};

const filterParams = getFilterTypeData(baseFilters);

Check warning on line 59 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L59

Added line #L59 was not covered by tests
const filterParamsFormat = filterParams[filterType] || baseFilters;

setFilterParams(filterParamsFormat);

Check warning on line 62 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L62

Added line #L62 was not covered by tests

// TODO: Probably need to reset the page number to 1
};

const handleSearchLibrariesV2 = (searchValueDebounced) => {
const valueFormatted = searchValueDebounced.trim();
const filterParams = {

Check warning on line 69 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L68-L69

Added lines #L68 - L69 were not covered by tests
search: valueFormatted.length > 0 ? valueFormatted : undefined,
order,
};
const hasOnlySpaces = regexOnlyWhiteSpaces.test(searchValueDebounced);

Check warning on line 73 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L73

Added line #L73 was not covered by tests

if (valueFormatted !== search && !hasOnlySpaces) {
setIsFiltered(true);
setSearch(valueFormatted);
setFilterParams(filterParams);

Check warning on line 78 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L76-L78

Added lines #L76 - L78 were not covered by tests

// TODO: Probably need to reset the page number to 1
}
};

const handleSearchLibrariesV2Debounced = useCallback(
debounce((value) => handleSearchLibrariesV2(value), 400),

Check warning on line 85 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L85

Added line #L85 was not covered by tests
[order],
);

return (
<div className="d-flex">
<div className="d-flex flex-row">
<SearchField
onSubmit={() => {}}

Check warning on line 93 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L93

Added line #L93 was not covered by tests
onChange={handleSearchLibrariesV2Debounced}
value={search}
className="mr-4"
data-testid="input-filter-courses-search"
placeholder="Search"
/>
{isLoading && (
<span className="search-field-loading" data-testid="loading-search-spinner">

Check warning on line 101 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L101

Added line #L101 was not covered by tests
<LoadingSpinner size="sm" />
</span>
)}
</div>

<LibrariesV2OrderFilterMenu onItemMenuSelected={handleMenuFilterItemSelected} isFiltered={isFiltered} />
</div>
);
};

LibrariesV2Filters.defaultProps = {
isLoading: false,
setIsFiltered: () => {},

Check warning on line 114 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/index.jsx#L114

Added line #L114 was not covered by tests
};

LibrariesV2Filters.propTypes = {
isLoading: PropTypes.bool,
setIsFiltered: PropTypes.func,
setFilterParams: PropTypes.func.isRequired,
isFiltered: PropTypes.bool.isRequired,
};

export default LibrariesV2Filters;
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Icon, Dropdown } from '@openedx/paragon';
import { Check } from '@openedx/paragon/icons';

const LibrariesV2FilterMenu = ({
id: idProp,
menuItems,
onItemMenuSelected,
defaultItemSelectedText,
isFiltered,
}) => {
const [itemMenuSelected, setItemMenuSelected] = useState(defaultItemSelectedText);
const handleCourseTypeSelected = (name, value) => {
setItemMenuSelected(name);
onItemMenuSelected(value);

Check warning on line 16 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-filter-menu/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-filter-menu/index.jsx#L15-L16

Added lines #L15 - L16 were not covered by tests
};

const libraryV2TypeSelectedIcon = (itemValue) => (itemValue === itemMenuSelected ? (
<Icon src={Check} className="ml-2" data-testid="menu-item-icon" />
) : null);

useEffect(() => {
if (!isFiltered) {
setItemMenuSelected(defaultItemSelectedText);
}
}, [isFiltered]);

return (
<Dropdown id={`dropdown-toggle-${idProp}`}>
<Dropdown.Toggle
alt="dropdown-toggle-menu-items"
id={idProp}
variant="none"
className="dropdown-toggle-menu-items"
data-testid={idProp}
>
{itemMenuSelected}
</Dropdown.Toggle>
<Dropdown.Menu>
{menuItems.map(({ id, name, value }) => (
<Dropdown.Item
key={id}
onClick={() => handleCourseTypeSelected(name, value)}

Check warning on line 44 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-filter-menu/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-filter-menu/index.jsx#L44

Added line #L44 was not covered by tests
data-testid={`item-menu-${id}`}
>
{name} {libraryV2TypeSelectedIcon(name)}
</Dropdown.Item>
))}
</Dropdown.Menu>
</Dropdown>
);
};

LibrariesV2FilterMenu.defaultProps = {
defaultItemSelectedText: '',
menuItems: [],
};

LibrariesV2FilterMenu.propTypes = {
onItemMenuSelected: PropTypes.func.isRequired,
defaultItemSelectedText: PropTypes.string,
id: PropTypes.string.isRequired,
menuItems: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
}),
),
isFiltered: PropTypes.bool.isRequired,
};

export default LibrariesV2FilterMenu;
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useMemo } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from '@edx/frontend-platform/i18n';

import messages from './messages';

import LibrariesV2FilterMenu from '../libraries-v2-filter-menu';

const LibrariesV2OrderFilterMenu = ({ onItemMenuSelected, isFiltered }) => {
const intl = useIntl();

const libraryV2Orders = useMemo(
() => [
{
id: 'az-libraries-v2',
name: intl.formatMessage(messages.librariesV2OrderFilterMenuAscendantLibrariesV2),
value: 'azLibrariesV2',
},
{
id: 'za-libraries-v2',
name: intl.formatMessage(messages.librariesV2OrderFilterMenuDescendantLibrariesV2),
value: 'zaLibrariesV2',
},
{
id: 'newest-libraries-v2',
name: intl.formatMessage(messages.librariesV2OrderFilterMenuNewestLibrariesV2),
value: 'newestLibrariesV2',
},
{
id: 'oldest-libraries-v2',
name: intl.formatMessage(messages.librariesV2OrderFilterMenuOldestLibrariesV2),
value: 'oldestLibrariesV2',
},
],
[intl],
);

const handleLibraryV2OrderSelected = (libraryV2Order) => {
onItemMenuSelected(libraryV2Order);

Check warning on line 39 in src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-order-filter-menu/index.jsx

View check run for this annotation

Codecov / codecov/patch

src/studio-home/tabs-section/libraries-v2-tab/libraries-v2-filters/libraries-v2-order-filter-menu/index.jsx#L39

Added line #L39 was not covered by tests
};

return (
<LibrariesV2FilterMenu
id="dropdown-toggle-courses-order-menu"
menuItems={libraryV2Orders}
onItemMenuSelected={handleLibraryV2OrderSelected}
defaultItemSelectedText={intl.formatMessage(messages.librariesV2OrderFilterMenuAscendantLibrariesV2)}
isFiltered={isFiltered}
/>
);
};

LibrariesV2OrderFilterMenu.propTypes = {
onItemMenuSelected: PropTypes.func.isRequired,
isFiltered: PropTypes.bool.isRequired,
};

export default LibrariesV2OrderFilterMenu;
Loading

0 comments on commit 4a27b49

Please sign in to comment.