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

Code eval tool: use toast upon deletion instead of alert #10165

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

Conversation

srietkerk
Copy link
Contributor

@srietkerk srietkerk commented Sep 9, 2024

Closes microsoft/pxt-microbit#5911

I found the alert experience to be really clunky, so, as per @eanders-ms's suggestion, I've changed it so when a user deletes a criteria, they can undo it via a toast that pops up.

As part of this change, I made it so deleting a criteria just performs a soft deletion so undoing a deletion was possible.

image

UPDATED, latest upload target (with export working correctly): https://makecode.microbit.org/app/9b9448bb0aa4de66361394d15e8a90bb8c7b1ac5-d565522e6f--eval

@srietkerk srietkerk requested a review from a team September 9, 2024 18:59
@eanders-ms
Copy link
Contributor

When I delete a criteria, if that criteria was at max (and therefore not able to be added), it doesn't become available again. To repro:

  1. Add "uses at least variables" to your checklist
  2. Once added, delete that criteria from your checklist
  3. Try to add it back again

It shows as Max in the catalog, and cannot be added.

image

teachertool/src/transforms/readdCriteriaToChecklist.ts Outdated Show resolved Hide resolved
import { Ticks } from "../constants";
import { getCriteriaInstanceWithId } from "../state/helpers";

export function readdCriteriaToChecklist(criteriaInstanceId: string) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Over in addCriteriaToChecklist do you want to look for an existing deleted matching criteria and restore it if found? Would that make the distinction between adding and re-adding unnecessary?

Copy link
Contributor

Choose a reason for hiding this comment

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

An alternative might be to permanently delete the criteria instance once the toast disappears.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think permanently deleting is probably for the best with this approach. Though see #10165 (comment) for some thoughts on a slightly different approach that I think would lessen the importance of it (would still ultimately be good to have, though).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just for clarity, we are now permanently deleting the criteria after the toast goes away. I also wanted to add since this is related to the max problem that @eanders-ms expressed above, I removed the check for whether a criteria is deleted on the CatalogOverlay. I did so because having the check would allow a user to add and also readd a criteria, thus possibly having two instances of a criteria that is supposed to only have one. Getting rid of the check in the overlay makes it so it still looks like I can't add the "one only" criteria until the criteria is permanently deleted, thus avoiding the ability for a user to add and readd a criteria that can only be used once.

import { Ticks } from "../constants";
import { getCriteriaInstanceWithId } from "../state/helpers";

export function readdCriteriaToChecklist(criteriaInstanceId: 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: I saw this as a typo of "read" instead of "re-add". Perhaps "reAdd" or "undelete" would be clearer?

}
removeCriteriaFromChecklist(criteriaId);
const toast = makeToast("info", Strings.CriteriaDeleted);
toast.jsx = <UndoDeleteCriteriaButton criteriaId={criteriaId} toastId={toast.id} />;
Copy link
Contributor

Choose a reason for hiding this comment

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

As much as I like the sleekness of this design, something to keep in mind is from an accessibility perspective, how can a user navigate to this button without a mouse? If this means we need to treat toasts like modals and have them steal focus, that could be somewhat confusing/irritating. Maybe we could do that only if the toast is "interactive" in some way? Or there may be other workarounds.

Not necessarily a blocker, but good to keep it in mind.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point. I wonder what Outlook does here.

setChecklist(newChecklist);

