Skip to content

Commit

Permalink
feat: Add filters/sorting for the libraries v2 tab on studio home (op…
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh authored Jul 8, 2024
1 parent 8cf26e1 commit 83489b0
Show file tree
Hide file tree
Showing 11 changed files with 535 additions and 52 deletions.
66 changes: 66 additions & 0 deletions src/library/data/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { camelCaseObject, snakeCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;

export interface LibraryV2 {
id: string,
type: string,
org: string,
slug: string,
title: string,
description: string,
numBlocks: number,
version: number,
lastPublished: string | null,
allowLti: boolean,
allowPublicLearning: boolean,
allowPublicRead: boolean,
hasUnpublishedChanges: boolean,
hasUnpublishedDeletes: boolean,
license: string,
}

export interface LibrariesV2Response {
next: string | null,
previous: string | null,
count: number,
numPages: number,
currentPage: number,
start: number,
results: LibraryV2[],
}

/* Additional custom parameters for the API request. */
export interface GetLibrariesV2CustomParams {
/* (optional) Library type, default `complex` */
type?: string,
/* (optional) Page number of results */
page?: number,
/* (optional) The number of results on each page, default `50` */
pageSize?: number,
/* (optional) Whether pagination is supported, default `true` */
pagination?: boolean,
/* (optional) Library field to order results by. Prefix with '-' for descending */
order?: string,
/* (optional) Search query to filter v2 Libraries by */
search?: string,
}

/**
* Get's studio home v2 Libraries.
*/
export async function getStudioHomeLibrariesV2(customParams: GetLibrariesV2CustomParams): Promise<LibrariesV2Response> {
// Set default params if not passed in
const customParamsDefaults = {
type: customParams.type || 'complex',
page: customParams.page || 1,
pageSize: customParams.pageSize || 50,
pagination: customParams.pagination !== undefined ? customParams.pagination : true,
order: customParams.order || 'title',
textSearch: customParams.search,
};
const customParamsFormat = snakeCaseObject(customParamsDefaults);
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat });
return camelCaseObject(data);
}
24 changes: 1 addition & 23 deletions src/studio-home/data/api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-check
import { camelCaseObject, snakeCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

Expand All @@ -8,7 +9,6 @@ export const getCourseNotificationUrl = (url) => new URL(url, getApiBaseUrl()).h

/**
* Get's studio home data.
* @param {string} search
* @returns {Promise<Object>}
*/
export async function getStudioHomeData() {
Expand Down Expand Up @@ -40,28 +40,6 @@ export async function getStudioHomeLibraries() {
return camelCaseObject(data);
}

/**
* Get's studio home v2 Libraries.
* @param {object} customParams - Additional custom paramaters for the API request.
* @param {string} [customParams.type] - (optional) Library type, default `complex`
* @param {number} [customParams.page] - (optional) Page number of results
* @param {number} [customParams.pageSize] - (optional) The number of results on each page, default `50`
* @param {boolean} [customParams.pagination] - (optional) Whether pagination is supported, default `true`
* @returns {Promise<Object>} - A Promise that resolves to the response data container the studio home v2 libraries.
*/
export async function getStudioHomeLibrariesV2(customParams) {
// Set default params if not passed in
const customParamsDefaults = {
type: customParams.type || 'complex',
page: customParams.page || 1,
pageSize: customParams.pageSize || 50,
pagination: customParams.pagination !== undefined ? customParams.pagination : true,
};
const customParamsFormat = snakeCaseObject(customParamsDefaults);
const { data } = await getAuthenticatedHttpClient().get(`${getApiBaseUrl()}/api/libraries/v2/`, { params: customParamsFormat });
return camelCaseObject(data);
}

/**
* Handle course notification requests.
* @param {string} url
Expand Down
2 changes: 1 addition & 1 deletion src/studio-home/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ import {
getStudioHomeCourses,
getStudioHomeCoursesV2,
getStudioHomeLibraries,
getStudioHomeLibrariesV2,
} from './api';
import { getStudioHomeLibrariesV2 } from '../../library/data/api';
import {
generateGetStudioCoursesApiResponse,
generateGetStudioHomeDataApiResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useQuery } from '@tanstack/react-query';

import { getStudioHomeLibrariesV2 } from './api';
import { GetLibrariesV2CustomParams, getStudioHomeLibrariesV2 } from '../../library/data/api';

/**
* Builds the query to fetch list of V2 Libraries
*/
const useListStudioHomeV2Libraries = (customParams) => (
const useListStudioHomeV2Libraries = (customParams: GetLibrariesV2CustomParams) => (
useQuery({
queryKey: ['listV2Libraries', customParams],
queryFn: () => getStudioHomeLibrariesV2(customParams),
keepPreviousData: true,
})
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,61 @@
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 = ({
const LibrariesV2Tab: React.FC<{
libraryAuthoringMfeUrl: string,
redirectToLibraryAuthoringMfe: boolean
}> = ({
libraryAuthoringMfeUrl,
redirectToLibraryAuthoringMfe,
}) => {
const intl = useIntl();

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

const handlePageSelect = (page) => {
const isFiltered = Object.keys(filterParams).length > 0;

const handlePageSelect = (page: number) => {
setCurrentPage(page);
};

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

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 />
</Row>
);
}

const libURL = (id) => (
const libURL = (id: string) => (
libraryAuthoringMfeUrl && redirectToLibraryAuthoringMfe
? constructLibraryAuthoringURL(libraryAuthoringMfeUrl, `library/${id}`)
// Redirection to the placeholder is done in the MFE rather than
Expand All @@ -46,6 +64,8 @@ const LibrariesV2Tab = ({
: `${window.location.origin}${getPath(getConfig().PUBLIC_PATH)}library/${id}`
);

const hasV2Libraries = !isLoading && ((data!.results.length || 0) > 0);

return (
isError ? (
<AlertMessage
Expand All @@ -61,18 +81,26 @@ 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}
isFiltered={isFiltered}
filterParams={filterParams}
setFilterParams={setFilterParams}
setCurrentPage={setCurrentPage}
/>
{ !isLoading
&& (
<p>
{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,16 +111,27 @@ const LibrariesV2Tab = ({
number={slug}
url={libURL(id)}
/>
))
}
)) : isFiltered && !isLoading && (
<Alert className="mt-4">
<Alert.Heading>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertTitle)}
</Alert.Heading>
<p>
{intl.formatMessage(messages.librariesV2TabLibraryNotFoundAlertMessage)}
</p>
<Button variant="primary" onClick={handleClearFilters}>
{intl.formatMessage(messages.coursesTabCourseNotFoundAlertCleanFiltersButton)}
</Button>
</Alert>
)}

{
data.numPages > 1
hasV2Libraries && (data!.numPages || 0) > 1
&& (
<Pagination
className="d-flex justify-content-center"
paginationLabel="pagination navigation"
pageCount={data.numPages}
pageCount={data!.numPages}
currentPage={currentPage}
onPageSelect={handlePageSelect}
/>
Expand All @@ -103,9 +142,4 @@ const LibrariesV2Tab = ({
);
};

LibrariesV2Tab.propTypes = {
libraryAuthoringMfeUrl: PropTypes.string.isRequired,
redirectToLibraryAuthoringMfe: PropTypes.bool.isRequired,
};

export default LibrariesV2Tab;
Loading

0 comments on commit 83489b0

Please sign in to comment.