Skip to content

Commit

Permalink
Merge pull request #142 from MiracleHorizon/feat/keep-processing-changes
Browse files Browse the repository at this point in the history
feat(frontend): add "keep changes" checkbox for file processing
  • Loading branch information
MiracleHorizon authored Apr 11, 2024
2 parents b143eaa + 4f869f1 commit 4e05dd3
Show file tree
Hide file tree
Showing 19 changed files with 250 additions and 107 deletions.
12 changes: 8 additions & 4 deletions apps/frontend/src/__setup__/__mocks__/zustand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ const createUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
return store
}

export const create = (<T>(stateCreator: zustand.StateCreator<T>) => typeof stateCreator === 'function' ? createUncurried(stateCreator) : createUncurried) as typeof zustand.create
export const create = (<T>(stateCreator: zustand.StateCreator<T>) =>
typeof stateCreator === 'function'
? createUncurried(stateCreator)
: createUncurried) as typeof zustand.create

const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
const store = actualCreateStore(stateCreator)
Expand All @@ -33,9 +36,10 @@ const createStoreUncurried = <T>(stateCreator: zustand.StateCreator<T>) => {
return store
}

export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) => typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried) as typeof zustand.createStore
export const createStore = (<T>(stateCreator: zustand.StateCreator<T>) =>
typeof stateCreator === 'function'
? createStoreUncurried(stateCreator)
: createStoreUncurried) as typeof zustand.createStore

