Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: shareable URLs for library components and searches [FC-0076] #1575

Open
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

pomegranited
Copy link
Contributor

@pomegranited pomegranited commented Dec 18, 2024

Description

Describe what this pull request changes, and why. Include implications for people using this change.
Design decisions and their rationales should be documented in the repo (docstring / ADR), per

Useful information to include:

  • This change makes it possible for Content Authors to share URLs for a specific component, collection, and/or set of library content search parameters.
  • No change to the UI -- only improves the URLs and consistency between page/component navigation.

Use cases covered:

  • As an author working with content libraries, I want to easily share any component in a library with other people on my team, by copying the URL from my browser and sending it to them.
  • As an author working with content libraries, I want to easily share any search results with other people on my team, by copying the URL from my browser and sending it to them.
  • As an author working with content libraries, I want to bookmark a search in my browser and return to it at any time, with the same filters and keywords applied.
  • As an author of a content library with public read access, I want to easily share any component in a library with any authors on the same Open edX instance, by copying the URL from my browser and sending it to them.
  • As an author of a content library, I want to easily share a library's "Manage Team" page with other people on my team by copying the URL from my browser and sending it to them.
  • As an author working with content libraries, I want to easily share any selected sidebar tab with other people on my team, by copying the URL from my browser and sending it to them.

Supporting information

See #1499
Private-ref: FAL-3984

Testing instructions

Recommend going commit-by-commit to do the code review, since this is a pretty big change.

This change involved some route additions and changes, and so to manually test them, please refresh your browser page after each step, and ensure you see exactly the same page -- including selected tabs, sidebar, and sidebar tabs. Please also check that using the back/forth buttons navigate smoothly between pages as expected, but that changing the library search parameters doesn't append to the browser history.

  1. Navigate to /libraries and create or select an existing library from the list.
    "All Content" should still be the default tab you see.
  2. Create a few components, collections, and add some components to some collections.
    Note that the component picker still works as expected.
  3. Navigate back to "All Content".
  4. Select a component from the list.
    Note that the URL changes to /library/:libraryId/component/:componentId, and the component is visible in the sidebar.
  5. Add or select a different component from the list.
    Note that the URL changes to reflect the new componentId.
  6. Add or select a collection from the list.
    Note that the URL changes to /library/:libraryId/:collectionId, and the collection is visible in the sidebar.
  7. Add or select a new collection from the list.
    Note that the URL changes to reflect the new collectionId.
  8. Change your sidebar tab to something other than the initial Preview, then select a different component from the list.
    Note that the sidebar tab is preserved (where possible) when navigating between components or collections.
    Clicking Library Info should reset the sidebar back to the Library.
  9. Change to the Components and Collections tabs.
    Note that you remain in the selected tab when switching between selected components/collections.
  10. Select a Collection, and either click the Open button in its sidebar or click the Collection card again to open it.
    Note that the URL changes to /library/:libraryId/collection/:collectionId, and the collection is visible in the sidebar.
  11. Select a component within the collection.
    Note that the URL changes to /library/:libraryId/collection/:collectionId/:componentId, and the component is visible in the sidebar.
  12. Change your sidebar tab to something other than the initial Preview, then select a different component from the list.
    Note that the sidebar tab is preserved (where possible) when navigating between components.
    Clicking Collection Info should reset the sidebar back to the Collection.
  13. Use the search bar keywords to search for components from any tab or page.
    Note that your keywords are stored in a q query string parameter, and preserved on page refresh.
  14. Repeat for the other search filters and sort options.
    Note that these are store in the query string, and cleared when de-selecting or clicking Clear Filters.
  15. Navigate back to the Library sidebar, and click Manage Access.
    Note that ?sa=manage-team is the query string, and refreshing the page re-opens the Library Team modal.
  16. Using the Component card menu, select Add to Collection.
    Note that ?sa=jump-to-add-collections is in the query string, and this is preserved when you navigate between components.
    Clicking "Confirm" or "Cancel" removes this parameter and closes the collection manager.
  17. Check that the Problem Bank and Library Content pickers still work as expected.

Other information

  1. When feat: allow filtering library by publish status #1570 merges, we'll need to apply the same "URL search params" treatment to the "Publish Status" filter that we've done with the other filters.
  2. I chose not to store whether the sidebar was open or closed in the query string, because the sidebar reopens itself almost everywhere in library authoring, so it seemed unnecessary.
  3. I left some TODOs in the SearchManager code about sanitizing parameters, because sanitising user input is good practice. But I'm not sure how best to sanitise things like tags and search keywords?
  4. I ran into a really tricky bug when running clearFilters on multiple search filters that are now using the useStateWithUrlSearchParam hook -- more details in this comment. Advice or alternatives welcome!
  5. The failing ● › can filter by capa problem type test is working on my local machine -- not sure why it's failing in CI? (The other 2 failing SearchUI tests are failing locally, so will address them soon.)

