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
16 changes: 15 additions & 1 deletion src/components/RightSidebar/EnvelopeFilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
class="file-preview-icon">
<FilePdfBox v-else :size="20" />
</template>
<template #actions>
<template v-if="!isTouchDevice" #actions>
<NcActionButton
:close-after-click="true"
@click="openFile(file)">
Expand All @@ -93,6 +93,18 @@
{{ t('libresign', 'Delete') }}
</NcActionButton>
</template>
<template v-if="isTouchDevice" #extra-actions>
<NcButton variant="tertiary" :aria-label="t('libresign', 'Open file')" @click="openFile(file)">
<template #icon>
<FileEye :size="20" />
</template>
</NcButton>
<NcButton v-if="canDelete" variant="tertiary" :aria-label="t('libresign', 'Delete')" @click="handleDelete(file)">
<template #icon>
<Delete :size="20" />
</template>
</NcButton>
</template>
</NcListItem>
<div v-if="isLoadingMore" class="loading-more">
<span class="icon-loading-small" />
Expand Down Expand Up @@ -138,13 +150,15 @@ import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcTextField from '@nextcloud/vue/components/NcTextField'

import UploadProgress from '../UploadProgress.vue'
import isTouchDevice from '../../mixins/isTouchDevice.js'

import { FILE_STATUS, ENVELOPE_NAME_MIN_LENGTH, ENVELOPE_NAME_MAX_LENGTH } from '../../constants.js'
import { openDocument } from '../../utils/viewer.js'
import { useFilesStore } from '../../store/files.js'

export default {
name: 'EnvelopeFilesList',
mixins: [isTouchDevice],
components: {
Delete,
FileEye,
Expand Down
11 changes: 9 additions & 2 deletions src/components/validation/EnvelopeValidation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,20 @@
<template #subname>
<strong>{{ t('libresign', 'Status:') }}</strong> {{ file.statusText }}
</template>
<template #actions>
<NcActionButton v-if="file.nodeId" @click.stop="viewFile(file)">
<template v-if="!isTouchDevice && file.nodeId" #actions>
<NcActionButton @click.stop="viewFile(file)">
<template #icon>
<NcIconSvgWrapper :path="mdiEye" :size="20" />
</template>
{{ t('libresign', 'View PDF') }}
</NcActionButton>
</template>
<template #extra-actions>
<NcButton v-if="isTouchDevice && file.nodeId" variant="tertiary" :aria-label="t('libresign', 'View PDF')" @click.stop="viewFile(file)">
<template #icon>
<NcIconSvgWrapper :path="mdiEye" :size="20" />
</template>
</NcButton>
<NcButton variant="tertiary" :aria-label="file.opened ? t('libresign', 'Hide details') : t('libresign', 'Show details')" @click.stop="toggleFileDetail(file)">
<template #icon>
<NcIconSvgWrapper v-if="file.opened" :path="mdiChevronUp" :size="20" />
Expand Down Expand Up @@ -177,11 +182,13 @@ import {
import Moment from '@nextcloud/moment'
import { getStatusLabel } from '../../utils/fileStatus.js'
import { openDocument } from '../../utils/viewer.js'
import isTouchDevice from '../../mixins/isTouchDevice.js'
import SignerDetails from './SignerDetails.vue'
import DocumentValidationDetails from './DocumentValidationDetails.vue'

export default {
name: 'EnvelopeValidation',
mixins: [isTouchDevice],
components: {
NcIconSvgWrapper,
NcListItem,
Expand Down
12 changes: 12 additions & 0 deletions src/mixins/isTouchDevice.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

export default {
computed: {
isTouchDevice() {
return ('ontouchstart' in window) || (navigator.maxTouchPoints > 0)
},
},
}
78 changes: 78 additions & 0 deletions src/tests/components/RightSidebar/EnvelopeFilesList.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -590,4 +590,82 @@ describe('EnvelopeFilesList', () => {
expect(() => wrapper.vm.cancelUpload()).not.toThrow()
})
})

describe('RULE: File actions visibility based on isTouchDevice', () => {
it('has isTouchDevice computed property from mixin', () => {
wrapper = createWrapper()
expect(wrapper.vm.isTouchDevice).toBeDefined()
expect(typeof wrapper.vm.isTouchDevice).toBe('boolean')
})

it('renders actions slot when not touch device', async () => {
wrapper = createWrapper()
await wrapper.setData({
files: [
{
id: 1,
uuid: 'test-uuid',
name: 'test.pdf',
statusText: 'Draft',
},
],
})

await wrapper.vm.$nextTick()

// Check that isTouchDevice is properly evaluated
expect(wrapper.vm.isTouchDevice).toBeDefined()
})

it('calls openFile when file open button is clicked', async () => {
wrapper = createWrapper()
const openFileSpy = vi.spyOn(wrapper.vm, 'openFile')

const testFile = {
id: 1,
uuid: 'test-uuid',
name: 'test.pdf',
statusText: 'Draft',
}

wrapper.vm.openFile(testFile)

expect(openFileSpy).toHaveBeenCalledWith(testFile)
openFileSpy.mockRestore()
})

it('calls handleDelete when delete button is clicked', async () => {
wrapper = createWrapper()
const handleDeleteSpy = vi.spyOn(wrapper.vm, 'handleDelete')

const testFile = {
id: 1,
uuid: 'test-uuid',
name: 'test.pdf',
statusText: 'Draft',
}

wrapper.vm.handleDelete(testFile)

expect(handleDeleteSpy).toHaveBeenCalledWith(testFile)
handleDeleteSpy.mockRestore()
})

it('openDocument invoked when openFile called', async () => {
const viewer = await import('../../../utils/viewer.js')
wrapper = createWrapper()

wrapper.vm.openFile({
id: 1,
uuid: 'test-uuid',
name: 'test.pdf',
})

expect(viewer.openDocument).toHaveBeenCalledWith({
fileUrl: '/apps/libresign/p/pdf/test-uuid',
filename: 'test.pdf',
nodeId: 1,
})
})
})
})
50 changes: 50 additions & 0 deletions src/tests/components/validation/EnvelopeValidation.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -388,4 +388,54 @@ describe('EnvelopeValidation', () => {
expect(signer.opened).toBe(false)
})
})

