Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions src/components/Request/RequestPicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,31 @@ function cancelUpload() {
uploadAbortController.value?.abort()
}

async function handleFilesSelected(files: UploadFile[]) {
if (files.length === 0) {
return
}

if (!validateMaxFileUploads(files.length)) {
return
}

if (files.length > 1 && !envelopeEnabled.value) {
// TRANSLATORS Shown when more than one file is selected/dropped but envelopes (multi-file requests) are not enabled.
showError(t('libresign', 'Only one file can be uploaded at a time.'))
return
}

if (files.length > 1 && envelopeEnabled.value) {
pendingFiles.value = files
envelopeNameInput.value = ''
showEnvelopeNameDialog.value = true
return
}

await upload(files)
}

function uploadFile() {
openedMenu.value = false
const input = document.createElement('input')
Expand All @@ -390,23 +415,7 @@ function uploadFile() {
input.onchange = async (event) => {
const target = event.target as HTMLInputElement | null
const files = Array.from(target?.files ?? []) as unknown as UploadFile[]
if (!validateMaxFileUploads(files.length)) {
input.remove()
return
}

if (files.length > 1 && envelopeEnabled.value) {
pendingFiles.value = files
envelopeNameInput.value = ''
showEnvelopeNameDialog.value = true
input.remove()
return
}

if (files.length > 0) {
await upload(files)
}

await handleFilesSelected(files)
input.remove()
}

Expand Down Expand Up @@ -523,6 +532,7 @@ defineExpose({
closeModalUploadFromUrl,
upload,
cancelUpload,
handleFilesSelected,
uploadFile,
uploadUrl,
handleFileChoose,
Expand Down
71 changes: 71 additions & 0 deletions src/tests/components/Request/RequestPicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,77 @@ describe('RequestPicker component rules', () => {
})
})

describe('handleFilesSelected (shared entry point for click-upload and drag-and-drop)', () => {
it('does nothing when no files are provided', async () => {
await wrapper.vm.handleFilesSelected([])
expect(filesStore.upload).not.toHaveBeenCalled()
})

it('shows error and does not upload when exceeding max file uploads limit', async () => {
getCapabilitiesMock.mockReturnValue({
libresign: {
config: {
envelope: { 'is-available': true },
upload: { 'max-file-uploads': 1 },
},
},
})
const files = [
{ name: 'document1.pdf', size: 1000 },
{ name: 'document2.pdf', size: 1000 },
]
await wrapper.vm.handleFilesSelected(files)
expect(showError).toHaveBeenCalled()
expect(filesStore.upload).not.toHaveBeenCalled()
})

it('shows error and does not upload when multiple files provided while envelope is disabled', async () => {
getCapabilitiesMock.mockReturnValue({
libresign: {
config: {
envelope: { 'is-available': false },
upload: { 'max-file-uploads': 20 },
},
},
})
const files = [
{ name: 'document1.pdf', size: 1000 },
{ name: 'document2.pdf', size: 1000 },
]
await wrapper.vm.handleFilesSelected(files)
expect(showError).toHaveBeenCalledWith('Only one file can be uploaded at a time.')
expect(filesStore.upload).not.toHaveBeenCalled()
expect(wrapper.vm.showEnvelopeNameDialog).toBe(false)
})

it('opens envelope name dialog for multiple files when envelope is enabled', async () => {
getCapabilitiesMock.mockReturnValue({
libresign: {
config: {
envelope: { 'is-available': true },
upload: { 'max-file-uploads': 20 },
},
},
})
const files = [
{ name: 'document1.pdf', size: 1000 },
{ name: 'document2.pdf', size: 1000 },
]
await wrapper.vm.handleFilesSelected(files)
expect(wrapper.vm.showEnvelopeNameDialog).toBe(true)
expect(wrapper.vm.pendingFiles).toEqual(files)
expect(filesStore.upload).not.toHaveBeenCalled()
})

it('uploads directly when a single file is provided', async () => {
filesStore.upload.mockResolvedValue(42)
const files = [{ name: 'document.pdf', size: 1000 }]
await wrapper.vm.handleFilesSelected(files)
expect(filesStore.upload).toHaveBeenCalled()
expect(filesStore.selectFile).toHaveBeenCalledWith(42)
})
})

describe('envelope name dialog submission', () => {
it('creates envelope with trimmed name when minimum length met', async () => {
filesStore.upload.mockResolvedValue(1)
Expand Down
119 changes: 119 additions & 0 deletions src/tests/views/FilesList/FilesList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,17 @@ vi.mock('../../../views/FilesList/FileListFilters.vue', () => ({
},
}))

const handleFilesSelectedMock = vi.fn()

vi.mock('../../../components/Request/RequestPicker.vue', () => ({
default: {
name: 'RequestPicker',
template: '<div class="request-picker-stub" />',
methods: {
handleFilesSelected(files: File[]) {
return handleFilesSelectedMock(files)
},
},
},
}))

Expand Down Expand Up @@ -313,4 +320,116 @@ describe('FilesList.vue rendering rules', () => {

expect(selectSpy).toHaveBeenCalledWith()
})

describe('drag-and-drop upload', () => {
function createDragEvent(files: File[] = [], types: string[] = ['Files']) {
return {
preventDefault: vi.fn(),
dataTransfer: {
types,
files,
dropEffect: '',
},
} as unknown as DragEvent
}

it('shows the drop overlay when a file is dragged over and user can request sign', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = true
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

expect(wrapper.vm.isDraggingFiles).toBe(false)
wrapper.vm.onDragEnter(createDragEvent())
expect(wrapper.vm.isDraggingFiles).toBe(true)
})

it('does not show the drop overlay when user cannot request sign', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = false
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

wrapper.vm.onDragEnter(createDragEvent())
expect(wrapper.vm.isDraggingFiles).toBe(false)
})

it('ignores drags that do not carry files', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = true
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

wrapper.vm.onDragEnter(createDragEvent([], ['text/plain']))
expect(wrapper.vm.isDraggingFiles).toBe(false)
})

it('hides the overlay only after the last dragleave of nested elements', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = true
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

wrapper.vm.onDragEnter(createDragEvent())
wrapper.vm.onDragEnter(createDragEvent())
expect(wrapper.vm.isDraggingFiles).toBe(true)

wrapper.vm.onDragLeave(createDragEvent())
expect(wrapper.vm.isDraggingFiles).toBe(true)

wrapper.vm.onDragLeave(createDragEvent())
expect(wrapper.vm.isDraggingFiles).toBe(false)
})

it('delegates dropped files to RequestPicker.handleFilesSelected and hides the overlay', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = true
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

const file = new File(['content'], 'document.pdf', { type: 'application/pdf' })
wrapper.vm.onDragEnter(createDragEvent([file]))
await wrapper.vm.onDrop(createDragEvent([file]))

expect(handleFilesSelectedMock).toHaveBeenCalledWith([file])
expect(wrapper.vm.isDraggingFiles).toBe(false)
})

it('does not upload dropped files when user cannot request sign', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = false
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

const file = new File(['content'], 'document.pdf', { type: 'application/pdf' })
await wrapper.vm.onDrop(createDragEvent([file]))

expect(handleFilesSelectedMock).not.toHaveBeenCalled()
})

it('does not call handleFilesSelected when no files are dropped', async () => {
const filesStore = useFilesStore()
filesStore.canRequestSign = true
vi.spyOn(filesStore, 'getAllFiles').mockResolvedValue({})

const wrapper = mountComponent()
await flushPromises()

await wrapper.vm.onDrop(createDragEvent([]))

expect(handleFilesSelectedMock).not.toHaveBeenCalled()
})
})
})
Loading
Loading