if (catalogCriteriaId) {
pxt.tickEvent(Ticks.RemoveCriteria, { catalogCriteriaId });
Copy link
Contributor

Choose a reason for hiding this comment

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

Is removeCriteria the correct tick to send in this case?


const newChecklist = {
...teacherTool.checklist,
criteria: teacherTool.checklist.criteria.filter(c => c.instanceId !== criteriaInstanceId),
criteria: allCriteria,
Copy link
Contributor

@thsparks thsparks Sep 10, 2024

Choose a reason for hiding this comment

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

So since we're just setting this flag but never removing the criteria, it this means that when a teacher exports their checklist, it will contain all the deleted checklist items which is somewhat wasteful since there's no way to get them back after the toast is dismissed.

One way to avoid this, which I think would ultimately be simpler all around, would be to move criteria out of the main checklist and into a separate deletedCriteria list, rather than add a delete flag. This means that, even if they export while the toast is open, the export will only contain the un-deleted criteria. It also means we don't need all the deleted checks within the code (which would be easy to forget when implementing future features).

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, if we do a separate list, it's probably still good to remove items once the toast is dismissed, but not quite as important (assuming it clears when the user closes the session).

Copy link
Contributor

Choose a reason for hiding this comment

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

A downside to a separate list for deleted items is that undoing the deletion wouldn't preserve original location in the active criteria list (unless we stored the original index). If undeletion appended to the active set, it may confuse the user, especially if the end of the list is off the page.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, that's true, but I still think it'd be simpler to store a separate list of { criteria + index } objects than have to filter based on deleted everywhere (or remember to call the appropriate helper function)

Copy link
Contributor

Choose a reason for hiding this comment

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

A helper to get active criteria should mitigate this. I agree we don't want to include deleted criteria in the exported JSON.

Copy link
Contributor

@thsparks thsparks Sep 10, 2024

Choose a reason for hiding this comment

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

Even with a helper, you have to remember to call it instead of getting the value from state :) Separate list makes it a non-issue as long as we can sort out the index thing. I don't feel that strongly, so I'm happy for Sarah to decide. But personally, I still think a separate list would be slightly cleaner.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would like to continue with the flag approach and use the "get active criteria" helper function. I have been thinking A LOT about the pros and cons of each approach, and I think both have really strong merit. The selling point for me is for the removing and readding scenario of criteria. Both approaches have the same amount of state operations for full removing and newly adding criteria. However, in the new scenario that we are supporting here (removing and readding), the "having a separate list for deleted criteria" would need four state operations along with list operations (two for removing, two for readding) while the flag approach only needs two state operations and no list operations are needed.

Copy link
Contributor

Choose a reason for hiding this comment

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

That's fine, as long as we can get the helper in place and filter deleted criteria out of the serialization.

@@ -80,7 +80,7 @@ const CatalogItem: React.FC<CatalogItemProps> = ({ catalogCriteria, recentlyAdde
const { state: teacherTool } = useContext(AppStateContext);

const existingInstanceCount = teacherTool.checklist.criteria.filter(
i => i.catalogCriteriaId === catalogCriteria.id
i => i.catalogCriteriaId === catalogCriteria.id && !i.deleted
Copy link
Contributor

Choose a reason for hiding this comment

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

For long-term code maintainability, I recommend adding a helper (in state/helpers.ts) for getting active criteria.

@@ -122,6 +122,7 @@ const CriteriaWithResultsTable: React.FC = () => {
<div className={css["results-list"]}>
{Object.values(criteria).map(criteriaInstance => {
Copy link
Contributor

Choose a reason for hiding this comment

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

For code clarity: Use helper to get active criteria, then render them.

flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--pxt-info-accent-darkened);
Copy link
Contributor

@thsparks thsparks Sep 10, 2024

Choose a reason for hiding this comment

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

We should probably change these colors based on the type of toast containing them (error, info, etc...). I believe there are different css classes for each type of toast we can reference.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think that's a good idea. Can this be done in a follow-up?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure

}
removeCriteriaFromChecklist(criteriaId);
const toast = makeToast("info", Strings.CriteriaDeleted);
toast.jsx = <UndoDeleteCriteriaButton criteriaId={criteriaId} toastId={toast.id} />;
Copy link
Contributor

Choose a reason for hiding this comment

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

In makeToast, it looks like you added a parameter for jsx, but it seems like we're not using it here. Was there a specific reason for that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes. I originally just had the <UndoDeleteCriteriaButton/> inside the make toast. However, in order to have clicking the undo button dismiss the toast, I needed to have access to the toast id so I switched to setting the toast's jsx to the button after making the toast. That said, I could see future scenarios where passing in jsx when making the toast would be helpful so I left it in. I can remove it, though, since it's not getting used with this change.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. I would probably lean towards removing it in that case, at least for now. Either way is fine though.

criteria: teacherTool.checklist.criteria.filter(c => c.instanceId !== criteriaInstanceId),
criteria: teacherTool.checklist.criteria.filter(c =>
c.instanceId !== criteriaInstanceId ||
c.instanceId === criteriaInstanceId && !c.deleted
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm, I'm wondering if there's a scenario where we would want this action to work even for non-deleted criteria (in which case we could have a separate action removeDeletedCriteriaFromChecklist that checks for deleted before running remove, or just an onlyIfDeleted parameter passed into this action). But I can't think of a realistic scenario at the moment so maybe it's fine...

criteria: teacherTool.checklist.criteria.filter(c => c.instanceId !== criteriaInstanceId),
criteria: teacherTool.checklist.criteria.filter(c =>
c.instanceId !== criteriaInstanceId ||
c.instanceId === criteriaInstanceId && !c.deleted
Copy link
Contributor

Choose a reason for hiding this comment

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

I think the c.instanceId === criteriaInstanceId check is redundant. Could just be c.instanceId !== criteriaInstanceId || !c.deleted

@@ -20,6 +20,7 @@ export interface CriteriaInstance {
instanceId: string;
params: CriteriaParameterValue[] | undefined;
userFeedback?: UserFeedback;
deleted?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should still ensure deleted criteria get filtered out of export/serialization. It's not as big of an issue since it gets removed once the toast goes away, but the user could still export while the toast is around (or more likely, close the browser/tab, so it gets serialized and stored in browser storage), at which point the deleted criteria would be there forever since the delete code won't run on it anymore.

setChecklist(newChecklist);

if (catalogCriteriaId) {
pxt.tickEvent(Ticks.RemoveCriteria, { catalogCriteriaId });
Copy link
Contributor

Choose a reason for hiding this comment

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

My original comment on this is out of date now, but just a note that I think we need to update this tick from Remove to Re-Add.

@eanders-ms
Copy link
Contributor

When I open the Add Criteria panel and add a singleton criteria (limited to a single instance), it correctly appears the catalog unselectable and tagged with "Max". But after deleting the criteria, it remains unselectable in the catalog until the Undo toast disappears. This behavior seems incorrect. The catalog entry should immediately become available to add again.

@eanders-ms
Copy link
Contributor

I think that soft deletion is complicating things here. Rather than soft delete, if the undo state were to hold onto the deleted criteria (alongside the toast id), it could simply re-add it at its original index when the user clicks Undo. Validation would need to run in the addCriteria transform to enforce constraints (Max instances, etc.). This is to handle the scenario where the user adds a new copy of the criteria before triggering Undo. Removing soft delete in favor of holding the deleted criteria in memory for a short time would simplify things a lot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Code eval tool: Change the alert for when a user is deleting a criteria
3 participants