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
4 changes: 4 additions & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

namespace OCA\Libresign;

use OCA\Libresign\AppInfo\Application;
use OCA\Libresign\Service\Envelope\EnvelopeService;
use OCA\Libresign\Service\SignatureTextService;
use OCA\Libresign\Service\SignerElementsService;
use OCP\App\IAppManager;
use OCP\Capabilities\IPublicCapability;
use OCP\IAppConfig;

/**
* @psalm-import-type LibresignCapabilities from ResponseDefinitions
Expand All @@ -27,6 +29,7 @@ public function __construct(
protected SignatureTextService $signatureTextService,
protected IAppManager $appManager,
protected EnvelopeService $envelopeService,
protected IAppConfig $appConfig,
) {
}

Expand All @@ -40,6 +43,7 @@ public function getCapabilities(): array {
$capabilities = [
'features' => self::FEATURES,
'config' => [
'show-confetti' => $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true),
'sign-elements' => [
'is-available' => $this->signerElementsService->isSignElementsAvailable(),
'can-create-signature' => $this->signerElementsService->canCreateSignature(),
Expand Down
1 change: 1 addition & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@
* @psalm-type LibresignCapabilities = array{
* features: list<string>,
* config: array{
* show-confetti: bool,
* sign-elements: array{
* is-available: bool,
* can-create-signature: bool,
Expand Down
1 change: 1 addition & 0 deletions lib/Settings/Admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public function getForm(): TemplateResponse {
$this->initialState->provideInitialState('approval_group', $this->appConfig->getValueArray(Application::APP_ID, 'approval_group', ['admin']));
$this->initialState->provideInitialState('envelope_enabled', $this->appConfig->getValueBool(Application::APP_ID, 'envelope_enabled', true));
$this->initialState->provideInitialState('parallel_workers', $this->appConfig->getValueString(Application::APP_ID, 'parallel_workers', '4'));
$this->initialState->provideInitialState('show_confetti_after_signing', $this->appConfig->getValueBool(Application::APP_ID, 'show_confetti_after_signing', true));
return new TemplateResponse(Application::APP_ID, 'admin_settings');
}

Expand Down
4 changes: 4 additions & 0 deletions openapi-administration.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
4 changes: 4 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
4 changes: 4 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@
"config": {
"type": "object",
"required": [
"show-confetti",
"sign-elements",
"envelope",
"upload"
],
"properties": {
"show-confetti": {
"type": "boolean"
},
"sign-elements": {
"type": "object",
"required": [
Expand Down
111 changes: 111 additions & 0 deletions src/tests/views/Settings/Confetti.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* SPDX-FileCopyrightText: 2026 LibreSign contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { mount } from '@vue/test-utils'

const loadStateMock = vi.fn()

vi.mock('@nextcloud/initial-state', () => ({
loadState: (...args: unknown[]) => loadStateMock(...args),
}))

vi.mock('@nextcloud/l10n', () => ({
t: vi.fn((_app: string, text: string) => text),
translate: vi.fn((_app: string, text: string) => text),
translatePlural: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)),
n: vi.fn((_app: string, singular: string, plural: string, count: number) => (count === 1 ? singular : plural)),
isRTL: vi.fn(() => false),
getLanguage: vi.fn(() => 'en'),
getLocale: vi.fn(() => 'en'),
}))

let Confetti: unknown

beforeAll(async () => {
;({ default: Confetti } = await import('../../../views/Settings/Confetti.vue'))
})

describe('Confetti', () => {
beforeEach(() => {
loadStateMock.mockReset()
})

it('defaults to true when state is not set', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback)

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

expect(wrapper.vm.showConfetti).toBe(true)
})

it('reads show_confetti_after_signing from initial state', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => {
if (key === 'show_confetti_after_signing') return true
return fallback
})

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

expect(wrapper.vm.showConfetti).toBe(true)
})

it('calls OCP.AppConfig.setValue with "1" when enabled', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => fallback)
const setValueMock = vi.fn()
vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } })

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

await wrapper.setData({ showConfetti: true })
wrapper.vm.saveShowConfetti()

expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '1')
})

it('calls OCP.AppConfig.setValue with "0" when disabled', async () => {
loadStateMock.mockImplementation((_app: string, key: string, fallback: unknown) => {
if (key === 'show_confetti_after_signing') return true
return fallback
})
const setValueMock = vi.fn()
vi.stubGlobal('OCP', { AppConfig: { setValue: setValueMock } })

const wrapper = mount(Confetti as never, {
global: {
stubs: {
NcSettingsSection: { template: '<div><slot /></div>' },
NcCheckboxRadioSwitch: { template: '<div><slot /></div>' },
},
},
})

await wrapper.setData({ showConfetti: false })
wrapper.vm.saveShowConfetti()

expect(setValueMock).toHaveBeenCalledWith('libresign', 'show_confetti_after_signing', '0')
})
})
25 changes: 25 additions & 0 deletions src/tests/views/Validation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { afterEach, describe, expect, it, beforeEach, vi } from 'vitest'
import { shallowMount } from '@vue/test-utils'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import JSConfetti from 'js-confetti'
import Validation from '../../views/Validation.vue'

Expand Down Expand Up @@ -97,6 +98,17 @@ const mockRouter = {
push: vi.fn(),
}

// Mock capabilities - show-confetti enabled by default so existing tests pass
vi.mock('@nextcloud/capabilities', () => ({
getCapabilities: vi.fn(() => ({
libresign: {
config: {
'show-confetti': true,
},
},
})),
}))

// Mock initial state
vi.mock('@nextcloud/initial-state', () => ({
loadState: vi.fn((app, key, defaultValue) => defaultValue),
Expand Down Expand Up @@ -647,6 +659,19 @@ describe('Validation.vue - Business Logic', () => {
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})

it('does not fire confetti when show-confetti capability is disabled', async () => {
vi.mocked(getCapabilities).mockReturnValueOnce({
libresign: {
config: {
'show-confetti': false,
},
},
} as ReturnType<typeof getCapabilities>)
vi.spyOn(wrapper.vm, 'isAfterSigned', 'get').mockReturnValue(true)
wrapper.vm.handleValidationSuccess({ status: SIGNED_STATUS, signers: [] })
expect(mockAddConfetti).not.toHaveBeenCalled()
})
})

describe('handleSigningComplete method', () => {
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi-administration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1527,6 +1527,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,7 @@ export type components = {
Capabilities: {
features: string[];
config: {
"show-confetti": boolean;
"sign-elements": {
"is-available": boolean;
"can-create-signature": boolean;
Expand Down
41 changes: 41 additions & 0 deletions src/views/Settings/Confetti.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<!--
- SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<NcSettingsSection
:name="t('libresign', 'Confetti animation')"
:description="t('libresign', 'Control whether a confetti animation is shown after a document is signed.')">
<NcCheckboxRadioSwitch type="switch"
v-model="showConfetti"
@update:modelValue="saveShowConfetti">
{{ t('libresign', 'Show confetti animation after signing') }}
</NcCheckboxRadioSwitch>
</NcSettingsSection>
</template>
<script>
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'

import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'

export default {
name: 'ConfettiSettings',
components: {
NcSettingsSection,
NcCheckboxRadioSwitch,
},
data() {
return {
showConfetti: loadState('libresign', 'show_confetti_after_signing', true) === true,
}
},
methods: {
t,
saveShowConfetti() {
OCP.AppConfig.setValue('libresign', 'show_confetti_after_signing', this.showConfetti ? '1' : '0')
},
},
}
</script>
3 changes: 3 additions & 0 deletions src/views/Settings/Settings.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Envelope />
<Reminders />
<TSA />
<Confetti />
</div>
</template>

Expand All @@ -51,6 +52,7 @@ import SignatureFlow from './SignatureFlow.vue'
import SignatureHashAlgorithm from './SignatureHashAlgorithm.vue'
import SignatureStamp from './SignatureStamp.vue'
import SigningMode from './SigningMode.vue'
import Confetti from './Confetti.vue'
import Envelope from './Envelope.vue'
import SupportProject from './SupportProject.vue'
import TSA from './TSA.vue'
Expand All @@ -76,6 +78,7 @@ export default {
SignatureFlow,
SignatureHashAlgorithm,
SignatureStamp,
Confetti,
SigningMode,
Envelope,
SupportProject,
Expand Down
7 changes: 5 additions & 2 deletions src/views/Validation.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ import {
} from '@mdi/js'
import JSConfetti from 'js-confetti'
import axios from '@nextcloud/axios'
import { getCapabilities } from '@nextcloud/capabilities'
import { formatFileSize } from '@nextcloud/files'
import { loadState } from '@nextcloud/initial-state'
import { t } from '@nextcloud/l10n'
Expand Down Expand Up @@ -614,8 +615,10 @@ export default {
&& this.document.files.every(file => isSignedStatus(file.status))
const signerCompleted = this.isCurrentSignerSigned()
if ((isSignedDoc || allFilesSigned || signerCompleted) && (this.isAfterSigned || this.shouldFireAsyncConfetti)) {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti()
if (getCapabilities()?.libresign?.config?.['show-confetti'] === true) {
const jsConfetti = new JSConfetti()
jsConfetti.addConfetti()
}
this.shouldFireAsyncConfetti = false
}
},
Expand Down
Loading
Loading