Improve how we display and manage file attachment states.

Review Request #13240 — Created Aug. 31, 2023 and submitted

Information

Review Board
release-6.x

Reviewers

There are several improvements that can be made to the way that we display and
manage file attachments. Currently, when looking at a review request draft we
have no way of knowing whether a file attachment is a draft or not. It would
be nice to be able to distinguish the draft file attachments from the published
ones. It would also be nice to be able to see if a draft is a new revision of
an attachment, a completely new one, or an update to an existing one (e.g. one
with an updated caption). We also can't tell when a file attachment is
pending deletion on a review request draft, i.e. when its been deleted from
the draft but the draft has not been published yet.

This change aims to address all of those issues. Now, we use a label to display
the state of file attachments on a review request draft (except for published
attachments). It's easier to undo draft updates to the attachments with a
new "Delete Draft" action, which can delete draft revisions and redisplay
the previously published attachment, delete new drafts, or undo any changes
to the caption. And, instead of making attachments disappear after deleting
them from a review request draft, we display them as pending deletion and
allow users to undo the deletion with an "Undo Delete" action.

  • Ran unit tests.
  • Ran JS unit tests.
  • Lots of manual testing, uploading/updating/deleting file attachments,
    changing the state, changing the caption, updating, reviewing, commenting,
    testing the new actions.
  • Made sure user file attachments and file attachments thumbnails in change
    descriptions were not affected.
  • Tested trying to view a draft review request as a user who doesn't have
    permission for the draft, saw that no file attachment states or any of
    the new actions appeared.
Summary
Improve how we display and manage file attachment states.

Description From Last Updated

Should have mentioned here instead of the change introducing the widget but to avoid shifting the layout or reserving space, …

chipx86chipx86

If we're not writing to this, let's use Mapping instead of Dict.

chipx86chipx86

All new variables (and any being modified) should have a type when it can't be inferred (by being directly assigned …

chipx86chipx86

This can be done as one statement with a list comprehension, which should be a bit faster. In fact, if …

chipx86chipx86

Missing a type.

chipx86chipx86

We fetch this from the data and then modify it below, which probably isn't safe.

chipx86chipx86

Missing a type.

chipx86chipx86

This might actually be better as a list comprehension. If you go that route, the other comments here won't apply. …

chipx86chipx86

Blank line between these.

chipx86chipx86

No parens on the conditional here.

chipx86chipx86

Our standard for list comprehensions are in the form of: [ value for value in loop if conditions ]

chipx86chipx86

Is there any situation where a file can be removed and another added to this list, resulting in the same …

chipx86chipx86

Can you add this in alphabetically? I know this dictionary's a bit of a mess, but it'd be a step …

chipx86chipx86

Missing the last : in :py:class:, and missing a trailing period.

chipx86chipx86