afterEach(() => {
act(() => {
Expand Down
6 changes: 5 additions & 1 deletion apps/frontend/src/api/convert-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ async function convertImageMutation({
const imageBlob = await convertImage(formData)
const link = URL.createObjectURL(imageBlob)

const newFile = new File([imageBlob], fileName, {
type: settings.outputFormat ? `image/${settings.outputFormat}` : file.type
})

return {
link,
fileName,
blob: imageBlob
file: newFile
}
}

Expand Down
6 changes: 5 additions & 1 deletion apps/frontend/src/api/handle-image-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,14 @@ async function handleImageMetadataMutation({
const imageBlob = await handleImageMetadata(formData)
const link = URL.createObjectURL(imageBlob)

const newFile = new File([imageBlob], fileName, {
type: file.type
})

return {
link,
fileName,
blob: imageBlob
file: newFile
}
}

Expand Down
26 changes: 15 additions & 11 deletions apps/frontend/src/api/resize-image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { FetchException } from './exceptions/FetchException'
import { createApiURL } from '@site/config'
import { PATH_API_RESIZE } from '@site/paths'
import { ABORT_TIMEOUT } from './config'
import { addImageSizesToFileName } from '@helpers/file/addImageSizesToFileName'
import type { MutationPayload } from './types'

async function resizeImage(formData: FormData): Promise<Blob> {
Expand Down Expand Up @@ -56,21 +55,26 @@ async function resizeImageMutation({
const imageBlob = await resizeImage(formData)
const link = URL.createObjectURL(imageBlob)

let outputFileName = fileName
const outputFileName = fileName

const { resize } = settings
if (resize && resize.width && resize.height) {
outputFileName = addImageSizesToFileName({
fullFileName: outputFileName,
width: resize.width,
height: resize.height
})
}
// TODO: rework
// const { resize } = settings
// if (resize && resize.width && resize.height) {
// outputFileName = addImageSizesToFileName({
// fullFileName: outputFileName,
// width: resize.width,
// height: resize.height
// })
// }

const newFile = new File([imageBlob], outputFileName, {
type: file.type
})

return {
link,
fileName: outputFileName,
blob: imageBlob
file: newFile
}
}

Expand Down
11 changes: 11 additions & 0 deletions apps/frontend/src/components/CheckboxKeepChanges.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { OptionCheckbox } from '@components/OptionCheckbox'
import { useOutputStore } from '@stores/output'

export const CheckboxKeepChanges = () => {
const checked = useOutputStore(state => state.keepChanges)
const toggleKeepChanges = useOutputStore(state => state.toggleKeepChanges)

const handleToggle = () => toggleKeepChanges()

return <OptionCheckbox label='Keep Changes' checked={checked} onClick={handleToggle} />
}
29 changes: 24 additions & 5 deletions apps/frontend/src/stores/__tests__/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
import { IMAGE_FILE_FORMAT } from '@scissors/sharp'

import { createOutputStore, defaultState, type DownloadPayload } from '@stores/output'
import pick from 'lodash.pick'

describe('@stores/output', () => {
const store = createOutputStore()
const store = createOutputStore({
withPersist: false
})

afterEach(() => {
store.setState(defaultState)
Expand All @@ -17,9 +20,11 @@ describe('@stores/output', () => {
})

expect(store.getState().file).toBe(null)
expect(store.getState().originalFile).toBe(null)
expect(store.getState().isFileUploaded()).toBeFalsy()
store.getState().setFile(file)
expect(store.getState().file).toStrictEqual(file)
expect(store.getState().originalFile).toStrictEqual(file)
expect(store.getState().isFileUploaded()).toBeTruthy()

store.getState().removeFile()
Expand All @@ -43,16 +48,30 @@ describe('@stores/output', () => {
})

it('should correctly set download payload', () => {
const file1 = new File([], 'foo.png', { type: 'image/png' })
const file2 = new File([], 'bar.webp', { type: 'image/webp' })
const testValue: DownloadPayload = {
blob: new Blob(),
fileName: 'foo-bar-baz',
link: 'blob:foo-bar-baz'
file: file2,
fileName: 'foo.png',
link: 'blob:foo.png'
}

expect(store.getState().downloadPayload).toBe(defaultState.downloadPayload)
expect(store.getState().originalFile).toBe(null)

store.setState({
file: file1,
originalFile: file1,
keepChanges: true
})

expect(store.getState().getFileForProcessing()).toStrictEqual(file1)

store.getState().setDownloadPayload(testValue)
expect(store.getState().downloadPayload).toStrictEqual(testValue)
expect(store.getState().downloadPayload).toStrictEqual(pick(testValue, ['fileName', 'link']))
expect(store.getState().file).toStrictEqual(file2)
expect(store.getState().originalFile).toStrictEqual(file1)
expect(store.getState().getFileForProcessing()).toStrictEqual(file2)
})

suite('should correctly set output file format', () => {
Expand Down
82 changes: 72 additions & 10 deletions apps/frontend/src/stores/output.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { create, type StateCreator } from 'zustand'
import { create, type StateCreator, type StoreApi, type UseBoundStore } from 'zustand'
import { persist } from 'zustand/middleware'

import type { ImageFileFormat } from '@scissors/sharp'

import { isValidFileName } from '@helpers/file/isValidFileName'
import { cropFileName } from '@helpers/file/cropFileName'
import { cropImageFileType } from '@helpers/file/cropImageFileType'
import type { ImageFileFormat } from '@scissors/sharp'

export interface DownloadPayload {
link: string
fileName: string
blob: Blob
file: File
}

/* eslint no-unused-vars: 0 */
interface Store extends State {
getFileForProcessing: () => File | null
getOutputFormat: () => ImageFileFormat | null
getFullFileName: () => string
isFileUploaded: () => boolean
Expand All @@ -24,27 +27,37 @@ interface Store extends State {
resetOutputFileName: VoidFunction
setOutputFormat: (outputFormat: ImageFileFormat | null) => void
setDownloadPayload: (downloadPayload: DownloadPayload) => void
toggleKeepChanges: VoidFunction
}

interface State {
file: File | null
originalFile: File | null
outputFileName: string
outputFormat: ImageFileFormat | null
downloadPayload: DownloadPayload | null
downloadPayload: Omit<DownloadPayload, 'file'> | null
keepChanges: boolean
}

export const defaultState: State = {
file: null,
originalFile: null,
outputFileName: '',
outputFormat: null,
downloadPayload: null
downloadPayload: null,
keepChanges: false
} as const

const outputStoreCreator: StateCreator<Store> = (set, get) => ({
// State
...defaultState,

// Computed
getFileForProcessing: () => {
const { file, originalFile, keepChanges } = get()

return keepChanges ? file : originalFile
},
getFullFileName: () => {
const file = get().file

Expand Down Expand Up @@ -102,24 +115,73 @@ const outputStoreCreator: StateCreator<Store> = (set, get) => ({

set({
file,
originalFile: file,
outputFormat,
downloadPayload: null
})
},
removeFile: () =>
set({
file: null,
originalFile: null,
outputFormat: null,
downloadPayload: null
}),

setOutputFileName: (outputFileName: string) => set({ outputFileName }),
resetOutputFileName: () => set({ outputFileName: defaultState.outputFileName }),
setOutputFileName: outputFileName =>
set({
outputFileName
}),
resetOutputFileName: () =>
set({
outputFileName: defaultState.outputFileName
}),

setOutputFormat: outputFormat => set({ outputFormat }),
setDownloadPayload: downloadPayload => set({ downloadPayload })
setDownloadPayload: ({ file, ...downloadPayload }) =>
set({
file,
downloadPayload
}),

toggleKeepChanges: () =>
set({
keepChanges: !get().keepChanges
})
})

export const createOutputStore = () => create<Store>()(outputStoreCreator)
export const createOutputStore = ({
withPersist
}: CreateStoreParams): UseBoundStore<StoreApi<Store>> => {
if (withPersist) {
return create(
persist<Store>(outputStoreCreator, {
name: 'scissors-output-store',
merge: <State>(persistedState: unknown, currentState: State): State => {
if (!persistedState || typeof persistedState !== 'object') {
return currentState
}

if ('keepChanges' in persistedState && typeof persistedState.keepChanges === 'boolean') {
return {
...currentState,
keepChanges: persistedState.keepChanges
}
}

return currentState
}
})
)
}

return create<Store>()(outputStoreCreator)
}

interface CreateStoreParams {
withPersist: boolean
}

export const useOutputStore = createOutputStore()
export const useOutputStore = createOutputStore({
withPersist: true
})
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ export default function AppFooterContent() {
return (
<Flex align='center' justify='end' gap='3' height='100%' width='100%'>
<ButtonDownload />

<Separator orientation='vertical' size='2' />

{selectedTab === TOOLBAR_TAB.CONVERT && <ButtonConvert />}
{selectedTab === TOOLBAR_TAB.RESIZE && <ButtonResize />}
{selectedTab === TOOLBAR_TAB.METADATA && <ButtonMetadata />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useOutputStore } from '@stores/output'
import { useRequestStore } from '@stores/request'

export function ButtonConvert() {
const file = useOutputStore(state => state.file)
const file = useOutputStore(state => state.getFileForProcessing())
const fileName = useOutputStore(state => state.getFullFileName())
const isLoading = useRequestStore(state => state.isLoading)
const setLoading = useRequestStore(state => state.setLoading)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useRef } from 'react'
import { Button, Link, Text } from '@radix-ui/themes'
import { Button, Link as RadixLink } from '@radix-ui/themes'
import MediaQuery from 'react-responsive'

import { DownloadIcon } from '@scissors/react-icons/DownloadIcon'
Expand All @@ -11,6 +11,7 @@ export function ButtonDownload() {
const linkRef = useRef<HTMLAnchorElement>(null)

const downloadPayload = useOutputStore(state => state.downloadPayload)
const disabled = !downloadPayload

const handleButtonClick = () => {
if (!linkRef.current) return
Expand All @@ -23,24 +24,24 @@ export function ButtonDownload() {
<Button
data-tourstep={TOUR_STEP.DOWNLOAD_BUTTON}
size='3'
variant='surface'
variant={disabled ? 'solid' : 'surface'}
radius='large'
disabled={!downloadPayload}
disabled={disabled}
onClick={handleButtonClick}
>
<DownloadIcon width='20px' height='20px' />

<MediaQuery minWidth={401}>
<Text>Download</Text>
</MediaQuery>
<MediaQuery minWidth={401}>Download</MediaQuery>
</Button>

<Link
ref={linkRef}
href={downloadPayload?.link}
download={downloadPayload?.fileName}
className='hidden'
/>
{downloadPayload && (
<RadixLink
ref={linkRef}
href={downloadPayload.link}
download={downloadPayload.fileName}
className='hidden'
/>
)}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useMetadataStore } from '@stores/metadata'
import { useMetadataMutation } from '@api/handle-image-metadata'

export function ButtonMetadata() {
const file = useOutputStore(state => state.file)
const file = useOutputStore(state => state.getFileForProcessing())
const fileName = useOutputStore(state => state.getFullFileName())
const isLoading = useRequestStore(state => state.isLoading)
const setLoading = useRequestStore(state => state.setLoading)
Expand Down
Loading

0 comments on commit 4e05dd3

Please sign in to comment.