feat: allow filtering library by publish status (#1570)
* Adds a filter widget for Publish status. * Adds "Unpublished changes" badge.
This commit is contained in:
committed by
GitHub
parent
31f39cb015
commit
05dddce920
@@ -539,7 +539,7 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
// Validate clear filters
|
||||
fireEvent.click(problemMenu);
|
||||
|
||||
const clearFitlersButton = screen.getByRole('button', { name: /clear filters/i });
|
||||
const clearFitlersButton = screen.getByText('Clear Filters');
|
||||
fireEvent.click(clearFitlersButton);
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
|
||||
@@ -713,6 +713,72 @@ describe('<LibraryAuthoringPage />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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 });
|
||||
|
||||
// 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',
|
||||
headers: expect.anything(),
|
||||
});
|
||||
});
|
||||
|
||||
// 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(() => {
|
||||
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(),
|
||||
});
|
||||
});
|
||||
|
||||
// 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('Disables Type filter on Collections tab', async () => {
|
||||
await renderLibraryPage();
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
ClearFiltersButton,
|
||||
FilterByBlockType,
|
||||
FilterByTags,
|
||||
FilterByPublished,
|
||||
SearchContextProvider,
|
||||
SearchKeywordsField,
|
||||
SearchSortWidget,
|
||||
@@ -266,6 +267,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
|
||||
<SearchKeywordsField className="mr-3" />
|
||||
<FilterByTags />
|
||||
{!insideCollections && <FilterByBlockType />}
|
||||
<FilterByPublished />
|
||||
<ClearFiltersButton />
|
||||
<ActionRow.Spacer />
|
||||
<SearchSortWidget />
|
||||
|
||||
@@ -22,6 +22,7 @@ import NotFoundAlert from '../../generic/NotFoundAlert';
|
||||
import {
|
||||
ClearFiltersButton,
|
||||
FilterByBlockType,
|
||||
FilterByPublished,
|
||||
FilterByTags,
|
||||
SearchContextProvider,
|
||||
SearchKeywordsField,
|
||||
@@ -211,6 +212,7 @@ const LibraryCollectionPage = () => {
|
||||
<SearchKeywordsField className="mr-3" />
|
||||
<FilterByTags />
|
||||
<FilterByBlockType />
|
||||
<FilterByPublished />
|
||||
<ClearFiltersButton />
|
||||
<ActionRow.Spacer />
|
||||
<SearchSortWidget />
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
Container,
|
||||
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';
|
||||
|
||||
type BaseComponentCardProps = {
|
||||
componentType: string,
|
||||
displayName: string, description: string,
|
||||
numChildren?: number,
|
||||
tags: ContentHitTags,
|
||||
actions: React.ReactNode,
|
||||
componentType: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
numChildren?: number;
|
||||
tags: ContentHitTags;
|
||||
actions: React.ReactNode;
|
||||
hasUnpublishedChanges?: boolean;
|
||||
onSelect: () => void
|
||||
};
|
||||
|
||||
@@ -27,6 +31,7 @@ const BaseComponentCard = ({
|
||||
tags,
|
||||
actions,
|
||||
onSelect,
|
||||
...props
|
||||
} : BaseComponentCardProps) => {
|
||||
const tagCount = useMemo(() => {
|
||||
if (!tags) {
|
||||
@@ -37,6 +42,7 @@ const BaseComponentCard = ({
|
||||
}, [tags]);
|
||||
|
||||
const componentIcon = getItemIcon(componentType);
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<Container className="library-component-card">
|
||||
@@ -75,7 +81,8 @@ const BaseComponentCard = ({
|
||||
<div className="text-truncate h3 mt-2">
|
||||
<Highlight text={displayName} />
|
||||
</div>
|
||||
<Highlight text={description} />
|
||||
<Highlight text={description} /><br />
|
||||
{props.hasUnpublishedChanges ? <Badge variant="warning">{intl.formatMessage(messages.unpublishedChanges)}</Badge> : null}
|
||||
</Card.Section>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,6 +197,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => {
|
||||
formatted,
|
||||
tags,
|
||||
usageKey,
|
||||
publishStatus,
|
||||
} = contentHit;
|
||||
const componentDescription: string = (
|
||||
showOnlyPublished ? formatted.published?.description : formatted.description
|
||||
@@ -228,6 +230,7 @@ const ComponentCard = ({ contentHit }: ComponentCardProps) => {
|
||||
)}
|
||||
</ActionRow>
|
||||
)}
|
||||
hasUnpublishedChanges={publishStatus !== PublishStatus.Published}
|
||||
onSelect={openComponent}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
107
src/search-manager/FilterByPublished.tsx
Normal file
107
src/search-manager/FilterByPublished.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Badge,
|
||||
Form,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@openedx/paragon';
|
||||
import { FilterList } from '@openedx/paragon/icons';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
import SearchFilterWidget from './SearchFilterWidget';
|
||||
import { useSearchContext } from './SearchManager';
|
||||
import { PublishStatus, SearchSortOption } from './data/api';
|
||||
|
||||
/**
|
||||
* A button with a dropdown that allows filtering the current search by publish status
|
||||
*/
|
||||
const FilterByPublished: React.FC<Record<never, never>> = () => {
|
||||
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)) {
|
||||
return oldList.filter(m => m !== mode);
|
||||
}
|
||||
return [...oldList, mode];
|
||||
});
|
||||
}, [setPublishStatusFilter]);
|
||||
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 (
|
||||
<SearchFilterWidget
|
||||
appliedFilters={appliedFilters}
|
||||
label={<FormattedMessage {...messages.publishStatusFilter} />}
|
||||
clearFilter={clearFilters}
|
||||
icon={FilterList}
|
||||
>
|
||||
<Form.Group className="mb-0">
|
||||
<Form.CheckboxSet
|
||||
name="publish-status-filter"
|
||||
value={publishStatusFilter}
|
||||
>
|
||||
<Menu className="block-type-refinement-menu" style={{ boxShadow: 'none' }}>
|
||||
<MenuItem
|
||||
as={Form.Checkbox}
|
||||
value={PublishStatus.Published}
|
||||
onChange={() => { toggleFilterMode(PublishStatus.Published); }}
|
||||
>
|
||||
<div>
|
||||
{intl.formatMessage(messages.publishStatusPublished)}
|
||||
<Badge variant="light" pill>{publishStatus[PublishStatus.Published] ?? 0}</Badge>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
as={Form.Checkbox}
|
||||
value={PublishStatus.Modified}
|
||||
onChange={() => { toggleFilterMode(PublishStatus.Modified); }}
|
||||
>
|
||||
<div>
|
||||
{intl.formatMessage(messages.publishStatusModified)}
|
||||
<Badge variant="light" pill>{publishStatus[PublishStatus.Modified] ?? 0}</Badge>
|
||||
</div>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
as={Form.Checkbox}
|
||||
value={PublishStatus.NeverPublished}
|
||||
onChange={() => { toggleFilterMode(PublishStatus.NeverPublished); }}
|
||||
disabled={onlyPublished}
|
||||
>
|
||||
<div>
|
||||
{intl.formatMessage(messages.publishStatusNeverPublished)}
|
||||
<Badge variant="light" pill>{publishStatus[PublishStatus.NeverPublished] ?? 0}</Badge>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Form.CheckboxSet>
|
||||
</Form.Group>
|
||||
</SearchFilterWidget>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterByPublished;
|
||||
@@ -9,7 +9,10 @@ import { MeiliSearch, type Filter } from 'meilisearch';
|
||||
import { union } from 'lodash';
|
||||
|
||||
import {
|
||||
CollectionHit, ContentHit, SearchSortOption, forceArray,
|
||||
CollectionHit,
|
||||
ContentHit,
|
||||
SearchSortOption,
|
||||
forceArray, PublishStatus,
|
||||
} from './data/api';
|
||||
import { TypesFilterData, useStateOrUrlSearchParam } from './hooks';
|
||||
import { useContentSearchConnection, useContentSearchResults } from './data/apiHooks';
|
||||
@@ -20,12 +23,15 @@ export interface SearchContextData {
|
||||
indexName?: string;
|
||||
searchKeywords: string;
|
||||
setSearchKeywords: React.Dispatch<React.SetStateAction<string>>;
|
||||
publishStatusFilter: PublishStatus[];
|
||||
setPublishStatusFilter: React.Dispatch<React.SetStateAction<PublishStatus[]>>;
|
||||
typesFilter: TypesFilterData;
|
||||
setTypesFilter: React.Dispatch<React.SetStateAction<TypesFilterData>>;
|
||||
tagsFilter: string[];
|
||||
setTagsFilter: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
blockTypes: Record<string, number>;
|
||||
problemTypes: Record<string, number>;
|
||||
publishStatus: Record<string, number>;
|
||||
extraFilter?: Filter;
|
||||
canClearFilters: boolean;
|
||||
clearFilters: () => void;
|
||||
@@ -98,6 +104,14 @@ export const SearchContextProvider: React.FC<{
|
||||
skipUrlUpdate,
|
||||
);
|
||||
|
||||
const [publishStatusFilter, setPublishStatusFilter] = useStateOrUrlSearchParam<PublishStatus>(
|
||||
[],
|
||||
'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 {
|
||||
@@ -144,12 +158,14 @@ export const SearchContextProvider: React.FC<{
|
||||
const canClearFilters = (
|
||||
!typesFilter.isEmpty()
|
||||
|| tagsFilter.length > 0
|
||||
|| publishStatusFilter.length > 0
|
||||
|| !!usageKey
|
||||
);
|
||||
const isFiltered = canClearFilters || (searchKeywords !== '');
|
||||
const clearFilters = React.useCallback(() => {
|
||||
setTypesFilter((types) => types.clear());
|
||||
setTagsFilter([]);
|
||||
setPublishStatusFilter([]);
|
||||
if (usageKey !== '') {
|
||||
setUsageKey('');
|
||||
}
|
||||
@@ -164,6 +180,7 @@ export const SearchContextProvider: React.FC<{
|
||||
indexName,
|
||||
extraFilter,
|
||||
searchKeywords,
|
||||
publishStatusFilter,
|
||||
blockTypesFilter: [...typesFilter.blocks],
|
||||
problemTypesFilter: [...typesFilter.problems],
|
||||
tagsFilter,
|
||||
@@ -177,6 +194,8 @@ export const SearchContextProvider: React.FC<{
|
||||
indexName,
|
||||
searchKeywords,
|
||||
setSearchKeywords,
|
||||
publishStatusFilter,
|
||||
setPublishStatusFilter,
|
||||
typesFilter,
|
||||
setTypesFilter,
|
||||
tagsFilter,
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -133,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, };
|
||||
}
|
||||
|
||||
@@ -179,6 +186,7 @@ interface FetchSearchParams {
|
||||
searchKeywords: string,
|
||||
blockTypesFilter?: string[],
|
||||
problemTypesFilter?: string[],
|
||||
publishStatusFilter?: PublishStatus[],
|
||||
/** The full path of tags that each result MUST have, e.g. ["Difficulty > Hard", "Subject > Math"] */
|
||||
tagsFilter?: string[],
|
||||
extraFilter?: Filter,
|
||||
@@ -194,6 +202,7 @@ export async function fetchSearchResults({
|
||||
searchKeywords,
|
||||
blockTypesFilter,
|
||||
problemTypesFilter,
|
||||
publishStatusFilter,
|
||||
tagsFilter,
|
||||
extraFilter,
|
||||
sort,
|
||||
@@ -205,6 +214,7 @@ export async function fetchSearchResults({
|
||||
totalHits: number,
|
||||
blockTypes: Record<string, number>,
|
||||
problemTypes: Record<string, number>,
|
||||
publishStatus: Record<string, number>,
|
||||
}> {
|
||||
const queries: MultiSearchQuery[] = [];
|
||||
|
||||
@@ -215,6 +225,8 @@ export async function fetchSearchResults({
|
||||
|
||||
const problemTypesFilterFormatted = problemTypesFilter?.length ? [problemTypesFilter.map(pt => `content.problem_types = ${pt}`)] : [];
|
||||
|
||||
const publishStatusFilterFormatted = publishStatusFilter?.length ? [publishStatusFilter.map(ps => `publish_status = ${ps}`)] : [];
|
||||
|
||||
const tagsFilterFormatted = formatTagsFilter(tagsFilter);
|
||||
|
||||
const limit = 20; // How many results to retrieve per page.
|
||||
@@ -235,6 +247,7 @@ export async function fetchSearchResults({
|
||||
...typeFilters,
|
||||
...extraFilterFormatted,
|
||||
...tagsFilterFormatted,
|
||||
...publishStatusFilterFormatted,
|
||||
],
|
||||
attributesToHighlight: ['display_name', 'description', 'published'],
|
||||
highlightPreTag: HIGHLIGHT_PRE_TAG,
|
||||
@@ -249,7 +262,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.
|
||||
@@ -266,6 +279,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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
fetchTagsThatMatchKeyword,
|
||||
getContentSearchConfig,
|
||||
fetchBlockTypes,
|
||||
type PublishStatus,
|
||||
} from './api';
|
||||
|
||||
/**
|
||||
@@ -53,6 +54,7 @@ export const useContentSearchResults = ({
|
||||
searchKeywords,
|
||||
blockTypesFilter = [],
|
||||
problemTypesFilter = [],
|
||||
publishStatusFilter = [],
|
||||
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[];
|
||||
publishStatusFilter?: 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,
|
||||
publishStatusFilter,
|
||||
tagsFilter,
|
||||
sort,
|
||||
],
|
||||
@@ -103,6 +107,7 @@ export const useContentSearchResults = ({
|
||||
searchKeywords,
|
||||
blockTypesFilter,
|
||||
problemTypesFilter,
|
||||
publishStatusFilter,
|
||||
tagsFilter,
|
||||
sort,
|
||||
// For infinite pagination of results, we can retrieve additional pages if requested.
|
||||
@@ -128,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,
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -221,6 +221,26 @@ 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',
|
||||
},
|
||||
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;
|
||||
|
||||
@@ -371,7 +371,7 @@ describe('<SearchUI />', () => {
|
||||
// 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('<SearchUI />', () => {
|
||||
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('<SearchUI />', () => {
|
||||
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([
|
||||
|
||||
Reference in New Issue
Block a user