-
-
Notifications
You must be signed in to change notification settings - Fork 69
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
Added image upload handling to CTA Card #1421
Changes from all commits
58a0371
35722e7
d835bd7
4a05742
5c18f35
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -1,6 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||||||||
import CardContext from '../context/CardContext'; | ||||||||||||||||||||||||||||||||||||||||||||
import KoenigComposerContext from '../context/KoenigComposerContext.jsx'; | ||||||||||||||||||||||||||||||||||||||||||||
import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||
import React, {useRef} from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||
import {$getNodeByKey} from 'lexical'; | ||||||||||||||||||||||||||||||||||||||||||||
import {ActionToolbar} from '../components/ui/ActionToolbar.jsx'; | ||||||||||||||||||||||||||||||||||||||||||||
import {CtaCard} from '../components/ui/cards/CtaCard'; | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -26,14 +26,18 @@ export const CallToActionNodeComponent = ({ | |||||||||||||||||||||||||||||||||||||||||||
}) => { | ||||||||||||||||||||||||||||||||||||||||||||
const [editor] = useLexicalComposerContext(); | ||||||||||||||||||||||||||||||||||||||||||||
const {isEditing, isSelected, setEditing} = React.useContext(CardContext); | ||||||||||||||||||||||||||||||||||||||||||||
const {cardConfig} = React.useContext(KoenigComposerContext); | ||||||||||||||||||||||||||||||||||||||||||||
const {fileUploader, cardConfig} = React.useContext(KoenigComposerContext); | ||||||||||||||||||||||||||||||||||||||||||||
const [showSnippetToolbar, setShowSnippetToolbar] = React.useState(false); | ||||||||||||||||||||||||||||||||||||||||||||
const handleToolbarEdit = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||
event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||
event.stopPropagation(); | ||||||||||||||||||||||||||||||||||||||||||||
setEditing(true); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const fileInputRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const imageUploader = fileUploader.useFileUpload('image'); | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const toggleShowButton = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||
editor.update(() => { | ||||||||||||||||||||||||||||||||||||||||||||
const node = $getNodeByKey(nodeKey); | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -77,6 +81,27 @@ export const CallToActionNodeComponent = ({ | |||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const handleImageChange = async (files) => { | ||||||||||||||||||||||||||||||||||||||||||||
const result = await imageUploader.upload(files); | ||||||||||||||||||||||||||||||||||||||||||||
// reset original src so it can be replaced with preview and upload progress | ||||||||||||||||||||||||||||||||||||||||||||
editor.update(() => { | ||||||||||||||||||||||||||||||||||||||||||||
const node = $getNodeByKey(nodeKey); | ||||||||||||||||||||||||||||||||||||||||||||
node.imageUrl = result?.[0].url; | ||||||||||||||||||||||||||||||||||||||||||||
node.hasImage = true; | ||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const onFileChange = async (e) => { | ||||||||||||||||||||||||||||||||||||||||||||
handleImageChange(e.target.files); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+94
to
+96
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add file validation before upload. The file input handler should validate the files before uploading. const onFileChange = async (e) => {
+ const files = e.target.files;
+ if (!files || files.length === 0) {
+ return;
+ }
+
+ const file = files[0];
+ const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
+ if (!validTypes.includes(file.type)) {
+ editor.update(() => {
+ const node = $getNodeByKey(nodeKey);
+ node.uploadError = 'Invalid file type. Please upload an image.';
+ });
+ return;
+ }
+
handleImageChange(e.target.files);
}; 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
const onRemoveMedia = () => { | ||||||||||||||||||||||||||||||||||||||||||||
editor.update(() => { | ||||||||||||||||||||||||||||||||||||||||||||
const node = $getNodeByKey(nodeKey); | ||||||||||||||||||||||||||||||||||||||||||||
node.imageUrl = ''; | ||||||||||||||||||||||||||||||||||||||||||||
node.hasImage = false; | ||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||
const handleUpdatingLayout = (val) => { | ||||||||||||||||||||||||||||||||||||||||||||
editor.update(() => { | ||||||||||||||||||||||||||||||||||||||||||||
const node = $getNodeByKey(nodeKey); | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -99,17 +124,21 @@ export const CallToActionNodeComponent = ({ | |||||||||||||||||||||||||||||||||||||||||||
hasSponsorLabel={hasSponsorLabel} | ||||||||||||||||||||||||||||||||||||||||||||
htmlEditor={htmlEditor} | ||||||||||||||||||||||||||||||||||||||||||||
imageSrc={imageUrl} | ||||||||||||||||||||||||||||||||||||||||||||
imageUploader={imageUploader} | ||||||||||||||||||||||||||||||||||||||||||||
isEditing={isEditing} | ||||||||||||||||||||||||||||||||||||||||||||
isSelected={isSelected} | ||||||||||||||||||||||||||||||||||||||||||||
layout={layout} | ||||||||||||||||||||||||||||||||||||||||||||
setEditing={setEditing} | ||||||||||||||||||||||||||||||||||||||||||||
setFileInputRef={ref => fileInputRef.current = ref} | ||||||||||||||||||||||||||||||||||||||||||||
showButton={showButton} | ||||||||||||||||||||||||||||||||||||||||||||
text={textValue} | ||||||||||||||||||||||||||||||||||||||||||||
updateButtonText={handleButtonTextChange} | ||||||||||||||||||||||||||||||||||||||||||||
updateButtonUrl={handleButtonUrlChange} | ||||||||||||||||||||||||||||||||||||||||||||
updateHasSponsorLabel={handleHasSponsorLabelChange} | ||||||||||||||||||||||||||||||||||||||||||||
updateLayout={handleUpdatingLayout} | ||||||||||||||||||||||||||||||||||||||||||||
updateShowButton={toggleShowButton} | ||||||||||||||||||||||||||||||||||||||||||||
onFileChange={onFileChange} | ||||||||||||||||||||||||||||||||||||||||||||
onRemoveMedia={onRemoveMedia} | ||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
<ActionToolbar | ||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,9 @@ | ||
import path from 'path'; | ||
import {assertHTML, focusEditor, html, initialize, insertCard} from '../../utils/e2e'; | ||
import {expect, test} from '@playwright/test'; | ||
import {fileURLToPath} from 'url'; | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = path.dirname(__filename); | ||
|
||
test.describe('Call To Action Card', async () => { | ||
let page; | ||
|
@@ -206,6 +210,26 @@ test.describe('Call To Action Card', async () => { | |
} | ||
}); | ||
|
||
test('can add and remove CTA Card image', async function () { | ||
const filePath = path.relative(process.cwd(), __dirname + `/../fixtures/large-image.jpeg`); | ||
|
||
await focusEditor(page); | ||
await insertCard(page, {cardName: 'call-to-action'}); | ||
|
||
const fileChooserPromise = page.waitForEvent('filechooser'); | ||
|
||
await page.click('[data-testid="media-upload-placeholder"]'); | ||
|
||
const fileChooser = await fileChooserPromise; | ||
await fileChooser.setFiles([filePath]); | ||
|
||
const imgLocator = page.locator('[data-kg-card="call-to-action"] img[src^="blob:"]'); | ||
const imgElement = await imgLocator.first(); | ||
await expect(imgElement).toHaveAttribute('src', /blob:/); | ||
await page.click('[data-testid="media-upload-remove"]'); | ||
await expect(imgLocator).not.toBeVisible(); | ||
}); | ||
Comment on lines
+213
to
+231
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add test cases for error scenarios. The image upload tests should cover error scenarios such as:
Here's an example test case to add: test('shows error for invalid file type', async function () {
const filePath = path.relative(process.cwd(), __dirname + '/../fixtures/invalid.txt');
await focusEditor(page);
await insertCard(page, {cardName: 'call-to-action'});
const fileChooserPromise = page.waitForEvent('filechooser');
await page.click('[data-testid="media-upload-placeholder"]');
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles([filePath]);
await expect(page.locator('[data-testid="upload-error"]')).toBeVisible();
await expect(page.locator('[data-testid="upload-error"]')).toHaveText('Invalid file type. Please upload an image.');
}); |
||
|
||
test('default layout is minimal', async function () { | ||
await focusEditor(page); | ||
await insertCard(page, {cardName: 'call-to-action'}); | ||
|
@@ -239,4 +263,22 @@ test.describe('Call To Action Card', async () => { | |
await expect(page.locator(firstChildSelector)).toHaveAttribute('data-cta-layout', 'minimal'); | ||
expect(await page.getAttribute('[data-testid="cta-card-content-editor"]', 'class')).toContain('text-left'); | ||
}); | ||
|
||
test('has image preview', async function () { | ||
const filePath = path.relative(process.cwd(), __dirname + `/../fixtures/large-image.jpeg`); | ||
|
||
await focusEditor(page); | ||
await insertCard(page, {cardName: 'call-to-action'}); | ||
|
||
const fileChooserPromise = page.waitForEvent('filechooser'); | ||
|
||
await page.click('[data-testid="media-upload-placeholder"]'); | ||
|
||
const fileChooser = await fileChooserPromise; | ||
await fileChooser.setFiles([filePath]); | ||
|
||
await page.waitForSelector('[data-testid="media-upload-filled"]'); | ||
const previewImage = await page.locator('[data-testid="media-upload-filled"] img'); | ||
await expect(previewImage).toBeVisible(); | ||
}); | ||
}); |
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.
🛠️ Refactor suggestion
Add error handling and loading state management.
The image upload implementation should include:
Consider this implementation:
📝 Committable suggestion