@7365admin1/layer-common 1.10.8 → 1.10.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @iservice365/layer-common
2
2
 
3
+ ## 1.10.9
4
+
5
+ ### Patch Changes
6
+
7
+ - added web security feedbacks
8
+
3
9
  ## 1.10.8
4
10
 
5
11
  ### Patch Changes
@@ -333,7 +333,7 @@
333
333
  <v-col cols="12">
334
334
  <InputLabel
335
335
  class="text-capitalize font-weight-bold"
336
- title="Building"
336
+ title="Block"
337
337
  />
338
338
  <v-autocomplete
339
339
  v-model="card.assignBuilding"
@@ -12,7 +12,7 @@
12
12
  <v-col cols="12" class="px-1">
13
13
  <InputLabel
14
14
  class="text-capitalize font-weight-bold"
15
- title="Building"
15
+ title="Block"
16
16
  required
17
17
  />
18
18
  <v-autocomplete
@@ -151,7 +151,7 @@ const props = defineProps({
151
151
  type: Array as PropType<Array<Record<string, any>>>,
152
152
  default: () => [
153
153
  {
154
- title: "Building",
154
+ title: "Block",
155
155
  value: "block.name",
156
156
  },
157
157
  {
@@ -0,0 +1,474 @@
1
+ <template>
2
+ <v-carousel
3
+ eager
4
+ :show-arrows="mediaItems && mediaItems.length > 1"
5
+ v-if="!isConverting"
6
+ :height="height || 400"
7
+ >
8
+ <template v-for="(item, idx) in mediaItems" :key="idx">
9
+ <!-- IMAGE -->
10
+ <v-carousel-item v-if="item.type === 'image'">
11
+ <v-img
12
+ :src="item.url"
13
+ class="cursor-pointer"
14
+ cover
15
+ height="100%"
16
+ @click.stop="openMediaDialog(item.url)"
17
+ >
18
+ <template #placeholder>
19
+ <v-skeleton-loader height="100%" width="100%" />
20
+ </template>
21
+ </v-img>
22
+
23
+ <div class="edit-btn mb-5" v-if="imgEditable">
24
+ <v-btn
25
+ color="primary"
26
+ height="42px"
27
+ width="100px"
28
+ @click.stop="onMediaEdit(item.url, idx)"
29
+ >
30
+ Edit
31
+ </v-btn>
32
+ </div>
33
+
34
+ <div class="delete-btn mb-3" v-if="imgDelete">
35
+ <v-btn
36
+ color="red"
37
+ height="42px"
38
+ width="100px"
39
+ @click.stop="onMediaDelete(item.url)"
40
+ >
41
+ Delete
42
+ </v-btn>
43
+ </div>
44
+ </v-carousel-item>
45
+
46
+ <!-- VIDEO -->
47
+ <v-carousel-item v-else-if="item.type === 'video'">
48
+ <div
49
+ class="media-fill cursor-pointer"
50
+ @click.stop="openMediaDialog(item.url)"
51
+ >
52
+ <video class="media-video" muted playsinline preload="metadata">
53
+ <source :src="item.url" />
54
+ </video>
55
+ <div class="play-overlay">
56
+ <v-btn class="play-btn" icon size="55" color="primary">
57
+ <v-icon icon="mdi-play" size="40" />
58
+ </v-btn>
59
+ </div>
60
+
61
+ <div class="delete-btn pb-4" v-if="imgDelete">
62
+ <v-btn
63
+ color="red"
64
+ height="42px"
65
+ width="100px"
66
+ @click.stop="onMediaDelete(item.url)"
67
+ >
68
+ Delete
69
+ </v-btn>
70
+ </div>
71
+ </div>
72
+ </v-carousel-item>
73
+
74
+ <!-- DOCUMENT -->
75
+ <v-carousel-item v-else>
76
+ <div
77
+ class="media-fill d-flex flex-column align-center justify-center cursor-pointer"
78
+ @click.stop="
79
+ downloadFile({ name: item.name, url: item.url, type: item.type })
80
+ "
81
+ >
82
+ <v-icon
83
+ :icon="getFileIcon(item.type)"
84
+ size="150"
85
+ :color="getFileIconColor(getFileIcon(item.type))"
86
+ />
87
+ <div class="text-h6 text-center text-wrap mt-2">{{ item.name }}</div>
88
+
89
+ <div class="delete-btn pb-4" v-if="imgDelete">
90
+ <v-btn
91
+ color="red"
92
+ height="42px"
93
+ width="100px"
94
+ @click.stop="onMediaDelete(item.url)"
95
+ >
96
+ Delete
97
+ </v-btn>
98
+ </div>
99
+ </div>
100
+ </v-carousel-item>
101
+ </template>
102
+ </v-carousel>
103
+
104
+ <v-skeleton-loader height="400px" width="100%" v-else />
105
+
106
+ <!-- Dialog for fullscreen view -->
107
+ <v-dialog v-model="isDialogOpen" max-width="700">
108
+ <v-card>
109
+ <v-btn icon class="close-btn" @click="isDialogOpen = false">
110
+ <v-icon icon="mdi-close" size="40" />
111
+ </v-btn>
112
+
113
+ <!-- Image preview -->
114
+ <v-img
115
+ v-if="selectedMediaType === 'image'"
116
+ :src="selectedMediaUrl"
117
+ contain
118
+ />
119
+
120
+ <!-- Video preview with playback controls -->
121
+ <div
122
+ v-else-if="selectedMediaType === 'video'"
123
+ class="dialog-video-container"
124
+ >
125
+ <video autoplay controls class="full-size-video" @click.stop>
126
+ <source :src="selectedMediaUrl" />
127
+ </video>
128
+ </div>
129
+ </v-card>
130
+ </v-dialog>
131
+
132
+ <!-- Image editor component -->
133
+ <DrawImage
134
+ v-if="isShowImageEdit"
135
+ :is-show-dialog="isShowImageEdit"
136
+ :image-url="imageUrl"
137
+ :image-idx="imageIdx"
138
+ @on-submit="onImageSubmitEdit"
139
+ @on-close-dialog="isShowImageEdit = false"
140
+ />
141
+ <Snackbar v-model="messageSnackbar" :text="message" :color="messageColor" />
142
+ </template>
143
+
144
+ <script setup lang="ts">
145
+ const { fileToBase64 } = useUtils();
146
+
147
+ const emit = defineEmits([
148
+ "on-click-carousel",
149
+ "on-file-edit",
150
+ "on-file-delete",
151
+ ]);
152
+
153
+ const props = defineProps<{
154
+ height?: string | number;
155
+ urls?: string[];
156
+ clickable?: boolean;
157
+ imgEditable?: boolean;
158
+ dataFiles?: {
159
+ name: string;
160
+ data: File;
161
+ progress: number;
162
+ id?: string;
163
+ url?: string;
164
+ type?: string;
165
+ }[];
166
+ imgDelete?: boolean;
167
+ }>();
168
+
169
+ const isConverting = ref(false);
170
+ const isShowImageEdit = ref(false);
171
+ const imageUrl = ref();
172
+ const imageIdx = ref();
173
+ const videoRefs = ref([]);
174
+
175
+ // Media items with thumbnail property
176
+ const mediaItems = ref<
177
+ {
178
+ url: string;
179
+ name: string;
180
+ type: string;
181
+ mimeType?: string;
182
+ thumbnail?: string;
183
+ }[]
184
+ >([]);
185
+
186
+ // Dialog state
187
+ const isDialogOpen = ref(false);
188
+ const selectedMediaUrl = ref("");
189
+ const selectedMediaType = ref<string>("");
190
+ const selectedMediaMimeType = ref("");
191
+
192
+ function getFileType(file: any) {
193
+ return file.type.startsWith("video/")
194
+ ? "video"
195
+ : file.type.startsWith("image/")
196
+ ? "image"
197
+ : file.type;
198
+ }
199
+
200
+ function getMimeType(file: any) {
201
+ return file.type || "application/octet-stream";
202
+ }
203
+
204
+ // Edit functions
205
+ const onMediaEdit = (url: string, idx: number) => {
206
+ isShowImageEdit.value = true;
207
+ imageUrl.value = url;
208
+ imageIdx.value = idx;
209
+ };
210
+
211
+ const onMediaDelete = (url: string) => {
212
+ mediaItems.value = mediaItems.value.filter((file) => file.url !== url);
213
+ URL.revokeObjectURL(url);
214
+ emit("on-file-delete", url);
215
+ };
216
+
217
+ const onImageSubmitEdit = (url: string, idx: number) => {
218
+ if (mediaItems.value && mediaItems.value.length > 0) {
219
+ mediaItems.value[idx].url = url;
220
+ }
221
+ isShowImageEdit.value = false;
222
+ emit("on-file-edit", url, idx);
223
+ };
224
+
225
+ // Open dialog for fullscreen view
226
+ const openMediaDialog = (url: string) => {
227
+ const mediaItem = mediaItems.value.find((item) => item.url === url);
228
+ if (mediaItem) {
229
+ selectedMediaUrl.value = url;
230
+ selectedMediaType.value = mediaItem.type;
231
+ selectedMediaMimeType.value = mediaItem.mimeType || "";
232
+ isDialogOpen.value = true;
233
+ }
234
+ };
235
+
236
+ async function getMimeFromHead(url: string) {
237
+ const res = await fetch(url);
238
+ if (!res.ok) throw new Error(`HEAD failed: ${res.status}`);
239
+ return res.headers.get("content-type");
240
+ }
241
+
242
+ watchEffect(async (onCleanup) => {
243
+ let isCancelled = false;
244
+
245
+ onCleanup(() => {
246
+ isCancelled = true;
247
+ });
248
+
249
+ // Local temporary storage
250
+ const newMediaItems: any = [];
251
+
252
+ if (props.dataFiles && props.dataFiles.length > 0) {
253
+ isConverting.value = true;
254
+
255
+ for (const file of props.dataFiles) {
256
+ try {
257
+ let fileType;
258
+ let mimeType;
259
+ let finalUrl;
260
+
261
+ // 1. Process the file data
262
+ if ("type" in file && "size" in file) {
263
+ mimeType = getMimeType(file);
264
+ fileType = getFileType(file);
265
+
266
+ if (fileType === "video") {
267
+ finalUrl = URL.createObjectURL(file);
268
+ } else {
269
+ // Awaiting async conversion
270
+ finalUrl = await fileToBase64(file);
271
+ }
272
+ } else {
273
+ finalUrl = file.url;
274
+ fileType = getFileType(file);
275
+ mimeType = getMimeType(file);
276
+ }
277
+
278
+ // 2. Check if the watcher was reset during the 'await'
279
+ if (isCancelled) return;
280
+
281
+ // 3. Push to LOCAL array, not the reactive .value
282
+ newMediaItems.push({
283
+ url: finalUrl,
284
+ name: file?.name,
285
+ type: fileType,
286
+ mimeType: mimeType,
287
+ thumbnail: undefined,
288
+ });
289
+ } catch (error) {
290
+ console.error("Error processing file:", error);
291
+ }
292
+ }
293
+ } else if (props.urls && props.urls.length > 0) {
294
+ isConverting.value = true;
295
+
296
+ for (const url of props.urls) {
297
+ try {
298
+ const mimeType = await getMimeFromHead(url);
299
+ if (isCancelled) return;
300
+
301
+ newMediaItems.push({
302
+ url,
303
+ name: "",
304
+ type: mimeType
305
+ ? mimeType.startsWith("image/")
306
+ ? "image"
307
+ : mimeType.startsWith("video/")
308
+ ? "video"
309
+ : mimeType
310
+ : mimeType,
311
+ });
312
+ } catch (error) {
313
+ console.error("Error fetching MIME type:", error);
314
+ }
315
+ }
316
+ }
317
+
318
+ // 4. Finalizing state
319
+ if (!isCancelled) {
320
+ isConverting.value = false;
321
+ // Replace the entire array at once to avoid "doubling" or partial updates
322
+ mediaItems.value = newMediaItems;
323
+ console.log("watchEffect mediaItems updated:", mediaItems.value);
324
+ }
325
+ });
326
+
327
+ const getFileIcon = (type: string) => {
328
+ const iconMap: any = {
329
+ "application/pdf": "mdi-file-pdf-box",
330
+ "application/msword": "mdi-file-word-box",
331
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
332
+ "mdi-file-word-box",
333
+ "application/vnd.ms-excel": "mdi-file-excel-box",
334
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
335
+ "mdi-file-excel-box",
336
+ "application/vnd.ms-powerpoint": "mdi-file-powerpoint-box",
337
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation":
338
+ "mdi-file-powerpoint-box",
339
+ "text/plain": "mdi-file-document-outline",
340
+ "text/csv": "mdi-file-delimited-outline",
341
+ "application/zip": "mdi-zip-box-outline",
342
+ "application/x-rar-compressed": "mdi-zip-box-outline",
343
+ // Add more mappings as needed
344
+ };
345
+
346
+ return iconMap[type] || "mdi-file-outline";
347
+ };
348
+
349
+ function getFileIconColor(icon?: string) {
350
+ if (icon === "mdi-file-pdf-box") return "#F44336"; // Red
351
+ if (icon === "mdi-file-word-box") return "#2196F3"; // Blue
352
+ if (icon === "mdi-file-excel-box" || icon === "mdi-file-delimited-outline")
353
+ return "#4CAF50"; // Green
354
+ if (icon === "mdi-file-powerpoint-box") return "#FF9800"; // Orange
355
+ if (icon === "mdi-zip-box-outline") return "#9C27B0"; // Purple
356
+ return "grey";
357
+ }
358
+
359
+ const message = ref("");
360
+ const messageColor = ref("");
361
+ const messageSnackbar = ref(false);
362
+
363
+ function showMessage(msg: string, color: string) {
364
+ message.value = msg;
365
+ messageColor.value = color;
366
+ messageSnackbar.value = true;
367
+ }
368
+
369
+ async function downloadFile({
370
+ name,
371
+ url,
372
+ type,
373
+ }: {
374
+ name: string;
375
+ url: string;
376
+ type: string;
377
+ }) {
378
+ try {
379
+ const link = document.createElement("a");
380
+ link.href = url;
381
+ if (type == "application/pdf") {
382
+ link.target = "_blank";
383
+ }
384
+ link.rel = "noopener noreferrer";
385
+ link.download = name;
386
+
387
+ document.body.appendChild(link);
388
+ link.click();
389
+ document.body.removeChild(link);
390
+
391
+ URL.revokeObjectURL(url);
392
+ } catch (err) {
393
+ showMessage(err as string, "error");
394
+ }
395
+ }
396
+
397
+ // // Clean up object URLs when component is unmounted
398
+ // onBeforeUnmount(() => {
399
+ // // Revoke any object URLs to prevent memory leaks
400
+ // mediaItems.value.forEach((item) => {
401
+ // if (item.type === "video" && item.url && item.url.startsWith("blob:")) {
402
+ // URL.revokeObjectURL(item.url);
403
+ // }
404
+ // });
405
+ // });
406
+ </script>
407
+
408
+ <style scoped>
409
+ .edit-btn {
410
+ position: absolute;
411
+ bottom: 24%;
412
+ right: 5%;
413
+ z-index: 5;
414
+ }
415
+
416
+ .delete-btn {
417
+ position: absolute;
418
+ bottom: 13%;
419
+ right: 5%;
420
+ }
421
+
422
+ .close-btn {
423
+ position: absolute;
424
+ top: 10px;
425
+ right: 10px;
426
+ background-color: rgba(0, 0, 0, 0.5);
427
+ color: white;
428
+ z-index: 10;
429
+ }
430
+
431
+ .play-overlay {
432
+ position: absolute;
433
+ inset: 0;
434
+ display: flex;
435
+ align-items: center;
436
+ justify-content: center;
437
+ pointer-events: none;
438
+ }
439
+ .play-overlay .v-btn {
440
+ pointer-events: auto;
441
+ }
442
+
443
+ .play-btn {
444
+ border-radius: 9999px;
445
+ }
446
+
447
+ .dialog-video-container {
448
+ display: flex;
449
+ justify-content: center;
450
+ align-items: center;
451
+ width: 100%;
452
+ padding: 20px;
453
+ }
454
+
455
+ .full-size-video {
456
+ max-width: 100%;
457
+ max-height: 80vh;
458
+ }
459
+
460
+ .media-fill {
461
+ position: relative;
462
+ width: 100%;
463
+ height: 100%;
464
+ }
465
+
466
+ .media-video {
467
+ position: absolute;
468
+ inset: 0;
469
+ width: 100%;
470
+ height: 100%;
471
+ object-fit: cover;
472
+ display: block;
473
+ }
474
+ </style>
@@ -0,0 +1,172 @@
1
+ <template>
2
+ <v-dialog
3
+ v-model="isDialogVisible"
4
+ transition="dialog-top-transition"
5
+ max-width="1000"
6
+ >
7
+ <v-card class="pa-1">
8
+ <v-toolbar>
9
+ <v-spacer></v-spacer>
10
+ <v-btn icon="mdi-close" @click="emit('onCloseDialog')" />
11
+ </v-toolbar>
12
+ <canvas id="imageCanvas" />
13
+ <v-card-title class="text-end">
14
+ <v-btn
15
+ color="orange-darken-3"
16
+ size="small"
17
+ variant="flat"
18
+ style="height: 40px; margin-right: 12px"
19
+ @click="onClearDrawing()"
20
+ >
21
+ Clear Drawing
22
+ </v-btn>
23
+ <v-btn
24
+ color="blue-darken-3"
25
+ size="small"
26
+ variant="flat"
27
+ style="height: 40px"
28
+ @click="submit"
29
+ >
30
+ Submit
31
+ </v-btn>
32
+ </v-card-title>
33
+ </v-card>
34
+ </v-dialog>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ const { getImage } = useUtils();
39
+
40
+ const props = defineProps({
41
+ message: {
42
+ type: String,
43
+ },
44
+ isShowDialog: {
45
+ type: Boolean,
46
+ default: false,
47
+ },
48
+ imageUrl: {
49
+ type: String,
50
+ },
51
+ imageIdx: {
52
+ type: Number,
53
+ },
54
+ });
55
+ const emit = defineEmits<{
56
+ (event: "onSubmit", value: string, idx: number): void;
57
+ (event: "onCloseDialog"): void;
58
+ }>();
59
+
60
+ const isDialogVisible = computed(() => props.isShowDialog);
61
+
62
+ var canvas: any = document.getElementById("imageCanvas");
63
+ var ctx: any = "";
64
+ var img: any = "";
65
+ var pos: any = { x: 0, y: 0 };
66
+
67
+ onMounted(async () => {
68
+ canvas = document.getElementById("imageCanvas");
69
+ ctx = canvas?.getContext("2d");
70
+
71
+ var rect = canvas.getBoundingClientRect();
72
+ canvas.width = rect.width < 712 ? 712 : rect.width;
73
+ canvas.height = rect.height < 400 ? 400 : rect.height;
74
+
75
+ const response = await getImage(props.imageUrl!);
76
+ if (!response) return;
77
+ handleImage(response);
78
+
79
+ canvas.addEventListener("mousedown", setPosition);
80
+ canvas.addEventListener("mousemove", draw);
81
+ canvas.addEventListener(
82
+ "touchstart",
83
+ function (e: any) {
84
+ var touch = e.touches[0];
85
+ var mouseEvent = new MouseEvent("mousedown", {
86
+ buttons: 1,
87
+ clientX: touch.clientX,
88
+ clientY: touch.clientY,
89
+ });
90
+ canvas.dispatchEvent(mouseEvent);
91
+ },
92
+ false,
93
+ );
94
+ canvas.addEventListener(
95
+ "touchmove",
96
+ function (e: any) {
97
+ var touch = e.touches[0];
98
+ var mouseEvent = new MouseEvent("mousemove", {
99
+ buttons: 1,
100
+ clientX: touch.clientX,
101
+ clientY: touch.clientY,
102
+ });
103
+ canvas.dispatchEvent(mouseEvent);
104
+ },
105
+ false,
106
+ );
107
+
108
+ var imageCanvas: any = document.getElementById("imageCanvas");
109
+ imageCanvas.style.cursor = "crosshair";
110
+ });
111
+
112
+ function handleImage(image: Blob) {
113
+ var reader = new FileReader();
114
+ reader.onload = function (event: any) {
115
+ img = new Image();
116
+ img.onload = function () {
117
+ var ratio = this.height / this.width;
118
+ canvas.height = canvas.width * ratio;
119
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
120
+ };
121
+ img.src = event.target.result;
122
+ };
123
+ reader.readAsDataURL(image);
124
+ }
125
+
126
+ async function setPosition(e: any) {
127
+ var rect = canvas.getBoundingClientRect();
128
+
129
+ if (rect.width < 712 && rect.height < 400) {
130
+ var scaleX = 712 / rect.width;
131
+ var scaleY = 400 / rect.height;
132
+
133
+ pos.x = (e.clientX - rect.left) * scaleX;
134
+ pos.y = (e.clientY - rect.top) * scaleY;
135
+ return;
136
+ }
137
+
138
+ pos.x = e.clientX - rect.left;
139
+ pos.y = e.clientY - rect.top;
140
+ }
141
+
142
+ async function draw(e: any) {
143
+ canvas.style.cursor = "crosshair";
144
+
145
+ if (e.buttons !== 1) return;
146
+ if (pos.x === 0 && pos.y === 0) setPosition(e);
147
+
148
+ ctx.beginPath();
149
+ ctx.lineWidth = 5;
150
+ ctx.lineCap = "round";
151
+ ctx.strokeStyle = "#FF0000";
152
+ ctx.moveTo(pos.x, pos.y);
153
+ setPosition(e);
154
+ ctx.lineTo(pos.x, pos.y);
155
+ ctx.stroke();
156
+ ctx.closePath();
157
+ }
158
+
159
+ const submit = () => {
160
+ canvas.toBlob((blob: any) => {
161
+ const url = URL.createObjectURL(blob);
162
+ console.log("URL", url);
163
+
164
+ emit("onSubmit", url, props.imageIdx!);
165
+ });
166
+ };
167
+
168
+ async function onClearDrawing() {
169
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
170
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
171
+ }
172
+ </script>