@usssa/component-library 1.0.0-alpha.226 → 1.0.0-alpha.228

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/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Component Library v1.0.0-alpha.226
1
+ # Component Library v1.0.0-alpha.227
2
2
 
3
3
  **This library provides custom UI components for USSSA applications**
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usssa/component-library",
3
- "version": "1.0.0-alpha.226",
3
+ "version": "1.0.0-alpha.228",
4
4
  "description": "A Quasar component library project",
5
5
  "productName": "Quasar component library App",
6
6
  "author": "Engineering Team <engineering@usssa.com>",
@@ -39,6 +39,7 @@
39
39
  "@usssa/core-client": "^0.0.19",
40
40
  "algoliasearch": "4",
41
41
  "flag-icons": "^7.2.3",
42
+ "heic2any": "^0.0.4",
42
43
  "quasar": "^2.16.0",
43
44
  "vue": "^3.4.18",
44
45
  "vue-router": "^4.0.12",
@@ -1,7 +1,9 @@
1
1
  <script setup>
2
2
  import * as d3 from 'd3'
3
+ import { useQuasar } from 'quasar'
3
4
  import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue'
4
- import UBtnIcon from './UBtnIcon.vue'
5
+ import { UBtnIcon, USheet, UMenuItem, UBtnStd } from '../../components'
6
+ import { useScreenType } from '../../composables/useScreenType'
5
7
 
6
8
  const emit = defineEmits(['open-expand'])
7
9
 
@@ -9,22 +11,58 @@ defineOptions({
9
11
  name: 'UBracket',
10
12
  })
11
13
 
