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: maximum selectable rows on the Datatable #2451

Merged
merged 6 commits into from
Aug 18, 2023

Conversation

johnvente
Copy link
Contributor

@johnvente johnvente commented Jul 17, 2023

Description

When we pass the isSelectable prop to the Datatable component, we can select any quantity of rows: all, one, or any other number we prefer. With the new maxSelectableRows prop, we can specify the maximum number of selectable rows and handle it more effectively

Demo
selected-row-demo

Deploy Preview

https://deploy-preview-2451--paragon-openedx.netlify.app/components/datatable/

Merge Checklist

  • If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Netlify deploy preview, if applicable.
  • Does your change adhere to the documented style conventions?
  • Do any prop types have missing descriptions in the Props API tables in the documentation site (check deploy preview)?
  • Were your changes tested using all available themes (see theme switcher in the header of the deploy preview, under the "Settings" icon)?
  • Were your changes tested in the example app?
  • Is there adequate test coverage for your changes?
  • Consider whether this change needs to reviewed/QA'ed for accessibility (a11y). If so, please add wittjeff and adamstankiewicz as reviewers on this PR.

Post-merge Checklist

  • Verify your changes were released to NPM at the expected version.
  • If you'd like, share your contribution in #show-and-tell.
  • 🎉 🙌 Celebrate! Thanks for your contribution.

@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 17, 2023
@openedx-webhooks
Copy link

openedx-webhooks commented Jul 17, 2023

Thanks for the pull request, @johnvente! Please note that it may take us up to several weeks or months to complete a review and merge your PR.

Feel free to add as much of the following information to the ticket as you can:

  • supporting documentation
  • Open edX discussion forum threads
  • timeline information ("this must be merged by XX date", and why that is)
  • partner information ("this is a course on edx.org")
  • any other information that can help Product understand the context for the PR

All technical communication about the code itself will be done via the GitHub pull request interface. As a reminder, our process documentation is here.

Please let us know once your PR is ready for our review and all tests are green.

@netlify
Copy link

netlify bot commented Jul 17, 2023

Deploy Preview for paragon-openedx ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit 75f7dc4
🔍 Latest deploy log https://app.netlify.com/sites/paragon-openedx/deploys/64dba68d9315760008ab5c7c
😎 Deploy Preview https://deploy-preview-2451--paragon-openedx.netlify.app/components/datatable
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@mphilbrick211 mphilbrick211 added the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Jul 17, 2023
@johnvente johnvente changed the title [Feat]: maximum selectable rows on the table [Feat]: maximum selectable rows on the Datatable Jul 18, 2023
@adamstankiewicz adamstankiewicz self-requested a review July 18, 2023 10:24
Copy link
Member

@adamstankiewicz adamstankiewicz left a comment

Choose a reason for hiding this comment

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

Appreciate the contribution, @johnvente!

Limiting the number of selected rows in DataTable is definitely a use case we've faced at 2U/edX as well in at least one micro-frontend.

That said, I might recommend bringing your solution to the weekly Open edX Paragon Working Group meeting for discussion/review and/or posting in Slack as well, given this also has some design implications around the approach/mechanism to limit the number of selections.

