Browse Source

feat(gui-v2): make attachments list scrollable in cell

pull/2972/head
braks 2 years ago
parent
commit
104cfd03c5
  1. 205
      packages/nc-gui-v2/components/cell/Attachment.vue
  2. 2
      packages/nc-gui-v2/components/smartsheet/Grid.vue
  3. 4
      packages/nc-gui-v2/utils/urlUtils.ts

205
packages/nc-gui-v2/components/cell/Attachment.vue

@ -3,22 +3,35 @@ import { notification } from 'ant-design-vue'
import { computed, inject, ref, useApi, useDropZone, useFileDialog, useProject, watch } from '#imports' import { computed, inject, ref, useApi, useDropZone, useFileDialog, useProject, watch } from '#imports'
import { ColumnInj, EditModeInj, MetaInj } from '~/context' import { ColumnInj, EditModeInj, MetaInj } from '~/context'
import { NOCO } from '~/lib' import { NOCO } from '~/lib'
import { isImage } from '~/utils' import { isImage, openLink } from '~/utils'
import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file' import MaterialSymbolsAttachFile from '~icons/material-symbols/attach-file'
import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand' import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline' import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import MdiReload from '~icons/mdi/reload'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
import MdiPdfBox from '~icons/mdi/pdf-box'
import MdiFileWordOutline from '~icons/mdi/file-word-outline'
import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
import MdiFileExcelOutline from '~icons/mdi/file-excel-outline'
interface Props { interface Props {
modelValue: string | Record<string, any>[] | null modelValue: string | Record<string, any>[] | null
} }
interface Emits {
(event: 'update:modelValue', value: string | Record<string, any>): void
}
const { modelValue } = defineProps<Props>() const { modelValue } = defineProps<Props>()
const emit = defineEmits(['update:modelValue']) const emits = defineEmits<Emits>()
const isPublicForm = inject('isPublicForm', false)
const isPublicForm = inject<boolean>('isPublicForm', false) const isForm = inject('isForm', false)
const isForm = inject<boolean>('isForm', false) // todo: replace placeholder var
const isPublicGrid = $ref(false)
const meta = inject(MetaInj)! const meta = inject(MetaInj)!
@ -26,13 +39,13 @@ const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false)) const editEnabled = inject(EditModeInj, ref(false))
const attachments = ref([]) const storedFiles = ref<{ title: string; file: File }[]>([])
const uploading = ref(false) const attachments = ref<File[]>([])
const dropZoneRef = ref<HTMLDivElement>() const dropZoneRef = ref<HTMLDivElement>()
const { api } = useApi() const { api, isLoading } = useApi()
const { project } = useProject() const { project } = useProject()
@ -47,12 +60,13 @@ watch(
attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean) attachments.value = ((typeof nextModel === 'string' ? JSON.parse(nextModel) : nextModel) || []).filter(Boolean)
} }
}, },
{ immediate: true },
) )
function onDrop(droppedFiles: File[] | null) { function onDrop(droppedFiles: File[] | null) {
if (droppedFiles) { if (droppedFiles) {
// set files // set files
console.log(droppedFiles) onFileSelection(droppedFiles)
} }
} }
@ -60,42 +74,30 @@ const selectImage = (file: any, i: unknown) => {
// todo: implement // todo: implement
} }
const openUrl = (url: string, target = '_blank') => { async function onFileSelection(selectedFiles: FileList | File[]) {
window.open(url, target) if (!selectedFiles.length || isPublicGrid) return
}
if (isPublicForm) {
const onFileSelection = async (e: unknown) => { storedFiles.value.push(
if (!files.value) return ...Array.from(selectedFiles).map((file) => {
const res = { file, title: file.name }
// if (this.isPublicGrid) { if (isImage(file.name, (file as any).mimetype)) {
// return const reader = new FileReader()
// } reader.readAsDataURL(file)
// if (!this.$refs.file.files || !this.$refs.file.files.length) { }
// return return res
// } }),
)
// if (this.isPublicForm) {
// this.localFilesState.push(...Array.from(this.$refs.file.files).map((file) => { emits(
// const res = { file, title: file.name } 'update:modelValue',
// if (isImage(file.name, file.mimetype)) { storedFiles.value.map((f) => f.file),
// const reader = new FileReader() )
// reader.onload = (e) => { }
// this.$set(res, 'data', e.target.result)
// }
// reader.readAsDataURL(file)
// }
// return res
// }))
//
// this.$emit('input', this.localFilesState.map(f => f.file))
// return
// }
uploading.value = true
const newAttachments = [] const newAttachments = []
for (const file of files.value) { for (const file of selectedFiles) {
try { try {
const data = await api.storage.upload( const data = await api.storage.upload(
{ {
@ -115,74 +117,91 @@ const onFileSelection = async (e: unknown) => {
} }
} }
uploading.value = false emits('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments]))
emit('update:modelValue', JSON.stringify([...attachments.value, ...newAttachments]))
} }
watch(files, console.log) watch(files, (nextFiles) => nextFiles && onFileSelection(nextFiles))
const items = computed(() => (isPublicForm ? files.value : attachments.value) || []) const items = computed(() => (isPublicForm ? storedFiles.value : attachments.value) || [])
const FileIcon = (icon: string) => {
switch (icon) {
case 'mdi-pdf-box':
return MdiPdfBox
case 'mdi-file-word-outline':
return MdiFileWordOutline
case 'mdi-file-powerpoint-box':
return MdiFilePowerpointBox
case 'mdi-file-excel-outline':
return MdiFileExcelOutline
default:
return IcOutlineInsertDriveFile
}
}
</script> </script>
<template> <template>
<div class="flex items-center"> <div ref="dropZoneRef" class="flex-1 color-transition flex items-center justify-between gap-1">
<div ref="dropZoneRef" class="flex-1 group color-transition flex items-center p-1 hover:text-primary"> <template v-if="isOverDropZone">
<template v-if="isOverDropZone"> <div
class="w-full h-full flex items-center justify-center p-1 rounded gap-1 bg-gradient-to-t from-primary/10 via-primary/25 to-primary/10 !text-primary"
>
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here
</div>
</template>
<template v-else>
<div
:class="{ 'mx-auto px-4': !items.length }"
class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10"
@click.stop="open"
>
<MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
<a-tooltip v-else placement="bottom">
<template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
<div v-if="!items.length" class="group-hover:text-primary">Add file(s)</div>
</div>
</a-tooltip>
</div>
<div class="h-full w-full flex flex-wrap flex-col overflow-x-scroll overflow-y-hidden gap-2 scrollbar-thin-primary py-1">
<div <div
class="w-full h-full flex items-center justify-center p-1 rounded gap-1 bg-gradient-to-t from-primary/10 via-primary/25 to-primary/10 !text-primary" v-for="(item, i) of items"
:key="item.url || item.title"
class="flex-auto flex items-center justify-center w-[45px] border-1"
> >
<MaterialSymbolsFileCopyOutline class="text-pink-500" /> Drop here <a-tooltip placement="bottom">
</div> <template #title>
</template> <div class="text-center w-full">{{ item.title }}</div>
<template v-else> </template>
<div class="flex overflow-hidden">
<div v-for="(item, i) of items" :key="item.url || item.title" class="thumbnail align-center justify-center d-flex">
<img <img
v-if="isImage(item.title, item.mimetype)" v-if="isImage(item.title, item.mimetype)"
alt="#" :alt="item.title || `#${i}`"
style="max-height: 30px; max-width: 30px"
:src="item.url || item.data" :src="item.url || item.data"
@click="selectImage(item.url || item.data, i)" @click="selectImage(item.url || item.data, i)"
/> />
<v-icon v-else-if="item.icon" :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')">
{{ item.icon }}
</v-icon>
<v-icon v-else :size="active ? 33 : 22" v-on="on" @click="openUrl(item.url || item.data, '_blank')">
mdi-file
</v-icon>
</div>
</div>
<!-- todo: hide or toggle based on ancestor --> <component :is="FileIcon(item.icon)" v-else-if="item.icon" @click="openLink(item.url || item.data)" />
<div class="mx-auto flex gap-1 items-center active:ring rounded border-1 py-1 px-4" @click.stop="open">
<v-icon v-if="uploading" small color="primary" class="nc-attachment-add-spinner"> mdi-loading mdi-spin</v-icon>
<a-tooltip placement="bottom"> <IcOutlineInsertDriveFile v-else @click.stop="openLink(item.url || item.data)" />
<template #title> Click or drop a file into cell </template>
<div class="flex items-center gap-2">
<MaterialSymbolsAttachFile class="transform group-hover:(text-pink-500 scale-120)" />
<div v-if="!items.length">Add file(s)</div>
</div>
</a-tooltip> </a-tooltip>
</div> </div>
</div>
<MaterialArrowExpandIcon v-if="items.length" @click.stop /> <div v-if="items.length" class="group flex gap-1 items-center active:ring rounded border-1 p-1 hover:bg-primary/10">
</template> <MdiReload v-if="isLoading" :class="{ 'animate-infinite animate-spin': isLoading }" />
</div>
</div>
</template>
<style scoped lang="scss"> <a-tooltip v-else placement="bottom">
.thumbnail { <template #title> View attachments </template>
height: 30px;
width: 30px;
margin: 2px;
border-radius: 4px;
img { <MaterialArrowExpandIcon class="transform group-hover:(text-pink-500 scale-120)" @click.stop />
max-height: 33px; </a-tooltip>
max-width: 33px; </div>
} </template>
} </div>
</style> </template>

2
packages/nc-gui-v2/components/smartsheet/Grid.vue

@ -213,7 +213,7 @@ if (meta) useProvideColumnCreateStore(meta)
// todo: replace with css variable // todo: replace with css variable
td.active::after { td.active::after {
border: 2px solid #0040bc; /*var(--v-primary-lighten1);*/ @apply border-2 border-solid border-primary;
} }
td.active::before { td.active::before {

4
packages/nc-gui-v2/utils/urlUtils.ts

@ -27,3 +27,7 @@ export const isValidURL = (str: string) => {
/^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)(?:\.(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)*(?:\.(?:[a-z\u00A1-\uFFFF]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)(?:\.(?:[a-z\u00A1-\uFFFF0-9]-*)*[a-z\u00A1-\uFFFF0-9]+)*(?:\.(?:[a-z\u00A1-\uFFFF]{2,}))\.?)(?::\d{2,5})?(?:[/?#]\S*)?$/i
return !!pattern.test(str) return !!pattern.test(str)
} }
export const openLink = (url: string, target = '_blank') => {
window.open(url, target)
}

Loading…
Cancel
Save