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 ID
Improve how we display and manage file attachment states.
78741ec211448d1c4514321e11a60b183c7d17c7

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. Show all issues

    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)
     
     
     
     
     
     
    Show all issues

    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)
     
     
     
     
     
     
     
     
     
    Show all issues

    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. Show all issues

    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. Show all issues

    Missing a type.

  7. reviewboard/reviews/views/review_request_detail.py (Diff revision 1)
     
     
     
     
     
     
    Show all issues

    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.

  8. Show all issues

    Blank line between these.

  9. Show all issues

    No parens on the conditional here.

  10. Show all issues

    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.

  11. Show all issues

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

  12. Show all issues

    These should all be in /** form.

  13. Show all issues

    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.

  14. Show all issues

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

  15. Show all issues

    Summary should be on the next line.

  16. Show all issues

    The callback should align with other parameters.

  17. Show all issues

    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.

  18. Show all issues

    Comment contents should start on the next line.

  19. Show all issues

    No blank line here.

  20. Show all issues

    Same here.

  21. Show all issues

    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.

  22. Show all issues

    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>.
  23. Show all issues

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

  24. Show all issues

    Indentation issue with the comment.

  25. Show all issues

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

  26. Show all issues

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

  27. Show all issues

    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.

  28. Show all issues

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

    const deleteText = (draftStates.has(state)
                        ? _`Delete`
                        : _`Delete Draft`);
    
  29. Show all issues

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

  30. Show all issues

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

  31. Show all issues

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

  32. Show all issues

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

  33. Show all issues

    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.)

  34. Show all issues

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

  35. 
      
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 ID
Improve how we display and manage file attachment states.
d0a08db33fc80bef5577eb1b55c4023936d3c58e
Improve how we display and manage file attachment states.
2a95eb18ac3ec439c22f593444674d5e574e34d7
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)
     
     
    Show all issues

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

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

    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)
     
     
     
     
    Show all issues

    Our standard for list comprehensions are in the form of:

    [
        value
        for value in loop
        if conditions
    ]
    
  5. Show all issues

    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. Show all issues

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

  7. Show all issues

    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
Commits:
Summary ID
Improve how we display and manage file attachment states.
9f7c8b67c9aed41c921ffd62232ad26c71fa0941
Improve how we display and manage file attachment states.
c881145888e509778251676d17b83821e1a72d33

Checks run (1 failed, 1 succeeded)

flake8 failed.
JSHint passed.

flake8

maubin
maubin
david
  1. Ship It!
  2. 
      
maubin
Review request changed
Status:
Completed
Change Summary:
Pushed to release-6.x (2dbe8d8)