These should all be in /** form.

chipx86chipx86

I mentioned in another change that we should probably slugify the IDs, avoiding the temptation to use them as labels …

chipx86chipx86

This should be on the next line, since it's a multi-line comment.

chipx86chipx86

Summary should be on the next line.

chipx86chipx86

The callback should align with other parameters.

chipx86chipx86

I wanted to check a couple things with this signal. I note this is happening in-between removing and adding attachment …

chipx86chipx86

Comment contents should start on the next line.

chipx86chipx86

No blank line here.

chipx86chipx86

Same here.

chipx86chipx86

There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its".

chipx86chipx86

The ? and : should align with the value being checked. Actually, looks like we compute this in other places. …

chipx86chipx86

A few notes: The <span> should be indented one space. We want this to be described as a button in …

chipx86chipx86

We can probably combine this into one statement, make sure the result is only computed and set once.

chipx86chipx86

The ? false indicates we can just do this as a boolean condition. this.model.get('state') != this.states.PENDING_DELETION && options.canEdit

chipx86chipx86

Indentation issue with the comment.

chipx86chipx86

We access this.model three times. Let's pull it out into a local variable.

chipx86chipx86

This should be formatted in the usual way, with ? and : being aligned with the start of the condition.

chipx86chipx86

I don't think a Set buys us anything here. We know the entirety of the list of possible values, and …

chipx86chipx86

Since _ isn't free, we want to avoid using and then overriding it. How about: const deleteText = (draftStates.has(state) ? …

chipx86chipx86

We use this.model several times in this method, so let's pull it out into a local variable.

chipx86chipx86

You can use the set(key, value) form here and below, which is a bit cheaper when there's only one thing …

chipx86chipx86

We use this.model several times in this function, so let's pull it out into a local variable.

chipx86chipx86

We use this.model several times in this method, so let's pull it out into a local variable.

chipx86chipx86

When using template literals, every single piece of whitespace across lines is preserved, newlines included. We should avoid that here. …

chipx86chipx86

We access this.#fileAttachmentThumbnailViews on every line, so let's pull it out into some views local variable.

chipx86chipx86

redefinition of unused 'FileAttachment' from line 17 Column: 5 Error code: F811

reviewbotreviewbot

'typing.Dict' imported but unused Column: 1 Error code: F401

reviewbotreviewbot
maubin
  1. 
      
  2. Not sure how I feel about the red, maybe it'd work better as green like the other labels?

  3. 
      
chipx86
  1. This is a lot of work! Well done :)

    The majority of these comments are code consistency issues (bad indentation in places) or missing typing. Some are optimizations or code movement. The biggest bit of feedback is about presentation, which can also be addressed as a follow-up change if needed (and we can discuss options in Slack for this as well).

  2. Should have mentioned here instead of the change introducing the widget but to avoid shifting the layout or reserving space, can we badge these onto the thumbnail instead? Overlay the top-left of the thumbnail a bit?

    1. What do you think of the top-right instead (see updated pics).

    2. Works for me!

  3. reviewboard/reviews/templatetags/reviewtags.py (Diff revision 1)
     
     
     
     
     
     

    All new variables (and any being modified) should have a type when it can't be inferred (by being directly assigned something that does have an explicit type).

    Same below.

  4. reviewboard/reviews/templatetags/reviewtags.py (Diff revision 1)
     
     
     
     
     
     
     
     
     

    This can be done as one statement with a list comprehension, which should be a bit faster.

    In fact, if you do that, you can avoid the [] + above and just concatenate that list with this, giving a brand new list.

  5. We fetch this from the data and then modify it below, which probably isn't safe.

    1. I don't modify it, I just read from it to build the pending_deletion_attachments list. I only pull it out into the all_file_attachments variable so that I don't have to access data.all_file_attachments multiple times.

    2. I misread the code. I think I saw the file_attachments += and got mixed up.

  6. reviewboard/reviews/views/review_request_detail.py (Diff revision 1)
     
     
     
     
     
     

    This might actually be better as a list comprehension. If you go that route, the other comments here won't apply.

    But the nice thing there is, you don't have to store pending_deletion_attachments and then do the conditional below. You just concatenate above with data.all_file_attachments.

  7. Blank line between these.

  8. No parens on the conditional here.

  9. Can you add this in alphabetically? I know this dictionary's a bit of a mess, but it'd be a step toward the right direction.

  10. Missing the last : in :py:class:, and missing a trailing period.

  11. These should all be in /** form.

  12. I mentioned in another change that we should probably slugify the IDs, avoiding the temptation to use them as labels and to make them more useful in certain conditions. Just a note that they'd need to be updated here as well.

  13. This should be on the next line, since it's a multi-line comment.

  14. Summary should be on the next line.

  15. The callback should align with other parameters.

  16. I wanted to check a couple things with this signal. I note this is happening in-between removing and adding attachment information. I assume this order is important. Can we document that here, and what the responsibility is expected to be of the signal handler?

    Also, replaceAttachment is like an action-invoking signal, rather than a notifying signal, which is why I assume (at the point I'm at in the review) that this is an important part of the replacement step. If it's meant to be a notifying signal, we should probably call it attachmentReplaced.

  17. Comment contents should start on the next line.

  18. The ? and : should align with the value being checked.

    Actually, looks like we compute this in other places. Let's pull that result out into a variable and reuse it.

  19. A few notes:

    1. The <span> should be indented one space.
    2. We want this to be described as a button in the accessibility tree. The <a> will need to be <a role="button" href="#"> to make that happen, and the .fa should be <span class="..." aria-hidden="true"></span>.
  20. We can probably combine this into one statement, make sure the result is only computed and set once.

  21. Indentation issue with the comment.

  22. We access this.model three times. Let's pull it out into a local variable.

  23. This should be formatted in the usual way, with ? and : being aligned with the start of the condition.

  24. I don't think a Set buys us anything here. We know the entirety of the list of possible values, and it's a lot cheaper to check them one-by-one than to build a Set and check it each time this function is called.

  25. Since _ isn't free, we want to avoid using and then overriding it. How about:

    const deleteText = (draftStates.has(state)
                        ? _`Delete`
                        : _`Delete Draft`);
    
  26. We use this.model several times in this method, so let's pull it out into a local variable.

  27. You can use the set(key, value) form here and below, which is a bit cheaper when there's only one thing being changed.

  28. We use this.model several times in this function, so let's pull it out into a local variable.

  29. We use this.model several times in this method, so let's pull it out into a local variable.

  30. When using template literals, every single piece of whitespace across lines is preserved, newlines included. We should avoid that here.

    Also, nobody will see the log output unless they're us, so we should instead display an alert.

    (Ideally we'd have something better than alert(), but that's the best option we have at the moment.)

  31. We access this.#fileAttachmentThumbnailViews on every line, so let's pull it out into some views local variable.

  32. 
      
maubin
Review request changed

Change Summary:

  • Added more typing to various files.
  • Improved accessibility of all file actions.
  • Moved location of the state label to overlay the top right of thumbnails.

Commits:

Summary
-
Improve how we display and manage file attachment states.
+
Improve how we display and manage file attachment states.

Diff:

Revision 2 (+1590 -200)

Show changes

Added Files:

Checks run (1 failed, 1 succeeded)

flake8 failed.
JSHint passed.

flake8

maubin
chipx86
  1. 
      
  2. reviewboard/reviews/templatetags/reviewtags.py (Diff revisions 1 - 3)
     
     

    If we're not writing to this, let's use Mapping instead of Dict.

  3. reviewboard/reviews/templatetags/reviewtags.py (Diff revisions 1 - 3)
     
     

    Missing a type.

    1. This gets implicitely typed to list[Dict[str, Any]] because I added typing to FileAttachmentsField.get_attachment_js_model_attrs. Same with file_attachments_data above, both get implicitely typed.

      Image

    2. Even though it can be implicitly typed, I think we do gain something from being explicit here, and it's minimal to add it.

    3. Sounds good, I'll add it in before I land this.

  4. reviewboard/reviews/views/review_request_detail.py (Diff revisions 1 - 3)
     
     
     
     

    Our standard for list comprehensions are in the form of:

    [
        value
        for value in loop
        if conditions
    ]
    
  5. Is there any situation where a file can be removed and another added to this list, resulting in the same length but different contents?

    1. No, between calculating the length before and after, we only do a file_attachments.extend which would add 0 or more items to the list. There's no way that anything can be removed from the list.

  6. There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its".

  7. The ? false indicates we can just do this as a boolean condition.

    this.model.get('state') != this.states.PENDING_DELETION && options.canEdit
    
  8. 
      
maubin
Review request changed

Checks run (1 failed, 1 succeeded)

flake8 failed.
JSHint passed.

flake8

maubin
maubin
david
  1. Ship It!
  2. 
      
maubin
Review request changed

Status: Closed (submitted)

Change Summary:

Pushed to release-6.x (2dbe8d8)
Loading...