Repository: openedx/frontend-app-authoring
Pull request #1947: fix: Remove never published filter from component picker
Author: login: ChrisChV
Labels: name: open-source-contribution
description: PR author is not from Axim or 2U
Created: 2025-05-14T00:26:21Z | Merged: 2025-05-22T00:37:42Z

## Description

Removes the never-published filter option from the component picker and unit picker.

![image](https://github.com/user-attachments/assets/7a4e3055-2765-4b95-9339-48d12d71a6d0)


## Supporting information

- Github issue: https://github.com/openedx/frontend-app-authoring/issues/1748
- Internal ticket: [FAL-4160](https://tasks.opencraft.com/browse/FAL-4160)

## Testing instructions

- Go to the library home of a library
- Create two components and two units. Publish all.
- Make an update in one component and one unit. Not publish.
- Go to the unit outline in a course and add a library component.
- In the component picker modal, verify that the never-published filter is not present.
- Go to the course outline and add a unit from a library.
- In the unit picker modal,l verify that the never-published filter is not present


## Other information

N/A


Diff:
diff --git a/src/library-authoring/LibraryAuthoringPage.tsx b/src/library-authoring/LibraryAuthoringPage.tsx
index 6e6100329f..9d42611c14 100644
--- a/src/library-authoring/LibraryAuthoringPage.tsx
+++ b/src/library-authoring/LibraryAuthoringPage.tsx
@@ -32,7 +32,6 @@ import {
   ClearFiltersButton,
   FilterByBlockType,
   FilterByTags,
-  FilterByPublished,
   SearchContextProvider,
   SearchKeywordsField,
   SearchSortWidget,
@@ -46,6 +45,7 @@ import { SidebarBodyComponentId, useSidebarContext } from './common/context/Side
 import { allLibraryPageTabs, ContentType, useLibraryRoutes } from './routes';
 
 import messages from './messages';
+import LibraryFilterByPublished from './generic/filter-by-published';
 
 const HeaderActions = () => {
   const intl = useIntl();
@@ -246,6 +246,17 @@ const LibraryAuthoringPage = ({
     extraFilter.push(activeTypeFilters[activeKey]);
   }
 
+  /*
+
+  <FilterByPublished key={
+                // It is necessary to re-render `FilterByPublished` every time `FilterByBlockType`
+                // appears or disappears, this is because when the menu is opened it is rendered
+                // in a previous state, causing an inconsistency in its position.
+                // By changing the key we can re-render the component.
+                !(insideCollections || insideUnits) ? 'filter-published-1' : 'filter-published-2'
+              }
+  */
+
   // Disable filtering by block/problem type when viewing the Collections tab.
   const overrideTypesFilter = (insideCollections || insideUnits) ? new TypesFilterData() : undefined;
 
@@ -299,8 +310,8 @@ const LibraryAuthoringPage = ({
               <SearchKeywordsField className="mr-3" />
               <FilterByTags />
               {!(insideCollections || insideUnits) && <FilterByBlockType />}
-              <FilterByPublished key={
-                // It is necessary to re-render `FilterByPublished` every time `FilterByBlockType`
+              <LibraryFilterByPublished key={
+                // It is necessary to re-render `LibraryFilterByPublished` every time `FilterByBlockType`
                 // appears or disappears, this is because when the menu is opened it is rendered
                 // in a previous state, causing an inconsistency in its position.
                 // By changing the key we can re-render the component.
diff --git a/src/library-authoring/collections/LibraryCollectionPage.tsx b/src/library-authoring/collections/LibraryCollectionPage.tsx
index de3c7ce234..943664788d 100644
--- a/src/library-authoring/collections/LibraryCollectionPage.tsx
+++ b/src/library-authoring/collections/LibraryCollectionPage.tsx
@@ -22,7 +22,6 @@ import NotFoundAlert from '../../generic/NotFoundAlert';
 import {
   ClearFiltersButton,
   FilterByBlockType,
-  FilterByPublished,
   FilterByTags,
   SearchContextProvider,
   SearchKeywordsField,
@@ -36,6 +35,7 @@ import { SidebarBodyComponentId, useSidebarContext } from '../common/context/Sid
 import messages from './messages';
 import { LibrarySidebar } from '../library-sidebar';
 import LibraryCollectionComponents from './LibraryCollectionComponents';
+import LibraryFilterByPublished from '../generic/filter-by-published';
 
 const HeaderActions = () => {
   const intl = useIntl();
@@ -218,7 +218,7 @@ const LibraryCollectionPage = () => {
               <SearchKeywordsField className="mr-3" />
               <FilterByTags />
               <FilterByBlockType />
-              <FilterByPublished />
+              <LibraryFilterByPublished />
               <ClearFiltersButton />
               <ActionRow.Spacer />
               <SearchSortWidget />
diff --git a/src/library-authoring/component-picker/ComponentPicker.test.tsx b/src/library-authoring/component-picker/ComponentPicker.test.tsx
index 8e9fbbaf9d..2d492adf65 100644
--- a/src/library-authoring/component-picker/ComponentPicker.test.tsx
+++ b/src/library-authoring/component-picker/ComponentPicker.test.tsx
@@ -302,4 +302,43 @@ describe('<ComponentPicker />', () => {
     expect(screen.queryByRole('tab', { name: /collections/i })).not.toBeInTheDocument();
     expect(screen.queryByRole('tab', { name: /components/i })).not.toBeInTheDocument();
   });
+
+  it('should not display never published filter', async () => {
+    render(<ComponentPicker />);
+
+    expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
+    fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i));
+
+    // Wait for the content library to load
+    const filterButton = await screen.findByRole('button', { name: /publish status/i });
+    fireEvent.click(filterButton);
+
+    // Verify the filters. Note: It's hard to verify the `published` filter,
+    // because there are many components with that text on the screen, but that's not the important thing.
+    expect(screen.getByText(/modified since publish/i)).toBeInTheDocument();
+    expect(screen.queryByText(/never published/i)).not.toBeInTheDocument();
+  });
+
+  it('should not display never published filter in collection page', async () => {
+    render(<ComponentPicker />);
+
+    expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
+    fireEvent.click(screen.getByDisplayValue(/lib:sampletaxonomyorg1:tl1/i));
+
+    // Wait for the content library to load
+    await screen.findByText(/Change Library/i);
+    expect(await screen.findByText('Test Library 1')).toBeInTheDocument();
+
+    // Click on the collection card to open the sidebar
+    fireEvent.click(screen.queryAllByText('Collection 1')[0]);
+
+    // Wait for the content library to load
+    const filterButton = await screen.findByRole('button', { name: /publish status/i });
+    fireEvent.click(filterButton);
+
+    // Verify the filters. Note: It's hard to verify the `published` filter,
+    // because there are many components with that text on the screen, but that's not the important thing.
+    expect(screen.getByText(/modified since publish/i)).toBeInTheDocument();
+    expect(screen.queryByText(/never published/i)).not.toBeInTheDocument();
+  });
 });
diff --git a/src/library-authoring/generic/filter-by-published/index.tsx b/src/library-authoring/generic/filter-by-published/index.tsx
new file mode 100644
index 0000000000..825ac56f4d
--- /dev/null
+++ b/src/library-authoring/generic/filter-by-published/index.tsx
@@ -0,0 +1,26 @@
+import React from 'react';
+import { useLibraryContext } from '../../common/context/LibraryContext';
+import { FilterByPublished, PublishStatus } from '../../../search-manager';
+
+/**
+ * When browsing library content for insertion into a course, we only show published
+ * content. In that case, there is no need for a 'Never Published' filter, which will
+ * never show results. This component removes that option from FilterByPublished
+ * when not relevant.
+ */
+const LibraryFilterByPublished : React.FC<Record<never, never>> = () => {
+  const { showOnlyPublished } = useLibraryContext();
+
+  if (showOnlyPublished) {
+    return (
+      <FilterByPublished visibleFilters={
+        [PublishStatus.Published, PublishStatus.Modified]
+      }
+      />
+    );
+  }
+
+  return <FilterByPublished />;
+};
+
+export default LibraryFilterByPublished;
diff --git a/src/search-manager/FilterByPublished.tsx b/src/search-manager/FilterByPublished.tsx
index f8ede2a956..079c6d43ee 100644
--- a/src/search-manager/FilterByPublished.tsx
+++ b/src/search-manager/FilterByPublished.tsx
@@ -10,12 +10,18 @@ import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
 import messages from './messages';
 import SearchFilterWidget from './SearchFilterWidget';
 import { useSearchContext } from './SearchManager';
-import { PublishStatus } from './data/api';
+import { allPublishFilters, PublishStatus } from './data/api';
+
+interface FilterByPublishedProps {
+  visibleFilters?: PublishStatus[],
+}
 
 /**
  * A button with a dropdown that allows filtering the current search by publish status
  */
-const FilterByPublished: React.FC<Record<never, never>> = () => {
+const FilterByPublished = ({
+  visibleFilters = allPublishFilters,
+}: FilterByPublishedProps) => {
   const intl = useIntl();
   const {
     publishStatus,
@@ -42,6 +48,26 @@ const FilterByPublished: React.FC<Record<never, never>> = () => {
   };
   const appliedFilters = publishStatusFilter.map(mode => ({ label: modeToLabel[mode] }));
 
+  const filterLabels = {
+    [PublishStatus.Published]: intl.formatMessage(messages.publishStatusPublished),
+    [PublishStatus.Modified]: intl.formatMessage(messages.publishStatusModified),
+    [PublishStatus.NeverPublished]: intl.formatMessage(messages.publishStatusNeverPublished),
+  };
+
+  const visibleFiltersToRender = visibleFilters.map((filter) => (
+    <MenuItem
+      key={filter}
+      as={Form.Checkbox}
+      value={filter}
+      onChange={() => { toggleFilterMode(filter); }}
+    >
+      <div>
+        {filterLabels[filter]}
+        <Badge variant="light" pill>{publishStatus[filter] ?? 0}</Badge>
+      </div>
+    </MenuItem>
+  ));
+
   return (
     <SearchFilterWidget
       appliedFilters={appliedFilters}
@@ -55,36 +81,7 @@ const FilterByPublished: React.FC<Record<never, never>> = () => {
           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); }}
-            >
-              <div>
-                {intl.formatMessage(messages.publishStatusNeverPublished)}
-                <Badge variant="light" pill>{publishStatus[PublishStatus.NeverPublished] ?? 0}</Badge>
-              </div>
-            </MenuItem>
+            {visibleFiltersToRender}
           </Menu>
         </Form.CheckboxSet>
       </Form.Group>
diff --git a/src/search-manager/data/api.ts b/src/search-manager/data/api.ts
index 01811883cd..aa7ec2f619 100644
--- a/src/search-manager/data/api.ts
+++ b/src/search-manager/data/api.ts
@@ -31,6 +31,8 @@ export enum PublishStatus {
   NeverPublished = 'never',
 }
 
+export const allPublishFilters: PublishStatus[] = Object.values(PublishStatus);
+
 /**
  * Get the content search configuration from the CMS.
  */


Reviews:
user: login: navinkarkera
body: @ChrisChV Works as expected in the main page but if you open any collection, the `Never published` filter can be seen.

![image](https://github.com/user-attachments/assets/3f4a19ae-4994-4360-8d37-cb6a23dce4a5)

state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T06:23:59Z
user: login: ChrisChV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T17:23:37Z
user: login: navinkarkera
body: @ChrisChV :+1:
- [x] I tested this: (Verified that never published filter is hidden in library selection modal)
- [x] I read through the code
- [x] I checked for accessibility issues
- [x] Includes documentation

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T07:00:19Z
user: login: bradenmacdonald
body: Approved, but please address these two minor points before you merge.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-21T17:16:43Z
user: login: ChrisChV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T00:30:15Z
user: login: ChrisChV
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-22T00:30:21Z

Review comments:
diff_hunk: @@ -259,6 +260,11 @@ const LibraryAuthoringPage = ({
     <Tab key={contentType} eventKey={contentType} title={tabTitles[contentType]} />
   ));
 
+  const publishedFilters = [PublishStatus.Published, PublishStatus.Modified];
+  if (!showOnlyPublished) {
+    publishedFilters.push(PublishStatus.NeverPublished);
+  }
+
path: src/library-authoring/LibraryAuthoringPage.tsx
user: login: navinkarkera
body: NIT: Check for `showOnlyPublished` flag inside `FilterByPublished` instead of passing filters via props. This should take care of all the places that this component is used.
created_at: 2025-05-14T06:23:09Z
author_association: CONTRIBUTOR
diff_hunk: @@ -259,6 +260,11 @@ const LibraryAuthoringPage = ({
     <Tab key={contentType} eventKey={contentType} title={tabTitles[contentType]} />
   ));
 
+  const publishedFilters = [PublishStatus.Published, PublishStatus.Modified];
+  if (!showOnlyPublished) {
+    publishedFilters.push(PublishStatus.NeverPublished);
+  }
+
path: src/library-authoring/LibraryAuthoringPage.tsx
user: login: ChrisChV
body: The `FilterByPublished` component is outside the context of libraries, so it could be used on other screens. In https://github.com/openedx/frontend-app-authoring/pull/1947/commits/f768f29900ef5c7f3782742b803c8a5ae241336b I created `LibraryFilterByPublished` to reuse it and I fixed the collections bug
created_at: 2025-05-14T17:23:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import React from 'react';
+import { useLibraryContext } from '../../common/context/LibraryContext';
+import { FilterByPublished, PublishStatus } from '../../../search-manager';
+
+const LibraryFilterByPublished : React.FC<Record<never, never>> = () => {
path: src/library-authoring/generic/filter-by-published/index.tsx
user: login: bradenmacdonald
body: Nit: it would be good to have a JSDoc comment here explaining why we have this component ("When browsing library content for insertion into a course, we only show published content. In that case, there is no need for a 'Never Published' filter, which will never show results. This component removes that option from FilterByPublished when not relevant.")
created_at: 2025-05-21T17:14:30Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +48,25 @@ const FilterByPublished: React.FC<Record<never, never>> = () => {
   };
   const appliedFilters = publishStatusFilter.map(mode => ({ label: modeToLabel[mode] }));
 
+  const filterLabels = {
+    [PublishStatus.Published]: intl.formatMessage(messages.publishStatusPublished),
+    [PublishStatus.Modified]: intl.formatMessage(messages.publishStatusModified),
+    [PublishStatus.NeverPublished]: intl.formatMessage(messages.publishStatusNeverPublished),
+  };
+
+  const visibleFiltersToRender = visibleFilters.map((filter) => (
+    <MenuItem
+      as={Form.Checkbox}
+      value={filter}
+      onChange={() => { toggleFilterMode(filter); }}
path: src/search-manager/FilterByPublished.tsx
user: login: bradenmacdonald
body: Don't you need a `key` here since you're using `map` ? Please test new things with your JS console open so you don't miss warnings.
created_at: 2025-05-21T17:16:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+import React from 'react';
+import { useLibraryContext } from '../../common/context/LibraryContext';
+import { FilterByPublished, PublishStatus } from '../../../search-manager';
+
+const LibraryFilterByPublished : React.FC<Record<never, never>> = () => {
path: src/library-authoring/generic/filter-by-published/index.tsx
user: login: ChrisChV
body: Updated: https://github.com/openedx/frontend-app-authoring/pull/1947/commits/df819d36c76c7d05059efb4538b470729f96cd59
created_at: 2025-05-22T00:30:15Z
author_association: CONTRIBUTOR
diff_hunk: @@ -42,6 +48,25 @@ const FilterByPublished: React.FC<Record<never, never>> = () => {
   };
   const appliedFilters = publishStatusFilter.map(mode => ({ label: modeToLabel[mode] }));
 
+  const filterLabels = {
+    [PublishStatus.Published]: intl.formatMessage(messages.publishStatusPublished),
+    [PublishStatus.Modified]: intl.formatMessage(messages.publishStatusModified),
+    [PublishStatus.NeverPublished]: intl.formatMessage(messages.publishStatusNeverPublished),
+  };
+
+  const visibleFiltersToRender = visibleFilters.map((filter) => (
+    <MenuItem
+      as={Form.Checkbox}
+      value={filter}
+      onChange={() => { toggleFilterMode(filter); }}
path: src/search-manager/FilterByPublished.tsx
user: login: ChrisChV
body: Updated: https://github.com/openedx/frontend-app-authoring/pull/1947/commits/df819d36c76c7d05059efb4538b470729f96cd59
created_at: 2025-05-22T00:30:21Z
author_association: CONTRIBUTOR

PR comments:
user: login: openedx-webhooks
created_at: 2025-05-14T00:26:26Z
body: Thanks for the pull request, @ChrisChV!


This repository is currently maintained by `@bradenmacdonald`.


Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

<details><summary>:radio_button: Get product approval</summary>

If you haven't already, [check this list](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review#Does-my-contribution-require-Product-Review%3F) to see if your contribution needs to go through the product review process.

- If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the [Product Working Group](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3449028609/Product+Working+Group).
    - This process (including the steps you'll need to take) is documented [here](https://openedx.atlassian.net/wiki/spaces/COMM/pages/3875962884/How+to+submit+an+open+source+contribution+for+Product+Review#Product-Review-Process).
- If it doesn't, simply proceed with the next step.
</details>

<details><summary>:radio_button: Provide context</summary>

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

- Dependencies
  > This PR must be merged before / after / at the same time as ...
- Blockers
  > This PR is waiting for OEP-1234 to be accepted.
- Timeline information
  > This PR must be merged by XX date **because** ...
- Partner information
  > This is for a course on edx.org.
- Supporting documentation
- Relevant [Open edX discussion forum](https://discuss.openedx.org/) threads
</details>





<details><summary>:radio_button: Get a green build</summary>

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.
</details><details>


---

<details><summary>Where can I find more information?</summary>

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

- [Overview of Review Process for Community Contributions](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/FAQ-about-pull-requests.html)
- [Pull Request Status Guide](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/pull-request-statuses.html)
- [Making changes to your pull request](https://docs.openedx.org/en/latest/documentors/how-tos/make_changes_to_your_pull_request.html)
</details>

<details><summary>When can I expect my changes to be merged?</summary>

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

- The size and impact of the changes that it introduces
- The need for product review
- Maintenance status of the parent repository

:bulb: *As a result it may take up to several weeks or months to complete a review and merge your PR.*
</details>
<!-- comment:external_pr -->
<!-- data: eyJkcmFmdCI6IGZhbHNlfQ== -->

author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-14T00:35:41Z
body: ## [Codecov](https://app.codecov.io/gh/openedx/frontend-app-authoring/pull/1947?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openedx) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 93.95%. Comparing base [(`9e97805`)](https://app.codecov.io/gh/openedx/frontend-app-authoring/commit/9e978057bc004d8d1a3a55f872a3b352ffc304ef?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openedx) to head [(`df819d3`)](https://app.codecov.io/gh/openedx/frontend-app-authoring/commit/df819d36c76c7d05059efb4538b470729f96cd59?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openedx).
> Report is 1 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #1947      +/-   ##
==========================================
- Coverage   93.95%   93.95%   -0.01%     
==========================================
  Files        1154     1155       +1     
  Lines       24116    24128      +12     
  Branches     5219     5116     -103     
==========================================
+ Hits        22659    22670      +11     
- Misses       1389     1390       +1     
  Partials       68       68              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/openedx/frontend-app-authoring/pull/1947?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openedx).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=openedx).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: ChrisChV
created_at: 2025-05-14T17:23:51Z
body: @navinkarkera  It's ready for another review
author_association: CONTRIBUTOR