Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
yusuf-musleh committed Dec 1, 2023
1 parent d8ac46a commit dd96c20
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 25 deletions.
42 changes: 41 additions & 1 deletion src/content-tags-drawer/ContentTagsCollapsible.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ModalPopup,
useToggle,
useCheckboxSetValues,
SearchField,
} from '@edx/paragon';
import PropTypes from 'prop-types';
import classNames from 'classnames';
Expand All @@ -21,6 +22,19 @@ import ContentTagsTree from './ContentTagsTree';

import { useContentTaxonomyTagsMutation } from './data/apiHooks';


const debounce = (func, delay) => {
let timeoutId;

return (...args) => {
clearTimeout(timeoutId);

timeoutId = setTimeout(() => {
func.apply(this, args);
}, delay);
};
};

/**
* Util function that consolidates two tag trees into one, sorting the keys in
* alphabetical order.
Expand Down Expand Up @@ -186,6 +200,8 @@ const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) =>
const [isOpen, open, close] = useToggle(false);
const [addTagsButtonRef, setAddTagsButtonRef] = React.useState(null);

const [searchTerm, setSearchTerm] = React.useState(null);

// Keeps track of the content objects tags count (both implicit and explicit)
const [contentTagsCount, setContentTagsCount] = React.useState(0);

Expand Down Expand Up @@ -308,6 +324,21 @@ const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) =>
tagChangeHandler(e.target.value, e.target.checked);
});

const handleSearch = debounce((term) => {
console.log('Searching for:', term);
setSearchTerm(term.trim());
}, 500); // Perform search after 500ms

const handleSearchChange = React.useCallback((value) => {
handleSearch(value);
});

const modalPopupOnCloseHandler = React.useCallback((event) => {
close(event);
// Clear search term
setSearchTerm(null);
});

return (
<div className="d-flex">
<Collapsible title={name} styling="card-lg" className="taxonomy-tags-collapsible">
Expand All @@ -331,7 +362,7 @@ const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) =>
placement="bottom"
positionRef={addTagsButtonRef}
isOpen={isOpen}
onClose={close}
onClose={modalPopupOnCloseHandler}
>
<div className="bg-white p-3 shadow">

Expand All @@ -344,11 +375,20 @@ const ContentTagsCollapsible = ({ contentId, taxonomyAndTagsData, editable }) =>
onChange={handleSelectableBoxChange}
value={checkedTags}
>
<SearchField
onSubmit={(value) => setSearchTerm(value.trim())}
// onChange={(value) => setSearchTerm(value)}
onChange={handleSearchChange}
// onFocus={(event) => setSearchTerm(event.value)}
onClear={() => setSearchTerm(null)}
/>

<ContentTagsDropDownSelector
key={`selector-${id}`}
taxonomyId={id}
level={0}
tagsTree={tagsTree}
searchTerm={searchTerm}
/>
</SelectableBox.Set>
</div>
Expand Down
55 changes: 39 additions & 16 deletions src/content-tags-drawer/ContentTagsDropDownSelector.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
SelectableBox,
Icon,
Expand All @@ -14,9 +14,12 @@ import './ContentTagsDropDownSelector.scss';
import { useTaxonomyTagsData } from './data/apiHooks';

const ContentTagsDropDownSelector = ({
taxonomyId, level, subTagsUrl, lineage, tagsTree,
taxonomyId, level, subTagsUrl, lineage, tagsTree, searchTerm,
}) => {
const intl = useIntl();

const prevSearchTerm = useRef(searchTerm);

// This object represents the states of the dropdowns on this level
// The keys represent the index of the dropdown with
// the value true (open) false (closed)
Expand All @@ -25,25 +28,32 @@ const ContentTagsDropDownSelector = ({
const [tags, setTags] = useState([]);
const [nextPage, setNextPage] = useState(null);

// `fetchUrl` is initially `subTagsUrl` to fetch the initial data,
// however if it is null that means it is the root, and the apiHooks
// would automatically handle it. Later this url is set to the next
// page of results (if any)
//
// TODO: In the future we may need to refactor this to keep track
// of the count for how many times the user clicked on "load more" then
// use useQueries to load all the pages based on that.
const [fetchUrl, setFetchUrl] = useState(subTagsUrl);
const [currentPage, setCurrentPage] = useState(1);

useEffect(() => {
if (searchTerm !== prevSearchTerm.current) {
setCurrentPage(1);
}
}, [searchTerm]);

const isOpen = (i) => dropdownStates[i];
const closeAllDropdowns = () => {
const updatedStates = { ...dropdownStates };
// eslint-disable-next-line no-return-assign
Object.keys(updatedStates).map((key) => updatedStates[key] = false);
setDropdownStates(updatedStates);
};

const clickAndEnterHandler = (i) => {
// This flips the state of the dropdown at index false (closed) -> true (open)
// and vice versa. Initially they are undefined which is falsy.
setDropdownStates({ ...dropdownStates, [i]: !dropdownStates[i] });
};

const { data: taxonomyTagsData, isSuccess: isTaxonomyTagsLoaded } = useTaxonomyTagsData(taxonomyId, fetchUrl);
const {
data: taxonomyTagsData,
isSuccess: isTaxonomyTagsLoaded,
} = useTaxonomyTagsData(taxonomyId, subTagsUrl, currentPage, searchTerm);

const isImplicit = (tag) => {
// Traverse the tags tree using the lineage
Expand All @@ -58,14 +68,25 @@ const ContentTagsDropDownSelector = ({

useEffect(() => {
if (isTaxonomyTagsLoaded && taxonomyTagsData) {
setTags([...tags, ...taxonomyTagsData.results]);
console.log('new data loaded');
if (searchTerm !== prevSearchTerm.current) {
// Since the searchTerm has changed, we need to reset the results/contents
// of the dropdown selectors.
setTags([...taxonomyTagsData.results]);
prevSearchTerm.current = searchTerm;
closeAllDropdowns();
} else {
setTags([...tags, ...taxonomyTagsData.results]);
}

setNextPage(taxonomyTagsData.next);
}
}, [isTaxonomyTagsLoaded, taxonomyTagsData]);
}, [isTaxonomyTagsLoaded, taxonomyTagsData, searchTerm]);

const loadMoreTags = useCallback(() => {
setFetchUrl(nextPage);
}, [nextPage]);
setCurrentPage(currentPage + 1);
// setFetchUrl(nextPage);
}, [currentPage]);

return (
<>
Expand Down Expand Up @@ -139,6 +160,7 @@ const ContentTagsDropDownSelector = ({
ContentTagsDropDownSelector.defaultProps = {
subTagsUrl: undefined,
lineage: [],
searchTerm: null,
};

ContentTagsDropDownSelector.propTypes = {
Expand All @@ -152,6 +174,7 @@ ContentTagsDropDownSelector.propTypes = {
children: PropTypes.shape({}).isRequired,
}).isRequired,
).isRequired,
searchTerm: PropTypes.string,
};

export default ContentTagsDropDownSelector;
35 changes: 30 additions & 5 deletions src/content-tags-drawer/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,17 @@ import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getTaxonomyTagsApiUrl = (taxonomyId) => new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl()).href;
export const getTaxonomyTagsApiUrl = (taxonomyId, page, searchTerm) => {
const url = new URL(`api/content_tagging/v1/taxonomies/${taxonomyId}/tags/`, getApiBaseUrl());
if (page) {
url.searchParams.append('page', page);
}
console.log("calling with searchTerm??", searchTerm);
if (searchTerm) {
url.searchParams.append('search_term', searchTerm);
}
return url.href;
};
export const getContentTaxonomyTagsApiUrl = (contentId) => new URL(`api/content_tagging/v1/object_tags/${contentId}/`, getApiBaseUrl()).href;
export const getContentDataApiUrl = (contentId) => new URL(`/xblock/outline/${contentId}`, getApiBaseUrl()).href;

Expand All @@ -14,10 +24,25 @@ export const getContentDataApiUrl = (contentId) => new URL(`/xblock/outline/${co
* If provided, we use it instead of generating the URL. This is usually for fetching subTags
* @returns {Promise<Object>}
*/
export async function getTaxonomyTagsData(taxonomyId, fullPathProvided) {
const { data } = await getAuthenticatedHttpClient().get(
fullPathProvided ? new URL(`${fullPathProvided}`) : getTaxonomyTagsApiUrl(taxonomyId),
);
export async function getTaxonomyTagsData(taxonomyId, fullPathProvided, page, searchTerm) {
let url;

if (fullPathProvided && searchTerm && !fullPathProvided.includes(searchTerm)) {
url = getTaxonomyTagsApiUrl(taxonomyId, page, searchTerm);
} else {
url = fullPathProvided || getTaxonomyTagsApiUrl(taxonomyId, page, searchTerm);
}

// TODO: This is a hack to see if it works, will implement properly
if (!url.includes('page')) {
if (!url.includes('?')) {
url += `?page=${page}`;
} else {
url += `&page=${page}`;
}
}

const { data } = await getAuthenticatedHttpClient().get(url);
return camelCaseObject(data);
}

Expand Down
6 changes: 3 additions & 3 deletions src/content-tags-drawer/data/apiHooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import {
* If provided, we use it instead of generating the URL. This is usually for fetching subTags
* @returns {import("@tanstack/react-query").UseQueryResult<import("./types.mjs").TaxonomyTagsData>}
*/
export const useTaxonomyTagsData = (taxonomyId, fullPathProvided) => (
export const useTaxonomyTagsData = (taxonomyId, fullPathProvided, page, searchTerm) => (
useQuery({
queryKey: [`taxonomyTags${ fullPathProvided || taxonomyId }`],
queryFn: () => getTaxonomyTagsData(taxonomyId, fullPathProvided),
queryKey: [`taxonomyTags${ fullPathProvided || taxonomyId }-${page}-${searchTerm}`],
queryFn: () => getTaxonomyTagsData(taxonomyId, fullPathProvided, page, searchTerm),
})
);

Expand Down

0 comments on commit dd96c20

Please sign in to comment.