Browse Source

feat(gui-v2): add sortable to attachment items

pull/2972/head
braks 2 years ago
parent
commit
4ff2c47ae1
  1. 109
      packages/nc-gui-v2/components/cell/attachment/Modal.vue
  2. 43
      packages/nc-gui-v2/components/cell/attachment/index.vue
  3. 67
      packages/nc-gui-v2/components/cell/attachment/sort.ts
  4. 226
      packages/nc-gui-v2/components/cell/attachment/utils.ts

109
packages/nc-gui-v2/components/cell/attachment/Modal.vue

@ -8,17 +8,34 @@ import MdiCloseCircle from '~icons/mdi/close-circle'
import MdiDownload from '~icons/mdi/download'
import MaterialSymbolsFileCopyOutline from '~icons/material-symbols/file-copy-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
import { useSortable } from './sort'
const { isUIAllowed } = useUIPermission()
const { open, isLoading, isPublicGrid, isForm, visibleItems, modalVisible, column, FileIcon, removeFile, onDrop, downloadFile } =
useAttachmentCell()
const {
open,
isLoading,
isPublicGrid,
isForm,
visibleItems,
modalVisible,
column,
FileIcon,
removeFile,
onDrop,
downloadFile,
updateModelValue,
} = useAttachmentCell()
// todo: replace placeholder var
const isLocked = ref(false)
const dropZoneRef = ref<HTMLDivElement>()
const sortableRef = ref<HTMLDivElement>()
const { dragging } = useSortable(dropZoneRef, visibleItems, updateModelValue)
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
onKeyDown('Escape', () => {
@ -47,52 +64,55 @@ onKeyDown('Escape', () => {
</div>
</template>
<div ref="dropZoneRef" class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-6 relative p-6">
<div ref="dropZoneRef" :class="{ dragging }">
<div
:class="isOverDropZone ? 'opacity-100' : 'opacity-0 pointer-events-none'"
v-if="!dragging"
:class="[isOverDropZone ? 'opacity-100' : 'opacity-0 pointer-events-none']"
class="transition-all duration-150 ease-in-out ring ring-pink-500 rounded bg-blue-100/75 flex items-center justify-center gap-4 z-99 absolute top-0 bottom-0 left-0 right-0 backdrop-blur-xl"
>
<MaterialSymbolsFileCopyOutline class="text-pink-500" height="35" width="35" />
<div class="text-3xl text-primary">Drop here</div>
</div>
<div v-for="(item, i) of visibleItems" :key="`${item.title}-${i}`" class="flex flex-col gap-1">
<a-card class="nc-attachment-item group">
<a-tooltip>
<template #title> Remove File </template>
<MdiCloseCircle
v-if="isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
class="nc-attachment-remove"
@click.stop="removeFile(i)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> Download file </template>
<div class="nc-attachment-download group-hover:(opacity-100)">
<MdiDownload @click.stop="downloadFile(item)" />
<div ref="sortableRef" class="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-6 relative p-6">
<div v-for="(item, i) of visibleItems" :key="`${item.title}-${i}`" class="flex flex-col gap-1">
<a-card class="nc-attachment-item group">
<a-tooltip>
<template #title> Remove File </template>
<MdiCloseCircle
v-if="isUIAllowed('tableAttachment') && !isPublicGrid && !isLocked"
class="nc-attachment-remove"
@click.stop="removeFile(i)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title> Download file </template>
<div class="nc-attachment-download group-hover:(opacity-100)">
<MdiDownload @click.stop="downloadFile(item)" />
</div>
</a-tooltip>
<div class="nc-attachment p-2 flex items-center cursor-pointer">
<img v-if="isImage(item.title, item.mimetype)" :alt="item.title || `#${i}`" :src="item.url || item.data" />
<component
:is="FileIcon(item.icon)"
v-else-if="item.icon"
height="150"
width="150"
@click.stop="openLink(item.url || item.data)"
/>
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openLink(item.url || item.data)" />
</div>
</a-tooltip>
<div class="p-2 flex items-center cursor-pointer">
<img v-if="isImage(item.title, item.mimetype)" :alt="item.title || `#${i}`" :src="item.url || item.data" />
<component
:is="FileIcon(item.icon)"
v-else-if="item.icon"
height="150"
width="150"
@click.stop="openLink(item.url || item.data)"
/>
</a-card>
<IcOutlineInsertDriveFile v-else height="150" width="150" @click.stop="openLink(item.url || item.data)" />
<div class="truncate" :title="item.title">
{{ item.title }}
</div>
</a-card>
<div class="truncate" :title="item.title">
{{ item.title }}
</div>
</div>
</div>
@ -108,7 +128,7 @@ onKeyDown('Escape', () => {
}
.nc-attachment-item {
@apply cursor-pointer !h-2/3 !min-h-[200px] flex items-center justify-center relative;
@apply cursor-pointer !h-2/3 !min-h-[200px] flex items-center justify-center relative hover:(!border-0);
&::after {
@apply pointer-events-none rounded absolute top-0 left-0 right-0 bottom-0 transition-all duration-150 ease-in-out;
@ -145,5 +165,16 @@ onKeyDown('Escape', () => {
.ant-modal-body {
@apply !p-0;
}
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
&.dragging {
.ant-tooltip {
@apply !hidden;
}
}
}
</style>

43
packages/nc-gui-v2/components/cell/attachment/index.vue

@ -8,6 +8,7 @@ import MaterialArrowExpandIcon from '~icons/mdi/arrow-expand'
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 { useSortable } from './sort'
interface Props {
modelValue: string | Record<string, any>[] | null
@ -23,8 +24,11 @@ const emits = defineEmits<Emits>()
const dropZoneRef = ref<HTMLDivElement>()
const { modalVisible, attachments, visibleItems, onDrop, isLoading, open, FileIcon, fileRemovedHook, fileAddedHook } =
useProvideAttachmentCell()
const sortableRef = ref<HTMLDivElement>()
const { modalVisible, attachments, visibleItems, onDrop, isLoading, open, FileIcon } = useProvideAttachmentCell(updateModelValue)
const { dragging } = useSortable(sortableRef, visibleItems, updateModelValue)
const { isOverDropZone } = useDropZone(dropZoneRef, onDrop)
@ -38,13 +42,10 @@ watch(
{ immediate: true },
)
fileRemovedHook.on((data) => {
emits('update:modelValue', data)
})
fileAddedHook.on((data) => {
emits('update:modelValue', data)
})
function updateModelValue(data: string | Record<string, any>) {
console.log(data)
emits('update:modelValue', typeof data !== 'string' ? JSON.stringify(data) : data)
}
const selectImage = (file: any, i: unknown) => {
// todo: implement
@ -52,8 +53,8 @@ const selectImage = (file: any, i: unknown) => {
</script>
<template>
<div ref="dropZoneRef" class="flex-1 color-transition flex items-center justify-between gap-1">
<template v-if="isOverDropZone">
<div ref="dropZoneRef" class="nc-attachment-cell flex-1 color-transition flex items-center justify-between gap-1">
<template v-if="!dragging && 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"
>
@ -82,12 +83,15 @@ const selectImage = (file: any, i: unknown) => {
<template v-if="visibleItems.length">
<div
ref="sortableRef"
:class="{ dragging }"
class="h-full w-full flex flex-wrap flex-col gap-2 content-start py-1 overflow-x-scroll overflow-y-hidden scrollbar-thin-primary"
>
<div
v-for="(item, i) of visibleItems"
:id="item.url"
:key="item.url || item.title"
class="flex-auto flex items-center justify-center w-[45px] border-1"
class="nc-attachment flex-auto flex items-center justify-center w-[45px] border-1"
>
<a-tooltip placement="bottom">
<template #title>
@ -123,3 +127,18 @@ const selectImage = (file: any, i: unknown) => {
<Modal />
</div>
</template>
<style lang="scss">
.nc-attachment-cell {
.ghost,
.ghost > * {
@apply !pointer-events-none;
}
&.dragging {
.ant-tooltip {
@apply !hidden;
}
}
}
</style>

67
packages/nc-gui-v2/components/cell/attachment/sort.ts

@ -0,0 +1,67 @@
import type { SortableEvent } from 'sortablejs'
import Sortable from 'sortablejs'
import type { MaybeRef } from '@vueuse/core'
import { watchPostEffect } from '@vue/runtime-core'
import { unref } from '#imports'
export function useSortable(
element: MaybeRef<HTMLElement | undefined>,
items: MaybeRef<any[]>,
updateModelValue: (data: string | Record<string, any>[]) => void,
) {
let dragging = $ref(false)
function onSortStart(evt: SortableEvent) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging = true
}
async function onSortEnd(evt: SortableEvent) {
evt.stopImmediatePropagation()
evt.preventDefault()
dragging = false
const _items = unref(items)
if (_items.length < 2) return
const { newIndex = 0, oldIndex = 0 } = evt
console.log(newIndex, oldIndex)
if (newIndex === oldIndex) return
_items.splice(newIndex, 0, ..._items.splice(oldIndex, 1))
updateModelValue(_items)
}
let sortable: Sortable
// todo: replace with vuedraggable
const initSortable = (el: HTMLElement) => {
if (sortable) sortable.destroy()
sortable = new Sortable(el, {
handle: '.nc-attachment',
ghostClass: 'ghost',
onStart: onSortStart,
onEnd: onSortEnd,
})
}
watchPostEffect((onCleanup) => {
onCleanup(() => {
if (sortable) sortable.destroy()
})
const _element = unref(element)
if (_element) initSortable(_element)
})
return {
dragging: $$(dragging),
initSortable,
}
}

226
packages/nc-gui-v2/components/cell/attachment/utils.ts

@ -1,6 +1,6 @@
import { notification } from 'ant-design-vue'
import FileSaver from 'file-saver'
import { computed, createEventHook, inject, ref, useApi, useFileDialog, useInjectionState, useProject, watch } from '#imports'
import { computed, inject, ref, useApi, useFileDialog, useInjectionState, useProject, watch } from '#imports'
import { ColumnInj, EditModeInj, MetaInj } from '~/context'
import { isImage } from '~/utils'
import { NOCO } from '~/lib'
@ -10,139 +10,137 @@ import MdiFilePowerpointBox from '~icons/mdi/file-powerpoint-box'
import MdiFileExcelOutline from '~icons/mdi/file-excel-outline'
import IcOutlineInsertDriveFile from '~icons/ic/outline-insert-drive-file'
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(() => {
const isPublicForm = inject('isPublicForm', false)
export const [useProvideAttachmentCell, useAttachmentCell] = useInjectionState(
(updateModelValue: (data: string | Record<string, any>[]) => void) => {
const isPublicForm = inject('isPublicForm', false)
const isForm = inject('isForm', false)
const isForm = inject('isForm', false)
// todo: replace placeholder var
const isPublicGrid = $ref(false)
// todo: replace placeholder var
const isPublicGrid = $ref(false)
const meta = inject(MetaInj)!
const meta = inject(MetaInj)!
const column = inject(ColumnInj)!
const column = inject(ColumnInj)!
const editEnabled = inject(EditModeInj, ref(false))
const editEnabled = inject(EditModeInj, ref(false))
const storedFiles = ref<{ title: string; file: File }[]>([])
const storedFiles = ref<{ title: string; file: File }[]>([])
const attachments = ref<File[]>([])
const attachments = ref<File[]>([])
const modalVisible = ref(false)
const modalVisible = ref(false)
const { project } = useProject()
const { project } = useProject()
const { api, isLoading } = useApi()
const { api, isLoading } = useApi()
const { files, open } = useFileDialog()
const { files, open } = useFileDialog()
const fileRemovedHook = createEventHook<string | Record<string, any>[]>()
function removeFile(i: number) {
if (isPublicForm) {
storedFiles.value.splice(i, 1)
const fileAddedHook = createEventHook<File[]>()
function removeFile(i: number) {
if (isPublicForm) {
storedFiles.value.splice(i, 1)
fileRemovedHook.trigger(storedFiles.value.map((storedFile) => storedFile.file))
} else {
attachments.value.splice(i, 1)
fileRemovedHook.trigger(attachments.value)
}
}
async function onFileSelect(selectedFiles: FileList | File[]) {
if (!selectedFiles.length || isPublicGrid) return
if (isPublicForm) {
storedFiles.value.push(
...Array.from(selectedFiles).map((file) => {
const res = { file, title: file.name }
if (isImage(file.name, (file as any).mimetype)) {
const reader = new FileReader()
reader.readAsDataURL(file)
}
return res
}),
)
return fileAddedHook.trigger(storedFiles.value.map((storedFile) => storedFile.file))
updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
} else {
attachments.value.splice(i, 1)
updateModelValue(attachments.value)
}
}
const newAttachments = []
for (const file of selectedFiles) {
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value.title, column.title].join('/'),
},
{
files: file,
json: '{}',
},
async function onFileSelect(selectedFiles: FileList | File[]) {
if (!selectedFiles.length || isPublicGrid) return
if (isPublicForm) {
storedFiles.value.push(
...Array.from(selectedFiles).map((file) => {
const res = { file, title: file.name }
if (isImage(file.name, (file as any).mimetype)) {
const reader = new FileReader()
reader.readAsDataURL(file)
}
return res
}),
)
newAttachments.push(...data)
} catch (e: any) {
notification.error({
message: e.message || 'Some internal error occurred',
})
return updateModelValue(storedFiles.value.map((storedFile) => storedFile.file))
}
const newAttachments = []
for (const file of selectedFiles) {
try {
const data = await api.storage.upload(
{
path: [NOCO, project.value.title, meta.value.title, column.title].join('/'),
},
{
files: file,
json: '{}',
},
)
newAttachments.push(...data)
} catch (e: any) {
notification.error({
message: e.message || 'Some internal error occurred',
})
}
}
updateModelValue([...attachments.value, ...newAttachments])
}
fileAddedHook.trigger([...attachments.value, ...newAttachments])
}
async function onDrop(droppedFiles: File[] | null) {
if (droppedFiles) {
// set files
await onFileSelect(droppedFiles)
}
}
async function downloadFile(item: Record<string, any>) {
FileSaver.saveAs(item.url || item.data, item.title)
}
function onDrop(droppedFiles: File[] | null) {
if (droppedFiles) {
// set files
onFileSelect(droppedFiles)
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
}
}
}
async function downloadFile(item: Record<string, any>) {
FileSaver.saveAs(item.url || item.data, item.title)
}
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
const visibleItems = computed<any[]>(() => (isPublicForm ? storedFiles.value : attachments.value) || ([] as any[]))
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
attachments,
storedFiles,
visibleItems,
isPublicForm,
isForm,
isPublicGrid,
meta,
column,
editEnabled,
isLoading,
api,
open,
onDrop,
modalVisible,
FileIcon,
removeFile,
downloadFile,
updateModelValue,
}
}
const visibleItems = computed(() => (isPublicForm ? storedFiles.value : attachments.value) || [])
watch(files, (nextFiles) => nextFiles && onFileSelect(nextFiles))
return {
attachments,
storedFiles,
visibleItems,
isPublicForm,
isForm,
isPublicGrid,
meta,
column,
editEnabled,
isLoading,
api,
open,
onDrop,
modalVisible,
FileIcon,
fileRemovedHook,
fileAddedHook,
removeFile,
downloadFile,
}
}, 'attachmentCell')
},
'attachmentCell',
)

Loading…
Cancel
Save