@meistrari/tela-build 1.55.2 → 1.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/tela/reasoning/tool-use-display/tool-use-display.vue +5 -0
- package/components/tela/uploader/uploader-file-list.vue +1 -1
- package/components/tela/uploader/uploader-html.ts +28 -0
- package/components/tela/uploader/uploader-mappings.test.ts +33 -1
- package/components/tela/uploader/uploader-mappings.ts +33 -4
- package/components/tela/uploader/uploader-variable-assignment.vue +1 -1
- package/components/tela/uploader/uploader.mdx +33 -7
- package/components/tela/uploader/uploader.stories.ts +20 -0
- package/components/tela/uploader/uploader.vue +17 -20
- package/components/tela/variable-input.vue +7 -1
- package/package.json +1 -1
- package/utils/files.ts +59 -0
|
@@ -25,6 +25,7 @@ const props = withDefaults(defineProps<{
|
|
|
25
25
|
iconWrapperClass?: any
|
|
26
26
|
iconClass?: any
|
|
27
27
|
iconColor?: string
|
|
28
|
+
icon?: string
|
|
28
29
|
open?: boolean
|
|
29
30
|
}>(), {
|
|
30
31
|
runningLabel: 'Running',
|
|
@@ -121,6 +122,10 @@ function getValueDisplay(value: string | boolean | number) {
|
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function getIconName() {
|
|
125
|
+
if (props.icon) {
|
|
126
|
+
return props.icon
|
|
127
|
+
}
|
|
128
|
+
|
|
124
129
|
const toolName = props.toolUse.name.toLowerCase()
|
|
125
130
|
|
|
126
131
|
if (toolName.includes('bash')) {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function extractAttribute(tag: string, attribute: string): string {
|
|
2
|
+
const match = tag.match(new RegExp(`${attribute}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i'))
|
|
3
|
+
return match?.[1] ?? match?.[2] ?? match?.[3] ?? ''
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function extractFileNodeAttributesFromHTML(html: string): Array<{ id: string, fileName: string }> {
|
|
7
|
+
if (!html)
|
|
8
|
+
return []
|
|
9
|
+
|
|
10
|
+
if (typeof DOMParser === 'undefined') {
|
|
11
|
+
const fileNodeTags = html.match(/<file-node\b[^>]*>/gi) ?? []
|
|
12
|
+
|
|
13
|
+
return fileNodeTags.map(tag => ({
|
|
14
|
+
id: extractAttribute(tag, 'id'),
|
|
15
|
+
fileName: extractAttribute(tag, 'filename'),
|
|
16
|
+
}))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const parser = new DOMParser()
|
|
20
|
+
const doc = parser.parseFromString(html, 'text/html')
|
|
21
|
+
const fileNodes = doc.querySelectorAll('file-node')
|
|
22
|
+
|
|
23
|
+
return Array.from(fileNodes).map((fileNode) => {
|
|
24
|
+
const id = fileNode.getAttribute('id') || ''
|
|
25
|
+
const fileName = fileNode.getAttribute('filename') || ''
|
|
26
|
+
return { id, fileName }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
3
|
import type { ItemDefinition } from './uploader-mappings'
|
|
4
|
-
import { buildProcessedMappings } from './uploader-mappings'
|
|
4
|
+
import { buildProcessedMappings, isFileTypeSupported } from './uploader-mappings'
|
|
5
5
|
|
|
6
6
|
function createFile(name: string, content = 'content', lastModified = Date.now()) {
|
|
7
7
|
return new File([content], name, { type: 'application/pdf', lastModified })
|
|
@@ -182,3 +182,35 @@ describe('buildProcessedMappings', () => {
|
|
|
182
182
|
}])
|
|
183
183
|
})
|
|
184
184
|
})
|
|
185
|
+
|
|
186
|
+
describe('isFileTypeSupported', () => {
|
|
187
|
+
const supportedFileTypes = {
|
|
188
|
+
'application/pdf': ['.pdf'],
|
|
189
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
190
|
+
'text/csv': ['.csv', '.CSV'],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
it('accepts a file whose mime type is a supported key', () => {
|
|
194
|
+
expect(isFileTypeSupported('application/pdf', 'report.pdf', supportedFileTypes)).toBe(true)
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('accepts a file whose mime type carries parameters', () => {
|
|
198
|
+
expect(isFileTypeSupported('text/csv; charset=utf-8', 'rows.csv', supportedFileTypes)).toBe(true)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('falls back to the extension when the browser reports a generic mime type', () => {
|
|
202
|
+
expect(isFileTypeSupported('application/octet-stream', 'report.pdf', supportedFileTypes)).toBe(true)
|
|
203
|
+
expect(isFileTypeSupported('', 'photo.JPEG', supportedFileTypes)).toBe(true)
|
|
204
|
+
expect(isFileTypeSupported(undefined, 'rows.csv', supportedFileTypes)).toBe(true)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('rejects unsupported mime types and extensions', () => {
|
|
208
|
+
expect(isFileTypeSupported('image/gif', 'animation.gif', supportedFileTypes)).toBe(false)
|
|
209
|
+
expect(isFileTypeSupported('application/octet-stream', 'archive.zip', supportedFileTypes)).toBe(false)
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('rejects a file without an extension when the mime type is unknown', () => {
|
|
213
|
+
expect(isFileTypeSupported('application/octet-stream', 'README', supportedFileTypes)).toBe(false)
|
|
214
|
+
expect(isFileTypeSupported(undefined, undefined, supportedFileTypes)).toBe(false)
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -1,5 +1,34 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import { extractFileNodeAttributesFromHTML } from './uploader-html'
|
|
2
|
+
|
|
3
|
+
export interface FileVariableMapping {
|
|
4
|
+
file?: File
|
|
5
|
+
content?: string
|
|
6
|
+
variableName: string | null
|
|
7
|
+
taskName?: string
|
|
8
|
+
metadata?: { row: string }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** MIME type to the file extensions it covers, e.g. `{ 'application/pdf': ['.pdf'] }`. */
|
|
12
|
+
export type SupportedFileTypes = Record<string, string[]>
|
|
13
|
+
|
|
14
|
+
export function isFileTypeSupported(
|
|
15
|
+
fileType: string | undefined,
|
|
16
|
+
filename: string | undefined,
|
|
17
|
+
supportedFileTypes: SupportedFileTypes,
|
|
18
|
+
): boolean {
|
|
19
|
+
const mimeType = fileType?.split(';').at(0)?.trim().toLowerCase() ?? ''
|
|
20
|
+
|
|
21
|
+
if (mimeType in supportedFileTypes)
|
|
22
|
+
return true
|
|
23
|
+
|
|
24
|
+
const extension = filename?.slice(filename.lastIndexOf('.')).toLowerCase()
|
|
25
|
+
|
|
26
|
+
if (!extension?.startsWith('.'))
|
|
27
|
+
return false
|
|
28
|
+
|
|
29
|
+
return Object.values(supportedFileTypes)
|
|
30
|
+
.some(extensions => extensions.some(supported => supported.toLowerCase() === extension))
|
|
31
|
+
}
|
|
3
32
|
|
|
4
33
|
export interface FileWithStatus {
|
|
5
34
|
file: File
|
|
@@ -122,7 +151,7 @@ export function buildProcessedMappings(
|
|
|
122
151
|
items: ItemDefinition[],
|
|
123
152
|
editorFilesMap: Map<string, Map<string, File>>,
|
|
124
153
|
variables: { name: string, isRequired: boolean, type?: 'file' | 'text' }[],
|
|
125
|
-
):
|
|
154
|
+
): FileVariableMapping[] {
|
|
126
155
|
return items.flatMap(item =>
|
|
127
156
|
Object.entries(item.variables).flatMap(([variableName, filesWithStatus]) => {
|
|
128
157
|
const variableType = resolveVariableType(variableName, variables)
|
|
@@ -182,5 +211,5 @@ export function buildProcessedMappings(
|
|
|
182
211
|
|
|
183
212
|
return [{ variableName, taskName: item.name }]
|
|
184
213
|
}),
|
|
185
|
-
) as
|
|
214
|
+
) as FileVariableMapping[]
|
|
186
215
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { getMimeTypeIcon, formatFileSize } from '
|
|
2
|
+
import { getMimeTypeIcon, formatFileSize } from '../../../utils/files'
|
|
3
3
|
import type { FileWithStatus, ItemDefinition } from './uploader-mappings'
|
|
4
4
|
|
|
5
5
|
const props = withDefaults(defineProps<{
|
|
@@ -33,6 +33,12 @@ const variables = [
|
|
|
33
33
|
{ name: 'notes', isRequired: false, type: 'text' }
|
|
34
34
|
]
|
|
35
35
|
|
|
36
|
+
const supportedFileTypes = {
|
|
37
|
+
'application/pdf': ['.pdf'],
|
|
38
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
39
|
+
'text/csv': ['.csv'],
|
|
40
|
+
}
|
|
41
|
+
|
|
36
42
|
const uploaderState = ref({
|
|
37
43
|
isFormValid: false,
|
|
38
44
|
itemCountLabel: '',
|
|
@@ -52,6 +58,7 @@ function handleProcess() {
|
|
|
52
58
|
<TelaModal v-model="isOpen">
|
|
53
59
|
<TelaUploader
|
|
54
60
|
:model-value="isOpen"
|
|
61
|
+
:supported-file-types="supportedFileTypes"
|
|
55
62
|
:files="files"
|
|
56
63
|
:variables="variables"
|
|
57
64
|
item-label="Task"
|
|
@@ -105,6 +112,7 @@ function handleProcess() {
|
|
|
105
112
|
<TelaModal v-model="isOpen">
|
|
106
113
|
<TelaUploader
|
|
107
114
|
:model-value="isOpen"
|
|
115
|
+
:supported-file-types="supportedFileTypes"
|
|
108
116
|
:files="files"
|
|
109
117
|
:variables="variables"
|
|
110
118
|
item-label="Invoice"
|
|
@@ -142,6 +150,7 @@ const uploaderState = ref({
|
|
|
142
150
|
<TelaModal v-model="isOpen">
|
|
143
151
|
<TelaUploader
|
|
144
152
|
:model-value="isOpen"
|
|
153
|
+
:supported-file-types="supportedFileTypes"
|
|
145
154
|
:files="files"
|
|
146
155
|
:variables="variables"
|
|
147
156
|
item-label="Documento"
|
|
@@ -208,6 +217,7 @@ function handleProcess() {
|
|
|
208
217
|
<TelaModal v-model="isOpen">
|
|
209
218
|
<TelaUploader
|
|
210
219
|
:model-value="isOpen"
|
|
220
|
+
:supported-file-types="supportedFileTypes"
|
|
211
221
|
:files="files"
|
|
212
222
|
:variables="variables"
|
|
213
223
|
:edit-data="editData"
|
|
@@ -264,6 +274,7 @@ function handleProcess() {
|
|
|
264
274
|
<TelaModal v-model="isOpen">
|
|
265
275
|
<TelaUploader
|
|
266
276
|
:model-value="isOpen"
|
|
277
|
+
:supported-file-types="supportedFileTypes"
|
|
267
278
|
:files="files"
|
|
268
279
|
:variables="variables"
|
|
269
280
|
item-label="Task"
|
|
@@ -314,8 +325,11 @@ interface UploaderState {
|
|
|
314
325
|
mappings: FileVariableMapping[] | null
|
|
315
326
|
}
|
|
316
327
|
|
|
328
|
+
type SupportedFileTypes = Record<string, string[]>
|
|
329
|
+
|
|
317
330
|
type UploaderProps = {
|
|
318
331
|
modelValue: boolean
|
|
332
|
+
supportedFileTypes: SupportedFileTypes
|
|
319
333
|
files?: File[]
|
|
320
334
|
variables?: Variable[]
|
|
321
335
|
lastItemReference?: number
|
|
@@ -460,12 +474,17 @@ The Uploader component emits the following events:
|
|
|
460
474
|
|
|
461
475
|
## Supported File Types
|
|
462
476
|
|
|
463
|
-
The
|
|
477
|
+
The accepted file types come from the required `supportedFileTypes` prop, a map of MIME type to the extensions it covers:
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
const supportedFileTypes = {
|
|
481
|
+
'application/pdf': ['.pdf'],
|
|
482
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
483
|
+
'text/csv': ['.csv'],
|
|
484
|
+
}
|
|
485
|
+
```
|
|
464
486
|
|
|
465
|
-
|
|
466
|
-
- **Images**: JPG, PNG, GIF, SVG, etc.
|
|
467
|
-
- **Spreadsheets**: XLS, XLSX, CSV, etc.
|
|
468
|
-
- **Other**: Various other parseable formats
|
|
487
|
+
It drives the `accept` attribute of the file inputs, the supported-formats hint in the drop zone, and validation. A dropped file is accepted when its MIME type matches a key or its extension matches one of the values, so files reported by the browser as `application/octet-stream` still resolve by extension.
|
|
469
488
|
|
|
470
489
|
The component automatically validates file types and shows error messages for unsupported formats.
|
|
471
490
|
|
|
@@ -564,7 +583,7 @@ This example shows the recommended pattern using `@state-changed` event with an
|
|
|
564
583
|
```vue
|
|
565
584
|
<script setup>
|
|
566
585
|
import { ref } from 'vue'
|
|
567
|
-
import type {
|
|
586
|
+
import type { FileVariableMapping } from '~/components/tela/uploader/uploader-mappings'
|
|
568
587
|
import TelaUploader from '~/components/tela/uploader/uploader.vue'
|
|
569
588
|
import { useNotification } from '~/composables/useNotification'
|
|
570
589
|
|
|
@@ -579,11 +598,17 @@ const workflowVariables = [
|
|
|
579
598
|
{ name: 'reference', isRequired: false, type: 'file' }
|
|
580
599
|
]
|
|
581
600
|
|
|
601
|
+
const supportedFileTypes = {
|
|
602
|
+
'application/pdf': ['.pdf'],
|
|
603
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
604
|
+
'text/csv': ['.csv'],
|
|
605
|
+
}
|
|
606
|
+
|
|
582
607
|
const uploaderState = ref({
|
|
583
608
|
isFormValid: false,
|
|
584
609
|
itemCountLabel: '',
|
|
585
610
|
hasAnyFiles: false,
|
|
586
|
-
mappings: null as
|
|
611
|
+
mappings: null as FileVariableMapping[] | null,
|
|
587
612
|
})
|
|
588
613
|
|
|
589
614
|
async function handleFileSelect(event: Event) {
|
|
@@ -629,6 +654,7 @@ function handleValidationError(title: string, description: string) {
|
|
|
629
654
|
|
|
630
655
|
<TelaUploader
|
|
631
656
|
:model-value="isUploaderOpen"
|
|
657
|
+
:supported-file-types="supportedFileTypes"
|
|
632
658
|
:files="uploadedFiles"
|
|
633
659
|
:variables="workflowVariables"
|
|
634
660
|
item-label="Task"
|
|
@@ -5,6 +5,16 @@ import TelaButton from '../button/button.vue'
|
|
|
5
5
|
import TelaIconButton from '../icon-button/icon-button.vue'
|
|
6
6
|
import TelaModal from '../modal/modal.vue'
|
|
7
7
|
|
|
8
|
+
const supportedFileTypes = {
|
|
9
|
+
'application/pdf': ['.pdf'],
|
|
10
|
+
'image/jpeg': ['.jpg', '.jpeg'],
|
|
11
|
+
'image/png': ['.png'],
|
|
12
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
|
|
13
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
|
|
14
|
+
'text/csv': ['.csv'],
|
|
15
|
+
'text/plain': ['.txt'],
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
const meta: Meta<typeof Uploader> = {
|
|
9
19
|
title: 'Core/Uploader',
|
|
10
20
|
component: Uploader,
|
|
@@ -21,6 +31,10 @@ const meta: Meta<typeof Uploader> = {
|
|
|
21
31
|
control: 'boolean',
|
|
22
32
|
description: 'Controls the visibility of the uploader modal. Use `v-model` for two-way binding.',
|
|
23
33
|
},
|
|
34
|
+
supportedFileTypes: {
|
|
35
|
+
control: 'object',
|
|
36
|
+
description: 'Required. Map of accepted MIME types to their file extensions, e.g. `{ "application/pdf": [".pdf"] }`. Drives the accept attribute, the supported-formats hint and file validation.',
|
|
37
|
+
},
|
|
24
38
|
files: {
|
|
25
39
|
control: 'object',
|
|
26
40
|
description: 'Array of File objects to be processed and assigned to variables.',
|
|
@@ -91,6 +105,7 @@ export const Default: Story = {
|
|
|
91
105
|
|
|
92
106
|
return {
|
|
93
107
|
isOpen,
|
|
108
|
+
supportedFileTypes,
|
|
94
109
|
files,
|
|
95
110
|
variables,
|
|
96
111
|
uploaderState,
|
|
@@ -143,6 +158,7 @@ export const Default: Story = {
|
|
|
143
158
|
<div flex="~ col" min-h-0>
|
|
144
159
|
<Uploader
|
|
145
160
|
:model-value="isOpen"
|
|
161
|
+
:supported-file-types="supportedFileTypes"
|
|
146
162
|
:files="files"
|
|
147
163
|
:variables="variables"
|
|
148
164
|
item-label="Task"
|
|
@@ -211,6 +227,7 @@ export const WithFileUpload: Story = {
|
|
|
211
227
|
|
|
212
228
|
return {
|
|
213
229
|
isOpen,
|
|
230
|
+
supportedFileTypes,
|
|
214
231
|
files,
|
|
215
232
|
variables,
|
|
216
233
|
uploaderState,
|
|
@@ -264,6 +281,7 @@ export const WithFileUpload: Story = {
|
|
|
264
281
|
<div flex="~ col" min-h-0>
|
|
265
282
|
<Uploader
|
|
266
283
|
:model-value="isOpen"
|
|
284
|
+
:supported-file-types="supportedFileTypes"
|
|
267
285
|
:files="files"
|
|
268
286
|
:variables="variables"
|
|
269
287
|
item-label="Document"
|
|
@@ -319,6 +337,7 @@ export const WithTextVariables: Story = {
|
|
|
319
337
|
|
|
320
338
|
return {
|
|
321
339
|
isOpen,
|
|
340
|
+
supportedFileTypes,
|
|
322
341
|
files,
|
|
323
342
|
variables,
|
|
324
343
|
uploaderState,
|
|
@@ -371,6 +390,7 @@ export const WithTextVariables: Story = {
|
|
|
371
390
|
<div flex="~ col" min-h-0>
|
|
372
391
|
<Uploader
|
|
373
392
|
:model-value="isOpen"
|
|
393
|
+
:supported-file-types="supportedFileTypes"
|
|
374
394
|
:files="files"
|
|
375
395
|
:variables="variables"
|
|
376
396
|
item-label="Item"
|
|
@@ -1,18 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Tela } from 'tela-shared/types/tela'
|
|
3
2
|
import type { PreviewLabels, PreviewSelectVariableLabels, PreviewContentLabels, PdfDocumentHandle } from '@meistrari/tela-build/components/tela/preview/types'
|
|
4
|
-
import type { FileWithStatus, ItemDefinition, ActiveFileAction } from './uploader-mappings'
|
|
3
|
+
import type { FileWithStatus, ItemDefinition, ActiveFileAction, FileVariableMapping, SupportedFileTypes } from './uploader-mappings'
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
supportedFileTypesToParse,
|
|
9
|
-
} from 'tela-shared/src/schemas/parse-api'
|
|
10
|
-
import { extractFileNodeAttributesFromHTML } from 'tela-shared/src/helpers/html'
|
|
11
|
-
|
|
12
|
-
import { buildProcessedMappings, isTextContentEmpty } from './uploader-mappings'
|
|
5
|
+
import { extractFileNodeAttributesFromHTML } from './uploader-html'
|
|
6
|
+
import { buildProcessedMappings, isFileTypeSupported, isTextContentEmpty } from './uploader-mappings'
|
|
13
7
|
|
|
14
8
|
const props = withDefaults(defineProps<{
|
|
15
9
|
modelValue: boolean
|
|
10
|
+
supportedFileTypes: SupportedFileTypes
|
|
16
11
|
files?: File[]
|
|
17
12
|
variables?: { name: string, isRequired: boolean, type?: 'file' | 'text' }[]
|
|
18
13
|
lastItemReference?: number
|
|
@@ -130,14 +125,14 @@ const props = withDefaults(defineProps<{
|
|
|
130
125
|
|
|
131
126
|
const emit = defineEmits<{
|
|
132
127
|
(e: 'update:modelValue', value: boolean): void
|
|
133
|
-
(e: 'process', mappings:
|
|
128
|
+
(e: 'process', mappings: FileVariableMapping[]): void
|
|
134
129
|
(e: 'validationError', title: string, description: string): void
|
|
135
130
|
(e: 'csvFileDropped', files: File[]): void
|
|
136
131
|
(e: 'stateChanged', state: {
|
|
137
132
|
isFormValid: boolean
|
|
138
133
|
itemCountLabel: string
|
|
139
134
|
hasAnyFiles: boolean
|
|
140
|
-
mappings:
|
|
135
|
+
mappings: FileVariableMapping[] | null
|
|
141
136
|
}): void
|
|
142
137
|
}>()
|
|
143
138
|
|
|
@@ -161,19 +156,21 @@ const editorRefs = ref<Map<string, { editor: any }>>(new Map())
|
|
|
161
156
|
const variableList = computed(() => props.variables || [])
|
|
162
157
|
const isEditMode = computed(() => !!props.editData)
|
|
163
158
|
|
|
164
|
-
const
|
|
165
|
-
Object.values(
|
|
166
|
-
.
|
|
159
|
+
const supportedFileTypesLabel = computed(() =>
|
|
160
|
+
Object.values(props.supportedFileTypes)
|
|
161
|
+
.flatMap(fileTypes => fileTypes.at(-1) ?? [])
|
|
162
|
+
.map(fileType => fileType.toUpperCase())
|
|
167
163
|
.join(', '),
|
|
168
164
|
)
|
|
169
165
|
|
|
170
166
|
const supportedFileTypesAccept = computed(() =>
|
|
171
|
-
Object.keys(
|
|
167
|
+
Object.keys(props.supportedFileTypes).join(','),
|
|
172
168
|
)
|
|
173
169
|
|
|
174
170
|
const fileTypesDisplay = computed(() => {
|
|
175
|
-
const types = Object.values(
|
|
176
|
-
.
|
|
171
|
+
const types = Object.values(props.supportedFileTypes)
|
|
172
|
+
.flatMap(fileTypes => fileTypes.at(-1) ?? [])
|
|
173
|
+
.map(fileType => fileType.toUpperCase().replace(/\./g, ''))
|
|
177
174
|
|
|
178
175
|
return {
|
|
179
176
|
text: types.slice(0, 3).join(', '),
|
|
@@ -607,11 +604,11 @@ function handleItemNameBlur(itemId: string) {
|
|
|
607
604
|
}
|
|
608
605
|
|
|
609
606
|
function validateFiles(fileList: File[]) {
|
|
610
|
-
const validFiles = fileList.filter(file =>
|
|
611
|
-
const invalidFiles = fileList.filter(file => !
|
|
607
|
+
const validFiles = fileList.filter(file => isFileTypeSupported(file.type, file.name, props.supportedFileTypes))
|
|
608
|
+
const invalidFiles = fileList.filter(file => !isFileTypeSupported(file.type, file.name, props.supportedFileTypes))
|
|
612
609
|
|
|
613
610
|
if (invalidFiles.length > 0) {
|
|
614
|
-
emit('validationError', props.fileTypeNotSupportedTitle, props.fileTypeNotSupportedDescription.replace('{formats}',
|
|
611
|
+
emit('validationError', props.fileTypeNotSupportedTitle, props.fileTypeNotSupportedDescription.replace('{formats}', supportedFileTypesLabel.value))
|
|
615
612
|
}
|
|
616
613
|
|
|
617
614
|
return validFiles
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import type { Variable } from 'tela-shared'
|
|
3
2
|
import { useI18n } from 'vue-i18n'
|
|
4
3
|
|
|
4
|
+
interface Variable {
|
|
5
|
+
name: string
|
|
6
|
+
type: 'file' | 'text' | 'mixed'
|
|
7
|
+
required: boolean
|
|
8
|
+
processingOptions?: { allowMultimodal: boolean }
|
|
9
|
+
}
|
|
10
|
+
|
|
5
11
|
defineProps<{
|
|
6
12
|
variable: Variable
|
|
7
13
|
tabindex?: number
|
package/package.json
CHANGED
package/utils/files.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const fileIconsSrc = {
|
|
2
|
+
'application/pdf': '/files/pdf.svg',
|
|
3
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '/files/doc.svg',
|
|
4
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.template': '/files/doc.svg',
|
|
5
|
+
'application/msword': '/files/doc.svg',
|
|
6
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '/files/sheet.svg',
|
|
7
|
+
'application/vnd.ms-excel': '/files/sheet.svg',
|
|
8
|
+
'application/vnd.openxmlformats-officedocument.presentationml.presentation': '/files/presentation.svg',
|
|
9
|
+
'application/vnd.ms-powerpoint': '/files/presentation.svg',
|
|
10
|
+
'application/vnd.oasis.opendocument.spreadsheet': '/files/sheet.svg',
|
|
11
|
+
'application/vnd.oasis.opendocument.text': '/files/doc.svg',
|
|
12
|
+
'application/vnd.oasis.opendocument.presentation': '/files/presentation.svg',
|
|
13
|
+
'text/csv': '/files/sheet.svg',
|
|
14
|
+
'text/plain': '/files/generic.svg',
|
|
15
|
+
'application/json': '/files/json.svg',
|
|
16
|
+
'application/xml': '/files/json.svg',
|
|
17
|
+
'text/xml': '/files/json.svg',
|
|
18
|
+
'application/x-yaml': '/files/json.svg',
|
|
19
|
+
'text/yaml': '/files/json.svg',
|
|
20
|
+
'message/rfc822': '/files/doc.svg',
|
|
21
|
+
'application/vnd.ms-outlook': '/files/doc.svg',
|
|
22
|
+
'video/mp4': '/files/video.svg',
|
|
23
|
+
'video/webm': '/files/video.svg',
|
|
24
|
+
'audio/mpeg': '/files/audio.svg',
|
|
25
|
+
'audio/ogg': '/files/audio.svg',
|
|
26
|
+
'audio/wav': '/files/audio.svg',
|
|
27
|
+
'image/jpeg': '/files/image.svg',
|
|
28
|
+
'image/png': '/files/image.svg',
|
|
29
|
+
'unknown': '/files/generic.svg',
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getMimeTypeIcon(mimeType?: string): string {
|
|
33
|
+
if (!mimeType)
|
|
34
|
+
return '/files/generic.svg'
|
|
35
|
+
|
|
36
|
+
if (mimeType.startsWith('image/'))
|
|
37
|
+
return '/files/image.svg'
|
|
38
|
+
|
|
39
|
+
if (mimeType.startsWith('audio/'))
|
|
40
|
+
return '/files/audio.svg'
|
|
41
|
+
|
|
42
|
+
if (mimeType.startsWith('video/'))
|
|
43
|
+
return '/files/video.svg'
|
|
44
|
+
|
|
45
|
+
return fileIconsSrc[mimeType as keyof typeof fileIconsSrc] || '/files/generic.svg'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function formatFileSize(bytes: number): string {
|
|
49
|
+
const units = ['B', 'KB', 'MB']
|
|
50
|
+
let size = bytes
|
|
51
|
+
let unitIndex = 0
|
|
52
|
+
|
|
53
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
54
|
+
size /= 1024
|
|
55
|
+
unitIndex++
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return `${size.toFixed(1)}${units[unitIndex]}`
|
|
59
|
+
}
|