-
Notifications
You must be signed in to change notification settings - Fork 112
Image button #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Image button #167
Conversation
Reviewer's GuideAdds support for copying actual image data from the image block toolbar (rather than just the URL), including handling authenticated AppFlowy storage URLs, and cleans up unused copy-link behavior from the file block toolbar while introducing an end-to-end test for the image copy feature. Sequence diagram for copying an image from the image toolbarsequenceDiagram
actor User
participant ImageToolbar
participant ImageUtils as fetchImageBlob
participant AuthUtils
participant AppFlowyStorage
participant PublicImageHost
participant ClipboardAPI as navigator_clipboard
participant Notify
User ->> ImageToolbar: Click copy image button
ImageToolbar ->> ImageUtils: fetchImageBlob(imageUrl)
alt URL is AppFlowy storage
ImageUtils ->> AuthUtils: isAppFlowyFileStorageUrl(imageUrl)
AuthUtils -->> ImageUtils: true
ImageUtils ->> AuthUtils: getTokenParsed()
AuthUtils -->> ImageUtils: token or null
alt token is available
ImageUtils ->> AuthUtils: resolveImageUrl(imageUrl)
AuthUtils -->> ImageUtils: fullUrl
ImageUtils ->> AppFlowyStorage: fetch(fullUrl, Authorization Bearer access_token)
AppFlowyStorage -->> ImageUtils: Response(blob or error)
alt response ok
ImageUtils -->> ImageToolbar: Blob
else response not ok or error
ImageUtils -->> ImageToolbar: null
end
else token missing
ImageUtils -->> ImageToolbar: null
end
else URL is public
ImageUtils ->> AuthUtils: isAppFlowyFileStorageUrl(imageUrl)
AuthUtils -->> ImageUtils: false
ImageUtils ->> PublicImageHost: fetch(imageUrl)
PublicImageHost -->> ImageUtils: Response(blob or error)
alt response ok
ImageUtils -->> ImageToolbar: Blob
else response not ok or error
ImageUtils -->> ImageToolbar: null
end
end
alt Blob is available
ImageToolbar ->> ClipboardAPI: write([ClipboardItem({ blobType: blob })])
alt Clipboard write success
ClipboardAPI -->> ImageToolbar: success
ImageToolbar ->> Notify: success(document.plugins.image.copiedToPasteBoard)
else Clipboard write failure
ClipboardAPI -->> ImageToolbar: error
ImageToolbar ->> Notify: error(Failed to copy image)
end
else Blob is null
ImageToolbar -->> User: Copy silently fails (no notification)
end
Class diagram for updated image utilities and toolbarsclassDiagram
class ImageUtils {
+checkImage(url)
+fetchImageBlob(url) Blob
}
class ImageToolbar {
+node ImageBlockNode
+onOpenPreview()
+onCopyImage()
+onDelete()
}
class FileToolbar {
+node FileNode
+onDelete()
+onChangeName(event)
+onDownload()
}
class ImageBlockNode {
+data ImageBlockData
+blockId string
}
class FileNode {
+data FileBlockData
+blockId string
}
class ImageBlockData {
+url string
}
class FileBlockData {
+url string
+name string
}
class ClipboardAPI {
+write(items)
}
class AuthUtils {
+isAppFlowyFileStorageUrl(url) boolean
+getTokenParsed() Token
+resolveImageUrl(url) string
}
class Token {
+access_token string
}
ImageToolbar --> ImageBlockNode
ImageToolbar --> ImageUtils : uses
ImageToolbar --> ClipboardAPI : uses
ImageToolbar --> Notify : uses
FileToolbar --> FileNode
FileToolbar --> DownloadUtils : uses
ImageBlockNode --> ImageBlockData
FileNode --> FileBlockData
ImageUtils --> AuthUtils
AuthUtils --> Token
class Notify {
+success(message)
+error(message)
}
class DownloadUtils {
+downloadFile(url name)
}
Flow diagram for fetchImageBlob image retrieval logicflowchart TD
A_start[Start fetchImageBlob url]
B_checkStorage{isAppFlowyFileStorageUrl url}
C_getToken[getTokenParsed]
D_tokenNull{token is null}
E_resolveUrl[resolveImageUrl url]
F_fetchStorage[fetch fullUrl with Authorization Bearer access_token]
G_fetchPublic[fetch url]
H_respOkStorage{response.ok}
I_respOkPublic{response.ok}
J_returnBlobStorage[return response.blob]
K_returnBlobPublic[return response.blob]
L_returnNullError[return null]
M_returnNullEnd[return null]
A_start --> B_checkStorage
B_checkStorage -- yes --> C_getToken
C_getToken --> D_tokenNull
D_tokenNull -- yes --> M_returnNullEnd
D_tokenNull -- no --> E_resolveUrl
E_resolveUrl --> F_fetchStorage
F_fetchStorage --> H_respOkStorage
H_respOkStorage -- yes --> J_returnBlobStorage
H_respOkStorage -- no --> L_returnNullError
B_checkStorage -- no --> G_fetchPublic
G_fetchPublic --> I_respOkPublic
I_respOkPublic -- yes --> K_returnBlobPublic
I_respOkPublic -- no --> L_returnNullError
J_returnBlobStorage -->|success| End_success_storage((End))
K_returnBlobPublic -->|success| End_success_public((End))
L_returnNullError --> M_returnNullEnd
M_returnNullEnd --> End_null((End null))
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey there - I've reviewed your changes - here's some feedback:
- In fetchImageBlob, the authenticated and unauthenticated branches largely duplicate the fetch logic and swallow errors; consider extracting a shared helper/returning more detailed error information (or at least logging) to make failures easier to diagnose.
- The new onCopyImage handler assumes navigator.clipboard.write and ClipboardItem are available; add feature detection and a graceful fallback or user message for browsers that don't support the async Clipboard API.
- The error notification message 'Failed to copy image' is hard-coded; switch this to use the existing i18n translation mechanism to keep messaging consistent with the rest of the UI.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In fetchImageBlob, the authenticated and unauthenticated branches largely duplicate the fetch logic and swallow errors; consider extracting a shared helper/returning more detailed error information (or at least logging) to make failures easier to diagnose.
- The new onCopyImage handler assumes navigator.clipboard.write and ClipboardItem are available; add feature detection and a graceful fallback or user message for browsers that don't support the async Clipboard API.
- The error notification message 'Failed to copy image' is hard-coded; switch this to use the existing i18n translation mechanism to keep messaging consistent with the rest of the UI.
## Individual Comments
### Comment 1
<location> `src/components/editor/components/blocks/image/ImageToolbar.tsx:30-39` </location>
<code_context>
- const onCopy = async () => {
- await copyTextToClipboard(node.data.url || '');
- notify.success(t('document.plugins.image.copiedToPasteBoard'));
+ const onCopyImage = async () => {
+ const blob = await fetchImageBlob(node.data.url || '');
+
+ if (blob) {
+ try {
+ await navigator.clipboard.write([
+ new ClipboardItem({
+ [blob.type]: blob,
+ }),
+ ]);
+ notify.success(t('document.plugins.image.copiedToPasteBoard'));
+ } catch (error) {
+ notify.error('Failed to copy image');
+ }
+ }
};
</code_context>
<issue_to_address>
**issue:** When `fetchImageBlob` returns null, the handler fails silently without user feedback.
In that case, the function just returns and the button appears unresponsive to the user. Consider showing an error toast when `blob` is null so users know the copy failed (and ideally why).
</issue_to_address>
### Comment 2
<location> `src/components/editor/components/blocks/image/ImageToolbar.tsx:34-42` </location>
<code_context>
+ const clipboardMock = {
+ write: cy.stub().as('clipboardWrite')
+ };
+ try {
+ // @ts-ignore
+ win.navigator.clipboard = clipboardMock;
</code_context>
<issue_to_address>
**nitpick:** The error message for failed image copy is hardcoded and not localized.
The success path already uses `document.plugins.image.copiedToPasteBoard`, but the error path still uses a hardcoded English string. Please also source the error text from your i18n system (e.g., `t('document.plugins.image.copyFailed')`) to keep behavior consistent and localizable.
</issue_to_address>
### Comment 3
<location> `src/utils/image.ts:109` </location>
<code_context>
});
+};
+
+export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
+ if (isAppFlowyFileStorageUrl(url)) {
+ const token = getTokenParsed();
</code_context>
<issue_to_address>
**issue (complexity):** Consider refactoring fetchImageBlob so URL/header resolution is separated from the fetch call and the network logic is shared in a single try/catch block to reduce duplication.
You can simplify `fetchImageBlob` by separating URL/header resolution from the network call and using a single `try/catch`. This removes duplicated `fetch`/`response.ok`/`blob()` logic while preserving behavior.
For example:
```ts
export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
const isStorageUrl = isAppFlowyFileStorageUrl(url);
// Resolve URL and headers up front
if (isStorageUrl) {
const token = getTokenParsed();
if (!token) return null;
const fullUrl = resolveImageUrl(url);
const headers = { Authorization: `Bearer ${token.access_token}` };
try {
const response = await fetch(fullUrl, { headers });
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
}
try {
const response = await fetch(url);
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
};
```
You can go a step further and share the `fetch` logic completely:
```ts
export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
const isStorageUrl = isAppFlowyFileStorageUrl(url);
let finalUrl = url;
let headers: HeadersInit | undefined;
if (isStorageUrl) {
const token = getTokenParsed();
if (!token) return null;
finalUrl = resolveImageUrl(url);
headers = { Authorization: `Bearer ${token.access_token}` };
}
try {
const response = await fetch(finalUrl, headers ? { headers } : undefined);
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
};
```
This keeps:
- A single exit path for the network call.
- All branching limited to computing `finalUrl` and `headers`.
- The early return on missing token.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const onCopyImage = async () => { | ||
| const blob = await fetchImageBlob(node.data.url || ''); | ||
|
|
||
| if (blob) { | ||
| try { | ||
| await navigator.clipboard.write([ | ||
| new ClipboardItem({ | ||
| [blob.type]: blob, | ||
| }), | ||
| ]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: When fetchImageBlob returns null, the handler fails silently without user feedback.
In that case, the function just returns and the button appears unresponsive to the user. Consider showing an error toast when blob is null so users know the copy failed (and ideally why).
| try { | ||
| await navigator.clipboard.write([ | ||
| new ClipboardItem({ | ||
| [blob.type]: blob, | ||
| }), | ||
| ]); | ||
| notify.success(t('document.plugins.image.copiedToPasteBoard')); | ||
| } catch (error) { | ||
| notify.error('Failed to copy image'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpick: The error message for failed image copy is hardcoded and not localized.
The success path already uses document.plugins.image.copiedToPasteBoard, but the error path still uses a hardcoded English string. Please also source the error text from your i18n system (e.g., t('document.plugins.image.copyFailed')) to keep behavior consistent and localizable.
| }); | ||
| }; | ||
|
|
||
| export const fetchImageBlob = async (url: string): Promise<Blob | null> => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue (complexity): Consider refactoring fetchImageBlob so URL/header resolution is separated from the fetch call and the network logic is shared in a single try/catch block to reduce duplication.
You can simplify fetchImageBlob by separating URL/header resolution from the network call and using a single try/catch. This removes duplicated fetch/response.ok/blob() logic while preserving behavior.
For example:
export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
const isStorageUrl = isAppFlowyFileStorageUrl(url);
// Resolve URL and headers up front
if (isStorageUrl) {
const token = getTokenParsed();
if (!token) return null;
const fullUrl = resolveImageUrl(url);
const headers = { Authorization: `Bearer ${token.access_token}` };
try {
const response = await fetch(fullUrl, { headers });
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
}
try {
const response = await fetch(url);
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
};You can go a step further and share the fetch logic completely:
export const fetchImageBlob = async (url: string): Promise<Blob | null> => {
const isStorageUrl = isAppFlowyFileStorageUrl(url);
let finalUrl = url;
let headers: HeadersInit | undefined;
if (isStorageUrl) {
const token = getTokenParsed();
if (!token) return null;
finalUrl = resolveImageUrl(url);
headers = { Authorization: `Bearer ${token.access_token}` };
}
try {
const response = await fetch(finalUrl, headers ? { headers } : undefined);
if (!response.ok) return null;
return await response.blob();
} catch {
return null;
}
};This keeps:
- A single exit path for the network call.
- All branching limited to computing
finalUrlandheaders. - The early return on missing token.
* refactor: remove 'Copy to original' button from Image and File toolbars * feat: add 'Copy image' button to ImageToolbar * chore: add test
Description
Checklist
General
Testing
Feature-Specific
Summary by Sourcery
Add support for copying image content to the clipboard from the image block toolbar while simplifying the file block toolbar actions.
New Features:
Enhancements:
Tests: