Skip to content

Commit 7220274

Browse files
authored
Merge pull request #298 from lyjeileen/input
fix(input): add popover menu to multimodalInput and uploader
2 parents cf6172c + 666c791 commit 7220274

File tree

5 files changed

+154
-22
lines changed

5 files changed

+154
-22
lines changed

src/components/input/multimodal/multimodalInput/multimodalInput.cy.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ describe('Input', () => {
4141
getUploadData={() => {
4242
return { userId: testUser.id }
4343
}}
44+
uploadOptions={[
45+
{
46+
label: 'Upload Excel Sheet',
47+
iconName: 'request_quote',
48+
metadata: { file_meta: '{"uploadedBy":"id","category":"Finance"}' },
49+
},
50+
{
51+
label: 'Upload Video',
52+
iconName: 'movie',
53+
metadata: { file_meta: '{"uploadedBy":"id","category": "Video"}' },
54+
acceptedFileTypes:
55+
'.mp4, .mov, .avi, .mkv, .wmv, .flv, .webm, .m4v',
56+
},
57+
]}
4458
/>
4559
)
4660
})
@@ -333,5 +347,34 @@ describe('Input', () => {
333347
cy.get(fileName).should('not.exist')
334348
})
335349
})
350+
351+
it(`displays upload options correctly on ${viewport} screen`, () => {
352+
cy.viewport(viewport)
353+
cy.get(uploadButton).click()
354+
cy.contains('Upload Excel Sheet').should('be.visible')
355+
cy.contains('Upload Video').should('be.visible')
356+
})
357+
358+
it('uploads file with correct metadata', () => {
359+
cy.viewport(viewport)
360+
361+
cy.intercept(
362+
{
363+
method: 'POST',
364+
url: '/upload?message-id=*',
365+
},
366+
{ url: '' }
367+
).as('upload')
368+
cy.get(uploadButton).click()
369+
cy.contains('Upload Video').click()
370+
cy.get('input[type=file]').selectFile([videoFile], {
371+
force: true,
372+
})
373+
cy.wait('@upload').interceptFormData((formData) => {
374+
expect(formData['file_meta']).to.eq(
375+
'{"uploadedBy":"id","category": "Video"}'
376+
)
377+
})
378+
})
336379
})
337380
})

src/components/input/multimodal/multimodalInput/multimodalInput.stories.tsx

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,21 @@ multiModalInputMeta.argTypes = {
139139
type: { summary: '(fileName: string) => { [key: string]: any }' },
140140
},
141141
},
142+
uploadOptions: {
143+
description:
144+
'Optional props. Defines the available options for file upload, displayed within a popover menu. If no options are provided, an upload icon button will be displayed by default.',
145+
table: {
146+
type: {
147+
summary: 'An array of UploadOption.\n',
148+
detail:
149+
'Each UploadOption has the following fields:\n' +
150+
' label: The text label displayed for the upload option, shown in the popover menu.\n' +
151+
' metadata: Metadata associated with this upload option, provided as key-value pairs. This data is typically sent along with the uploaded file to provide additional context or configuration.\n' +
152+
' acceptedFileTypes: Optional props. Specifies the types of files accepted for this upload option, formatted as a comma-separated string. If not provided, acceptedFileTypes for the entire component will be applied. \n' +
153+
' iconName: Optional props. The name of the Material Symbol to display alongside this upload option.',
154+
},
155+
},
156+
},
142157
}
143158

144159
export default multiModalInputMeta
@@ -182,7 +197,7 @@ export const PDFAndImageOnly = {
182197
...Default.args,
183198
acceptedFileTypes: 'image/*,.pdf',
184199
getUploadData: () => {
185-
return { userId: 'testUserId' }
200+
return { file_meta: '{ "uploadedBy": "testId" }' }
186201
},
187202
},
188203
}
@@ -207,3 +222,26 @@ export const SpeechToText = {
207222
enableSpeechToText: true,
208223
},
209224
}
225+
226+
export const HasMenu = {
227+
args: {
228+
...Default.args,
229+
acceptedFileTypes: '.xlsx, .xls, .csv',
230+
getUploadData: () => {
231+
return { file_meta: '{ "uploadedBy": "testId" }' }
232+
},
233+
uploadOptions: [
234+
{
235+
label: 'Upload Excel Sheet',
236+
iconName: 'request_quote',
237+
metadata: { file_meta: '{"uploadedBy":"id","category":"Finance"}' },
238+
},
239+
{
240+
label: 'Upload Video',
241+
iconName: 'movie',
242+
metadata: { file_meta: '{"uploadedBy":"id","category": "Video"}' },
243+
acceptedFileTypes: '.mp4, .mov, .avi, .mkv, .wmv, .flv, .webm, .m4v',
244+
},
245+
],
246+
},
247+
}

