Skip to content

Commit

Permalink
Merge pull request #4197 from serlo/feat/image-upload-rework-staging-…
Browse files Browse the repository at this point in the history
…only

feat(image): add new upload code for testing
  • Loading branch information
LarsTheGlidingSquirrel authored Oct 23, 2024
2 parents 86018ef + 92fb558 commit b7144b2
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 14 deletions.
4 changes: 4 additions & 0 deletions packages/editor/src/core/contexts/editor-variant-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { EditorVariant } from '@editor/package/storage-format'
import { createContext } from 'react'

export const EditorVariantContext = createContext<EditorVariant>('unknown')
19 changes: 11 additions & 8 deletions packages/editor/src/package/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Editor, type EditorProps } from '@editor/core'
import { EditorVariantContext } from '@editor/core/contexts/editor-variant-context'
import { type GetDocument } from '@editor/core/types'
import { createBasicPlugins } from '@editor/editor-integration/create-basic-plugins'
import { createRenderers } from '@editor/editor-integration/create-renderers'
Expand Down Expand Up @@ -71,14 +72,16 @@ export function SerloEditor(props: SerloEditorProps) {
return (
<StaticStringsProvider value={staticStrings}>
<EditStringsProvider value={editStrings}>
<LtikContext.Provider value={_ltik}>
<Editor
initialState={migratedState.document}
onChange={handleDocumentChange}
>
{children}
</Editor>
</LtikContext.Provider>
<EditorVariantContext.Provider value={editorVariant}>
<LtikContext.Provider value={_ltik}>
<Editor
initialState={migratedState.document}
onChange={handleDocumentChange}
>
{children}
</Editor>
</LtikContext.Provider>
</EditorVariantContext.Provider>
</EditStringsProvider>
</StaticStringsProvider>
)
Expand Down
9 changes: 5 additions & 4 deletions packages/editor/src/plugins/image/controls/upload-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import { useState } from 'react'

import type { ImageProps } from '..'
import { useUploadFile } from '../utils/upload-file'

interface UploadButtonProps {
config: ImageProps['config']
Expand All @@ -27,6 +28,8 @@ export function UploadButton({
const imageStrings = useEditStrings().plugins.image
const isFailed = isTempFile(src.value) && src.value.failed

const upload = useUploadFile(config.upload)

const [isLabelFocused, setIsLabelFocused] = useState(false)

return (
Expand Down Expand Up @@ -64,7 +67,7 @@ export function UploadButton({
const filesArray = Array.from(target.files)

// Upload the first file like normal
void src.upload(filesArray[0], config.upload)
void src.upload(filesArray[0], upload)

// If multiple upload is allowed, call the multiple upload callback
// with the remaining files
Expand All @@ -81,9 +84,7 @@ export function UploadButton({
{isFailed ? (
<button
className="serlo-button-editor-primary serlo-tooltip-trigger mr-2 scale-90"
onClick={() =>
src.upload((src.value as TempFile).failed!, config.upload)
}
onClick={() => src.upload((src.value as TempFile).failed!, upload)}
data-qa="plugin-image-retry"
>
<EditorTooltip text={imageStrings.retry} className="top-10" />
Expand Down
5 changes: 3 additions & 2 deletions packages/editor/src/plugins/image/editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ImageSelectionScreen } from './components/image-selection-screen'
import { ImageRenderer } from './renderer'
import { ImageToolbar } from './toolbar'
import { isImageUrl } from './utils/check-image-url'
import { useUploadFile } from './utils/upload-file'

const captionFormattingOptions = [
TextEditorFormattingOption.richTextBold,
Expand All @@ -24,11 +25,11 @@ const captionFormattingOptions = [
export function ImageEditor(props: ImageProps) {
const { id, focused, state, config } = props
const imageStrings = useEditStrings().plugins.image
const upload = useUploadFile(config.upload)
usePendingFileUploader(state.src, upload)

const [showInlineImageUrl, setShowInlineImageUrl] = useState(!state.src.value)

usePendingFileUploader(state.src, config.upload)

// eslint-disable-next-line @typescript-eslint/no-base-to-string
const src = state.src.value.toString()

Expand Down
90 changes: 90 additions & 0 deletions packages/editor/src/plugins/image/utils/upload-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { EditorVariantContext } from '@editor/core/contexts/editor-variant-context'
import { type EditorVariant } from '@editor/package/storage-format'
import { type UploadHandler } from '@editor/plugin'
import { useContext } from 'react'

import { handleError, validateFile } from './validate-file'

export function useUploadFile(oldFileUploader: UploadHandler<string>) {
const editorVariant = useContext(EditorVariantContext)
return shouldUseNewUpload()
? (file: File) => uploadFile(file, editorVariant)
: oldFileUploader
}

// while testing
export function shouldUseNewUpload() {
if (typeof window === 'undefined') return false
const host = window.location.hostname
const isDevOrPreviewOrStaging =
(host.startsWith('frontend-git') && host.endsWith('vercel.app')) ||
host.endsWith('serlo-staging.dev') ||
host === 'localhost' ||
process.env.NODE_ENV === 'development' ||
host.endsWith('serlo.dev')

if (isDevOrPreviewOrStaging) {
// eslint-disable-next-line no-console
console.warn('using new upload method and temporary bucket')
}
return isDevOrPreviewOrStaging
}

export async function uploadFile(file: File, editorVariant: EditorVariant) {
const validated = validateFile(file)
if (!validated) return Promise.reject()

const data = await getSignedUrlAndSrc(file.type, editorVariant)
if (!data) return Promise.reject('Could not get signed URL')

const { signedUrl, imgSrc } = data

const success = await uploadToBucket(file, signedUrl)
if (!success) return Promise.reject('Could not upload file')
return Promise.resolve(imgSrc)
}

const signedUrlHost =
process.env.NODE_ENV === 'development'
? 'editor.serlo.dev'
: 'editor.serlo.dev' // TODO: Change to production bucket after testing

async function getSignedUrlAndSrc(
mimeType: string,
editorVariant: EditorVariant
) {
const url = `https://${signedUrlHost}/media/presigned-url?mimeType=${encodeURIComponent(mimeType)}&editorVariant=${encodeURIComponent(editorVariant)}`

const result = await fetch(url).catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
handleError(errorMessage)
})

const data = (await result?.json()) as { signedUrl: string; imgSrc: string }
return data
}

const errorMessage = 'Error while uploading'

async function uploadToBucket(file: File, signedUrl: string) {
const response = await fetch(signedUrl, {
method: 'PUT',
body: file,
headers: {
'Content-Type': file.type,
'Access-Control-Allow-Origin': '*',
},
}).catch((e) => {
// eslint-disable-next-line no-console
console.error(e)
handleError(errorMessage)
return
})

if (!response || response.status !== 200) {
handleError(errorMessage)
return
}
return true
}
45 changes: 45 additions & 0 deletions packages/editor/src/plugins/image/utils/validate-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { showToastNotice } from '@editor/editor-ui/show-toast-notice'

export enum FileErrorCode {
TOO_MANY_FILES,
NO_FILE_SELECTED,
BAD_EXTENSION,
FILE_TOO_BIG,
UPLOAD_FAILED,
}

export interface FileError {
errorCode: FileErrorCode
message: string
}
const maxFileSize = 2 * 1024 * 1024
const allowedExtensions = ['gif', 'jpg', 'jpeg', 'png', 'svg', 'webp']

export function validateFile(file: File) {
// TODO: i18n and make error messages actually helpful
if (!file) {
handleError('No file selected')
return false
}
if (!matchesAllowedExtensions(file.name)) {
handleError('Not an accepted file type')
return false
}
if (file.size > maxFileSize) {
handleError('File is too big')
return false
}

return true
}

function matchesAllowedExtensions(fileName: string) {
const extension = fileName.toLowerCase().slice(fileName.lastIndexOf('.') + 1)
return allowedExtensions.includes(extension)
}

export function handleError(message: string) {
// eslint-disable-next-line no-console
console.error(message)
showToastNotice(message, 'warning')
}

0 comments on commit b7144b2

Please sign in to comment.