Browse Source

fix: reload row if the attachments are expired (#9739)

* fix: reload row if the attachments are expired

* fix: handle attachment url expiry

* fix:build feat: image zoom

* fix:attchment comments

* fix: async issue
pull/9762/head
Anbarasu 3 weeks ago committed by GitHub
parent
commit
eef9ce0f72
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 35
      packages/nc-gui/components/cell/attachment/Carousel.vue
  2. 149
      packages/nc-gui/components/cell/attachment/Preview/Image.vue
  3. 10
      packages/nc-gui/components/cell/attachment/Preview/MiscOffice.vue
  4. 10
      packages/nc-gui/components/cell/attachment/Preview/Pdf.vue
  5. 9
      packages/nc-gui/components/cell/attachment/Preview/Video.vue
  6. 24
      packages/nc-gui/utils/attachmentUtils.ts

35
packages/nc-gui/components/cell/attachment/Carousel.vue

@ -10,10 +10,6 @@ const isExpandedFormOpen = inject(IsExpandedFormOpenInj, ref(false))
const { isSharedForm } = useSmartsheetStoreOrThrow()
/*
const openComments = ref(false)
*/
const { isUIAllowed } = useRoles()
const container = ref<HTMLElement | null>(null)
@ -107,6 +103,15 @@ watchOnce(emblaMainApi, async (emblaMainApi) => {
})
})
const { loadRow } = useSmartsheetRowStoreOrThrow()
const isUpdated = ref(1)
const triggerReload = async () => {
await loadRow()
isUpdated.value = isUpdated.value + 1
}
onMounted(() => {
document.addEventListener('keydown', onKeyDown)
})
@ -132,7 +137,10 @@ function onKeyDown(event: KeyboardEvent) {
}
}
/* const toggleComment = () => {
/*
const openComments = ref(false)
const toggleComment = () => {
openComments.value = !openComments.value
}
@ -181,13 +189,15 @@ const initEmblaApi = (val: any) => {
<NcCarousel class="!absolute inset-y-16 inset-x-24 keep-open flex justify-center items-center" @init-api="initEmblaApi">
<NcCarouselContent>
<NcCarouselItem v-for="(item, index) in visibleItems" :key="index">
<div v-if="selectedIndex === index" class="justify-center w-full h-full flex items-center">
<div v-if="selectedIndex === index" :key="isUpdated" class="justify-center w-full h-full flex items-center">
<LazyCellAttachmentPreviewImage
v-if="isImage(item.title, item.mimeType)"
class="nc-attachment-img-wrapper"
object-fit="contain"
controls
:alt="item.title"
:srcs="getPossibleAttachmentSrc(item)"
@error="triggerReload"
/>
<LazyCellAttachmentPreviewVideo
@ -196,16 +206,19 @@ const initEmblaApi = (val: any) => {
:mime-type="item.mimeType"
:title="item.title"
:src="getPossibleAttachmentSrc(item)"
@error="triggerReload"
/>
<LazyCellAttachmentPreviewPdf
v-else-if="isPdf(item.title, item.mimeType)"
class="keep-open"
:src="getPossibleAttachmentSrc(item)"
@error="triggerReload"
/>
<LazyCellAttachmentPreviewMiscOffice
v-else-if="isOffice(item.title, item.mimeType)"
class="keep-open"
:src="getPossibleAttachmentSrc(item)"
@error="triggerReload"
/>
<div v-else class="bg-white h-full flex flex-col justify-center rounded-md gap-1 items-center w-full">
<component :is="iconMap.file" class="text-gray-600 w-20 h-20" />
@ -266,6 +279,7 @@ const initEmblaApi = (val: any) => {
object-fit="contain"
:alt="item.title"
:srcs="getPossibleAttachmentSrc(item, 'tiny')"
@error="triggerReload"
/>
<div
v-else-if="isVideo(item.title, item.mimeType)"
@ -351,7 +365,14 @@ const initEmblaApi = (val: any) => {
</template>
</GeneralDeleteModal>
</div>
<!-- <div v-if="openComments && isUIAllowed('commentList') && !isExpandedFormOpen" class="bg-white w-88 min-w-88 max-w-88">
<!-- <div
v-if="isUIAllowed('commentList') && !isExpandedFormOpen"
:class="{
'w-0': !openComments,
'!w-88': openComments,
}"
class="bg-white max-w-88 transition-all"
>
<LazySmartsheetExpandedFormSidebarComments />
</div> -->
</div>

149
packages/nc-gui/components/cell/attachment/Preview/Image.vue

@ -1,33 +1,146 @@
<script setup lang="ts">
import { useEventListener } from '@vueuse/core'
interface Props {
srcs: string[]
alt?: string
objectFit?: string
controls?: boolean
}
const props = defineProps<Props>()
const emit = defineEmits(['error'])
const index = ref(0)
const scale = ref(1)
const isDragging = ref(false)
const startPos = ref({ x: 0, y: 0 })
const position = ref({ x: 0, y: 0 })
const containerRef = ref<HTMLDivElement | null>(null)
const imageRef = ref<HTMLImageElement | null>(null)
const MIN_SCALE = 1
const MAX_SCALE = 4
const ZOOM_STEP = 0.5
const transformStyle = computed(() => ({
transform: `translate(${position.value.x}px, ${position.value.y}px) scale(${scale.value})`,
transition: isDragging.value ? 'none' : 'transform 0.2s ease-out',
cursor: scale.value > 1 ? 'grab' : 'default',
}))
const limitDrag = (x: number, y: number) => {
if (!containerRef.value || !imageRef.value) return { x, y }
const containerRect = containerRef.value.getBoundingClientRect()
const imageRect = imageRef.value.getBoundingClientRect()
const maxX = (imageRect.width * scale.value - containerRect.width) / 8
const maxY = (imageRect.height * scale.value - containerRect.height) / 8
return {
x: Math.max(Math.min(x, maxX), -maxX),
y: Math.max(Math.min(y, maxY), -maxY),
}
}
const onError = async () => {
index.value++
if (index.value >= props.srcs.length) {
const isURLExp = await isURLExpired(props.srcs[0])
if (isURLExp.isExpired) {
emit('error')
}
}
}
const zoom = (direction: 'in' | 'out') => {
const newScale =
direction === 'in' ? Math.min(scale.value + ZOOM_STEP, MAX_SCALE) : Math.max(scale.value - ZOOM_STEP, MIN_SCALE)
scale.value = newScale
if (newScale === MIN_SCALE) {
position.value = { x: 0, y: 0 }
}
}
const onError = () => index.value++
const startDrag = (clientX: number, clientY: number) => {
if (scale.value <= 1) return
isDragging.value = true
startPos.value = {
x: clientX - position.value.x,
y: clientY - position.value.y,
}
}
const drag = (clientX: number, clientY: number) => {
if (!isDragging.value) return
const newPosition = {
x: clientX - startPos.value.x,
y: clientY - startPos.value.y,
}
position.value = limitDrag(newPosition.x, newPosition.y)
}
const stopDrag = () => {
isDragging.value = false
}
const stopPropagationIfScaled = (e: MouseEvent | TouchEvent) => {
if (scale.value <= 1) return
e.preventDefault()
e.stopPropagation()
}
useEventListener(window, 'mousemove', (e: MouseEvent) => drag(e.clientX, e.clientY))
useEventListener(window, 'mouseup', stopDrag)
useEventListener(window, 'touchmove', (e: TouchEvent) => drag(e.touches[0].clientX, e.touches[0].clientY))
useEventListener(window, 'touchend', stopDrag)
const onMouseDown = (e: MouseEvent) => {
stopPropagationIfScaled(e)
startDrag(e.clientX, e.clientY)
}
const onTouchStart = (e: TouchEvent) => {
stopPropagationIfScaled(e)
startDrag(e.touches[0].clientX, e.touches[0].clientY)
}
</script>
<template>
<!-- Replacing with Image component as nuxt-image is not triggering @error when the image doesn't load. Will fix later
TODO: @DarkPhoenix2704 Fix this later
-->
<img
v-if="index < props.srcs?.length"
:src="props.srcs[index]"
quality="75"
:placeholder="props.alt"
:class="{
'!object-contain': props.objectFit === 'contain',
}"
loading="lazy"
:alt="props?.alt || ''"
class="m-auto h-full max-h-full w-auto nc-attachment-image object-cover"
@error="onError"
/>
<component :is="iconMap.imagePlaceholder" v-else />
<div class="relative h-full w-full">
<div ref="containerRef" class="h-full w-full overflow-hidden" @mousedown="onMouseDown" @touchstart.prevent="onTouchStart">
<img
v-if="index < props.srcs?.length"
ref="imageRef"
:src="props.srcs[index]"
:alt="props?.alt || ''"
:style="transformStyle"
:class="{ '!object-contain': props.objectFit === 'contain' }"
class="m-auto h-full max-h-full w-auto nc-attachment-image object-cover origin-center"
loading="lazy"
@error="onError"
/>
<component :is="iconMap.imagePlaceholder" v-else />
</div>
<div v-if="controls" class="absolute mx-auto w-full bottom-4 flex items-center justify-center gap-2">
<button
class="rounded-full bg-gray-800/70 p-2 text-white hover:bg-gray-700/70 disabled:opacity-50"
:disabled="scale >= MAX_SCALE"
title="Zoom in"
@click="zoom('in')"
>
<GeneralIcon icon="ncZoomIn" class="h-5 w-5" />
</button>
<button
class="rounded-full bg-gray-800/70 p-2 text-white hover:bg-gray-700/70 disabled:opacity-50"
:disabled="scale <= MIN_SCALE"
title="Zoom out"
@click="zoom('out')"
>
<GeneralIcon icon="ncZoomOut" class="h-5 w-5" />
</button>
</div>
</div>
</template>

10
packages/nc-gui/components/cell/attachment/Preview/MiscOffice.vue

@ -6,13 +6,19 @@ interface Props {
const props = defineProps<Props>()
const emits = defineEmits(['error'])
const currentIndex = ref(0)
const handleError = () => {
const handleError = async () => {
if (currentIndex.value < props.src.length - 1) {
currentIndex.value = currentIndex.value + 1
} else {
currentIndex.value = -1
const isURLExp = await isURLExpired(props.src[0])
if (isURLExp.isExpired) {
emits('error')
}
currentIndex.value = 0
}
}

10
packages/nc-gui/components/cell/attachment/Preview/Pdf.vue

@ -6,13 +6,19 @@ interface Props {
const props = defineProps<Props>()
const emits = defineEmits(['error'])
const currentIndex = ref(0)
const handleError = () => {
const handleError = async () => {
if (currentIndex.value < props.src.length - 1) {
currentIndex.value = currentIndex.value + 1
} else {
currentIndex.value = -1
const isURLExp = await isURLExpired(props.src[0])
if (isURLExp.isExpired) {
emits('error')
}
currentIndex.value = 0
}
}
</script>

9
packages/nc-gui/components/cell/attachment/Preview/Video.vue

@ -17,6 +17,7 @@ const emit = defineEmits<Emits>()
interface Emits {
(event: 'init', player: any): void
(event: 'error'): void
}
const videoPlayer = ref<HTMLElement>()
@ -36,6 +37,13 @@ onBeforeUnmount(() => {
player.value.destroy()
}
})
const handleError = async () => {
const isURLExp = await isURLExpired(props.src?.[0])
if (isURLExp.isExpired) {
emit('error')
}
}
</script>
<template>
@ -47,6 +55,7 @@ onBeforeUnmount(() => {
[props.class]: props.class,
}"
class="videoplayer !min-w-128 !min-h-72 w-full"
@error="handleError"
>
<source v-for="(source, id) in props.src" :key="id" :src="source" :type="mimeType" />
</video>

24
packages/nc-gui/utils/attachmentUtils.ts

@ -68,3 +68,27 @@ export const createThumbnail = async (file: File): Promise<string | null> => {
reader.readAsDataURL(file)
})
}
export async function isURLExpired(url?: string) {
if (!url) return { isExpired: false, status: 0, error: 'URL is empty' }
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Range: 'bytes=0-0', // Request only the first byte
},
cache: 'no-store',
})
return {
isExpired: response.status === 403,
status: response.status,
}
} catch (error) {
return {
isExpired: true,
status: 0,
error: error.message,
}
}
}

Loading…
Cancel
Save