to support optional parameters in test paths
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Dec 18, 2024
@openedx-webhooks
Copy link

openedx-webhooks commented Dec 18, 2024

Thanks for the pull request, @pomegranited!

What's next?

Please work through the following steps to get your changes ready for engineering review:

🔘 Get product approval

If you haven't already, check this list 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.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.

🔘 Provide context

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 threads

🔘 Get a green build

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

🔘 Let us know that your PR is ready for review:

Who will review my changes?

This repository is currently maintained by @openedx/2u-tnl. Tag them in a comment and let them know that your changes are ready for review.

Where can I find more information?

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:

When can I expect my changes to be merged?

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

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@mphilbrick211 mphilbrick211 added the FC Relates to an Axim Funded Contribution project label Dec 18, 2024
@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from 8fdbe9b to aa25827 Compare December 19, 2024 01:11
Copy link

codecov bot commented Dec 19, 2024

Codecov Report

Attention: Patch coverage is 94.87179% with 10 lines in your changes missing coverage. Please review.

Project coverage is 92.95%. Comparing base (230960b) to head (f6b46c4).

Files with missing lines Patch % Lines
src/library-authoring/routes.ts 86.66% 6 Missing ⚠️
...c/library-authoring/collections/CollectionInfo.tsx 92.30% 1 Missing ⚠️
...library-authoring/component-info/ComponentInfo.tsx 83.33% 1 Missing ⚠️
...library-authoring/components/BaseComponentCard.tsx 0.00% 1 Missing ⚠️
src/library-authoring/library-info/LibraryInfo.tsx 88.88% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1575      +/-   ##
==========================================
- Coverage   92.96%   92.95%   -0.01%     
==========================================
  Files        1075     1076       +1     
  Lines       21197    21297     +100     
  Branches     4560     4515      -45     
==========================================
+ Hits        19706    19797      +91     
- Misses       1425     1434       +9     
  Partials       66       66              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from 5fad981 to 18b4e2d Compare December 19, 2024 03:41
* Restructures LibraryLayout so that LibraryContext can useParams() to
  initialize its componentId/collectionId instead of having to parse
  complicated route strings.

  Initialization-from-URL can be disabled for the content pickers by
  passing skipUrlUpdate to the LibraryContext -- which is needed by the
  component picker.

* Clicking/selecting a ComponentCard/CollectionCard navigates to an
  appropriate component/collection route given the current page.

* Adds useLibraryRoutes() hook so components can easily navigate to the
  best available route without having to know the route strings or
  maintain search params.

* Moves ContentType declaration to the new routes.ts to avoid circular
  imports.

* Renames openInfoSidebar to openLibrarySidebar, so that openInfoSidebar
  can be used to open the best sidebar for a given
  library/component/collection.
@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from 18b4e2d to ce4c27e Compare December 19, 2024 03:56
This triggers a navigate() call when the searchbox changes, which resets
the query counts in testing. So had to reduce the expected counts by 1.
@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from d202a8d to b1fe66e Compare December 19, 2024 04:45
Related changes:

* adds "manage-team" sidebar action to trigger LibraryInfo's "Manage Team" modal
* moves useStateWithUrlSearchParam up to hooks.ts so it can be used by
  search-manager and library-authoring
* splits sidebarCurrentTab and additionalAction out of SidebarComponentInfo
  -- they're now managed independently as url parameters.

  This also simplifies setSidebarComponentInfo: we can simply set the
  new id + key, and so there's no need to merge the ...prev state and
  new state.
* shortens some sidebar property names:
   sidebarCurrentTab => sidebarTab
   sidebarAdditionalAction => sidebarAction
* test: Tab changes now trigger a navigate() call, which invalidates the
  within(sidebar) element in the tests. So using screen instead.
@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from b1fe66e to f6b46c4 Compare December 19, 2024 05:14
Copy link
Contributor

@bradenmacdonald bradenmacdonald left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooops, queued up these comments when I took an early look at this PR and forgot to submit. Not sure if they're still useful.