src/components/input/multimodal/multimodalInput/multimodalInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export default function MultimodalInput(props: MultimodalInputProps) {
104104
errorMessagesContainer={errorMessagesContainer}
105105
showFullName={props.showFullName}
106106
getUploadData={props.getUploadData}
107+
uploadOptions={props.uploadOptions}
107108
/>
108109
</Box>
109110
</BaseInput>

src/components/input/multimodal/uploader/uploader.tsx

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { createPortal } from 'react-dom'
1212
import { v4 as getUUID } from 'uuid'
1313

1414
import FilePreview from '../../../filePreview/filePreview'
15-
import type { UploaderProps } from '../../../types'
15+
import Icon from '../../../icon'
16+
import PopoverMenu from '../../../menu/popoverMenu'
17+
import type { UploaderProps, UploadOption } from '../../../types'
1618

1719
const maximumLoadingProgress = 100
1820

@@ -56,6 +58,10 @@ export function getFileSizeAbbrev(bytes: number): string {
5658
function Uploader(props: UploaderProps) {
5759
const [addedFiles, setAddedFiles] = useState<FileInfo[]>([])
5860
const [errorMessages, setErrorMessages] = useState<string[]>([])
61+
const [additionalMetadata, setAdditionalMetadata] = useState<{
62+
[key: string]: any
63+
}>({})
64+
5965
const fileNamesRef = useRef<{ [key: string]: number }>({})
6066
const inputId = getUUID()
6167

@@ -176,11 +182,19 @@ function Uploader(props: UploaderProps) {
176182
const fileName = getFileName(file)
177183
const updatedFile = new File([file], fileName, { type: file.type })
178184

179-
if (props.getUploadData) {
180-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
181-
const extraData: { [key: string]: any } = props.getUploadData(fileName)
182-
Object.keys(extraData).map((key) => {
183-
formData.append(key, extraData[key])
185+
if (props.getUploadData || additionalMetadata) {
186+
const generalMetadata = props.getUploadData
187+
? props.getUploadData(fileName)
188+
: {}
189+
190+
const combinedData = { ...generalMetadata, ...additionalMetadata }
191+
192+
Object.keys(combinedData).forEach((key) => {
193+
const value = combinedData[key]
194+
formData.append(
195+
key,
196+
typeof value === 'object' ? JSON.stringify(value) : value
197+
)
184198
})
185199
}
186200

@@ -327,25 +341,52 @@ function Uploader(props: UploaderProps) {
327341
}
328342
}
329343

344+
function handleMenuClick(option: UploadOption) {
345+
setAdditionalMetadata(option.metadata)
346+
const fileInput = document.getElementById(inputId) as HTMLInputElement
347+
if (fileInput) {
348+
fileInput.accept = option.acceptedFileTypes || props.acceptedFileTypes
349+
}
350+
351+
fileInput.click()
352+
}
353+
354+
const menuItems = props.uploadOptions?.map((option) => {
355+
return {
356+
label: option.label,
357+
onClick: () => handleMenuClick(option),
358+
startDecorator: option.iconName && <Icon name={option.iconName} />,
359+
}
360+
})
361+
330362
return (
331363
<>
332364
<Box className="rustic-uploader">
333365
<label htmlFor={inputId} data-cy="upload-button">
334-
<Tooltip title="Upload">
335-
<IconButton
336-
component="span"
337-
aria-label="Upload file"
338-
sx={{
339-
color: 'primary.light',
340-
'&:hover': {
341-
color: 'primary.main',
342-
},
343-
}}
344-
>
345-
<span className="material-symbols-rounded">upload_2</span>
346-
</IconButton>
347-
</Tooltip>
366+
{menuItems?.length ? (
367+
<PopoverMenu
368+
ariaLabel="Upload"
369+
icon={<Icon name="upload_2" />}
370+
menuItems={menuItems}
371+
/>
372+
) : (
373+
<Tooltip title="Upload">
374+
<IconButton
375+
component="span"
376+
aria-label="Upload file"
377+
sx={{
378+
color: 'primary.light',
379+
'&:hover': {
380+
color: 'primary.main',
381+
},
382+
}}
383+
>
384+
<span className="material-symbols-rounded">upload_2</span>
385+
</IconButton>
386+
</Tooltip>
387+
)}
348388
</label>
389+
349390
<input
350391
type="file"
351392
id={inputId}

src/components/types.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,13 @@ export interface TextInputProps
261261
ws: WebSocketClient
262262
}
263263

264+
export interface UploadOption {
265+
label: string
266+
metadata: { [key: string]: any }
267+
acceptedFileTypes?: string
268+
iconName?: string
269+
}
270+
264271
export interface UploaderProps {
265272
/** The types of files that are allowed to be selected for upload. For safety reasons, only allow file types that can be handled by your server. Avoid accepting executable file types like .exe, .bat, or .msi. For more information, refer to the [mdn web docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers). */
266273
acceptedFileTypes: string
@@ -284,6 +291,8 @@ export interface UploaderProps {
284291
showFullName?: boolean
285292
/** A function that can be used to define additional data to be sent along with the file upload. */
286293
getUploadData?: (fileName: string) => { [key: string]: any }
294+
/** Defines the available options for file upload, displayed within a popover menu. If no options are provided, an upload icon button will be displayed by default.*/
295+
uploadOptions?: UploadOption[]
287296
}
288297

289298
export type MultimodalInputProps = TextInputProps &
@@ -364,4 +373,4 @@ export interface FormFormat extends DataFormat {
364373
schema: any
365374
}
366375

367-
export interface DynamicFormProps extends FormFormat, ConversationProps {}
376+
export interface DynamicFormProps extends FormFormat, ConversationProps {}

0 commit comments

Comments
 (0)