Improve how we display and manage file attachment states.
Review Request #13240 — Created Aug. 31, 2023 and submitted
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 |
---|
Description | From | Last Updated |
---|---|---|
Should have mentioned here instead of the change introducing the widget but to avoid shifting the layout or reserving space, … |
|
|
If we're not writing to this, let's use Mapping instead of Dict. |
|
|
All new variables (and any being modified) should have a type when it can't be inferred (by being directly assigned … |
|
|
This can be done as one statement with a list comprehension, which should be a bit faster. In fact, if … |
|
|
Missing a type. |
|
|
We fetch this from the data and then modify it below, which probably isn't safe. |
|
|
Missing a type. |
|
|
This might actually be better as a list comprehension. If you go that route, the other comments here won't apply. … |
|
|
Blank line between these. |
|
|
No parens on the conditional here. |
|
|
Our standard for list comprehensions are in the form of: [ value for value in loop if conditions ] |
|
|
Is there any situation where a file can be removed and another added to this list, resulting in the same … |
|
|
Can you add this in alphabetically? I know this dictionary's a bit of a mess, but it'd be a step … |
|
|
Missing the last : in :py:class:, and missing a trailing period. |
|
|
These should all be in /** form. |
|
|
I mentioned in another change that we should probably slugify the IDs, avoiding the temptation to use them as labels … |
|
|
This should be on the next line, since it's a multi-line comment. |
|
|
Summary should be on the next line. |
|
|
The callback should align with other parameters. |
|
|
I wanted to check a couple things with this signal. I note this is happening in-between removing and adding attachment … |
|
|
Comment contents should start on the next line. |
|
|
No blank line here. |
|
|
Same here. |
|
|
There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its". |
|
|
The ? and : should align with the value being checked. Actually, looks like we compute this in other places. … |
|
|
A few notes: The <span> should be indented one space. We want this to be described as a button in … |
|
|
We can probably combine this into one statement, make sure the result is only computed and set once. |
|
|
The ? false indicates we can just do this as a boolean condition. this.model.get('state') != this.states.PENDING_DELETION && options.canEdit |
|
|
Indentation issue with the comment. |
|
|
We access this.model three times. Let's pull it out into a local variable. |
|
|
This should be formatted in the usual way, with ? and : being aligned with the start of the condition. |
|
|
I don't think a Set buys us anything here. We know the entirety of the list of possible values, and … |
|
|
Since _ isn't free, we want to avoid using and then overriding it. How about: const deleteText = (draftStates.has(state) ? … |
|
|
We use this.model several times in this method, so let's pull it out into a local variable. |
|
|
You can use the set(key, value) form here and below, which is a bit cheaper when there's only one thing … |
|
|
We use this.model several times in this function, so let's pull it out into a local variable. |
|
|
We use this.model several times in this method, so let's pull it out into a local variable. |
|
|
When using template literals, every single piece of whitespace across lines is preserved, newlines included. We should avoid that here. … |
|
|
We access this.#fileAttachmentThumbnailViews on every line, so let's pull it out into some views local variable. |
|
|
redefinition of unused 'FileAttachment' from line 17 Column: 5 Error code: F811 |
![]() |
|
'typing.Dict' imported but unused Column: 1 Error code: F401 |
![]() |
-
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).
-
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?
-
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.
-
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. -
reviewboard/reviews/views/review_request_detail.py (Diff revision 1) We fetch this from the data and then modify it below, which probably isn't safe.
-
-
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 withdata.all_file_attachments
. -
-
reviewboard/reviews/views/review_request_detail.py (Diff revision 1) No parens on the conditional here.
-
reviewboard/reviews/views/review_request_detail.py (Diff revision 1) 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.
-
reviewboard/static/rb/js/common/resources/models/fileAttachmentModel.ts (Diff revision 1) Missing the last
:
in:py:class:
, and missing a trailing period. -
reviewboard/static/rb/js/common/resources/models/fileAttachmentModel.ts (Diff revision 1) These should all be in
/**
form. -
reviewboard/static/rb/js/common/resources/models/fileAttachmentModel.ts (Diff revision 1) 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.
-
reviewboard/static/rb/js/common/resources/models/fileAttachmentModel.ts (Diff revision 1) This should be on the next line, since it's a multi-line comment.
-
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revision 1) Summary should be on the next line.
-
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revision 1) The callback should align with other parameters.
-
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revision 1) 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 itattachmentReplaced
. -
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revision 1) Comment contents should start on the next line.
-
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revision 1) No blank line here.
-
-
reviewboard/static/rb/js/reviews/models/reviewablePageModel.ts (Diff revision 1) 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.
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) A few notes:
- The
<span>
should be indented one space. - 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>
.
- The
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) We can probably combine this into one statement, make sure the result is only computed and set once.
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) Indentation issue with the comment.
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) We access
this.model
three times. Let's pull it out into a local variable. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) This should be formatted in the usual way, with
?
and:
being aligned with the start of the condition. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) 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 aSet
and check it each time this function is called. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) Since
_
isn't free, we want to avoid using and then overriding it. How about:const deleteText = (draftStates.has(state) ? _`Delete` : _`Delete Draft`);
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) We use
this.model
several times in this method, so let's pull it out into a local variable. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) You can use the
set(key, value)
form here and below, which is a bit cheaper when there's only one thing being changed. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) We use
this.model
several times in this function, so let's pull it out into a local variable. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) We use
this.model
several times in this method, so let's pull it out into a local variable. -
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revision 1) 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.) -
reviewboard/static/rb/js/reviews/views/reviewRequestEditorView.ts (Diff revision 1) We access
this.#fileAttachmentThumbnailViews
on every line, so let's pull it out into someviews
local variable.

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: |
|
||||||
---|---|---|---|---|---|---|---|
Diff: |
Revision 2 (+1590 -200) |
||||||
Added Files: |
Checks run (1 failed, 1 succeeded)
flake8
-
reviewboard/reviews/builtin_fields.py (Diff revision 2) redefinition of unused 'FileAttachment' from line 17 Column: 5 Error code: F811

Commits: |
|
||||||
---|---|---|---|---|---|---|---|
Diff: |
Revision 3 (+1584 -200) |
Checks run (2 succeeded)
-
-
reviewboard/reviews/templatetags/reviewtags.py (Diff revisions 1 - 3) If we're not writing to this, let's use
Mapping
instead ofDict
. -
-
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 ]
-
reviewboard/reviews/views/review_request_detail.py (Diff revisions 1 - 3) Is there any situation where a file can be removed and another added to this list, resulting in the same length but different contents?
-
reviewboard/static/rb/js/reviews/models/reviewRequestEditorModel.ts (Diff revisions 1 - 3) There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its".
-
reviewboard/static/rb/js/reviews/views/fileAttachmentThumbnailView.ts (Diff revisions 1 - 3) The
? false
indicates we can just do this as a boolean condition.this.model.get('state') != this.states.PENDING_DELETION && options.canEdit

Commits: |
|
||||||
---|---|---|---|---|---|---|---|
Diff: |
Revision 4 (+1584 -200) |
Checks run (1 failed, 1 succeeded)
flake8
-
reviewboard/reviews/templatetags/reviewtags.py (Diff revision 4) 'typing.Dict' imported but unused Column: 1 Error code: F401

Change Summary:
Removed an unused import.
Commits: |
|
||||||
---|---|---|---|---|---|---|---|
Diff: |
Revision 5 (+1584 -200) |
Checks run (2 succeeded)

Change Summary:
- Added a missing type.
Commits: |
|
||||||
---|---|---|---|---|---|---|---|
Diff: |
Revision 6 (+1586 -202) |