From fd2c35795b43d0e8db338efef50d3891738c4679 Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Fri, 27 Sep 2024 14:26:32 -0700 Subject: [PATCH 01/16] feat: allow filtering library by publish status --- .../LibraryAuthoringPage.tsx | 2 + .../components/BaseComponentCard.tsx | 19 +++-- .../components/ComponentCard.tsx | 3 + src/search-manager/FilterByPublished.tsx | 77 +++++++++++++++++++ src/search-manager/index.ts | 1 + 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/search-manager/FilterByPublished.tsx diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 01fc146b60..c3594519f4 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -31,6 +31,7 @@ import { ClearFiltersButton, FilterByBlockType, FilterByTags, + FilterByPublished, SearchContextProvider, SearchKeywordsField, SearchSortWidget, @@ -254,6 +255,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
+
diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index 3b5aa748c9..1c3d137ee5 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -1,5 +1,6 @@ import React, { useMemo } from 'react'; import { + Badge, Card, Container, Icon, @@ -11,12 +12,14 @@ import TagCount from '../../generic/tag-count'; import { BlockTypeLabel, type ContentHitTags, Highlight } from '../../search-manager'; type BaseComponentCardProps = { - componentType: string, - displayName: string, description: string, - numChildren?: number, - tags: ContentHitTags, - actions: React.ReactNode, - openInfoSidebar: () => void + componentType: string; + displayName: string; + description: string; + numChildren?: number; + tags: ContentHitTags; + actions: React.ReactNode; + openInfoSidebar: () => void; + hasUnpublishedChanges?: boolean; }; const BaseComponentCard = ({ @@ -27,6 +30,7 @@ const BaseComponentCard = ({ tags, actions, openInfoSidebar, + ...props } : BaseComponentCardProps) => { const tagCount = useMemo(() => { if (!tags) { @@ -75,7 +79,8 @@ const BaseComponentCard = ({
- +
+ {props.hasUnpublishedChanges ? Unpublished changes : null} diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 813255b97a..0cffa8f9c2 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -192,6 +192,8 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { formatted, tags, usageKey, + modified, + lastPublished, } = contentHit; const componentDescription: string = ( showOnlyPublished ? formatted.published?.description : formatted.description @@ -216,6 +218,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { )} openInfoSidebar={() => openComponentInfoSidebar(usageKey)} + hasUnpublishedChanges={modified >= (lastPublished ?? 0)} /> ); }; diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx new file mode 100644 index 0000000000..ad9ef662a3 --- /dev/null +++ b/src/search-manager/FilterByPublished.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import { FormattedMessage } from '@edx/frontend-platform/i18n'; +import { + Badge, + Form, + Menu, + MenuItem, +} from '@openedx/paragon'; +import { FilterList } from '@openedx/paragon/icons'; +import SearchFilterWidget from './SearchFilterWidget'; +import messages from './messages'; +// import { useSearchContext } from './SearchManager'; + +/** + * A button with a dropdown that allows filtering the current search by publish status + */ +const FilterByPublished: React.FC> = () => { + // const { + // publishedFilter, + // setPublishedFilter, + // } = useSearchContext(); + + const clearFilters = React.useCallback(() => { + // setPublishedFilter(undefined); + }, []); + + return ( + + + + + {}} + > +
+ Published + 15 +
+
+ {}} + > +
+ Modified since publish + 5 +
+
+ {}} + > +
+ Never published + 2 +
+
+
+
+
+
+ ); +}; + +export default FilterByPublished; diff --git a/src/search-manager/index.ts b/src/search-manager/index.ts index e2d4188be1..495f8c822f 100644 --- a/src/search-manager/index.ts +++ b/src/search-manager/index.ts @@ -3,6 +3,7 @@ export { default as BlockTypeLabel } from './BlockTypeLabel'; export { default as ClearFiltersButton } from './ClearFiltersButton'; export { default as FilterByBlockType } from './FilterByBlockType'; export { default as FilterByTags } from './FilterByTags'; +export { default as FilterByPublished } from './FilterByPublished'; export { default as Highlight } from './Highlight'; export { default as SearchKeywordsField } from './SearchKeywordsField'; export { default as SearchSortWidget } from './SearchSortWidget'; From 7cf5d2e29b81645c74bb6d3094c9f03e0fe3bc5b Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Fri, 18 Oct 2024 18:30:05 -0700 Subject: [PATCH 02/16] temp: more work toward working publish status filter --- src/search-manager/FilterByPublished.tsx | 42 +++++++++++++++--------- src/search-manager/SearchManager.ts | 12 ++++++- src/search-manager/data/api.ts | 19 +++++++++++ src/search-manager/data/apiHooks.ts | 5 +++ 4 files changed, 61 insertions(+), 17 deletions(-) diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index ad9ef662a3..9f369f18f7 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -9,19 +9,29 @@ import { import { FilterList } from '@openedx/paragon/icons'; import SearchFilterWidget from './SearchFilterWidget'; import messages from './messages'; -// import { useSearchContext } from './SearchManager'; +import { useSearchContext } from './SearchManager'; +import { PublishStatus } from './data/api'; /** * A button with a dropdown that allows filtering the current search by publish status */ const FilterByPublished: React.FC> = () => { - // const { - // publishedFilter, - // setPublishedFilter, - // } = useSearchContext(); + const { + publishedFilter, + setPublishedFilter, + } = useSearchContext(); const clearFilters = React.useCallback(() => { - // setPublishedFilter(undefined); + setPublishedFilter([]); + }, []); + + const toggleFilterMode = React.useCallback((mode: PublishStatus) => { + setPublishedFilter(oldList => { + if (oldList.includes(mode)) { + return oldList.filter(m => m !== mode); + } + return [...oldList, mode]; + }); }, []); return ( @@ -34,37 +44,37 @@ const FilterByPublished: React.FC> = () => { {}} + value={PublishStatus.Published} + onChange={() => { toggleFilterMode(PublishStatus.Published); }} >
Published - 15 + {' '}15
{}} + value={PublishStatus.Modified} + onChange={() => { toggleFilterMode(PublishStatus.Modified); }} >
Modified since publish - 5 + {' '}5
{}} + value={PublishStatus.NeverPublished} + onChange={() => { toggleFilterMode(PublishStatus.NeverPublished); }} >
Never published - 2 + {' '}2
diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 314c90020a..bce30f47d7 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -10,7 +10,11 @@ import { MeiliSearch, type Filter } from 'meilisearch'; import { union } from 'lodash'; import { - CollectionHit, ContentHit, SearchSortOption, forceArray, + CollectionHit, + ContentHit, + SearchSortOption, + forceArray, + type PublishStatus, } from './data/api'; import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; @@ -23,6 +27,8 @@ export interface SearchContextData { setBlockTypesFilter: React.Dispatch>; problemTypesFilter: string[]; setProblemTypesFilter: React.Dispatch>; + publishedFilter: PublishStatus[]; + setPublishedFilter: React.Dispatch>; tagsFilter: string[]; setTagsFilter: React.Dispatch>; blockTypes: Record; @@ -99,6 +105,7 @@ export const SearchContextProvider: React.FC<{ const [searchKeywords, setSearchKeywords] = React.useState(''); const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); const [problemTypesFilter, setProblemTypesFilter] = React.useState([]); + const [publishedFilter, setPublishedFilter] = React.useState([]); const [tagsFilter, setTagsFilter] = React.useState([]); const [usageKey, setUsageKey] = useStateWithUrlSearchParam( '', @@ -163,6 +170,7 @@ export const SearchContextProvider: React.FC<{ searchKeywords, blockTypesFilter, problemTypesFilter, + publishedFilter, tagsFilter, sort, skipBlockTypeFetch, @@ -178,6 +186,8 @@ export const SearchContextProvider: React.FC<{ setBlockTypesFilter, problemTypesFilter, setProblemTypesFilter, + publishedFilter, + setPublishedFilter, tagsFilter, setTagsFilter, extraFilter, diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 0763000f55..f00a675d24 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -25,6 +25,12 @@ export enum SearchSortOption { RECENTLY_MODIFIED = 'modified:desc', } +export enum PublishStatus { + Published = 'published', + Modified = 'modified', + NeverPublished = 'never', +} + /** * Get the content search configuration from the CMS. */ @@ -179,6 +185,7 @@ interface FetchSearchParams { searchKeywords: string, blockTypesFilter?: string[], problemTypesFilter?: string[], + publishedFilter?: PublishStatus[], /** The full path of tags that each result MUST have, e.g. ["Difficulty > Hard", "Subject > Math"] */ tagsFilter?: string[], extraFilter?: Filter, @@ -194,6 +201,7 @@ export async function fetchSearchResults({ searchKeywords, blockTypesFilter, problemTypesFilter, + publishedFilter, tagsFilter, extraFilter, sort, @@ -215,6 +223,16 @@ export async function fetchSearchResults({ const problemTypesFilterFormatted = problemTypesFilter?.length ? [problemTypesFilter.map(pt => `content.problem_types = ${pt}`)] : []; + /* eslint-disable */ + const publishStatusFilterFormatted = publishedFilter?.length ? publishedFilter.map(pt => ( + pt === PublishStatus.Published ? 'modified = last_published' : + pt === PublishStatus.Modified ? 'modified > last_published' : + pt === PublishStatus.NeverPublished ? 'last_published IS NULL' : + 'false' + )) : []; + console.log(publishStatusFilterFormatted) + /* eslint-enable */ + const tagsFilterFormatted = formatTagsFilter(tagsFilter); const limit = 20; // How many results to retrieve per page. @@ -235,6 +253,7 @@ export async function fetchSearchResults({ ...typeFilters, ...extraFilterFormatted, ...tagsFilterFormatted, + ...publishStatusFilterFormatted, ], attributesToHighlight: ['display_name', 'description', 'published'], highlightPreTag: HIGHLIGHT_PRE_TAG, diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index 923749b20d..1e759fb6ac 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -10,6 +10,7 @@ import { fetchTagsThatMatchKeyword, getContentSearchConfig, fetchBlockTypes, + type PublishStatus, } from './api'; /** @@ -53,6 +54,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter = [], problemTypesFilter = [], + publishedFilter = [], tagsFilter = [], sort = [], skipBlockTypeFetch = false, @@ -69,6 +71,7 @@ export const useContentSearchResults = ({ blockTypesFilter?: string[]; /** Only search for these problem types (e.g. `["choiceresponse", "multiplechoiceresponse"]`) */ problemTypesFilter?: string[]; + publishedFilter?: PublishStatus[]; /** Required tags (all must match), e.g. `["Difficulty > Hard", "Subject > Math"]` */ tagsFilter?: string[]; /** Sort search results using these options */ @@ -88,6 +91,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter, problemTypesFilter, + publishedFilter, tagsFilter, sort, ], @@ -103,6 +107,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter, problemTypesFilter, + publishedFilter, tagsFilter, sort, // For infinite pagination of results, we can retrieve additional pages if requested. From 8889017f9a12e029b70437e32610ba343795563e Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Fri, 13 Dec 2024 18:41:50 -0300 Subject: [PATCH 03/16] feat: implement pills and filter --- src/search-manager/FilterByPublished.tsx | 19 +++++++++---------- src/search-manager/SearchManager.ts | 13 +++++++------ src/search-manager/data/api.ts | 18 ++++++------------ src/search-manager/data/apiHooks.ts | 9 +++++---- 4 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index 9f369f18f7..b0d7ee18ec 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { Badge, Form, @@ -8,7 +7,6 @@ import { } from '@openedx/paragon'; import { FilterList } from '@openedx/paragon/icons'; import SearchFilterWidget from './SearchFilterWidget'; -import messages from './messages'; import { useSearchContext } from './SearchManager'; import { PublishStatus } from './data/api'; @@ -17,16 +15,17 @@ import { PublishStatus } from './data/api'; */ const FilterByPublished: React.FC> = () => { const { - publishedFilter, - setPublishedFilter, + publishStatus, + publishStatusFilter, + setPublishStatusFilter, } = useSearchContext(); const clearFilters = React.useCallback(() => { - setPublishedFilter([]); + setPublishStatusFilter([]); }, []); const toggleFilterMode = React.useCallback((mode: PublishStatus) => { - setPublishedFilter(oldList => { + setPublishStatusFilter(oldList => { if (oldList.includes(mode)) { return oldList.filter(m => m !== mode); } @@ -44,7 +43,7 @@ const FilterByPublished: React.FC> = () => { > = () => { >
Published - {' '}15 + {' '}{publishStatus[PublishStatus.Published] ?? 0}
> = () => { >
Modified since publish - {' '}5 + {' '}{publishStatus[PublishStatus.Modified] ?? 0}
> = () => { >
Never published - {' '}2 + {' '}{publishStatus[PublishStatus.NeverPublished] ?? 0}
diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index bce30f47d7..6fdf5ec744 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -27,12 +27,13 @@ export interface SearchContextData { setBlockTypesFilter: React.Dispatch>; problemTypesFilter: string[]; setProblemTypesFilter: React.Dispatch>; - publishedFilter: PublishStatus[]; - setPublishedFilter: React.Dispatch>; + publishStatusFilter: PublishStatus[]; + setPublishStatusFilter: React.Dispatch>; tagsFilter: string[]; setTagsFilter: React.Dispatch>; blockTypes: Record; problemTypes: Record; + publishStatus: Record; extraFilter?: Filter; canClearFilters: boolean; clearFilters: () => void; @@ -105,7 +106,7 @@ export const SearchContextProvider: React.FC<{ const [searchKeywords, setSearchKeywords] = React.useState(''); const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); const [problemTypesFilter, setProblemTypesFilter] = React.useState([]); - const [publishedFilter, setPublishedFilter] = React.useState([]); + const [publishStatusFilter, setPublishStatusFilter] = React.useState([]); const [tagsFilter, setTagsFilter] = React.useState([]); const [usageKey, setUsageKey] = useStateWithUrlSearchParam( '', @@ -170,7 +171,7 @@ export const SearchContextProvider: React.FC<{ searchKeywords, blockTypesFilter, problemTypesFilter, - publishedFilter, + publishStatusFilter, tagsFilter, sort, skipBlockTypeFetch, @@ -186,8 +187,8 @@ export const SearchContextProvider: React.FC<{ setBlockTypesFilter, problemTypesFilter, setProblemTypesFilter, - publishedFilter, - setPublishedFilter, + publishStatusFilter, + setPublishStatusFilter, tagsFilter, setTagsFilter, extraFilter, diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index f00a675d24..f1a0f0a288 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -185,7 +185,7 @@ interface FetchSearchParams { searchKeywords: string, blockTypesFilter?: string[], problemTypesFilter?: string[], - publishedFilter?: PublishStatus[], + publishStatusFilter?: PublishStatus[], /** The full path of tags that each result MUST have, e.g. ["Difficulty > Hard", "Subject > Math"] */ tagsFilter?: string[], extraFilter?: Filter, @@ -201,7 +201,7 @@ export async function fetchSearchResults({ searchKeywords, blockTypesFilter, problemTypesFilter, - publishedFilter, + publishStatusFilter, tagsFilter, extraFilter, sort, @@ -213,6 +213,7 @@ export async function fetchSearchResults({ totalHits: number, blockTypes: Record, problemTypes: Record, + publishStatus: Record, }> { const queries: MultiSearchQuery[] = []; @@ -223,15 +224,7 @@ export async function fetchSearchResults({ const problemTypesFilterFormatted = problemTypesFilter?.length ? [problemTypesFilter.map(pt => `content.problem_types = ${pt}`)] : []; - /* eslint-disable */ - const publishStatusFilterFormatted = publishedFilter?.length ? publishedFilter.map(pt => ( - pt === PublishStatus.Published ? 'modified = last_published' : - pt === PublishStatus.Modified ? 'modified > last_published' : - pt === PublishStatus.NeverPublished ? 'last_published IS NULL' : - 'false' - )) : []; - console.log(publishStatusFilterFormatted) - /* eslint-enable */ + const publishStatusFilterFormatted = publishStatusFilter?.length ? [publishStatusFilter.map(ps => `publish_status = ${ps}`)] : []; const tagsFilterFormatted = formatTagsFilter(tagsFilter); @@ -268,7 +261,7 @@ export async function fetchSearchResults({ if (!skipBlockTypeFetch) { queries.push({ indexUid: indexName, - facets: ['block_type', 'content.problem_types'], + facets: ['block_type', 'content.problem_types', 'publish_status'], filter: [ ...extraFilterFormatted, // We exclude the block type filter here so we get all the other available options for it. @@ -285,6 +278,7 @@ export async function fetchSearchResults({ totalHits: results[0].totalHits ?? results[0].estimatedTotalHits ?? hitLength, blockTypes: results[1]?.facetDistribution?.block_type ?? {}, problemTypes: results[1]?.facetDistribution?.['content.problem_types'] ?? {}, + publishStatus: results[1]?.facetDistribution?.publish_status ?? {}, nextOffset: hitLength === limit ? offset + limit : undefined, }; } diff --git a/src/search-manager/data/apiHooks.ts b/src/search-manager/data/apiHooks.ts index 1e759fb6ac..c2fe73bf7c 100644 --- a/src/search-manager/data/apiHooks.ts +++ b/src/search-manager/data/apiHooks.ts @@ -54,7 +54,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter = [], problemTypesFilter = [], - publishedFilter = [], + publishStatusFilter = [], tagsFilter = [], sort = [], skipBlockTypeFetch = false, @@ -71,7 +71,7 @@ export const useContentSearchResults = ({ blockTypesFilter?: string[]; /** Only search for these problem types (e.g. `["choiceresponse", "multiplechoiceresponse"]`) */ problemTypesFilter?: string[]; - publishedFilter?: PublishStatus[]; + publishStatusFilter?: PublishStatus[]; /** Required tags (all must match), e.g. `["Difficulty > Hard", "Subject > Math"]` */ tagsFilter?: string[]; /** Sort search results using these options */ @@ -91,7 +91,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter, problemTypesFilter, - publishedFilter, + publishStatusFilter, tagsFilter, sort, ], @@ -107,7 +107,7 @@ export const useContentSearchResults = ({ searchKeywords, blockTypesFilter, problemTypesFilter, - publishedFilter, + publishStatusFilter, tagsFilter, sort, // For infinite pagination of results, we can retrieve additional pages if requested. @@ -133,6 +133,7 @@ export const useContentSearchResults = ({ // The distribution of block type filter options blockTypes: pages?.[0]?.blockTypes ?? {}, problemTypes: pages?.[0]?.problemTypes ?? {}, + publishStatus: pages?.[0]?.publishStatus ?? {}, status: query.status, isLoading: query.isLoading, isError: query.isError, From 6d3db8dc2431f06fc0dc36590a738bd4cb23ab54 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Sat, 14 Dec 2024 19:26:42 -0300 Subject: [PATCH 04/16] fix: improve test coverage --- .../LibraryAuthoringPage.test.tsx | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index 0c725fbf2d..c135e4a808 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -706,6 +706,49 @@ describe('', () => { }); }); + it('filters by publish status', async () => { + await renderLibraryPage(); + + // Open the publish status filter dropdown + const filterButton = screen.getByRole('button', { name: /publish status/i }); + fireEvent.click(filterButton); + + // Test each publish status filter option + const publishedCheckbox = screen.getByRole('checkbox', { name: /^published \d+$/i }); + const modifiedCheckbox = screen.getByRole('checkbox', { name: /^modified since publish \d+$/i }); + const neverPublishedCheckbox = screen.getByRole('checkbox', { name: /^never published \d+$/i }); + + // Test Published filter + fireEvent.click(publishedCheckbox); + await waitFor(() => { + expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { + body: expect.stringContaining('"publish_status = published"'), + method: 'POST', + headers: expect.anything(), + }); + }); + + // Test Modified filter + fireEvent.click(modifiedCheckbox); + await waitFor(() => { + expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { + body: expect.stringContaining('"publish_status = modified"'), + method: 'POST', + headers: expect.anything(), + }); + }); + + // Test Never Published filter + fireEvent.click(neverPublishedCheckbox); + await waitFor(() => { + expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { + body: expect.stringContaining('"publish_status = never"'), + method: 'POST', + headers: expect.anything(), + }); + }); + }); + it('Shows an error if libraries V2 is disabled', async () => { const { axiosMock } = initializeMocks(); axiosMock.onGet(getStudioHomeApiUrl()).reply(200, { From 73ef429dc81cdba229a7640008df8072c4ae3533 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Fri, 20 Dec 2024 20:30:52 -0300 Subject: [PATCH 05/16] fix: address PR comments and improve coverage --- .../LibraryAuthoringPage.test.tsx | 25 ++++++++++++++++++- .../components/BaseComponentCard.tsx | 6 +++-- src/library-authoring/components/messages.ts | 6 ++++- src/search-manager/ClearFiltersButton.tsx | 18 +++++++------ src/search-manager/FilterByPublished.tsx | 17 +++++++------ src/search-manager/SearchManager.ts | 2 ++ src/search-manager/messages.ts | 15 +++++++++++ 7 files changed, 70 insertions(+), 19 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.test.tsx b/src/library-authoring/LibraryAuthoringPage.test.tsx index c135e4a808..d3a739ffc6 100644 --- a/src/library-authoring/LibraryAuthoringPage.test.tsx +++ b/src/library-authoring/LibraryAuthoringPage.test.tsx @@ -532,7 +532,7 @@ describe('', () => { expect(submenu).toBeInTheDocument(); fireEvent.click(submenu); - const clearFitlersButton = screen.getByRole('button', { name: /clear filters/i }); + const clearFitlersButton = screen.getByText('Clear Filters'); fireEvent.click(clearFitlersButton); await waitFor(() => { expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { @@ -718,9 +718,15 @@ describe('', () => { const modifiedCheckbox = screen.getByRole('checkbox', { name: /^modified since publish \d+$/i }); const neverPublishedCheckbox = screen.getByRole('checkbox', { name: /^never published \d+$/i }); + // Verify initial state - no clear filters button + expect(screen.queryByRole('button', { name: /clear filters/i })).not.toBeInTheDocument(); + // Test Published filter fireEvent.click(publishedCheckbox); + + // Wait for both the API call and the UI update await waitFor(() => { + // Check that the API was called with the correct filter expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { body: expect.stringContaining('"publish_status = published"'), method: 'POST', @@ -728,6 +734,12 @@ describe('', () => { }); }); + // Wait for the clear filters button to appear + await waitFor(() => { + const clearFiltersButton = screen.getByText('Clear Filters'); + expect(clearFiltersButton).toBeInTheDocument(); + }); + // Test Modified filter fireEvent.click(modifiedCheckbox); await waitFor(() => { @@ -747,6 +759,17 @@ describe('', () => { headers: expect.anything(), }); }); + + // Test clearing filters + const clearFiltersButton = screen.getByText('Clear Filters'); + fireEvent.click(clearFiltersButton); + await waitFor(() => { + expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, { + body: expect.stringContaining('"filter":[[],'), // Empty filter array + method: 'POST', + headers: expect.anything(), + }); + }); }); it('Shows an error if libraries V2 is disabled', async () => { diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index 1c3d137ee5..f7f8a754ab 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -6,7 +6,8 @@ import { Icon, Stack, } from '@openedx/paragon'; - +import { useIntl } from '@edx/frontend-platform/i18n'; +import messages from './messages'; import { getItemIcon, getComponentStyleColor } from '../../generic/block-type-utils'; import TagCount from '../../generic/tag-count'; import { BlockTypeLabel, type ContentHitTags, Highlight } from '../../search-manager'; @@ -41,6 +42,7 @@ const BaseComponentCard = ({ }, [tags]); const componentIcon = getItemIcon(componentType); + const intl = useIntl(); return ( @@ -80,7 +82,7 @@ const BaseComponentCard = ({

- {props.hasUnpublishedChanges ? Unpublished changes : null} + {props.hasUnpublishedChanges ? {intl.formatMessage(messages.unpublishedChanges)} : null} diff --git a/src/library-authoring/components/messages.ts b/src/library-authoring/components/messages.ts index 0e466736e6..4a40746838 100644 --- a/src/library-authoring/components/messages.ts +++ b/src/library-authoring/components/messages.ts @@ -151,6 +151,10 @@ const messages = defineMessages({ defaultMessage: 'Select', description: 'Button title for selecting multiple components', }, + unpublishedChanges: { + id: 'course-authoring.library-authoring.component.unpublished-changes', + defaultMessage: 'Unpublished changes', + description: 'Badge text shown when a component has unpublished changes', + }, }); - export default messages; diff --git a/src/search-manager/ClearFiltersButton.tsx b/src/search-manager/ClearFiltersButton.tsx index 0328d38616..f970bbebf8 100644 --- a/src/search-manager/ClearFiltersButton.tsx +++ b/src/search-manager/ClearFiltersButton.tsx @@ -17,14 +17,16 @@ const ClearFiltersButton = ({ size = 'sm', }: ClearFiltersButtonProps) => { const { canClearFilters, clearFilters } = useSearchContext(); - if (canClearFilters) { - return ( - - ); - } - return null; + return ( + + ); }; export default ClearFiltersButton; diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index b0d7ee18ec..9fe4fefe9c 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -6,6 +6,8 @@ import { MenuItem, } from '@openedx/paragon'; import { FilterList } from '@openedx/paragon/icons'; +import { useIntl } from '@edx/frontend-platform/i18n'; +import messages from './messages'; import SearchFilterWidget from './SearchFilterWidget'; import { useSearchContext } from './SearchManager'; import { PublishStatus } from './data/api'; @@ -14,6 +16,7 @@ import { PublishStatus } from './data/api'; * A button with a dropdown that allows filtering the current search by publish status */ const FilterByPublished: React.FC> = () => { + const intl = useIntl(); const { publishStatus, publishStatusFilter, @@ -42,7 +45,7 @@ const FilterByPublished: React.FC> = () => { > @@ -52,8 +55,8 @@ const FilterByPublished: React.FC> = () => { onChange={() => { toggleFilterMode(PublishStatus.Published); }} >
- Published - {' '}{publishStatus[PublishStatus.Published] ?? 0} + {intl.formatMessage(messages.publishStatusPublished)} + {publishStatus[PublishStatus.Published] ?? 0}
> = () => { onChange={() => { toggleFilterMode(PublishStatus.Modified); }} >
- Modified since publish - {' '}{publishStatus[PublishStatus.Modified] ?? 0} + {intl.formatMessage(messages.publishStatusModified)} + {publishStatus[PublishStatus.Modified] ?? 0}
> = () => { onChange={() => { toggleFilterMode(PublishStatus.NeverPublished); }} >
- Never published - {' '}{publishStatus[PublishStatus.NeverPublished] ?? 0} + {intl.formatMessage(messages.publishStatusNeverPublished)} + {publishStatus[PublishStatus.NeverPublished] ?? 0}
diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 6fdf5ec744..270f92af71 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -148,6 +148,7 @@ export const SearchContextProvider: React.FC<{ blockTypesFilter.length > 0 || problemTypesFilter.length > 0 || tagsFilter.length > 0 + || publishStatusFilter.length > 0 || !!usageKey ); const isFiltered = canClearFilters || (searchKeywords !== ''); @@ -155,6 +156,7 @@ export const SearchContextProvider: React.FC<{ setBlockTypesFilter([]); setTagsFilter([]); setProblemTypesFilter([]); + setPublishStatusFilter([]); if (usageKey !== '') { setUsageKey(''); } diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts index aca799f93c..03bf205deb 100644 --- a/src/search-manager/messages.ts +++ b/src/search-manager/messages.ts @@ -221,6 +221,21 @@ const messages = defineMessages({ defaultMessage: 'Most Relevant', description: 'Label for the content search sort drop-down which sorts keyword searches by relevance', }, + publishStatusPublished: { + id: 'course-authoring.search-manager.publishStatus.published', + defaultMessage: 'Published', + description: 'Label for published content in the publish status filter', + }, + publishStatusModified: { + id: 'course-authoring.search-manager.publishStatus.modified', + defaultMessage: 'Modified since publish', + description: 'Label for content modified since last publish in the publish status filter', + }, + publishStatusNeverPublished: { + id: 'course-authoring.search-manager.publishStatus.neverPublished', + defaultMessage: 'Never published', + description: 'Label for content that has never been published in the publish status filter', + }, }); export default messages; From 3f0f95e36c37bafea4fcac581b6ceea5646f3f1c Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 19:43:26 -0300 Subject: [PATCH 06/16] feat: update published filter list and update filters when sorted --- src/search-manager/FilterByPublished.tsx | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index 9fe4fefe9c..538cbc1e15 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -10,23 +10,34 @@ import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import SearchFilterWidget from './SearchFilterWidget'; import { useSearchContext } from './SearchManager'; -import { PublishStatus } from './data/api'; +import { PublishStatus, SearchSortOption } from './data/api'; /** * A button with a dropdown that allows filtering the current search by publish status */ const FilterByPublished: React.FC> = () => { + const [onlyPublished, setOnlyPublished] = React.useState(false); const intl = useIntl(); const { publishStatus, publishStatusFilter, setPublishStatusFilter, + searchSortOrder, } = useSearchContext(); const clearFilters = React.useCallback(() => { setPublishStatusFilter([]); }, []); + React.useEffect(() => { + if (searchSortOrder === SearchSortOption.RECENTLY_PUBLISHED) { + setPublishStatusFilter([PublishStatus.Published, PublishStatus.Modified]); + setOnlyPublished(true); + } else { + setOnlyPublished(false); + } + }, [searchSortOrder]); + const toggleFilterMode = React.useCallback((mode: PublishStatus) => { setPublishStatusFilter(oldList => { if (oldList.includes(mode)) { @@ -35,10 +46,16 @@ const FilterByPublished: React.FC> = () => { return [...oldList, mode]; }); }, []); + const modeToLabel = { + published: intl.formatMessage(messages.publishStatusPublished), + modified: intl.formatMessage(messages.publishStatusModified), + never: intl.formatMessage(messages.publishStatusNeverPublished), + }; + const appliedFilters = publishStatusFilter.map(mode => ({ label: modeToLabel[mode] })); return ( > = () => { as={Form.Checkbox} value={PublishStatus.NeverPublished} onChange={() => { toggleFilterMode(PublishStatus.NeverPublished); }} + disabled={onlyPublished} >
{intl.formatMessage(messages.publishStatusNeverPublished)} From 09d12841cc8295be8fff92a19b0b97c9794a52e9 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 21:06:07 -0300 Subject: [PATCH 07/16] fix: use url for publish status --- src/search-manager/SearchManager.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index 624a659972..c5ad48d132 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -12,8 +12,7 @@ import { CollectionHit, ContentHit, SearchSortOption, - forceArray, - type PublishStatus, + forceArray, PublishStatus, } from './data/api'; import { TypesFilterData, useStateOrUrlSearchParam } from './hooks'; import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks'; @@ -24,10 +23,6 @@ export interface SearchContextData { indexName?: string; searchKeywords: string; setSearchKeywords: React.Dispatch>; - blockTypesFilter: string[]; - setBlockTypesFilter: React.Dispatch>; - problemTypesFilter: string[]; - setProblemTypesFilter: React.Dispatch>; publishStatusFilter: PublishStatus[]; setPublishStatusFilter: React.Dispatch>; typesFilter: TypesFilterData; @@ -72,9 +67,6 @@ export const SearchContextProvider: React.FC<{ skipUrlUpdate, ...props }) => { - const [blockTypesFilter, setBlockTypesFilter] = React.useState([]); - const [problemTypesFilter, setProblemTypesFilter] = React.useState([]); - const [publishStatusFilter, setPublishStatusFilter] = React.useState([]); // Search parameters can be set via the query string // E.g. ?q=draft+text // TODO -- how to sanitize search terms? @@ -112,6 +104,14 @@ export const SearchContextProvider: React.FC<{ skipUrlUpdate, ); + const [publishStatusFilter, setPublishStatusFilter] = useStateOrUrlSearchParam( + [], + 'published', + (value: string) => Object.values(PublishStatus).find((enumValue) => value === enumValue), + (value: PublishStatus) => value.toString(), + skipUrlUpdate, + ); + // E.g ?usageKey=lb:OpenCraft:libA:problem:5714eb65-7c36-4eee-8ab9-a54ed5a95849 const sanitizeUsageKey = (value: string): string | undefined => { try { @@ -195,10 +195,6 @@ export const SearchContextProvider: React.FC<{ indexName, searchKeywords, setSearchKeywords, - blockTypesFilter, - setBlockTypesFilter, - problemTypesFilter, - setProblemTypesFilter, publishStatusFilter, setPublishStatusFilter, typesFilter, From 54913f75e81046da90031b0167616bbdf5642648 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 21:10:52 -0300 Subject: [PATCH 08/16] fix: remove unused code --- src/library-authoring/components/BaseComponentCard.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/library-authoring/components/BaseComponentCard.tsx b/src/library-authoring/components/BaseComponentCard.tsx index 00770be649..0c52c0a88f 100644 --- a/src/library-authoring/components/BaseComponentCard.tsx +++ b/src/library-authoring/components/BaseComponentCard.tsx @@ -19,7 +19,6 @@ type BaseComponentCardProps = { numChildren?: number; tags: ContentHitTags; actions: React.ReactNode; - openInfoSidebar: () => void; hasUnpublishedChanges?: boolean; onSelect: () => void }; @@ -31,7 +30,6 @@ const BaseComponentCard = ({ numChildren, tags, actions, - openInfoSidebar, onSelect, ...props } : BaseComponentCardProps) => { From 777fe258bec79456cbb2431e3962b78e67a8d441 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 21:12:16 -0300 Subject: [PATCH 09/16] fix: setProblemTypesFilter usage --- src/search-manager/SearchManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/search-manager/SearchManager.ts b/src/search-manager/SearchManager.ts index c5ad48d132..ebfe5a09cc 100644 --- a/src/search-manager/SearchManager.ts +++ b/src/search-manager/SearchManager.ts @@ -165,7 +165,6 @@ export const SearchContextProvider: React.FC<{ const clearFilters = React.useCallback(() => { setTypesFilter((types) => types.clear()); setTagsFilter([]); - setProblemTypesFilter([]); setPublishStatusFilter([]); if (usageKey !== '') { setUsageKey(''); From f28faaa54214da29420cc152884061ce2f3b2a32 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 21:17:58 -0300 Subject: [PATCH 10/16] fix: openinfosidebar removal update --- src/library-authoring/components/ComponentCard.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index ba047fa91d..65f618c07b 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -230,7 +230,6 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { )} )} - openInfoSidebar={() => openComponentInfoSidebar(usageKey)} hasUnpublishedChanges={modified >= (lastPublished ?? 0)} onSelect={openComponent} /> From fd35ce8a1e192d00dbec7886af3144b58f09928f Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 22:14:04 -0300 Subject: [PATCH 11/16] fix: duped filters --- src/library-authoring/LibraryAuthoringPage.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx index 4b4d055656..f40f187e5f 100644 --- a/src/library-authoring/LibraryAuthoringPage.tsx +++ b/src/library-authoring/LibraryAuthoringPage.tsx @@ -253,15 +253,6 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage headerActions={} hideBorder /> - -
- - - - -
- -
{!insideCollections && } + From 419ac2fef9088a4b7dd1b2302cf8afd9e326d153 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 23:35:44 -0300 Subject: [PATCH 12/16] fix: extract label to message --- src/search-manager/FilterByPublished.tsx | 2 +- src/search-manager/messages.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index 538cbc1e15..924a9f5bca 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -56,7 +56,7 @@ const FilterByPublished: React.FC> = () => { return ( } clearFilter={clearFilters} icon={FilterList} > diff --git a/src/search-manager/messages.ts b/src/search-manager/messages.ts index 03bf205deb..ba6da39387 100644 --- a/src/search-manager/messages.ts +++ b/src/search-manager/messages.ts @@ -236,6 +236,11 @@ const messages = defineMessages({ defaultMessage: 'Never published', description: 'Label for content that has never been published in the publish status filter', }, + publishStatusFilter: { + id: 'course-authoring.search-manager.publishStatus.filter', + defaultMessage: 'Publish Status', + description: 'Label for the filter that allows filtering content by publish status', + }, }); export default messages; From dc3f657babac3185ec902aa5c486f155b4b1fa61 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Mon, 3 Feb 2025 23:52:14 -0300 Subject: [PATCH 13/16] fix: missing import --- src/search-manager/FilterByPublished.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index 924a9f5bca..d3cf7ad911 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -6,7 +6,7 @@ import { MenuItem, } from '@openedx/paragon'; import { FilterList } from '@openedx/paragon/icons'; -import { useIntl } from '@edx/frontend-platform/i18n'; +import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; import SearchFilterWidget from './SearchFilterWidget'; import { useSearchContext } from './SearchManager'; From daa98b8ec3af8c2c9e6114f6e309c18bdb74bb39 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Tue, 4 Feb 2025 12:17:27 -0300 Subject: [PATCH 14/16] fix: publish filter dependencies --- src/search-manager/ClearFiltersButton.tsx | 18 ++++++++---------- src/search-manager/FilterByPublished.tsx | 2 +- src/search-modal/SearchUI.test.tsx | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/search-manager/ClearFiltersButton.tsx b/src/search-manager/ClearFiltersButton.tsx index f970bbebf8..0328d38616 100644 --- a/src/search-manager/ClearFiltersButton.tsx +++ b/src/search-manager/ClearFiltersButton.tsx @@ -17,16 +17,14 @@ const ClearFiltersButton = ({ size = 'sm', }: ClearFiltersButtonProps) => { const { canClearFilters, clearFilters } = useSearchContext(); - return ( - - ); + if (canClearFilters) { + return ( + + ); + } + return null; }; export default ClearFiltersButton; diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx index d3cf7ad911..cc20c2cc0d 100644 --- a/src/search-manager/FilterByPublished.tsx +++ b/src/search-manager/FilterByPublished.tsx @@ -45,7 +45,7 @@ const FilterByPublished: React.FC> = () => { } return [...oldList, mode]; }); - }, []); + }, [setPublishStatusFilter]); const modeToLabel = { published: intl.formatMessage(messages.publishStatusPublished), modified: intl.formatMessage(messages.publishStatusModified), diff --git a/src/search-modal/SearchUI.test.tsx b/src/search-modal/SearchUI.test.tsx index fb82694135..2990cd6e1e 100644 --- a/src/search-modal/SearchUI.test.tsx +++ b/src/search-modal/SearchUI.test.tsx @@ -371,7 +371,7 @@ describe('', () => { // Clear any search filters applied by the previous test. // We need to do this because search filters are stored in the URL, and so they can leak between tests. const { queryByRole } = rendered; - const clearFilters = await queryByRole('button', { name: /clear filters/i }); + const clearFilters = queryByRole('button', { name: /clear filters/i }); if (clearFilters) { fireEvent.click(clearFilters); } @@ -425,7 +425,7 @@ describe('', () => { await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); // Because we're mocking the results, there's no actual changes to the mock results, // but we can verify that the filter was sent in the request - expect(fetchMock).toBeDone((_url, req) => { + expect(fetchMock).toHaveLastFetched((_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries?.[0]?.filter; return JSON.stringify(requestedFilter) === JSON.stringify([ @@ -460,7 +460,7 @@ describe('', () => { await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(2, searchEndpoint, 'post'); }); // Because we're mocking the results, there's no actual changes to the mock results, // but we can verify that the filter was sent in the request - expect(fetchMock).toBeDone((_url, req) => { + expect(fetchMock).toHaveLastFetched((_url, req) => { const requestData = JSON.parse(req.body?.toString() ?? ''); const requestedFilter = requestData?.queries?.[0]?.filter; return JSON.stringify(requestedFilter) === JSON.stringify([ From 19f9420437ccbdf12c56c75d99c0d060d73080c4 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Tue, 4 Feb 2025 12:33:45 -0300 Subject: [PATCH 15/16] fix: use publish status for component card label --- src/library-authoring/components/ComponentCard.tsx | 6 +++--- src/search-manager/data/api.ts | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/library-authoring/components/ComponentCard.tsx b/src/library-authoring/components/ComponentCard.tsx index 65f618c07b..6820240875 100644 --- a/src/library-authoring/components/ComponentCard.tsx +++ b/src/library-authoring/components/ComponentCard.tsx @@ -29,6 +29,7 @@ import BaseComponentCard from './BaseComponentCard'; import { canEditComponent } from './ComponentEditorModal'; import messages from './messages'; import ComponentDeleter from './ComponentDeleter'; +import { PublishStatus } from '../../search-manager/data/api'; type ComponentCardProps = { contentHit: ContentHit, @@ -196,8 +197,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { formatted, tags, usageKey, - modified, - lastPublished, + publishStatus, } = contentHit; const componentDescription: string = ( showOnlyPublished ? formatted.published?.description : formatted.description @@ -230,7 +230,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => { )} )} - hasUnpublishedChanges={modified >= (lastPublished ?? 0)} + hasUnpublishedChanges={publishStatus !== PublishStatus.Published} onSelect={openComponent} /> ); diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts index 69d73cd6b5..eb5340d541 100644 --- a/src/search-manager/data/api.ts +++ b/src/search-manager/data/api.ts @@ -139,6 +139,7 @@ export interface ContentHit extends BaseContentHit { lastPublished: number | null; collections: { displayName?: string[], key?: string[] }; published?: ContentPublishedData; + publishStatus: PublishStatus; formatted: BaseContentHit['formatted'] & { published?: ContentPublishedData, }; } From a338fe8e1d73a2d8588a06ee090d8aba2fb54966 Mon Sep 17 00:00:00 2001 From: Daniel Valenzuela Date: Tue, 4 Feb 2025 12:39:55 -0300 Subject: [PATCH 16/16] fix: tests --- src/library-authoring/collections/LibraryCollectionPage.tsx | 2 ++ src/library-authoring/components/ComponentCard.test.tsx | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx index 1305183b68..6dd1a5c0be 100644 --- a/src/library-authoring/collections/LibraryCollectionPage.tsx +++ b/src/library-authoring/collections/LibraryCollectionPage.tsx @@ -22,6 +22,7 @@ import NotFoundAlert from '../../generic/NotFoundAlert'; import { ClearFiltersButton, FilterByBlockType, + FilterByPublished, FilterByTags, SearchContextProvider, SearchKeywordsField, @@ -211,6 +212,7 @@ const LibraryCollectionPage = () => { + diff --git a/src/library-authoring/components/ComponentCard.test.tsx b/src/library-authoring/components/ComponentCard.test.tsx index 9d0c53a304..2fed436455 100644 --- a/src/library-authoring/components/ComponentCard.test.tsx +++ b/src/library-authoring/components/ComponentCard.test.tsx @@ -9,6 +9,7 @@ import { LibraryProvider } from '../common/context/LibraryContext'; import { getClipboardUrl } from '../../generic/data/api'; import { ContentHit } from '../../search-manager'; import ComponentCard from './ComponentCard'; +import { PublishStatus } from '../../search-manager/data/api'; const contentHit: ContentHit = { id: '1', @@ -35,6 +36,7 @@ const contentHit: ContentHit = { modified: 1722434322294, lastPublished: null, collections: {}, + publishStatus: PublishStatus.Published, }; const clipboardBroadcastChannelMock = {