describe('RULE: View PDF button visibility based on isTouchDevice', () => {
it('has isTouchDevice computed property from mixin', () => {
wrapper = createWrapper()
expect(wrapper.vm.isTouchDevice).toBeDefined()
expect(typeof wrapper.vm.isTouchDevice).toBe('boolean')
})

it('renders actions slot when not touch device and file has nodeId', async () => {
const file = { nodeId: 123, opened: false, status: '3', name: 'test.pdf', statusText: 'Signed' }
wrapper = createWrapper({
document: { files: [file] },
})

await wrapper.vm.$nextTick()

// isTouchDevice value determines which template slot is used
expect(wrapper.vm.isTouchDevice).toBeDefined()
})

it('calls viewFile with correct parameters', async () => {
wrapper = createWrapper()
const testFile = {
uuid: 'test-uuid',
name: 'test.pdf',
nodeId: 123,
}

const viewFileSpy = vi.spyOn(wrapper.vm, 'viewFile')
wrapper.vm.viewFile(testFile)

expect(viewFileSpy).toHaveBeenCalledWith(testFile)
viewFileSpy.mockRestore()
})

it('openDocument is called when viewFile is invoked', () => {
wrapper = createWrapper()
wrapper.vm.viewFile({
uuid: 'test-uuid',
name: 'test.pdf',
nodeId: 123,
})

expect(viewer.openDocument).toHaveBeenCalledWith({
fileUrl: '/apps/libresign/p/pdf/test-uuid',
filename: 'test.pdf',
nodeId: 123,
})
})
})
})
74 changes: 74 additions & 0 deletions src/tests/mixins/isTouchDevice.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import isTouchDevice from '../../mixins/isTouchDevice.js'

describe('isTouchDevice Mixin', () => {
const TestComponent = {
mixins: [isTouchDevice],
template: '<div>{{ isTouchDevice }}</div>',
}

let wrapper

beforeEach(() => {
wrapper = null
})

afterEach(() => {
if (wrapper) {
wrapper.destroy()
}
})

describe('RULE: isTouchDevice detects touch capabilities', () => {
it('provides isTouchDevice as computed property', () => {
wrapper = mount(TestComponent)

expect(wrapper.vm.isTouchDevice).toBeDefined()
expect(typeof wrapper.vm.isTouchDevice).toBe('boolean')
})

it('returns true when environment supports touch (ontouchstart exists)', () => {
wrapper = mount(TestComponent)

// Verify the logic: should be true if either ontouchstart or maxTouchPoints exists
const hasTouchStart = 'ontouchstart' in window
const hasMaxTouchPoints = navigator.maxTouchPoints > 0
const expectedValue = hasTouchStart || hasMaxTouchPoints

expect(wrapper.vm.isTouchDevice).toBe(expectedValue)
})

it('correctly evaluates touch detection logic', () => {
wrapper = mount(TestComponent)

// Test the actual mixin logic
const result = wrapper.vm.isTouchDevice
const expected = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0)

expect(result).toBe(expected)
})

it('maintains same value on multiple accesses', () => {
wrapper = mount(TestComponent)

const firstAccess = wrapper.vm.isTouchDevice
const secondAccess = wrapper.vm.isTouchDevice

expect(firstAccess).toBe(secondAccess)
})

it('is a reactive computed property', () => {
wrapper = mount(TestComponent)

// computed properties in Vue are reactive
expect(wrapper.vm.$options.computed).toBeDefined()
expect(wrapper.vm.$options.computed.isTouchDevice).toBeDefined()
})
})
})