From 4d1a99835f3d533c86d00f573e3bf133912435df Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:21:35 -0300
Subject: [PATCH 01/25] chore: add test infrastructure for component testing
- Add @vue/test-utils for component mounting and interaction
- Add jsdom for DOM environment in Vitest
- Add @vitejs/plugin-vue for Vue SFC compilation in tests
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
package.json | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/package.json b/package.json
index ef7fbff..9418520 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,9 @@
"build": "NODE_ENV=production vite --mode production build",
"dev": "NODE_ENV=development vite --mode development build",
"screenshots:refresh": "node playwright/generate-screenshots.mjs",
+ "screenshots:refresh:changed": "node playwright/generate-screenshots.mjs --changed",
"typescript:generate": "npx openapi-typescript -t",
+ "ts:check": "npx tsc --noEmit",
"watch": "NODE_ENV=development vite --mode development build --watch",
"test": "vitest run",
"test:watch": "vitest",
@@ -33,9 +35,12 @@
"vuedraggable": "^4.1.0"
},
"devDependencies": {
+ "@vue/test-utils": "^2.4.6",
+ "@vitejs/plugin-vue": "^6.0.4",
"@nextcloud/browserslist-config": "^3.1.2",
"@nextcloud/vite-config": "^2.5.2",
"@playwright/test": "^1.54.2",
+ "jsdom": "^26.1.0",
"openapi-typescript": "^7.13.0",
"typescript": "^5.9.3",
"vitest": "^4.0.18"
From 6b075959c389bf9671caa6d0b372794a4bae3957 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:21:42 -0300
Subject: [PATCH 02/25] chore: update vitest config for component testing
- Add jsdom as test environment for browser APIs (localStorage, window)
- Configure @vitejs/plugin-vue for Vue SFC compilation
- Set component test include pattern with src/tests/ suffix
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
vitest.config.js | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/vitest.config.js b/vitest.config.js
index a2af810..a8e5ba6 100644
--- a/vitest.config.js
+++ b/vitest.config.js
@@ -1,11 +1,14 @@
// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
-import { defineConfig } from 'vitest/config'
+import { mergeConfig } from 'vitest/config'
+import vue from '@vitejs/plugin-vue'
-export default defineConfig({
+export default mergeConfig({
+ plugins: [vue()],
+}, {
test: {
include: ['src/tests/**/*.{test,spec}.ts'],
- environment: 'node',
+ environment: 'jsdom',
},
})
From 1b4e633b28d022979efcde4b16100b03e2fc9497 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:21:50 -0300
Subject: [PATCH 03/25] feat: create AdminSupportBanner component
- Extract LibreCode sponsorship banner into dedicated component
- Encapsulate visibility state with localStorage persistence
- Handle dismiss action and sponsor page navigation internally
- Props: storageKey, sponsorUrl (with sensible defaults)
- Fully autonomous - no parent event coordination needed
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
src/components/AdminSupportBanner.vue | 142 ++++++++++++++++++++++++++
1 file changed, 142 insertions(+)
create mode 100644 src/components/AdminSupportBanner.vue
diff --git a/src/components/AdminSupportBanner.vue b/src/components/AdminSupportBanner.vue
new file mode 100644
index 0000000..acd878a
--- /dev/null
+++ b/src/components/AdminSupportBanner.vue
@@ -0,0 +1,142 @@
+
+
+
+
+
+
+
Help keep Profile Fields sustainable.
+
Profile Fields is open source under the AGPL license and maintained by the LibreCode team, creators of LibreSign.
+
If your organization depends on it, please help us sustain its development and maintenance.
+
+
+
+ Sponsor LibreSign
+
+
+
+ Maybe later
+
+
+
+
+
+
+
+
+
+
+
+
From e1def5c223b5272894b84af479a466431ff016b0 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:21:57 -0300
Subject: [PATCH 04/25] feat: create AdminSelectOptionsSection component
- Extract select field options editor into dedicated admin component
- Implement drag-and-drop reordering with vuedraggable
- Add bulk import dialog for multiple options
- Validation: detect duplicate options (case-insensitive)
- Keyboard support: Enter to add, Backspace/Delete to remove
- Prevent empty option duplicates with smart add logic
- Maintains full feature parity with extracted code
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
.../admin/AdminSelectOptionsSection.vue | 424 ++++++++++++++++++
1 file changed, 424 insertions(+)
create mode 100644 src/components/admin/AdminSelectOptionsSection.vue
diff --git a/src/components/admin/AdminSelectOptionsSection.vue b/src/components/admin/AdminSelectOptionsSection.vue
new file mode 100644
index 0000000..45bd1b3
--- /dev/null
+++ b/src/components/admin/AdminSelectOptionsSection.vue
@@ -0,0 +1,424 @@
+
+
+
+
+
+
+
Options
+
+
+ {{ normalizedOptionCount }}
+ {{ normalizedOptionCount === 1 ? 'option' : 'options' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Move up
+
+
+
+
+
+ Move down
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Add single option
+
+
+ Add multiple options
+
+
+
+
+
+
+
+ {{ bulkOptionValues.length === 1 ? '1 option ready.' : `${bulkOptionValues.length} options ready.` }}
+
+
+
+
+
+ Cancel
+
+
+ Add selected options
+
+
+
+
+
+
+
+
+
From aa664ea42f68639ed2642385e84943de4606cfb0 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:22:05 -0300
Subject: [PATCH 05/25] test: add AdminSupportBanner component specs
- Test sponsor page opens with correct URL and window features
- Test dismiss button hides banner and persists state to localStorage
- Test pre-dismissed state: starts hidden when localStorage key exists
- Setup: vi.mock @nextcloud/vue components; mount with jsdom
- Cleanup: afterEach clears localStorage to prevent test pollution
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
.../admin/AdminSupportBanner.spec.ts | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
create mode 100644 src/tests/components/admin/AdminSupportBanner.spec.ts
diff --git a/src/tests/components/admin/AdminSupportBanner.spec.ts b/src/tests/components/admin/AdminSupportBanner.spec.ts
new file mode 100644
index 0000000..00a7996
--- /dev/null
+++ b/src/tests/components/admin/AdminSupportBanner.spec.ts
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { defineComponent, nextTick } from 'vue'
+import AdminSupportBanner from '../../../components/AdminSupportBanner.vue'
+
+vi.mock('@nextcloud/vue', () => ({
+ NcButton: defineComponent({
+ name: 'NcButton',
+ emits: ['click'],
+ template: ' ',
+ }),
+ NcNoteCard: defineComponent({
+ name: 'NcNoteCard',
+ template: '
',
+ }),
+}))
+
+afterEach(() => {
+ window.localStorage.clear()
+ vi.restoreAllMocks()
+})
+
+describe('AdminSupportBanner', () => {
+ it('opens sponsor page when sponsor button is clicked', async() => {
+ const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
+ const wrapper = mount(AdminSupportBanner)
+
+ await wrapper.get('button').trigger('click')
+
+ expect(openSpy).toHaveBeenCalledWith('https://github.com/sponsors/LibreCodeCoop', '_blank', 'noopener,noreferrer')
+ })
+
+ it('hides itself after dismiss and persists state', async() => {
+ const wrapper = mount(AdminSupportBanner)
+
+ const buttons = wrapper.findAll('button')
+ await buttons[1].trigger('click')
+
+ expect(wrapper.find('[data-testid="profile-fields-admin-support-banner"]').exists()).toBe(false)
+ expect(window.localStorage.getItem('profile_fields_support_banner_dismissed')).toBe('1')
+ })
+
+ it('starts hidden when dismissal key is already persisted', () => {
+ window.localStorage.setItem('profile_fields_support_banner_dismissed', '1')
+
+ const wrapper = mount(AdminSupportBanner)
+ return nextTick().then(() => {
+ expect(wrapper.find('[data-testid="profile-fields-admin-support-banner"]').exists()).toBe(false)
+ })
+ })
+})
From cd674fd9e3a3eea23f368c008245d9c75b0d95a1 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:22:12 -0300
Subject: [PATCH 06/25] test: add AdminSelectOptionsSection component specs
- Test add new option: button click emits update:modelValue with correct data
- Test skip add when empty exists: prevents duplicate empty option creation
- Setup: mock @nextcloud/vue + Draggable; mount with jsdom
- Validation: check emitted events and payload structure
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
.../admin/AdminSelectOptionsSection.spec.ts | 96 +++++++++++++++++++
1 file changed, 96 insertions(+)
create mode 100644 src/tests/components/admin/AdminSelectOptionsSection.spec.ts
diff --git a/src/tests/components/admin/AdminSelectOptionsSection.spec.ts b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts
new file mode 100644
index 0000000..7eb7036
--- /dev/null
+++ b/src/tests/components/admin/AdminSelectOptionsSection.spec.ts
@@ -0,0 +1,96 @@
+// SPDX-FileCopyrightText: 2026 LibreCode coop and LibreCode contributors
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+import { describe, expect, it, vi } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { defineComponent } from 'vue'
+import AdminSelectOptionsSection from '../../../components/admin/AdminSelectOptionsSection.vue'
+
+vi.mock('@nextcloud/vue', () => ({
+ NcActionButton: defineComponent({ template: '
' }),
+ NcActions: defineComponent({ template: '
' }),
+ NcButton: defineComponent({
+ name: 'NcButton',
+ emits: ['click'],
+ template: ' ',
+ }),
+ NcIconSvgWrapper: defineComponent({ template: '
' }),
+ NcInputField: defineComponent({ template: '
' }),
+}))
+
+vi.mock('@nextcloud/vue/components/NcDialog', () => ({
+ default: defineComponent({ template: '
' }),
+}))
+
+vi.mock('@nextcloud/vue/components/NcTextArea', () => ({
+ default: defineComponent({ template: '' }),
+}))
+
+const DraggableStub = defineComponent({
+ name: 'Draggable',
+ props: {
+ modelValue: {
+ type: Array,
+ required: true,
+ },
+ },
+ template: '
',
+})
+
+describe('AdminSelectOptionsSection', () => {
+ it('emits updated model when adding a new option', async() => {
+ const wrapper = mount(AdminSelectOptionsSection, {
+ props: {
+ modelValue: [{ id: 'option-0', value: 'Alpha' }],
+ isSaving: false,
+ },
+ global: {
+ stubs: {
+ Draggable: DraggableStub,
+ NcDialog: false,
+ NcTextArea: false,
+ NcActionButton: false,
+ NcActions: false,
+ NcIconSvgWrapper: false,
+ NcInputField: false,
+ },
+ },
+ })
+
+ const addButton = wrapper.find('[data-testid="profile-fields-admin-add-option"]')
+ await addButton.trigger('click')
+
+ const emissions = wrapper.emitted('update:modelValue')
+ expect(emissions).toBeTruthy()
+ expect((emissions as any[])[0][0]).toHaveLength(2)
+ expect((emissions as any[])[0][0][1].value).toBe('')
+ })
+
+ it('does not emit add when there is already an empty option', async() => {
+ const wrapper = mount(AdminSelectOptionsSection, {
+ props: {
+ modelValue: [
+ { id: 'option-0', value: 'Alpha' },
+ { id: 'option-1', value: '' },
+ ],
+ isSaving: false,
+ },
+ global: {
+ stubs: {
+ Draggable: DraggableStub,
+ NcDialog: false,
+ NcTextArea: false,
+ NcActionButton: false,
+ NcActions: false,
+ NcIconSvgWrapper: false,
+ NcInputField: false,
+ },
+ },
+ })
+
+ const addButton = wrapper.find('[data-testid="profile-fields-admin-add-option"]')
+ await addButton.trigger('click')
+
+ expect(wrapper.emitted('update:modelValue')).toBeUndefined()
+ })
+})
From db8df9e8246138b89e1472b755df44ebe5606d54 Mon Sep 17 00:00:00 2001
From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Date: Fri, 20 Mar 2026 10:22:20 -0300
Subject: [PATCH 07/25] refactor: extract select options editor into dedicated
component
- Remove ~200 lines of options editor markup and logic
- Extract to AdminSelectOptionsSection component
- Fix CSS button truncation: add flex: 0 0 auto + min-width: max-content
- Simplify AdminSettings template and reduce component complexity
- Use v-model binding for options sync with child component
- Maintain full functionality: drag, bulk import, validation, keyboard support
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
---
src/views/AdminSettings.vue | 574 +++++-------------------------------
1 file changed, 72 insertions(+), 502 deletions(-)
diff --git a/src/views/AdminSettings.vue b/src/views/AdminSettings.vue
index a83c93a..f6cf87c 100644
--- a/src/views/AdminSettings.vue
+++ b/src/views/AdminSettings.vue
@@ -7,36 +7,22 @@ SPDX-License-Identifier: AGPL-3.0-or-later
-
+
- {{ isSaving ? 'Saving...' : (isEditing ? 'Save changes' : 'Create field') }}
+ {{ isSaving ? 'Saving changes...' : (isEditing ? 'Save changes' : 'Create field') }}
Delete field
@@ -360,59 +262,26 @@ SPDX-License-Identifier: AGPL-3.0-or-later
Delete field
- {{ isSaving ? 'Saving...' : (isEditing ? 'Save changes' : 'Create field') }}
+ {{ isSaving ? 'Saving changes...' : (isEditing ? 'Save changes' : 'Create field') }}
-
-
-
-
- {{ bulkOptionValues.length === 1 ? '1 option ready to add.' : `${bulkOptionValues.length} options ready to add.` }}
-
-
-
-
-
- Cancel
-
-
- Add options
-
-
-