Skip to content

Commit

Permalink
Merge pull request #12 from d3i-infra/input-multiple-files
Browse files Browse the repository at this point in the history
multiple file input screen added
  • Loading branch information
trbKnl authored Aug 9, 2024
2 parents 7d14804 + a9bd6fc commit 27c0d42
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 7 deletions.
3 changes: 3 additions & 0 deletions src/assets/images/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions src/framework/processing/py/port/api/props.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,26 @@ def toDict(self):
return dict


@dataclass
class PropsUIPromptFileInputMultiple:
"""Prompt the user to submit multiple files
Attributes:
description: text with an explanation
extensions: accepted mime types, example: "application/zip, text/plain"
"""

description: Translatable
extensions: str

def toDict(self):
dict = {}
dict["__type__"] = "PropsUIPromptFileInputMultiple"
dict["description"] = self.description.toDict()
dict["extensions"] = self.extensions
return dict


@dataclass
class PropsUIPromptProgress:
"""Prompt the user information during the extraction
Expand Down Expand Up @@ -315,6 +335,7 @@ class PropsUIPageDonation:
PropsUIPromptRadioInput
| PropsUIPromptConsentForm
| PropsUIPromptFileInput
| PropsUIPromptFileInputMultiple
| PropsUIPromptConfirm
| PropsUIPromptQuestionnaire
)
Expand Down
23 changes: 17 additions & 6 deletions src/framework/processing/py_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,23 @@ function runCycle(payload) {

function unwrap(response) {
console.log('[ProcessingWorker] unwrap response: ' + JSON.stringify(response.payload))
const directoryName = "/file-input"
return new Promise((resolve) => {
switch (response.payload.__type__) {
case 'PayloadFile':
copyFileToPyFS(response.payload.value, resolve)
const file = response.payload.value
copyFileToPyFS([file], directoryName)
resolve({ __type__: 'PayloadString', value: `${directoryName}/${file.name}`})
break

case 'PayloadFileArray':
const filePaths = []
const files = response.payload.value
for (const file of files) {
filePaths.push(`${directoryName}/${file.name}`)
}
copyFileToPyFS(files, directoryName)
resolve({ __type__: 'PayloadStringArray', value: filePaths })
break

default:
Expand All @@ -59,9 +72,8 @@ function unwrap(response) {
})
}

function copyFileToPyFS(file, resolve) {
directoryName = `/file-input`
pathStats = self.pyodide.FS.analyzePath(directoryName)
function copyFileToPyFS(files, directoryName) {
const pathStats = self.pyodide.FS.analyzePath(directoryName)
if (!pathStats.exists) {
self.pyodide.FS.mkdir(directoryName)
} else {
Expand All @@ -70,11 +82,10 @@ function copyFileToPyFS(file, resolve) {
self.pyodide.FS.mount(
self.pyodide.FS.filesystems.WORKERFS,
{
files: [file]
files: files
},
directoryName
)
resolve({ __type__: 'PayloadString', value: directoryName + '/' + file.name })
}

function initialise() {
Expand Down
6 changes: 6 additions & 0 deletions src/framework/types/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type PayloadResolved =
PayloadTrue |
PayloadString |
PayloadFile |
PayloadFileArray |
PayloadJSON

export interface PayloadVoid {
Expand All @@ -65,6 +66,11 @@ export interface PayloadFile {
value: File
}

export interface PayloadFileArray {
__type__: 'PayloadFileArray'
value: File[]
}

export interface PayloadJSON {
__type__: 'PayloadJSON'
value: string
Expand Down
3 changes: 2 additions & 1 deletion src/framework/types/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from './elements'
import {
PropsUIPromptFileInput,
PropsUIPromptFileInputMultiple,
PropsUIPromptConfirm,
PropsUIPromptConsentForm,
PropsUIPromptRadioInput,
Expand Down Expand Up @@ -34,7 +35,7 @@ export interface PropsUIPageDonation {
__type__: 'PropsUIPageDonation'
platform: string
header: PropsUIHeader
body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptProgress | PropsUIPromptConsentForm | PropsUIPromptRadioInput | PropsUIPromptQuestionnaire
body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptProgress | PropsUIPromptConsentForm | PropsUIPromptRadioInput | PropsUIPromptQuestionnaire | PropsUIPromptFileInputMultiple
footer: PropsUIFooter
}
export function isPropsUIPageDonation (arg: any): arg is PropsUIPageDonation {
Expand Down
9 changes: 9 additions & 0 deletions src/framework/types/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ export function isPropsUIPromptFileInput (arg: any): arg is PropsUIPromptFileInp
return isInstanceOf<PropsUIPromptFileInput>(arg, 'PropsUIPromptFileInput', ['description', 'extensions'])
}

export interface PropsUIPromptFileInputMultiple {
__type__: "PropsUIPromptFileInputMultiple"
description: Text
extensions: string
}
export function isPropsUIPromptFileInputMultiple (arg: any): arg is PropsUIPromptFileInputMultiple {
return isInstanceOf<PropsUIPromptFileInputMultiple>(arg, 'PropsUIPromptFileInputMultiple', ['description', 'extensions'])
}

export interface PropsUIPromptProgress {
__type__: 'PropsUIPromptProgress'
description: Text
Expand Down
5 changes: 5 additions & 0 deletions src/framework/visualisation/react/ui/pages/donation_page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isPropsUIPromptConfirm,
isPropsUIPromptConsentForm,
isPropsUIPromptFileInput,
isPropsUIPromptFileInputMultiple,
isPropsUIPromptRadioInput,
isPropsUIPromptQuestionnaire
} from '../../../../types/prompts'
Expand All @@ -17,6 +18,7 @@ import { Title1 } from '../elements/text'
import { Confirm } from '../prompts/confirm'
import { ConsentForm } from '../prompts/consent_form'
import { FileInput } from '../prompts/file_input'
import { FileInputMultiple } from '../prompts/file_input_multiple'
import { Progress } from '../prompts/progress'
import { Questionnaire } from '../prompts/questionnaire'
import { RadioInput } from '../prompts/radio_input'
Expand All @@ -35,6 +37,9 @@ export const DonationPage = (props: Props): JSX.Element => {
if (isPropsUIPromptFileInput(body)) {
return <FileInput {...body} {...context} />
}
if (isPropsUIPromptFileInputMultiple(body)) {
return <FileInputMultiple {...body} {...context} />
}
if (isPropsUIPromptProgress(body)) {
return <Progress {...body} {...context} />
}
Expand Down
137 changes: 137 additions & 0 deletions src/framework/visualisation/react/ui/prompts/file_input_multiple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { Weak } from '../../../../helpers'
import * as React from 'react'
import { Translatable } from '../../../../types/elements'
import TextBundle from '../../../../text_bundle'
import { Translator } from '../../../../translator'
import { ReactFactoryContext } from '../../factory'
import { PropsUIPromptFileInputMultiple } from '../../../../types/prompts'
import { PrimaryButton } from '../elements/button'
import CloseSvg from '../../../../../assets/images/close.svg'
import { BodyLarge, BodySmall } from '../elements/text'

type Props = Weak<PropsUIPromptFileInputMultiple> & ReactFactoryContext

export const FileInputMultiple = (props: Props): JSX.Element => {
const [waiting, setWaiting] = React.useState<boolean>(false)
const [files, setFiles] = React.useState<File[]>([])
const input = React.useRef<HTMLInputElement>(null)

const { resolve } = props
const { description, note, extensions, selectButton, continueButton } = prepareCopy(props)

function handleClick (): void {
input.current?.click()
}

function addFile(file: File): void {
const fileExists = files.some(f => f.name === file.name && f.size === file.size);
if (!fileExists) {
setFiles(prevFiles => [...prevFiles, file]);
}
};

function removeFile(index: number): void {
setFiles(prevFiles => prevFiles.filter((_, i) => i !== index));
};

function handleSelect (event: React.ChangeEvent<HTMLInputElement>): void {
const selectedFiles = event.target.files
if (selectedFiles != null && selectedFiles.length > 0) {
for (let i = 0; i < selectedFiles.length; i++) {
addFile(selectedFiles[i])
}
} else {
console.log('[FileInput] Error selecting file: ' + JSON.stringify(selectedFiles))
}
}

function handleConfirm (): void {
if (files !== undefined && !waiting) {
setWaiting(true)
resolve?.({ __type__: 'PayloadFileArray', value: files })
}
}

return (
<>
<div id='select-panel'>
<div className='flex-wrap text-bodylarge font-body text-grey1 text-left'>
{description}
</div>
<div className='mt-8' />
<div className='p-6 border-grey4 '>
<input ref={input} id='input' type='file' className='hidden' accept={extensions} onChange={handleSelect} multiple/>
<div className='flex flex-row gap-4 items-center'>
<PrimaryButton onClick={handleClick} label={selectButton} color='bg-tertiary text-grey1' />
</div>
</div>
<div>
{files.map((file, index) => (
<div className="w-64 md:w-full px-4">
<div key={index} className="flex items-center justify-between">
<span className="truncate">{file.name}</span>
<button
onClick={() => removeFile(index)}
className="flex-shrink-0"
>
<img src={CloseSvg} className={"w-8 h-8"} />
</button>
</div>
<div className="w-full mt-2">
<hr className="border-grey4" />
</div>
</div>
))}
</div>
<div className='mt-4' />
<div className={`${files[0] === undefined ? 'opacity-30' : 'opacity-100'}`}>
<BodySmall text={note} margin='' />
<div className='mt-8' />
<div className='flex flex-row gap-4'>
<PrimaryButton label={continueButton} onClick={handleConfirm} enabled={files[0] !== undefined} spinning={waiting} />
</div>
</div>
</div>
</>
)
}

interface Copy {
description: string
note: string
extensions: string
selectButton: string
continueButton: string
}

function prepareCopy ({ description, extensions, locale }: Props): Copy {
return {
description: Translator.translate(description, locale),
note: Translator.translate(note(), locale),
extensions: extensions,
selectButton: Translator.translate(selectButtonLabel(), locale),
continueButton: Translator.translate(continueButtonLabel(), locale)
}
}

const continueButtonLabel = (): Translatable => {
return new TextBundle()
.add('en', 'Continue')
.add('de', 'Weiter')
.add('nl', 'Verder')
}

const selectButtonLabel = (): Translatable => {
return new TextBundle()
.add('en', 'Choose file(s)')
.add('de', 'Datei(en) auswählen')
.add('nl', 'Kies bestand(en)')
}

const note = (): Translatable => {
return new TextBundle()
.add('en', 'Note: The process to extract the correct data from the file is done on your own computer. No data is stored or sent yet.')
.add('de', 'Anmerkung: Die weitere Verarbeitung der Datei erfolgt auf Ihrem eigenen Endgerät. Es werden noch keine Daten gespeichert oder weiter gesendet.')
.add('nl', 'NB: Het proces om de juiste gegevens uit het bestand te halen gebeurt op uw eigen computer. Er worden nog geen gegevens opgeslagen of verstuurd.')
}

0 comments on commit 27c0d42

Please sign in to comment.