14
+ const $q = useQuasar()
15
+ const $screen = useScreenType()
16
+
12
17
  const props = defineProps({
13
18
  bracketData: {
14
19
  type: Object,
15
20
  required: true,
16
21
  },
22
+ bracketSheetApplyLabel: { type: String, default: 'Select Bracket' },
23
+ bracketSheetCancelLabel: { type: String, default: 'Cancel' },
24
+ bracketSheetHeading: { type: String, default: 'Select Bracket Side' },
25
+ bracketTooltip: {
26
+ type: String,
27
+ default: 'Bracket',
28
+ },
29
+ expandTooltip: {
30
+ type: String,
31
+ default: 'Expand Bracket',
32
+ },
17
33
  hGap: {
18
34
  type: Number,
19
35
  default: 650,
20
36
  },
37
+ isExpandedView: {
38
+ type: Boolean,
39
+ default: false,
40
+ },
41
+ roundSheetApplyLabel: { type: String, default: 'Select Round' },
42
+ roundSheetCancelLabel: { type: String, default: 'Cancel' },
43
+ roundSheetHeading: { type: String, default: 'Select Round' },
44
+ roundTooltip: {
45
+ type: String,
46
+ default: 'Rounds',
47
+ },
48
+ teamSelectTooltip: {
49
+ type: String,
50
+ default: 'Select Team',
51
+ },
52
+ teamSheetApplyLabel: { type: String, default: 'Select Team' },
53
+ teamSheetCancelLabel: { type: String, default: 'Cancel' },
54
+ teamSheetHeading: { type: String, default: 'Select Team' },
21
55
  vGap: {
22
56
  type: Number,
23
57
  default: 50,
24
58
  },
25
- isExpandedView: {
26
- type: Boolean,
27
- default: false,
59
+ zoomInTooltip: {
60
+ type: String,
61
+ default: 'Zoom In',
62
+ },
63
+ zoomOutTooltip: {
64
+ type: String,
65
+ default: 'Zoom Out',
28
66
  },
29
67
  })
30
68
 
@@ -45,14 +83,50 @@ const nodeGridPositions = ref({})
45
83
  const openBracketMenu = ref(false)
46
84
  const openMenu = ref(false)
47
85
  const openRoundMenu = ref(false)
86
+ const roundSheet = ref([])
87
+ const bracketSheet = ref([])
88
+ const teamSheet = ref([])
89
+ const tempSelectedRoundId = ref('')
90
+ const tempSelectedBracketSide = ref('')
91
+
92
+ const applyRoundSheetSelection = () => {
93
+ const selectedRound = props.bracketData.round.find(
94
+ (r) => `${r.roundNo}-${r.columnNo}` === tempSelectedRoundId.value
95
+ )
96
+ if (selectedRound) scrollToRound(selectedRound)
97
+ roundSheet.value = []
98
+ }
99
+ const cancelRoundSheetSelection = () => {
100
+ roundSheet.value = []
101
+ }
102
+
103
+ const toggleBracketSideInSheet = (side) => {
104
+ tempSelectedBracketSide.value = side
105
+ }
106
+
107
+ const toggleRoundInSheet = (round) => {
108
+ tempSelectedRoundId.value = `${round.roundNo}-${round.columnNo}`
109
+ }
110
+
111
+ const applyBracketSheetSelection = () => {
112
+ scrollToBracketSide(tempSelectedBracketSide.value)
113
+ bracketSheet.value = []
114
+ }
115
+ const cancelBracketSheetSelection = () => {
116
+ bracketSheet.value = []
117
+ }
118
+ const openRoundSheet = () => {
119
+ tempSelectedRoundId.value = ''
120
+ roundSheet.value = [{ open: true, height: $q.screen.height - 150 }]
121
+ }
122
+ const openBracketSheet = () => {
123
+ bracketSheet.value = [{ open: true, height: 200 }]
124
+ }
48
125
  const selectedTeamId = ref('')
49
126
  const svgRef = ref(null)
127
+ const tempSelectedTeamId = ref('')
50
128
  const zoomRef = ref(null)
51
129
 
52
- const openExpandView = () => {
53
- emit('open-expand')
54
- }
55
-
56
130
  // Flatten for rendering logic
57
131
  const games = computed(() =>
58
132
  props.bracketData.round.flatMap((r, i) =>
@@ -138,6 +212,17 @@ const toolbarTeams = computed(() => {
138
212
  return seedTeams
139
213
  })
140
214
 
215
+ const applyTeamSheetSelection = () => {
216
+ selectedTeamId.value = tempSelectedTeamId.value
217
+ teamSheet.value = []
218
+ renderBracket()
219
+ }
220
+
221
+ const cancelTeamSheetSelection = () => {
222
+ teamSheet.value = []
223
+ tempSelectedTeamId.value = selectedTeamId.value
224
+ }
225
+
141
226
  // Compute all node positions using a row/column approach
142
227
  const computeBracketLayout = () => {
143
228
  const newPositions = {}
@@ -274,7 +359,6 @@ const descriptiveRoundName = (round) => {
274
359
  return `Winner ${round.roundName}`
275
360
  }
276
361
 
277
- // Convert row/col to x/y coordinates for rendering
278
362
  const getNodePosition = (id) => {
279
363
  const positions = nodeGridPositions.value
280
364
  if (id && positions[id]) {
@@ -356,6 +440,62 @@ const getTeamPath = (teamId) => {
356
440
  }
357
441
  }
358
442
 
443
+ const openExpandView = () => {
444
+ emit('open-expand')
445
+ }
446
+
447
+ const openTeamOptions = () => {
448
+ if ($screen.value.isMobile) {
449
+ tempSelectedTeamId.value = selectedTeamId.value
450
+ teamSheet.value = [{ open: true, height: $q.screen.height - 150 }]
451
+ return
452
+ }
453
+ }
454
+
455
+ // Placed this before onMounted and any watcher/event listener usage
456
+ const reinitZoomAndLayout = () => {
457
+ computeBracketLayout()
458
+ const viewportWidth = svgRef.value.clientWidth
459
+ const viewportHeight = svgRef.value.clientHeight
460
+ const uniqueColumns = [
461
+ ...new Set(props.bracketData.round.map((r) => r.columnNo)),
462
+ ]
463
+ const numColumns = uniqueColumns.length
464
+ const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
465
+ let maxRow = 0
466
+ Object.values(nodeGridPositions.value).forEach(({ row }) => {
467
+ if (row > maxRow) maxRow = row
468
+ })
469
+ const contentHeight = 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
470
+
471
+ // Update zoom clamping
472
+ const zoomBehavior = zoomRef.value
473
+ if (zoomBehavior) {
474
+ zoomBehavior.on('zoom', (event) => {
475
+ let { transform } = event
476
+ const { k } = transform
477
+
478
+ const minTx = viewportWidth - contentWidth * k
479
+ const maxTx = 0
480
+ const minTy = viewportHeight - contentHeight * k
481
+ const maxTy = 0
482
+
483
+ const tx =
484
+ minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
485
+ const ty =
486
+ minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
487
+
488
+ const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
489
+
490
+ gRef.value && d3.select(gRef.value).attr('transform', clampedTransform)
491
+ currentTransform.value = clampedTransform
492
+ })
493
+ }
494
+
495
+ // Re-render the bracket
496
+ renderBracket()
497
+ }
498
+
359
499
  const renderBracket = () => {
360
500
  computeBracketLayout()
361
501
  const g = d3.select(gRef.value)
@@ -553,7 +693,7 @@ const renderBracket = () => {
553
693
  .attr('height', 24)
554
694
  .style('cursor', 'pointer')
555
695
  .append('title')
556
- .text('Match Date')
696
+ .text('Dallas, TX')
557
697
  }
558
698
  })
559
699
 
@@ -696,14 +836,14 @@ const renderBracket = () => {
696
836
 
697
837
  // After all edges are drawn, move highlighted edges to top
698
838
  nextTick(() => {
699
- const gEl = gRef.value;
839
+ const gEl = gRef.value
700
840
  if (gEl) {
701
- const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted');
841
+ const highlightedEdges = gEl.querySelectorAll('.edge-path.highlighted')
702
842
  highlightedEdges.forEach((el) => {
703
- gEl.appendChild(el);
704
- });
843
+ gEl.appendChild(el)
844
+ })
705
845
  }
706
- });
846
+ })
707
847
  }
