feat: media picker and image removal
This commit is contained in:
1 parent
008b0dfbb9
commit
b3793d5dbb
54 files changed
+3769
-1412
No files matched your search
@@ -1,148 +1,90 @@
|
||||
Feature: Removing an inline image from the editor
|
||||
Feature: Removing an inline image from the article
|
||||
|
||||
As a verified RenewCMS owner writing an article
|
||||
I want to remove an image I've already inserted into the article body,
|
||||
either through an explicit delete button or by pressing Backspace/Delete
|
||||
on the selected image, directly from the editor
|
||||
So that a mistaken or unwanted image doesn't have to stay in the article
|
||||
forever, and removing it behaves like every other destructive action in
|
||||
RenewCMS: it asks for confirmation first, however the user starts it
|
||||
I want to take an image out of the article body, from a button or with Backspace or Delete
|
||||
So that an unwanted image leaves the article without deleting the file from my library
|
||||
|
||||
Background:
|
||||
Given docs/design/ has no board or state for removing an inline image;
|
||||
this feature was added at the user's explicit request ahead of design
|
||||
coverage, reusing the editor's existing visual language rather than
|
||||
inventing new components: the trash icon and error color already used
|
||||
by the article "Delete" button, and the ConfirmDialog component
|
||||
already used by "Delete this article?" (docs/design/screens/editor.md's
|
||||
"Delete: confirm" state)
|
||||
And this slice changes ArticleEditor's exposed insertImage(src, alt) to
|
||||
insertImage(src, alt, imageId), stamping the inserted <img> node with
|
||||
the image's numeric id as a data-image-id attribute, persisted in the
|
||||
saved article body HTML; EditorView passes the id it now receives from
|
||||
a successful upload (features/remove_article_image.feature's change to
|
||||
POST .../image's 201 body) through to insertImage
|
||||
And an image already present in an article's body from before this slice
|
||||
has no data-image-id attribute and so shows no delete affordance and
|
||||
is not intercepted by Backspace/Delete, until it is removed and
|
||||
re-added
|
||||
And selecting an image node that does carry a data-image-id attribute
|
||||
shows a small delete button anchored to the image's top-right corner,
|
||||
icon i-lucide-trash-2, color error, matching the article Delete
|
||||
button's icon and color; deselecting the image (clicking elsewhere)
|
||||
Given this slice is SPA only for this file; the Go alias deletion is in
|
||||
features/remove_legacy_article_image_alias.feature
|
||||
And docs/design/screens/media-picker.md now covers this flow ("Remove image: confirm" and
|
||||
"Removed"); it supersedes the earlier version of this feature, which had no design
|
||||
coverage and used a red confirm that deleted through DELETE /v1/article/{id}/image/{imageId}
|
||||
And removing an image is a body edit only: no API call of any kind is sent, before, during
|
||||
or after the confirm, and the library and every other article are untouched
|
||||
And ArticleEditor keeps its exposed insertImage(src, alt, imageId) and removeImage(imageId),
|
||||
its "request-remove-image" event with the image id, and its Backspace and Delete handling,
|
||||
which still intercepts the keys before Tiptap deletes the node
|
||||
And an image already in a body from before data-image-id existed has no id, shows no
|
||||
"Remove image" button and is not intercepted by Backspace or Delete (unchanged)
|
||||
And selecting an image node that carries data-image-id shows a button "Remove image" at the
|
||||
image's top-right corner (8 px from the top and right): trash-2 icon at 14 px, neutral,
|
||||
28 px high, 12 px medium text, a 1 px accented border and the page background; deselecting
|
||||
hides it
|
||||
And there are two equivalent triggers for the same removal flow:
|
||||
activating that delete button, and pressing Backspace or Delete while
|
||||
the image node is selected (a Tiptap NodeSelection on the image).
|
||||
Tiptap's native node deletion on Backspace/Delete is intercepted
|
||||
before it reaches the document (e.g. via a keyboard-shortcut/keydown
|
||||
handler registered on the Image node or at the editor level, checked
|
||||
first while an image node is selected) so the key press never removes
|
||||
the node directly; instead it triggers exactly the same flow as the
|
||||
button
|
||||
And either trigger emits ArticleEditor's new "request-remove-image"
|
||||
event with the node's numeric image id; ArticleEditor does not call
|
||||
the API or show any dialog itself, mirroring how "request-image"
|
||||
already only opens ImageUploader from EditorView
|
||||
And EditorView reacts to "request-remove-image" by opening the existing
|
||||
ConfirmDialog component with title "Remove this image?", description
|
||||
"This image will be removed from the article. This can't be undone.",
|
||||
confirmLabel "Remove image", cancelLabel "Cancel", pendingLabel
|
||||
"Removing…", tone "danger" — the same component and tone already used
|
||||
for "Delete this article?"
|
||||
And ConfirmDialog's action calls DELETE /v1/article/{articleId}/image/
|
||||
{imageId} through useApi (not the raw XMLHttpRequest used for upload,
|
||||
since there is no progress to observe)
|
||||
And on success (204) the action removes the image node from
|
||||
ArticleEditor's content via a new exposed removeImage(imageId) method,
|
||||
which moves useArticleDraft's state to "dirty" the same way any other
|
||||
body edit does; ConfirmDialog then closes and a toast appears: color
|
||||
success, icon i-lucide-circle-check, title "Image removed",
|
||||
description "Save to keep this change."
|
||||
And on any failure (404 for either the article or the image, 500, or a
|
||||
network failure) the action throws, so ConfirmDialog shows its own
|
||||
inline error "Couldn't remove the image, so it's still there. Try
|
||||
again in a moment." and stays open; the image node is not removed and
|
||||
the draft's dirty state is unchanged
|
||||
And a 401 is handled entirely by useApi's global handling
|
||||
(features/api_client_global_error_handling.feature) and is out of
|
||||
scope for new scenarios here
|
||||
And once the confirm dialog is open, its cancel/confirm/failure
|
||||
behaviour (covered below) is identical regardless of which trigger
|
||||
opened it, so those scenarios are written once, against the button
|
||||
trigger, and are not duplicated per trigger
|
||||
And EditorView reacts to "request-remove-image" by opening ConfirmDialog with tone "neutral",
|
||||
title "Remove this image from the article?", description "It's removed from this article
|
||||
only. The file stays in your library and in any other article that uses it.", confirmLabel
|
||||
"Remove image" and cancelLabel "Cancel"; the confirm button is primary, not red
|
||||
And the body of the confirm is a bordered row with a 56 × 42 px thumbnail of the image, its
|
||||
filename, and "Stays in the library · {size}", for example "north-wall.jpg" and "Stays in
|
||||
the library · 3.9 MB"
|
||||
And the filename and size come from the images the SPA has seen this session, kept by image
|
||||
id: those inserted from either tab and those loaded in the Library list
|
||||
And confirming runs removeImage(imageId), moves useArticleDraft to "dirty", closes the
|
||||
dialog and shows the toast; the action cannot fail, so there is no inline error
|
||||
And a thumbnail uses the image's src from the node
|
||||
|
||||
Scenario: Selecting an inserted image reveals its delete button
|
||||
Given the user is on /article/12/edit with an image in the body carrying
|
||||
data-image-id "10"
|
||||
Scenario: Selecting an inserted image reveals "Remove image"
|
||||
Given the user is on /article/12/edit with an image in the body carrying data-image-id "1"
|
||||
When the user clicks that image
|
||||
Then a delete button (trash icon, error color) appears at the image's
|
||||
top-right corner
|
||||
Then a button "Remove image" with a trash-2 icon shows at the image's top-right corner
|
||||
When the user clicks elsewhere in the editor
|
||||
Then the delete button is no longer shown
|
||||
Then the button is no longer shown
|
||||
|
||||
Scenario Outline: Either trigger opens the confirm dialog and sends no request yet, without letting Tiptap delete the node itself
|
||||
Given the image with data-image-id "10" is selected on /article/12/edit
|
||||
Scenario: An image without data-image-id has no button
|
||||
Given the body has an image with no data-image-id
|
||||
When the user clicks it
|
||||
Then no "Remove image" button shows and Backspace does nothing special to it
|
||||
|
||||
Scenario Outline: Either trigger opens the neutral confirm and sends nothing
|
||||
Given the image with data-image-id "1" ("north-wall.jpg", 3.9 MB, inserted this session) is selected on /article/12/edit
|
||||
When the user <triggers-removal>
|
||||
Then a dialog titled "Remove this image?" opens, with description "This
|
||||
image will be removed from the article. This can't be undone.",
|
||||
cancel "Cancel" and confirm "Remove image"
|
||||
And no request has been sent to DELETE /v1/article/12/image/10
|
||||
And the image node is still present in the editor's content (Tiptap's
|
||||
native node deletion did not run)
|
||||
Then a dialog titled "Remove this image from the article?" opens, with the description "It's removed from this article only. The file stays in your library and in any other article that uses it."
|
||||
And its body shows the thumbnail, "north-wall.jpg" and "Stays in the library · 3.9 MB"
|
||||
And its buttons read "Cancel" and a primary "Remove image"
|
||||
And no request has been sent
|
||||
And the image node is still in the editor's content (Tiptap's native deletion did not run)
|
||||
|
||||
Examples:
|
||||
| triggers-removal |
|
||||
| activates the image's delete button |
|
||||
| presses Backspace with the image node selected |
|
||||
| triggers-removal |
|
||||
| activates the "Remove image" button |
|
||||
| presses Backspace with the image node selected |
|
||||
| presses Delete with the image node selected |
|
||||
|
||||
Scenario: Cancelling the confirm dialog changes nothing
|
||||
Given the confirm dialog is open for image id 10 on /article/12/edit
|
||||
Scenario: The confirm for an image the session has not seen
|
||||
Given the image with data-image-id "7" was in the saved body and is not in the session's record
|
||||
When the user activates "Remove image"
|
||||
Then the body row shows the thumbnail and "Stays in the library" with no filename and no size
|
||||
|
||||
Scenario: Cancelling changes nothing
|
||||
Given the confirm is open for image id 1
|
||||
When the user activates "Cancel"
|
||||
Then the dialog closes
|
||||
And no request has been sent to DELETE /v1/article/12/image/10
|
||||
And the image is still present in the editor's content
|
||||
And the draft's dirty state is unchanged
|
||||
Then the dialog closes, no request has been sent, the image is still in the content, and the draft's dirty state is unchanged
|
||||
|
||||
Scenario: Confirming removes the image, marks the draft dirty, and shows a success toast
|
||||
Given the confirm dialog is open for image id 10 on /article/12/edit,
|
||||
the draft in the Saved state
|
||||
Scenario: Confirming removes the image from the body only
|
||||
Given the confirm is open for image id 1 on /article/12/edit, the draft in the Saved state
|
||||
When the user activates "Remove image"
|
||||
Then DELETE /v1/article/12/image/10 is sent
|
||||
When that request resolves 204
|
||||
Then the dialog closes
|
||||
And the image is removed from the editor's content
|
||||
And SaveStatus reads "Unsaved changes" and the Save button becomes
|
||||
enabled
|
||||
And a toast appears: color success, icon i-lucide-circle-check, title
|
||||
"Image removed", description "Save to keep this change."
|
||||
Then the dialog closes and the image leaves the editor's content
|
||||
And SaveStatus reads "Unsaved changes" and Save is enabled
|
||||
And a toast appears: color success, icon i-lucide-circle-check, title "Image removed", description "It's still in your library."
|
||||
And no request of any kind has been sent
|
||||
|
||||
Scenario Outline: A failed removal keeps the image and shows the dialog's inline error
|
||||
Given the confirm dialog is open for image id 10 on /article/12/edit
|
||||
Scenario: A removal started by Backspace behaves like one started by the button
|
||||
Given the confirm is open for image id 1, opened by Backspace, the draft in the Saved state
|
||||
When the user activates "Remove image"
|
||||
And DELETE /v1/article/12/image/10 <failure>
|
||||
Then the dialog stays open, showing the inline error "Couldn't remove
|
||||
the image, so it's still there. Try again in a moment."
|
||||
And the image is still present in the editor's content
|
||||
And the draft's dirty state is unchanged
|
||||
Then the outcome is the same as above
|
||||
|
||||
Examples:
|
||||
| failure |
|
||||
| resolves with a 404 response |
|
||||
| resolves with a 500 response |
|
||||
| never reaches the server |
|
||||
|
||||
Scenario: Confirming a removal started by Backspace/Delete behaves exactly like confirming one started by the button
|
||||
Given the confirm dialog is open for image id 10 on /article/12/edit,
|
||||
opened by pressing Backspace with the image node selected, the draft
|
||||
in the Saved state
|
||||
When the user activates "Remove image"
|
||||
Then DELETE /v1/article/12/image/10 is sent
|
||||
When that request resolves 204
|
||||
Then the dialog closes
|
||||
And the image is removed from the editor's content
|
||||
And SaveStatus reads "Unsaved changes" and the Save button becomes
|
||||
enabled
|
||||
And a toast appears: color success, icon i-lucide-circle-check, title
|
||||
"Image removed", description "Save to keep this change."
|
||||
Scenario: The removed image can be picked again
|
||||
Given image id 1 was removed from the body this session
|
||||
When the user opens the Library tab
|
||||
Then "north-wall.jpg" no longer carries "In this article"
|
||||
Reference in new issue
Block a user