const toggleRowsSelectedProps = useMemo(
() => {
// determine if this selection is for an individual page or the entire table
const getToggleRowsSelectedProps = page ? getToggleAllPageRowsSelectedProps : getToggleAllRowsSelectedProps;
return getToggleRowsSelectedProps();
return isSelectable && maxSelectableRows ? { ...getToggleRowsSelectedProps(), disabled: 'disabled', onChange: () => {} } : getToggleRowsSelectedProps();
Copy link
Member

@adamstankiewicz adamstankiewicz Jul 18, 2023

Choose a reason for hiding this comment

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

[suggestion] If the goal if to ultimately get disabled attribute as part of the updatedProps below via modifying toggleRowsSelectedProps, I might recommend instead to add the new disabled prop directly to the CheckboxControl component below as opposed to extending react-table's internals with getToggleRowsSelectedProps().

Copy link
Contributor Author

@johnvente johnvente Jul 18, 2023

Choose a reason for hiding this comment

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

Yep, that is the goal to disable the CheckboxControl but also if the checkbox is marked not to allow doing anything because someone can modify disabled attribute in the DOM prop and try to mark the checkbox

Comment on lines 82 to 84
Note: We override the `toggleAllRowsSelected` action from react-table
because we need to reassign the `selectedRowIds` property depending on
the size of the maximum rows selected.
Copy link
Member

Choose a reason for hiding this comment

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

[clarification] I'm curious on the approach here. I'm not sure we should naively uncheck the previous selected row on the user's behalf when selecting another row after the user has exceeded the number of selections specified via the maxSelectedRows prop.

We have a similar use case in frontend-app-admin-portal, where we make the selection checkbox on each row disabled as well when the user has reached the maxSelectedRows as configured, though we have a custom MFE-specific implementation, e.g.: https://github.com/openedx/frontend-app-admin-portal/blob/71de4bc001eb9cf09e9118c0d495b250af9df2ee/src/components/ContentHighlights/HighlightStepper/SelectContentSelectionCheckbox.jsx#L21

Our approach in the above use case leaves the choice up to the user how they want to handle reaching the max number of selected rows, e.g. perhaps they want to uncheck the last checked row but they might also want to choose a lesser priority row from an earlier selection, too. Your current implementation prevents the user from making this intentional choice, which I feel is a usability issue.

I might recommend a pattern more alike to frontend-app-admin-portal's use case where the row's selection checkbox becomes disabled preventing the number of selected rows from exceeding the maxSelectedRows prop.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why could it be a usability issue? if the user wants to chose the previews selected row may check it again and uncheck the checkboxes that he doesn't want to select, even doing is the last checkbox that will be changed it will be just that one. I think that disable the other rows can be aggressive for the user, I mean can think that is not possible to check them after the selection

Copy link
Member

@adamstankiewicz adamstankiewicz Jul 25, 2023

Choose a reason for hiding this comment

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

@johnvente. I spoke with the designer responsible for the existing pattern for DataTable to limit the number of maximum selected rows. We both feel it's preferable to prevent going beyond the maximum configured by disabling selections once the user has reached the maximum.

The rationale for this decision is due to the usability issue of the proposed approach assuming the user will choose the de-select the previously selected row, which is not an assumption we can make. If that previously selected row was not a row the user would have de-selected on their own, they would now need to re-select the row that was programmatically de-selected. We feel this is not efficient for the user and likely to cause confusion; the user should be able to make an intentional choice of which row to deselect.

Further, while DataTable should enforce disabling of selection checkboxes, we also believe there is a separate design task to think through how to best communicate the maximum number of rows the user can select as an official part of the DataTable UI. For example, in our current use case, we have some micro-copy nearby the DataTable to explain to the user that "up to 12 rows may be selected." The design task would help understand whether such messaging be a generic, consistent pattern for all use cases.

Given the follow-up design task to explore this pattern, I recommend the following next steps:

  • Implement maxSelectedRows prop to disable selection checkboxes once a configured maximum has been reached. For the "Select all" checkbox in the table header, I feel it would be preferable to simply not include a checkbox in the table header rather than having its checkbox disabled.
    • Perhaps we could conditionally hide the "Select all" checkbox when maxSelectedRows=true and the number of selected rows === number of items in the table (i.e., itemCount)? 🤔
  • [consider] Understand whether consumers of the DataTable component have the ability to get at the current number of selected rows outside of DataTable children (i.e., external to the DataTableContext).
    • If not, perhaps introducing a callback function prop (e.g., onRowSelected or onMaxSelectedRows) which would get called by DataTable when the user selects a row, or reaches the configured maximum number of rows. Using such props could allow consumers of DataTable to render custom messaging where appropriate if/when the user reaches the maximum number of selected rows.

Copy link
Contributor Author

@johnvente johnvente Jul 31, 2023

Choose a reason for hiding this comment

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

Hey @adamstankiewicz! Thanks so much for being aware and giving me that feedback. I will check this ASAP

src/DataTable/index.jsx Outdated Show resolved Hide resolved
src/DataTable/index.jsx Outdated Show resolved Hide resolved
@e0d e0d removed the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Jul 18, 2023
@johnvente johnvente changed the title [Feat]: maximum selectable rows on the Datatable Feat: maximum selectable rows on the Datatable Jul 18, 2023
@adamstankiewicz
Copy link
Member

adamstankiewicz commented Jul 18, 2023

Note: I'm not sure how to share the Deploy preview yet. Can someone assist me with this? I would like to follow the same approach as other pull requests

@johnvente Netlify automatically adds a comment to your PR about it's deploy preview status and URL, e.g.: #2451 (comment)

This URL is the Paragon docs site running the latest commit in your PR. It's usually helpful to leave a (direct) link such as https://deploy-preview-2451--paragon-openedx.netlify.app/components/datatable/ to ease the review process a bit.

@johnvente
Copy link
Contributor Author

Appreciate the contribution, @johnvente!

Limiting the number of selected rows in DataTable is definitely a use case we've faced at 2U/edX as well in at least one micro-frontend.

That said, I might recommend bringing your solution to the weekly Open edX Paragon Working Group meeting for discussion/review and/or posting in Slack as well, given this also has some design implications around the approach/mechanism to limit the number of selections.

It's good heard that at 2U/edX have had the same case use. Last week I couldn't be in the meet but this week I will carry my solution to the weekly Open edX Paragon Working Group

@johnvente
Copy link
Contributor Author

Note: I'm not sure how to share the Deploy preview yet. Can someone assist me with this? I would like to follow the same approach as other pull requests

@johnvente Netlify automatically adds a comment to your PR about it's deploy preview status and URL, e.g.: #2451 (comment)

This URL is the Paragon docs site running the latest commit in your PR. It's usually helpful to leave a (direct) link such as https://deploy-preview-2451--paragon-openedx.netlify.app/components/datatable/ to ease the review process a bit.

Thanks a lot 👍

@johnvente johnvente force-pushed the feat-max-selectable-rows branch 2 times, most recently from 460a3c4 to 8b64e63 Compare August 1, 2023 15:27
@johnvente
Copy link
Contributor Author

johnvente commented Aug 1, 2023

Hi @adamstankiewicz! It's been a while since the last update, but I've done the changes with the feedback that you give me. Now it looks better 💪

@mphilbrick211
Copy link

Hi @adamstankiewicz! Would you mind enabling tests on this? They bounced back to needing approval.

@mphilbrick211 mphilbrick211 added the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Aug 2, 2023
@adamstankiewicz adamstankiewicz removed the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Aug 2, 2023
@codecov
Copy link

codecov bot commented Aug 2, 2023

Codecov Report

Patch coverage: 100.00% and project coverage change: +0.31% 🎉

Comparison is base (667b5e3) 91.38% compared to head (75f7dc4) 91.70%.
Report is 55 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #2451      +/-   ##
==========================================
+ Coverage   91.38%   91.70%   +0.31%     
==========================================
  Files         234      236       +2     
  Lines        4157     4219      +62     
  Branches     1001     1021      +20     
==========================================
+ Hits         3799     3869      +70     
+ Misses        351      346       -5     
+ Partials        7        4       -3     
Files Changed Coverage Δ
src/DataTable/index.jsx 94.87% <100.00%> (+0.58%) ⬆️
src/DataTable/selection/BaseSelectionStatus.jsx 100.00% <100.00%> (ø)
src/DataTable/utils/getVisibleColumns.jsx 100.00% <100.00%> (ø)

... and 10 files with indirect coverage changes

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

Copy link
Member

@adamstankiewicz adamstankiewicz left a comment

Choose a reason for hiding this comment

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

@johnvente Looking good; left some additional comments based on your changes. It's close! 😄

@@ -21,7 +23,7 @@ export const selectColumn = {
);
const updatedProps = useConvertIndeterminateProp(toggleRowsSelectedProps);

return (
return isSelectable && maxSelectedRows ? null : (
Copy link
Member

Choose a reason for hiding this comment

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

Would it be possible to split this logic up into explicit if statements? Might be a bit more readable. E.g.,

if (isSelectable && maxSelectedRows) {
  return null;
}

return (
  ...
);

const { index } = row;
const isRowSelected = index in selectedRowIds;
const selectedRowsLength = Object.keys(selectedRowIds).length;
const hasMaxSelectedRows = maxSelectedRows && maxSelectedRows === selectedRowsLength;
Copy link
Member

Choose a reason for hiding this comment

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

[sanity check] Does this conditional logic handle when maxSelectedRows === 0?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It doesn't because if maxSelectedRows is equal to 0 in this case would be false, should we handle it? It would be odd that the rows are selectable but I can not select one of them 😅 don't you think? I'm wondering if that would be a case use

Copy link
Member

Choose a reason for hiding this comment

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

If maxSelectedRows is 0, hasMaxSelectedRows would be 0, not false (which goes against the implied boolean by the variable name).

should we handle it? It would be odd that the rows are selectable but I can not select one of them 😅 don't you think? I'm wondering if that would be a case use

You're right that it's unlikely to be a use case. However, I still feel the component should adhere to the 0 if passed by the consumer. Otherwise, it's not really "correct" (the consumer is telling the component 0 rows should be selected lol). Allowing selections with maxSelectedRows=0 doesn't feel great imho 😄 What if that 0 is intentional?

src/DataTable/utils/getVisibleColumns.jsx Outdated Show resolved Hide resolved
@adamstankiewicz
Copy link
Member

@johnvente Also note, this commitlint CI check is failing. It's possible the commitlint check might not like the parentheses in the commit message?

image

@johnvente
Copy link
Contributor Author

johnvente commented Aug 3, 2023

Hello @adamstankiewicz! I've done the changes I'm having account when maxSelectedRows is equal to 0 I wasn't considering when maxSelectedRows is equal to a negative number so I added Math.max(0, maxSelectedRows); to put 0
as default, please let me know if that is ok or I should remove it. I will add some test cases

@johnvente
Copy link
Contributor Author

@johnvente Looking good; left some additional comments based on your changes. It's close! 😄

Hi @adamstankiewicz we're almost done 🚀 I've pushed some test cases I'll be pending to any change 👯

@mphilbrick211 mphilbrick211 added the waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. label Aug 7, 2023
@johnvente johnvente force-pushed the feat-max-selectable-rows branch 3 times, most recently from 728f047 to fd2ab83 Compare August 7, 2023 15:40
Copy link
Member

@brobro10000 brobro10000 left a comment

Choose a reason for hiding this comment

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

Testing this component in the playground, I noticed some odd behavior on the onMaxSelectedRows prop. (see video)

Test Case 1: User reaches max selected rows, where all selected rows are on the same pagination page of the datatable, and prop function executes.
Output: For every k number of selected rows where k = maxSelectedRows, the function executes k number of times.

Test Case 2: User reaches max selected rows where n number of selected rows one paginated page of the datatable, and k number of selected rows on a different page of the datatable, where n + k = maxSelectedRows.
Output: If you are on the paginated page of the datatable and the final selected row the user selects equals the max selected rows, then if the user is on the page with n selected rows and reaches the maximum number selected rows, then n number of calls will be made based on the function in onMaxSelectedRows.
Ex. maxSelectedRows = 4
Page 1: 1 selected item
Page 2: 3 selected items
if final selected item results in maxSelectedRows is page 1, onMaxSelectedRows will run once.
if final selected item results in maxSelectedRows is page 2, onMaxSelectedRows will run
3 times.

Screen.Recording.2023-08-07.at.10.22.44.AM.mov

@johnvente
Copy link
Contributor Author

johnvente commented Aug 7, 2023

Testing this component in the playground, I noticed some odd behavior on the onMaxSelectedRows prop. (see video)

Test Case 1: User reaches max selected rows, where all selected rows are on the same pagination page of the datatable, and prop function executes. Output: For every k number of selected rows where k = maxSelectedRows, the function executes k number of times.

Test Case 2: User reaches max selected rows where n number of selected rows one paginated page of the datatable, and k number of selected rows on a different page of the datatable, where n + k = maxSelectedRows. Output: If you are on the paginated page of the datatable and the final selected row the user selects equals the max selected rows, then if the user is on the page with n selected rows and reaches the maximum number selected rows, then n number of calls will be made based on the function in onMaxSelectedRows. Ex. maxSelectedRows = 4 Page 1: 1 selected item Page 2: 3 selected items if final selected item results in maxSelectedRows is page 1, onMaxSelectedRows will run once. if final selected item results in maxSelectedRows is page 2, onMaxSelectedRows will run 3 times.

Screen.Recording.2023-08-07.at.10.22.44.AM.mov

Probably this may be related to useEffect dependences I'll review it

@johnvente johnvente changed the title Feat: maximum selectable rows on the Datatable feat: maximum selectable rows on the Datatable Aug 7, 2023
@johnvente johnvente force-pushed the feat-max-selectable-rows branch from fd2ab83 to 1de1586 Compare August 7, 2023 23:37
@johnvente
Copy link
Contributor Author

johnvente commented Aug 7, 2023

Testing this component in the playground, I noticed some odd behavior on the onMaxSelectedRows prop. (see video)

Test Case 1: User reaches max selected rows, where all selected rows are on the same pagination page of the datatable, and prop function executes. Output: For every k number of selected rows where k = maxSelectedRows, the function executes k number of times.

Test Case 2: User reaches max selected rows where n number of selected rows one paginated page of the datatable, and k number of selected rows on a different page of the datatable, where n + k = maxSelectedRows. Output: If you are on the paginated page of the datatable and the final selected row the user selects equals the max selected rows, then if the user is on the page with n selected rows and reaches the maximum number selected rows, then n number of calls will be made based on the function in onMaxSelectedRows. Ex. maxSelectedRows = 4 Page 1: 1 selected item Page 2: 3 selected items if final selected item results in maxSelectedRows is page 1, onMaxSelectedRows will run once. if final selected item results in maxSelectedRows is page 2, onMaxSelectedRows will run 3 times.

Screen.Recording.2023-08-07.at.10.22.44.AM.mov

Hey @brobro10000 I've changed the behavior for that, please check if now works as expected

Copy link
Member

@brobro10000 brobro10000 left a comment

Choose a reason for hiding this comment

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

Thank you for the quick turnaround on the fixes. Very much appreciated :)

@mphilbrick211
Copy link

Hi @adamstankiewicz and @brobro10000! Would one of you be able to re-enable the tests? They bounced back to needing approval to run.

@mphilbrick211 mphilbrick211 added needs test run Author's first PR to this repository, awaiting test authorization from Axim and removed waiting on author PR author needs to resolve review requests, answer questions, fix tests, etc. labels Aug 8, 2023
Copy link
Contributor

@brian-smith-tcril brian-smith-tcril left a comment

Choose a reason for hiding this comment

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

Just clicked around a bit on the preview site and this looks like it's working really well! @johnvente is there any chance you could add an example to https://github.com/openedx/paragon/blob/master/src/DataTable/README.md demonstrating this functionality? (@adamstankiewicz if you feel like that's not something we want/it would just clutter up the docs page feel free to veto this idea)

@brian-smith-tcril brian-smith-tcril removed the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Aug 10, 2023
@johnvente
Copy link
Contributor Author

Just clicked around a bit on the preview site and this looks like it's working really well! @johnvente is there any chance you could add an example to https://github.com/openedx/paragon/blob/master/src/DataTable/README.md demonstrating this functionality? (@adamstankiewicz if you feel like that's not something we want/it would just clutter up the docs page feel free to veto this idea)

Hi @brian-smith-tcril! Absolutely, I will add an example for this. Thanks for the suggestion! 💪

@adamstankiewicz
Copy link
Member

@brian-smith-tcril @johnvente Sure, feel free to add an example. The overall docs for DataTable should likely get cleaned up to make that page a bit easier to understand (i.e., perceived information overload). That said, that's a larger documentation initiative. I agree having an explicit example would be helpful for now!

@johnvente
Copy link
Contributor Author

Hey @brian-smith-tcril @adamstankiewicz! I've included an example for these new props. Any change or suggestion will be very welcome

@mphilbrick211 mphilbrick211 added the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Aug 15, 2023
@mphilbrick211
Copy link

Hi @adamstankiewicz and @brian-smith-tcril! Would one of you mind enabling the checks again?

@@ -1569,4 +1569,201 @@ You can create your own cell content by passing the `Cell` property to a specifi
</DataTable>
);
}

```
## maxSelectedRows and onMaxSelectedRows props
Copy link
Member

Choose a reason for hiding this comment

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

🎉

```
#### example with react-intl
Copy link
Member

Choose a reason for hiding this comment

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

@johnvente What is the intent of this example with react-intl? I'm not quite sure what value it provides to consumers. What is this example trying to demonstrate for consumers? My two cents is that this particular example is unnecessary.

Also, as is, it results in a JS error within the example code block preview:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@adamstankiewicz Probably this is unnecessary I'll remove 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.

@adamstankiewicz adamstankiewicz removed the needs test run Author's first PR to this repository, awaiting test authorization from Axim label Aug 15, 2023
@adamstankiewicz adamstankiewicz merged commit 7194fba into openedx:master Aug 18, 2023
@openedx-webhooks
Copy link

@johnvente 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future.

@edx-semantic-release
Copy link
Contributor

🎉 This PR is included in version 21.1.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

@adamstankiewicz
Copy link
Member

@johnvente Thanks for your contribution! As the semantic-release bot commented above, your changes have been released on v21.1.0.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open-source-contribution PR author is not from Axim or 2U released
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

8 participants