src/hooks.ts Outdated Show resolved Hide resolved
Comment on lines +50 to +71
<Routes>
<Route
path={ROUTES.COMPONENTS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COLLECTIONS}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COMPONENT}
element={context(<LibraryAuthoringPage />)}
/>
<Route
path={ROUTES.COLLECTION}
element={context(<LibraryCollectionPage />)}
/>
<Route
path={ROUTES.HOME}
element={context(<LibraryAuthoringPage />)}
/>
</Routes>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this entire <Routes>...</Routes> worth having instead of just putting <LibraryAuthoringPage /> ? I guess it will show a 404 error if none of the routes match? Or does it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advantage of spelling out all of these routes is that it's now very easy to get the :componentId / :collectionId from useParams() and use these as the defaults in LibraryProvider.

I also had to wrap the page element in that context() wrapper so that the routes would be available when LibraryProvider is created -- previously <LibraryLayout> wrapped <LibraryProvider> around the <Routes>, and so <LibraryProvider> couldn't use them.


export const ROUTES = {
COMPONENTS: '/components/:componentId?',
COLLECTIONS: '/collections/:collectionId?',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is there a collectionId in COLLECTIONS? A comment would be helpful. Same question for HOME.

@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from fa8783d to f6bf086 Compare December 20, 2024 02:39
Related features:

1. We can filter on more than one tag, so the search field needs to
   store an Array of values which still need to be sanitised. This
   change adds useListHelpers to assist with the parsing and validating
   of an Array of Typed values.
2. SearchManager takes a skipUrlUpdate property which should disable
   using search params for state variables. Our "sort" parameters
   respected this, but none of the other search parameters did. So this
   change also adds a wrapper hook called useStateOrUrlSearchParam that
   handles this switch cleanly.

This feature also revealed two bugs fixed in useStateWithUrlSearchParam:

1. When the returnSetter is called with a function instead of a simple
   value, we need to pass in previous returnValue to the function so it
   can generate the new value.

2. When the returnSetter is called multiple times by a single callback
   (like with clearFilters), the latest changes to the UrlSearchParams
   weren't showing up.

   To fix this, we had to use the location.search string as the "latest"
   previous url search, not the prevParams passed into setSearchParams,
   because these params may not have the latest updates.
@pomegranited pomegranited force-pushed the jill/fal-3984-sharable-urls branch from f6bf086 to 37370d2 Compare December 20, 2024 13:16
Combine blockTypesFilter + problemTypesFilter into a single state
variable called typesFilter, and store selected block + problem types in
the url search parameters.

These two states are heavily interdependent, and so the
FilterByBlockType code was quite complicated. Then storing these states
as url search params caused re-renders that disrupted the delicate
dependencies between these two states.

This change stores both these type lists using a single TypesFilterData
class to simplify the code and avoid bugs when updating their states.
@pomegranited pomegranited marked this pull request as ready for review December 20, 2024 14:45
@pomegranited pomegranited requested a review from a team as a code owner December 20, 2024 14:45
Copy link
Contributor

@navinkarkera navinkarkera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pomegranited Everything works nicely! Great work!.

Just found a small bug: On collections page, select any collection so that its sidebar appears (this appends st=manage to query params), now click on Library Info button and then on Manage Access button. This opens up Library Team modal and adds sa=manage-team query param to url. On refreshing page, modal is not displayed, most probably due to presence of st=manage query param in url. We should remove it on clicking Library info button or display of library sidebar.

Using the Component card menu, select Add to Collection. Note that ?sa=jump-to-add-collections is in the query string, and this is preserved when you navigate between components.

Should we also add this parameter when author clicks on Add to Collection button in the component sidebar?

} = useLibraryContext();
const { openInfoSidebar, sidebarComponentInfo } = useSidebarContext();

const { insideCollections, insideComponents, navigateTo } = useLibraryRoutes();

// The activeKey determines the currently selected tab.
const [activeKey, setActiveKey] = useState<ContentType>(ContentType.home);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: We don't need useEffect as we can use a function to initialize activeKey state.

Something like this should work.

  const getActiveKey = () => {
    if (componentPickerMode) {
      return ContentType.home;
    }
    if (insideCollections) {
      return ContentType.collections;
    }
    if (insideComponents) {
      return ContentType.components;
    }
    return ContentType.home;
  };
  // The activeKey determines the currently selected tab.
  const [activeKey, setActiveKey] = useState<ContentType>(getActiveKey);

}: {
fromString: FromStringFn<Type>,
toString: ToStringFn<Type>,
separator?: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: We can make use of browsers capability to convert query params with same name to an array like object instead of manually adding a separator and splitting it. useSearchParams from react-router returns URLSearchParams which has getAll that returns an array.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
FC Relates to an Axim Funded Contribution project open-source-contribution PR author is not from Axim or 2U
Projects
Status: Waiting on Author
Development

Successfully merging this pull request may close these issues.

5 participants