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 | ID |
---|---|
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, … |
chipx86 | |
If we're not writing to this, let's use Mapping instead of Dict. |
chipx86 | |
All new variables (and any being modified) should have a type when it can't be inferred (by being directly assigned … |
chipx86 | |
This can be done as one statement with a list comprehension, which should be a bit faster. In fact, if … |
chipx86 | |
Missing a type. |
chipx86 | |
We fetch this from the data and then modify it below, which probably isn't safe. |
chipx86 | |
Missing a type. |
chipx86 | |
This might actually be better as a list comprehension. If you go that route, the other comments here won't apply. … |
chipx86 | |
Blank line between these. |
chipx86 | |
No parens on the conditional here. |
chipx86 | |
Our standard for list comprehensions are in the form of: [ value for value in loop if conditions ] |
chipx86 | |
Is there any situation where a file can be removed and another added to this list, resulting in the same … |
chipx86 | |
Can you add this in alphabetically? I know this dictionary's a bit of a mess, but it'd be a step … |
chipx86 | |
Missing the last : in :py:class:, and missing a trailing period. |
chipx86 | |
These should all be in /** form. |
chipx86 | |
I mentioned in another change that we should probably slugify the IDs, avoiding the temptation to use them as labels … |
chipx86 | |
This should be on the next line, since it's a multi-line comment. |
chipx86 | |
Summary should be on the next line. |
chipx86 | |
The callback should align with other parameters. |
chipx86 | |
I wanted to check a couple things with this signal. I note this is happening in-between removing and adding attachment … |
chipx86 | |
Comment contents should start on the next line. |
chipx86 | |
No blank line here. |
chipx86 | |
Same here. |
chipx86 | |
There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its". |
chipx86 | |
The ? and : should align with the value being checked. Actually, looks like we compute this in other places. … |
chipx86 | |
A few notes: The <span> should be indented one space. We want this to be described as a button in … |
chipx86 | |
We can probably combine this into one statement, make sure the result is only computed and set once. |
chipx86 | |
The ? false indicates we can just do this as a boolean condition. this.model.get('state') != this.states.PENDING_DELETION && options.canEdit |
chipx86 | |
Indentation issue with the comment. |
chipx86 | |
We access this.model three times. Let's pull it out into a local variable. |
chipx86 | |
This should be formatted in the usual way, with ? and : being aligned with the start of the condition. |
chipx86 | |
I don't think a Set buys us anything here. We know the entirety of the list of possible values, and … |
chipx86 | |
Since _ isn't free, we want to avoid using and then overriding it. How about: const deleteText = (draftStates.has(state) ? … |
chipx86 | |
We use this.model several times in this method, so let's pull it out into a local variable. |
chipx86 | |
You can use the set(key, value) form here and below, which is a bit cheaper when there's only one thing … |
chipx86 | |
We use this.model several times in this function, so let's pull it out into a local variable. |
chipx86 | |
We use this.model several times in this method, so let's pull it out into a local variable. |
chipx86 | |
When using template literals, every single piece of whitespace across lines is preserved, newlines included. We should avoid that here. … |
chipx86 | |
We access this.#fileAttachmentThumbnailViews on every line, so let's pull it out into some views local variable. |
chipx86 | |
redefinition of unused 'FileAttachment' from line 17 Column: 5 Error code: F811 |
reviewbot | |
'typing.Dict' imported but unused Column: 1 Error code: F401 |
reviewbot |
-
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?
-
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.
-
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. -
-
-
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
. -
-
-
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.
-
-
-
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.
-
-
-
-
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
. -
-
-
-
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.
-
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
-
We can probably combine this into one statement, make sure the result is only computed and set once.
-
-
-
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 it's a lot cheaper to check them one-by-one than to build aSet
and check it each time this function is called. -
Since
_
isn't free, we want to avoid using and then overriding it. How about:const deleteText = (draftStates.has(state) ? _`Delete` : _`Delete Draft`);
-
-
You can use the
set(key, value)
form here and below, which is a bit cheaper when there's only one thing being changed. -
-
-
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.) -
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:
-
Summary ID d0a08db33fc80bef5577eb1b55c4023936d3c58e 2a95eb18ac3ec439c22f593444674d5e574e34d7 - Diff:
-
Revision 2 (+1590 -200)
- Added Files:
- Commits:
-
Summary ID 2a95eb18ac3ec439c22f593444674d5e574e34d7 9f7c8b67c9aed41c921ffd62232ad26c71fa0941 - Diff:
-
Revision 3 (+1584 -200)
Checks run (2 succeeded)
-
-
-
-
-
Is there any situation where a file can be removed and another added to this list, resulting in the same length but different contents?
-
There's an old typo here, if you don't mind changing it while modifying this line. "it's" should be "its".
-
The
? false
indicates we can just do this as a boolean condition.this.model.get('state') != this.states.PENDING_DELETION && options.canEdit
- Commits:
-
Summary ID 9f7c8b67c9aed41c921ffd62232ad26c71fa0941 c881145888e509778251676d17b83821e1a72d33 - Diff:
-
Revision 4 (+1584 -200)
- Change Summary:
-
Removed an unused import.
- Commits:
-
Summary ID c881145888e509778251676d17b83821e1a72d33 ae09a3300d2bb3b0b45fd622fe67e96a65e7b84c - Diff:
-
Revision 5 (+1584 -200)
Checks run (2 succeeded)
- Change Summary:
-
- Added a missing type.
- Commits:
-
Summary ID ae09a3300d2bb3b0b45fd622fe67e96a65e7b84c 78741ec211448d1c4514321e11a60b183c7d17c7 - Diff:
-
Revision 6 (+1586 -202)