@meistrari/tela-build 1.56.0 → 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.
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { getMimeTypeIcon } from 'tela-shared/src/helpers/files'
2
+ import { getMimeTypeIcon } from '../../../utils/files'
3
3
  import type { FileWithStatus, ItemDefinition } from './uploader-mappings'
4
4
 
5
5
  withDefaults(defineProps<{
@@ -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 type { Tela } from 'tela-shared/types/tela'
2
- import { extractFileNodeAttributesFromHTML } from 'tela-shared/src/helpers/html'
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
- ): Tela.FileUpload.FileVariableMapping[] {
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 Tela.FileUpload.FileVariableMapping[]
214
+ ) as FileVariableMapping[]
186
215
  }
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { getMimeTypeIcon, formatFileSize } from 'tela-shared/src/helpers/files'
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 component supports all file types defined in `supportedFileTypesToParse` from `tela-shared`. Common formats include:
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
- - **Documents**: PDF, DOC, DOCX, TXT, etc.
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 { Tela } from 'tela-shared/types/tela'
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 Tela.FileUpload.FileVariableMapping[] | null,
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
- isFileTypeParseSupported,
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: Tela.FileUpload.FileVariableMapping[]): void
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: Tela.FileUpload.FileVariableMapping[] | null
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 supportedFileTypes = computed(() =>
165
- Object.values(supportedFileTypesToParse)
166
- .map(fileTypes => fileTypes.at(-1)!.toUpperCase())
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(supportedFileTypesToParse).join(','),
167
+ Object.keys(props.supportedFileTypes).join(','),
172
168
  )
173
169
 
174
170
  const fileTypesDisplay = computed(() => {
175
- const types = Object.values(supportedFileTypesToParse)
176
- .map(fileTypes => fileTypes.at(-1)!.toUpperCase().replace(/\./g, ''))
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 => isFileTypeParseSupported(file.type, file.name))
611
- const invalidFiles = fileList.filter(file => !isFileTypeParseSupported(file.type, file.name))
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}', supportedFileTypes.value))
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.56.0",
3
+ "version": "1.56.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",
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
+ }