diff --git a/package-lock.json b/package-lock.json index a34c29a20c..a4787d9a9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,6 @@ "@nextcloud/timezones": "^1.0.2", "@nextcloud/vue": "^8.41.0", "@pinia/testing": "^0.1.7", - "@riophae/vue-treeselect": "^0.4.0", "address-rfc2822": "^2.2.4", "ckeditor5": "^47.6.2-alpha.0", "cldr-core": "^48.2.0", @@ -55,15 +54,12 @@ "raw-loader": "^4.0.2", "stylelint": "^17.14.1", "uuid": "^14.0.2", - "v-tooltip": "^2.1.3", "vue": "^2.7.16", - "vue-autosize": "^1.0.2", "vue-dndrop": "^1.3.4", "vue-frag": "^1.4.3", "vue-material-design-icons": "^5.3.1", "vue-router": "^3.6.5", "vue-shortkey": "^3.1.7", - "vue-tabs-component": "^1.5.0", "webdav": "5.10.0" }, "devDependencies": { @@ -4980,24 +4976,6 @@ "node": ">=20" } }, - "node_modules/@riophae/vue-treeselect": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.4.0.tgz", - "integrity": "sha512-J4atYmBqXQmiPFK/0B5sXKjtnGc21mBJEiyKIDZwk0Q9XuynVFX6IJ4EpaLmUgL5Tve7HAS7wkiGGSti6Uaxcg==", - "dependencies": { - "@babel/runtime": "^7.3.1", - "babel-helper-vue-jsx-merge-props": "^2.0.3", - "easings-css": "^1.0.0", - "fuzzysearch": "^1.0.3", - "is-promise": "^2.1.0", - "lodash": "^4.0.0", - "material-colors": "^1.2.6", - "watch-size": "^2.0.0" - }, - "peerDependencies": { - "vue": "^2.2.0" - } - }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", @@ -6721,16 +6699,6 @@ "postcss": "^8.1.0" } }, - "node_modules/autosize": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.4.tgz", - "integrity": "sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==" - }, - "node_modules/autosize-input": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/autosize-input/-/autosize-input-0.4.0.tgz", - "integrity": "sha512-ibGSCkkw/Seo5BU2vS1ffj5q/s8YxCCzpvmL4fkGf52McrZhCo+t88w2wMPPME6EjYjEESdo0KsuW4N5EpmmMA==" - }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -6767,11 +6735,6 @@ "node": ">=10" } }, - "node_modules/babel-helper-vue-jsx-merge-props": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-2.0.3.tgz", - "integrity": "sha512-gsLiKK7Qrb7zYJNgiXKpXblxbV5ffSwR0f5whkPAaBAR4fhi6bwRZxX9wBlIc5M/v8CCkXUbXZL4N/nSE97cqg==" - }, "node_modules/babel-loader": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.1.1.tgz", @@ -8403,11 +8366,6 @@ "node": ">= 0.4" } }, - "node_modules/easings-css": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/easings-css/-/easings-css-1.0.0.tgz", - "integrity": "sha512-7Uq7NdazNfVtr0RNmPAys8it0zKCuaqxJStYKEl72D3j4gbvXhhaM7iWNbqhA4C94ygCye6VuyhzBRQC4szeBg==" - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -9958,11 +9916,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fuzzysearch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/fuzzysearch/-/fuzzysearch-1.0.3.tgz", - "integrity": "sha512-s+kNWQuI3mo9OALw0HJ6YGmMbLqEufCh2nX/zzV5CrICQ/y4AwPxM+6TIiF9ItFCHXFCyM/BfCCmN57NTIJuPg==" - }, "node_modules/fuzzysort": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", @@ -11416,11 +11369,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-promise": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", - "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -14035,16 +13983,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/popper.js": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz", - "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==", - "deprecated": "You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/popperjs" - } - }, "node_modules/possible-typed-array-names": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", @@ -17523,17 +17461,6 @@ "uuid": "dist-node/bin/uuid" } }, - "node_modules/v-tooltip": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.1.3.tgz", - "integrity": "sha512-xXngyxLQTOx/yUEy50thb8te7Qo4XU6h4LZB6cvEfVd9mnysUxLEoYwGWDdqR+l69liKsy3IPkdYff3J1gAJ5w==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21", - "popper.js": "^1.16.1", - "vue-resize": "^1.0.1" - } - }, "node_modules/vanilla-colorful": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz", @@ -17791,15 +17718,6 @@ "csstype": "^3.1.0" } }, - "node_modules/vue-autosize": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/vue-autosize/-/vue-autosize-1.0.2.tgz", - "integrity": "sha512-bw99eTjiCK9HI80w9vkv8tl+/wJWELl0rn3QlWfH0I4K37XGjJj0sZX0MDFh1O8Yb7lNq096nWlKJR2Nj0a4mg==", - "dependencies": { - "autosize": "^4.0.0", - "autosize-input": "^0.4.0" - } - }, "node_modules/vue-color": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.8.1.tgz", @@ -18022,14 +17940,6 @@ "node": ">=4.0.0" } }, - "node_modules/vue-tabs-component": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/vue-tabs-component/-/vue-tabs-component-1.5.0.tgz", - "integrity": "sha512-ld4p+hv49Fimw+zv/7GQqMhbjAHjpbWF3UiJtmMaSnvLKbsB1ysfs9dQH0SZ8NvdYpqqKay/VLIqR9yXgse1Sg==", - "peerDependencies": { - "vue": "^2.3.0" - } - }, "node_modules/vue-template-compiler": { "version": "2.7.16", "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", @@ -18101,11 +18011,6 @@ "node": ">=18" } }, - "node_modules/watch-size": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/watch-size/-/watch-size-2.0.0.tgz", - "integrity": "sha512-M92R89dNoTPWyCD+HuUEDdhaDnh9jxPGOwlDc0u51jAgmjUvzqaEMynXSr3BaWs+QdHYk4KzibPy1TFtjLmOZQ==" - }, "node_modules/watchpack": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.2.tgz", diff --git a/package.json b/package.json index 94350e9966..5d8c8b926c 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@nextcloud/timezones": "^1.0.2", "@nextcloud/vue": "^8.41.0", "@pinia/testing": "^0.1.7", - "@riophae/vue-treeselect": "^0.4.0", "address-rfc2822": "^2.2.4", "ckeditor5": "^47.6.2-alpha.0", "cldr-core": "^48.2.0", @@ -68,15 +67,12 @@ "raw-loader": "^4.0.2", "stylelint": "^17.14.1", "uuid": "^14.0.2", - "v-tooltip": "^2.1.3", "vue": "^2.7.16", - "vue-autosize": "^1.0.2", "vue-dndrop": "^1.3.4", "vue-frag": "^1.4.3", "vue-material-design-icons": "^5.3.1", "vue-router": "^3.6.5", "vue-shortkey": "^3.1.7", - "vue-tabs-component": "^1.5.0", "webdav": "5.10.0" }, "devDependencies": { diff --git a/src/components/AccountForm.vue b/src/components/AccountForm.vue index b48e25995e..287f590736 100644 --- a/src/components/AccountForm.vue +++ b/src/components/AccountForm.vue @@ -4,234 +4,236 @@ --> - diff --git a/src/components/MailboxThread.vue b/src/components/MailboxThread.vue index 02e492eca3..1dd0907624 100644 --- a/src/components/MailboxThread.vue +++ b/src/components/MailboxThread.vue @@ -200,7 +200,8 @@ diff --git a/src/main.js b/src/main.js index bc815d8fbf..712a2bb2a2 100644 --- a/src/main.js +++ b/src/main.js @@ -7,7 +7,6 @@ import { getRequestToken } from '@nextcloud/auth' import { registerDavProperty } from '@nextcloud/files' import { generateFilePath } from '@nextcloud/router' import { createPinia, PiniaVuePlugin } from 'pinia' -import vToolTip from 'v-tooltip' import Vue from 'vue' import VueShortKey from 'vue-shortkey' import App from './App.vue' @@ -27,7 +26,6 @@ const pinia = createPinia() Vue.mixin(Nextcloud) Vue.use(VueShortKey, { prevent: ['input', 'div', 'textarea'] }) -Vue.use(vToolTip) registerDavProperty('nc:share-attributes', { nc: 'http://nextcloud.org/ns' }) diff --git a/src/tests/unit/components/MailboxInlinePicker.vue.spec.js b/src/tests/unit/components/MailboxInlinePicker.vue.spec.js new file mode 100644 index 0000000000..69969e72c8 --- /dev/null +++ b/src/tests/unit/components/MailboxInlinePicker.vue.spec.js @@ -0,0 +1,71 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { createTestingPinia } from '@pinia/testing' +import { createLocalVue, shallowMount } from '@vue/test-utils' +import { PiniaVuePlugin, setActivePinia } from 'pinia' +import MailboxInlinePicker from '../../../components/MailboxInlinePicker.vue' +import Nextcloud from '../../../mixins/Nextcloud.js' +import useMainStore from '../../../store/mainStore.js' + +const localVue = createLocalVue() +localVue.use(PiniaVuePlugin) +localVue.mixin(Nextcloud) + +describe('MailboxInlinePicker', () => { + let store + + const mount = () => shallowMount(MailboxInlinePicker, { + propsData: { + account: { accountId: 1 }, + }, + localVue, + }) + + beforeEach(() => { + setActivePinia(createTestingPinia()) + store = useMainStore() + }) + + it('flattens nested mailboxes into indented options', () => { + store.getMailboxes = vi.fn().mockReturnValue([ + { databaseId: 1, displayName: 'INBOX' }, + { databaseId: 4, displayName: 'Sent' }, + ]) + store.getSubMailboxes = vi.fn().mockImplementation((id) => { + switch (id) { + case 1: + return [{ databaseId: 2, displayName: 'Archive' }] + case 2: + return [{ databaseId: 3, displayName: '2020' }] + default: + return [] + } + }) + + const view = mount() + + expect(view.vm.mailboxes).toEqual([ + { id: 1, label: 'INBOX', depth: 0 }, + { id: 2, label: 'Archive', depth: 1 }, + { id: 3, label: '2020', depth: 2 }, + { id: 4, label: 'Sent', depth: 0 }, + ]) + }) + + it('skips mailboxes the user may not insert into', () => { + store.getMailboxes = vi.fn().mockReturnValue([ + { databaseId: 1, displayName: 'INBOX' }, + { databaseId: 2, displayName: 'Shared', myAcls: 'lr' }, + ]) + store.getSubMailboxes = vi.fn().mockReturnValue([]) + + const view = mount() + + expect(view.vm.mailboxes).toEqual([ + { id: 1, label: 'INBOX', depth: 0 }, + ]) + }) +}) diff --git a/src/tests/unit/util/filePicker.spec.js b/src/tests/unit/util/filePicker.spec.js new file mode 100644 index 0000000000..c8e7193657 --- /dev/null +++ b/src/tests/unit/util/filePicker.spec.js @@ -0,0 +1,43 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getFilePickerBuilder } from '@nextcloud/dialogs' +import { pickFolder } from '../../../util/filePicker.js' + +vi.mock('@nextcloud/dialogs') + +describe('pickFolder', () => { + let builder + let picker + + beforeEach(() => { + picker = { pick: vi.fn().mockResolvedValue('/Documents') } + builder = { + allowDirectories: vi.fn(() => builder), + setMultiSelect: vi.fn(() => builder), + setMimeTypeFilter: vi.fn(() => builder), + addButton: vi.fn(() => builder), + build: vi.fn(() => picker), + } + getFilePickerBuilder.mockReturnValue(builder) + }) + + it('picks a single folder', async () => { + const path = await pickFolder('Choose a folder') + + expect(getFilePickerBuilder).toHaveBeenCalledWith('Choose a folder') + expect(builder.allowDirectories).toHaveBeenCalledWith(true) + expect(builder.setMultiSelect).toHaveBeenCalledWith(false) + expect(builder.setMimeTypeFilter).toHaveBeenCalledWith(['httpd/unix-directory']) + expect(path).toBe('/Documents') + }) + + it('propagates the rejection of a dismissed picker', async () => { + const closed = new Error('FilePicker: No nodes selected') + picker.pick.mockRejectedValue(closed) + + await expect(pickFolder('Choose a folder')).rejects.toThrow(closed) + }) +}) diff --git a/src/util/filePicker.js b/src/util/filePicker.js new file mode 100644 index 0000000000..2064984b33 --- /dev/null +++ b/src/util/filePicker.js @@ -0,0 +1,28 @@ +/** + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { getFilePickerBuilder } from '@nextcloud/dialogs' +import { t } from '@nextcloud/l10n' + +/** + * Let the user pick a folder from Files. + * + * @param {string} name Title of the file picker + * @return {Promise} Path of the picked folder + * @throws {import('@nextcloud/dialogs').FilePickerClosed} If the picker was closed without picking + */ +export function pickFolder(name) { + return getFilePickerBuilder(name) + .allowDirectories(true) + .setMultiSelect(false) + .setMimeTypeFilter(['httpd/unix-directory']) + .addButton({ + label: t('mail', 'Choose'), + variant: 'primary', + callback: () => {}, + }) + .build() + .pick() +}