708
848
 
709
849
  const scrollToBracketSide = (side) => {
@@ -786,6 +926,14 @@ const setSelectedTeamId = (teamId) => {
786
926
  openMenu.value = false
787
927
  }
788
928
 
929
+ const toggleTeamInSheet = (teamId) => {
930
+ if (teamId === tempSelectedTeamId.value) {
931
+ tempSelectedTeamId.value = ''
932
+ return
933
+ }
934
+ tempSelectedTeamId.value = teamId
935
+ }
936
+
789
937
  const zoomIn = () => {
790
938
  const svg = d3.select(svgRef.value)
791
939
  if (zoomRef.value) {
@@ -816,50 +964,6 @@ const zoomOut = () => {
816
964
  }
817
965
  }
818
966
 
819
- // Place this before onMounted and any watcher/event listener usage
820
- const reinitZoomAndLayout = () => {
821
- computeBracketLayout()
822
- const viewportWidth = svgRef.value.clientWidth
823
- const viewportHeight = svgRef.value.clientHeight
824
- const uniqueColumns = [
825
- ...new Set(props.bracketData.round.map((r) => r.columnNo)),
826
- ]
827
- const numColumns = uniqueColumns.length
828
- const contentWidth = 50 + (numColumns - 1) * props.hGap + nodeWidth + 50
829
- let maxRow = 0
830
- Object.values(nodeGridPositions.value).forEach(({ row }) => {
831
- if (row > maxRow) maxRow = row
832
- })
833
- const contentHeight = 50 + (maxRow - 1) * props.vGap + nodeHeight + 50
834
-
835
- // Update zoom clamping
836
- const zoomBehavior = zoomRef.value
837
- if (zoomBehavior) {
838
- zoomBehavior.on('zoom', (event) => {
839
- let { transform } = event
840
- const { k } = transform
841
-
842
- const minTx = viewportWidth - contentWidth * k
843
- const maxTx = 0
844
- const minTy = viewportHeight - contentHeight * k
845
- const maxTy = 0
846
-
847
- const tx =
848
- minTx > 0 ? minTx / 2 : Math.max(minTx, Math.min(maxTx, transform.x))
849
- const ty =
850
- minTy > 0 ? minTy / 2 : Math.max(minTy, Math.min(maxTy, transform.y))
851
-
852
- const clampedTransform = d3.zoomIdentity.translate(tx, ty).scale(k)
853
-
854
- gRef.value && d3.select(gRef.value).attr('transform', clampedTransform)
855
- currentTransform.value = clampedTransform
856
- })
857
- }
858
-
859
- // Re-render the bracket
860
- renderBracket()
861
- }
862
-
863
967
  onMounted(() => {
864
968
  // Initial layout computation to determine extents
865
969
  computeBracketLayout()
@@ -1021,7 +1125,7 @@ onUnmounted(() => {
1021
1125
  :anchor="anchor"
1022
1126
  :self="self"
1023
1127
  size="md"
1024
- tooltip="Zoom Out"
1128
+ :tooltip="zoomOutTooltip"
1025
1129
  @click="zoomOut"
1026
1130
  ></UBtnIcon>
1027
1131
  <UBtnIcon
@@ -1030,16 +1134,17 @@ onUnmounted(() => {
1030
1134
  :anchor="anchor"
1031
1135
  :self="self"
1032
1136
  size="md"
1033
- tooltip="Zoom In"
1137
+ :tooltip="zoomInTooltip"
1034
1138
  @click="zoomIn"
1035
1139
  ></UBtnIcon>
1036
1140
  <UBtnIcon
1141
+ v-if="!$screen.isMobile"
1037
1142
  iconClass="fa-kit-duotone fa-bracket"
1038
1143
  ariaLabel="Rounds"
1039
1144
  :anchor="anchor"
1040
1145
  :self="self"
1041
1146
  size="md"
1042
- tooltip="Rounds"
1147
+ :tooltip="roundTooltip"
1043
1148
  >
1044
1149
  <template v-slot:menu>
1045
1150
  <q-menu v-model="openRoundMenu">
@@ -1062,12 +1167,23 @@ onUnmounted(() => {
1062
1167
  </template>
1063
1168
  </UBtnIcon>
1064
1169
  <UBtnIcon
1170
+ v-if="$screen.isMobile"
1171
+ iconClass="fa-kit-duotone fa-bracket"
1172
+ ariaLabel="Rounds"
1173
+ :anchor="anchor"
1174
+ :self="self"
1175
+ size="md"
1176
+ :tooltip="roundTooltip"
1177
+ @click="openRoundSheet"
1178
+ />
1179
+ <UBtnIcon
1180
+ v-if="!$screen.isMobile"
1065
1181
  iconClass="fa-kit-duotone fa-game-center"
1066
1182
  ariaLabel="Bracket"
1067
1183
  :anchor="anchor"
1068
1184
  :self="self"
1069
1185
  size="md"
1070
- tooltip="Bracket"
1186
+ :tooltip="bracketTooltip"
1071
1187
  >
1072
1188
  <template v-slot:menu>
1073
1189
  <q-menu v-model="openBracketMenu">
@@ -1086,12 +1202,23 @@ onUnmounted(() => {
1086
1202
  </template>
1087
1203
  </UBtnIcon>
1088
1204
  <UBtnIcon
1205
+ v-if="$screen.isMobile"
1206
+ iconClass="fa-kit-duotone fa-game-center"
1207
+ ariaLabel="Bracket"
1208
+ :anchor="anchor"
1209
+ :self="self"
1210
+ size="md"
1211
+ :tooltip="bracketTooltip"
1212
+ @click="openBracketSheet"
1213
+ />
1214
+ <UBtnIcon
1215
+ v-if="!$screen.isMobile"
1089
1216
  iconClass="fa-kit-duotone fa-jersey"
1090
1217
  ariaLabel="Select Team"
1091
1218
  :anchor="anchor"
1092
1219
  :self="self"
1093
1220
  size="md"
1094
- tooltip="Select Team"
1221
+ :tooltip="selectTeamTooltip"
1095
1222
  >
1096
1223
  <template v-slot:menu>
1097
1224
  <q-menu v-model="openMenu">
@@ -1112,6 +1239,17 @@ onUnmounted(() => {
1112
1239
  </q-menu>
1113
1240
  </template>
1114
1241
  </UBtnIcon>
1242
+ <UBtnIcon
1243
+ v-if="$screen.isMobile"
1244
+ iconClass="fa-kit-duotone fa-jersey"
1245
+ ariaLabel="Select Team"
1246
+ :anchor="anchor"
1247
+ :self="self"
1248
+ size="md"
1249
+ :tooltip="selectTeamTooltip"
1250
+ @click="openTeamOptions"
1251
+ >
1252
+ </UBtnIcon>
1115
1253
  <UBtnIcon
1116
1254
  v-if="!props.isExpandedView"
1117
1255
  iconClass="fa-kit-duotone fa-expand"
@@ -1119,12 +1257,154 @@ onUnmounted(() => {
1119
1257
  :anchor="anchor"
1120
1258
  :self="self"
1121
1259
  size="md"
1122
- tooltip="Expand Bracket"
1260
+ :tooltip="expandTooltip"
1123
1261
  @click="openExpandView"
1124
1262
  ></UBtnIcon>
1125
1263
  </div>
1126
1264
  </div>
1127
1265
  </div>
1266
+ <USheet
1267
+ v-model:dialogs="teamSheet"
1268
+ data-test-id="team-selection-sheet"
1269
+ dialog-class="bracket-sheet"
1270
+ :heading="props.teamSheetHeading"
1271
+ show-action-buttons
1272
+ >
1273
+ <template #content>
1274
+ <div>
1275
+ <template v-for="team in toolbarTeams" :key="team.id">
1276
+ <UMenuItem
1277
+ class="season-menu-item"
1278
+ :data-test-id="`season-sheet-item-${team.value}`"
1279
+ in-sheet
1280
+ :label="team.label"
1281
+ :selected="tempSelectedTeamId === team.id"
1282
+ @onClick="toggleTeamInSheet(team.id)"
1283
+ >
1284
+ <template #trailing_slot>
1285
+ <q-icon
1286
+ v-if="tempSelectedTeamId === team.id"
1287
+ class="fa-kit-duotone fa-circle-check"
1288
+ color="primary"
1289
+ />
1290
+ </template>
1291
+ </UMenuItem>
1292
+ </template>
1293
+ </div>
1294
+ </template>
1295
+ <template #action_primary_one>
1296
+ <UBtnStd
1297
+ color="primary"
1298
+ data-test-id="team-sheet-cancel-button"
1299
+ :label="props.teamSheetCancelLabel"
1300
+ outline
1301
+ @onClick="cancelTeamSheetSelection"
1302
+ />
1303
+ </template>
1304
+ <template #action_primary_two>
1305
+ <UBtnStd
1306
+ color="primary"
1307
+ data-test-id="team-sheet-apply-button"
1308
+ :label="props.teamSheetApplyLabel"
1309
+ @onClick="applyTeamSheetSelection"
1310
+ />
1311
+ </template>
1312
+ </USheet>
1313
+ <USheet
1314
+ v-model:dialogs="roundSheet"
1315
+ dialog-class="bracket-sheet"
1316
+ :heading="props.roundSheetHeading"
1317
+ show-action-buttons
1318
+ >
1319
+ <template #content>
1320
+ <div>
1321
+ <template
1322
+ v-for="round in bracketData.round.filter(
1323
+ (r) => r.roundName && r.roundName.trim() !== ''
1324
+ )"
1325
+ :key="`${round.roundNo}-${round.columnNo}`"
1326
+ >
1327
+ <UMenuItem
1328
+ class="season-menu-item"
1329
+ :label="descriptiveRoundName(round)"
1330
+ :selected="
1331
+ tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1332
+ "
1333
+ in-sheet
1334
+ @onClick="toggleRoundInSheet(round)"
1335
+ >
1336
+ <template #trailing_slot>
1337
+ <q-icon
1338
+ v-if="
1339
+ tempSelectedRoundId === `${round.roundNo}-${round.columnNo}`
1340
+ "
1341
+ class="fa-kit-duotone fa-circle-check"
1342
+ color="primary"
1343
+ />
1344
+ </template>
1345
+ </UMenuItem>
1346
+ </template>
1347
+ </div>
1348
+ </template>
1349
+ <template #action_primary_one>
1350
+ <UBtnStd
1351
+ color="primary"
1352
+ :label="props.roundSheetCancelLabel"
1353
+ outline
1354
+ @onClick="cancelRoundSheetSelection"
1355
+ />
1356
+ </template>
1357
+ <template #action_primary_two>
1358
+ <UBtnStd
1359
+ color="primary"
1360
+ :label="props.roundSheetApplyLabel"
1361
+ @onClick="applyRoundSheetSelection"
1362
+ />
1363
+ </template>
1364
+ </USheet>
1365
+ <USheet
1366
+ v-model:dialogs="bracketSheet"
1367
+ dialog-class="bracket-sheet"
1368
+ :heading="props.bracketSheetHeading"
1369
+ show-action-buttons
1370
+ >
1371
+ <template #content>
1372
+ <div>
1373
+ <template v-for="side in bracketSides" :key="side.value">
1374
+ <UMenuItem
1375
+ class="season-menu-item"
1376
+ in-sheet
1377
+ :label="side.label"
1378
+ :selected="tempSelectedBracketSide === side.value"
1379
+ @onClick="toggleBracketSideInSheet(side.value)"
1380
+ >
1381
+ <template #trailing_slot>
1382
+ <q-icon
1383
+ v-if="tempSelectedBracketSide === side.value"
1384
+ class="fa-kit-duotone fa-circle-check"
1385
+ color="primary"
1386
+ />
1387
+ </template>
1388
+ </UMenuItem>
1389
+ </template>
1390
+ </div>
1391
+ </template>
1392
+ <template #action_primary_one>
1393
+ <UBtnStd
1394
+ color="primary"
1395
+ :label="props.bracketSheetCancelLabel"
1396
+ outline
1397
+ @onClick="cancelBracketSheetSelection"
1398
+ />
1399
+ </template>
1400
+ <template #action_primary_two>
1401
+ <UBtnStd
1402
+ color="primary"
1403
+ :label="props.bracketSheetApplyLabel"
1404
+ @onClick="applyBracketSheetSelection"
1405
+ />
1406
+ </template>
1407
+ </USheet>
1128
1408
  </template>
1129
1409
 
1130
1410
  <style scoped lang="sass">
@@ -1181,6 +1461,12 @@ svg
1181
1461
  height: 100% /* Make SVG fill the container */
1182
1462
  display: block
1183
1463
 
1464
+ .bracket-sheet
1465
+ .main-content-dialog
1466
+ padding-top: $ba !important
1467
+ .q-item
1468
+ margin-bottom: $xs
1469
+
1184
1470
  .bracket-toolbar
1185
1471
  bottom: 1rem /* Position from the bottom */
1186
1472
  left: 50% /* Center horizontally */
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
2
  import { useQuasar } from 'quasar'
3
- import { computed, ref } from 'vue'
3
+ import { computed, ref, watch } from 'vue'
4
+ import heic2any from 'heic2any'
4
5
  import { fixStringLength, formatDate, getFileCategory } from '../../utils/data'
5
6
  import UBtnIcon from './UBtnIcon.vue'
6
7
  import UBtnStd from './UBtnStd.vue'
@@ -8,6 +9,7 @@ import UInputTextStd from './UInputTextStd.vue'
8
9
  import UTooltip from './UTooltip.vue'
9
10
 
10
11
  const emit = defineEmits([
12
+ 'conversionStatus',
11
13
  'onUploadFactory',
12
14
  'onViewFile',
13
15
  'getFilesOnAdded',
@@ -29,6 +31,10 @@ const props = defineProps({
29
31
  type: String,
30
32
  default: 'Cancel',
31
33
  },
34
+ conversionText: {
35
+ type: String,
36
+ default: 'Converting into jpeg image',
37
+ },
32
38
  dataTestId: {
33
39
  type: String,
34
40
  default: 'uploader',
@@ -116,9 +122,18 @@ const props = defineProps({
116
122
 
117
123
  const $q = useQuasar()
118
124
 
125
+ const filesConverting = ref([]) // track converting state per file
119
126
  const fileDisplayName = ref([])
120
127
  const isEditing = ref([])
121
128
  const uploaderRef = ref(null)
129
+ const autoUploadRef = computed({
130
+ get() {
131
+ return props.autoUpload
132
+ },
133
+ set(val) {
134
+ return val
135
+ },
136
+ })
122
137
  const isSmallWidthDevices = computed(() => $q.screen.width < 350)
123
138
  const uploadedFiles = computed({
124
139
  get() {
@@ -143,13 +158,69 @@ const handleViewClick = (file) => {
143
158
  return emit('onViewFile', file)
144
159
  }
145
160
 
146
- const onFileAdded = (files) => {
147
- files.forEach((file, index) => {
148
- files[index]['displayName'] = files[index]['name']
149
- fileDisplayName.value[file['__key']] = files[index]['displayName']
150
- })
161
+ const onFileAdded = async (files) => {
162
+ try {
163
+ const uploader = uploaderRef.value
164
+ if (!uploader) return // uploader destroyed
165
+
166
+ const convertedFiles = []
167
+
168
+ const heicConversions = files.map(async (file) => {
169
+ const key = file.__key
170
+ const isHeic =
171
+ /\.(heic|heif)$/i.test(file.name) ||
172
+ file.type === 'image/heic' ||
173
+ file.type === 'image/heif'
174
+
175
+ if (!isHeic) return
176
+
177
+ // Add to converting array
178
+ filesConverting.value.push({ key, name: file.name, converting: true })
179
+
180
+ // Safely remove HEIC file
181
+ if (uploaderRef.value) {
182
+ const idx = uploader.files.findIndex((f) => f.__key === key)
183
+ if (idx !== -1) uploader.removeFile(uploader.files[idx])
184
+ }
185
+
186
+ try {
187
+ const convertedBlob = await heic2any({
188
+ blob: file,
189
+ toType: 'image/jpeg',
190
+ quality: 0.8,
191
+ })
192
+
193
+ // Skip if uploader closed while converting
194
+ if (!uploaderRef.value) return
195
+
196
+ const newFile = new File(
197
+ [convertedBlob],
198
+ file.name.replace(/\.(heic|heif)$/i, '.jpeg'),
199
+ { type: 'image/jpeg' }
200
+ )
201
+
202
+ newFile.__key = key
203
+ newFile.displayName = newFile.name
204
+ convertedFiles.push(newFile)
205
+ } catch (err) {
206
+ console.error('HEIC conversion failed:', err)
207
+ } finally {
208
+ const index = filesConverting.value.findIndex((f) => f.key === key)
209
+ if (index !== -1) filesConverting.value.splice(index, 1)
210
+ }
211
+ })
212
+
213
+ await Promise.allSettled(heicConversions)
214
+
215
+ // Safely add converted files
216
+ if (convertedFiles.length > 0 && uploaderRef.value) {
217
+ uploaderRef.value.addFiles(convertedFiles)
218
+ }
151
219
 
152
- return emit('getFilesOnAdded', uploaderRef.value.files)
220
+ emit('getFilesOnAdded', uploaderRef.value?.files || [])
221
+ } catch (err) {
222
+ console.warn('error during conversion:', err.message || err)
223
+ }
153
224
  }
154
225
 
155
226
  const onFileUploaded = ({ files, xhr }) => {
@@ -176,7 +247,6 @@ const removeFile = (payload) => {
176
247
  if (uploadedIndex !== -1) {
177
248
  const updated = [...uploadedFiles.value]
178
249
  updated.splice(uploadedIndex, 1)
179
-
180
250
  emit('update:uploadedFiles', updated)
181
251
  }
182
252
 
@@ -195,6 +265,8 @@ const scopedFiles = (files) => {
195
265
  __status: 'uploaded',
196
266
  uploaded: true,
197
267
  __key: uploadedFile.__key || uploadedFile.name,
268
+ displayName: uploadedFile.displayName || uploadedFile.name,
269
+ name: uploadedFile.displayName || uploadedFile.name,
198
270
  })
199
271
  }
200
272
  })
@@ -217,6 +289,11 @@ const upload = () => {
217
289
  uploaderRef.value.upload()
218
290
  }
219
291
 
292
+ watch(
293
+ () => filesConverting.value.length,
294
+ (newVal) => emit('conversionStatus', newVal > 0)
295
+ )
296
+
220
297
  defineExpose({ upload })
221
298
  </script>
222
299
  <template>
@@ -224,7 +301,7 @@ defineExpose({ upload })
224
301
  <q-uploader
225
302
  v-bind="$attrs"
226
303
  class="u-uploader"
227
- :auto-upload="autoUpload"
304
+ :auto-upload="autoUploadRef"
228
305
  :batch="batch"
229
306
  :dataTestId="dataTestId"
230
307
  label="Drag and drop your file or select choose file."
@@ -239,55 +316,69 @@ defineExpose({ upload })
239
316
  >
240
317
  <template v-slot:header="scope">
241
318
  <div
242
- v-if="scope.canAddFiles && !uploadedFiles.length"
243
- class="q-py-md q-px-xl bg-neutral-2 uploader-content"
319
+ v-if="filesConverting.some((f) => f.converting)"
320
+ class="row items-center justify-between uploaded-container"
244
321
  >
245
- <div class="text-center">
246
- <img
247
- alt="Upload Files"
248
- src="../../assets/upload-illustration.svg"
249
- />
250
- <div class="text-body-md q-mt-ba" dataTestId="description">
251
- {{ description }}
252
- </div>
322
+ <div class="row items-center">
323
+ <q-spinner class="q-mr-sm" color="black" size="20px" />
324
+ <span class="text-black">
325
+ {{ conversionText }}
326
+ </span>
253
327
  </div>
254
- <div class="list-items row">
255
- <UBtnStd
256
- class="q-mt-md selectFileBtn"
257
- :aria-label="selectFileBtnLabel"
258
- color="primary"
259
- dataTestId="select-file-btn"
260
- :disable="selectFileBtnDisable"
261
- :full-width="true"
262
- :label="selectFileBtnLabel"
263
- size="lg"
264
- @click="scope.pickFiles"
265
- >
266
- {{ selectFileBtnLabel }}
267
- <q-uploader-add-trigger
268
- v-if="!selectFileBtnDisable"
269
- :aria-label="selectFileBtnLabel"
270
- />
271
- <UTooltip
272
- v-if="selectFileBtnTooltip?.length"
273
- anchor="bottom middle"
274
- :description="selectFileBtnTooltip"
275
- :offset="[4, 4]"
276
- self="top middle"
328
+ </div>
329
+
330
+ <div v-else>
331
+ <div
332
+ v-if="scope.canAddFiles && !uploadedFiles.length"
333
+ class="q-py-md q-px-xl bg-neutral-2 uploader-content"
334
+ >
335
+ <div class="text-center">
336
+ <img
337
+ alt="Upload Files"
338
+ src="../../assets/upload-illustration.svg"
277
339
  />
278
- </UBtnStd>
279
- <UBtnStd
280
- v-if="scope.canUpload && showUploadBtn"
281
- class="q-mt-md q-mb-md"
282
- color="primary"
283
- dataTestId="upload-btn"
284
- flat
285
- icon="cloud_upload"
286
- size="sm"
287
- @click="scope.upload"
288
- >
289
- <UTooltip description="Upload Files" />
290
- </UBtnStd>
340
+ <div class="text-body-md q-mt-ba" dataTestId="description">
341
+ {{ description }}
342
+ </div>
343
+ </div>
344
+ <div class="list-items row">
345
+ <UBtnStd
346
+ class="q-mt-md selectFileBtn"
347
+ :aria-label="selectFileBtnLabel"
348
+ color="primary"
349
+ dataTestId="select-file-btn"
350
+ :disable="selectFileBtnDisable"
351
+ :full-width="true"
352
+ :label="selectFileBtnLabel"
353
+ size="lg"
354
+ @click="scope.pickFiles"
355
+ >
356
+ {{ selectFileBtnLabel }}
357
+ <q-uploader-add-trigger
358
+ v-if="!selectFileBtnDisable"
359
+ :aria-label="selectFileBtnLabel"
360
+ />
361
+ <UTooltip
362
+ v-if="selectFileBtnTooltip?.length"
363
+ anchor="bottom middle"
364
+ :description="selectFileBtnTooltip"
365
+ :offset="[4, 4]"
366
+ self="top middle"
367
+ />
368
+ </UBtnStd>
369
+ <UBtnStd
370
+ v-if="scope.canUpload && showUploadBtn"
371
+ class="q-mt-md q-mb-md"
372
+ color="primary"
373
+ dataTestId="upload-btn"
374
+ flat
375
+ icon="cloud_upload"
376
+ size="sm"
377
+ @click="scope.upload"
378
+ >
379
+ <UTooltip description="Upload Files" />
380
+ </UBtnStd>
381
+ </div>
291
382
  </div>
292
383
  </div>
293
384
  </template>
package/src/utils/data.ts CHANGED
@@ -24,7 +24,7 @@ export const fixStringLength = (str: string, length: number) => {
24
24
  if (str?.length > length) {
25
25
  return str.substring(0, length) + '....'
26
26
  } else {
27
- return str.padEnd(length, ' ')
27
+ return str?.padEnd(length, ' ')
28
28
  }
29
29
  }
30
30
 
@@ -55,6 +55,8 @@ export const getFileCategory = (mimeType) => {
55
55
  'image/bmp': 'image',
56
56
  'image/svg+xml': 'image',
57
57
  'image/webp': 'image',
58
+ 'image/heic': 'image',
59
+ 'image/heif': 'image',
58
60
 
59
61
  // Video MIME types
60
62
  'video/mp4': 'video',