From b01b23b7ec500ee883fe05d740c82f3f604d75cb Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Wed, 25 Jun 2025 23:56:40 +0200 Subject: [PATCH 1/2] feat(NcFilePicker): add picker component to select local files This can be used e.g. to upload files, for example in the forms app but also for the files or photos app. Signed-off-by: Ferdinand Thiessen --- l10n/messages.pot | 21 ++ src/components/NcFilePicker/NcFilePicker.vue | 288 ++++++++++++++++++ src/components/NcFilePicker/index.ts | 6 + src/components/index.ts | 1 + .../NcFilePicker/NcFilePicker.spec.ts | 201 ++++++++++++ .../NcFilePicker/NcFilePickerEmit.story.vue | 23 ++ .../components/NcFilePicker/folder/.gitkeep | 0 ...ng-with-default-props-1-chromium-linux.png | Bin 0 -> 1966 bytes ...ting-the-variant-prop-1-chromium-linux.png | Bin 0 -> 1966 bytes ...ting-the-variant-prop-2-chromium-linux.png | Bin 0 -> 1966 bytes ...ting-the-variant-prop-3-chromium-linux.png | Bin 0 -> 1928 bytes ...ting-the-variant-prop-4-chromium-linux.png | Bin 0 -> 1538 bytes 12 files changed, 540 insertions(+) create mode 100644 src/components/NcFilePicker/NcFilePicker.vue create mode 100644 src/components/NcFilePicker/index.ts create mode 100644 tests/component/components/NcFilePicker/NcFilePicker.spec.ts create mode 100644 tests/component/components/NcFilePicker/NcFilePickerEmit.story.vue create mode 100644 tests/component/components/NcFilePicker/folder/.gitkeep create mode 100644 tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/mounting-with-default-props-1-chromium-linux.png create mode 100644 tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-1-chromium-linux.png create mode 100644 tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-2-chromium-linux.png create mode 100644 tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-3-chromium-linux.png create mode 100644 tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-4-chromium-linux.png diff --git a/l10n/messages.pot b/l10n/messages.pot index 8f83149482..b57e92c9ee 100644 --- a/l10n/messages.pot +++ b/l10n/messages.pot @@ -381,6 +381,15 @@ msgstr "" msgid "Pick an emoji" msgstr "" +msgid "Pick file" +msgstr "" + +msgid "Pick files" +msgstr "" + +msgid "Pick folder" +msgstr "" + msgid "Please choose a date" msgstr "" @@ -549,6 +558,18 @@ msgstr "" msgid "Undo changes" msgstr "" +msgid "Upload file" +msgstr "" + +msgid "Upload files" +msgstr "" + +msgid "Upload folder" +msgstr "" + +msgid "Uploading …" +msgstr "" + msgid "User status: {status}" msgstr "" diff --git a/src/components/NcFilePicker/NcFilePicker.vue b/src/components/NcFilePicker/NcFilePicker.vue new file mode 100644 index 0000000000..666ac83141 --- /dev/null +++ b/src/components/NcFilePicker/NcFilePicker.vue @@ -0,0 +1,288 @@ + + + + + + + + + +This component allows to pick local files (or directories) which can be used to upload them to Nextcloud or directly process them in the browser. + +### Exposed methods + +- `function reset(): void` + This method allows to reset the internal state of the file picker to clear the current selection + +### Example + +```vue + + + +``` + diff --git a/src/components/NcFilePicker/index.ts b/src/components/NcFilePicker/index.ts new file mode 100644 index 0000000000..499dbee535 --- /dev/null +++ b/src/components/NcFilePicker/index.ts @@ -0,0 +1,6 @@ +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +export { default } from './NcFilePicker.vue' diff --git a/src/components/index.ts b/src/components/index.ts index 086153520d..284e5e29ca 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -59,6 +59,7 @@ export { default as NcDialogButton } from './NcDialogButton/index.ts' export { default as NcEllipsisedOption } from './NcEllipsisedOption/index.js' export { default as NcEmojiPicker } from './NcEmojiPicker/index.js' export { default as NcEmptyContent } from './NcEmptyContent/index.ts' +export { default as NcFilePicker } from './NcFilePicker/index.ts' export { default as NcFormBox } from './NcFormBox/index.ts' export { default as NcFormBoxButton } from './NcFormBoxButton/index.ts' export { default as NcFormBoxCopyButton } from './NcFormBoxCopyButton/index.ts' diff --git a/tests/component/components/NcFilePicker/NcFilePicker.spec.ts b/tests/component/components/NcFilePicker/NcFilePicker.spec.ts new file mode 100644 index 0000000000..fd1bce9a28 --- /dev/null +++ b/tests/component/components/NcFilePicker/NcFilePicker.spec.ts @@ -0,0 +1,201 @@ +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +import { expect, test } from '@playwright/experimental-ct-vue' +import { join } from 'node:path' +import NcFilePicker from '../../../../src/components/NcFilePicker/NcFilePicker.vue' +import NcFilePickerEmitStory from './NcFilePickerEmit.story.vue' + +test('mounting with default props', async ({ mount, page }) => { + await mount(NcFilePicker, { + props: {}, + }) + + const button = page.getByRole('button', { name: 'Pick file' }) + await expect(button).toBeVisible() + // check that the input is hidden + const input = page.locator('input[type="file"]') + await expect(input).toHaveAttribute('aria-hidden', 'true') +}) + +test('setting the variant prop', { tag: '@visual' }, async ({ mount, page, browserName }) => { + if (browserName !== 'chromium') { + test.skip() + } + + const component = await mount(NcFilePicker, { + props: {}, + }) + + const button = page.getByRole('button', { name: 'Pick file' }) + await expect(button).toHaveScreenshot() + + await component.update({ props: { variant: 'primary' } }) + await expect(button).toHaveScreenshot() + + await component.update({ props: { variant: 'secondary' } }) + await expect(button).toHaveScreenshot() + + await component.update({ props: { variant: 'tertiary' } }) + await expect(button).toHaveScreenshot() +}) + +test('label is adjusted to match requested mode', async ({ mount, page }) => { + const component = await mount(NcFilePicker, { + props: {}, + }) + + await expect(page.getByRole('button', { name: 'Pick file' })).toBeVisible() + + await component.update({ props: { multiple: true } }) + await expect(page.getByRole('button', { name: 'Pick files' })).toBeVisible() + + await component.update({ props: { directory: true, multiple: false } }) + await expect(page.getByRole('button', { name: 'Pick file' })).toBeVisible() + + await component.update({ props: { directory: true, multiple: true } }) + await expect(page.getByRole('button', { name: 'Pick files' })).toBeVisible() + + await component.update({ props: { directory: true, directoryOnly: true, multiple: false } }) + await expect(page.getByRole('button', { name: 'Pick folder' })).toBeVisible() + + await component.update({ props: { directory: true, directoryOnly: true, multiple: true } }) + await expect(page.getByRole('button', { name: 'Pick folder' })).toBeVisible() // ignored because browsers only support single folder picking +}) + +test('can override labels', async ({ mount, page }) => { + const label = 'Custom label' + const component = await mount(NcFilePicker, { + props: { + label, + }, + }) + + await expect(page.getByRole('button', { name: label })).toBeVisible() + + await component.update({ props: { label, multiple: true } }) + await expect(page.getByRole('button', { name: label })).toBeVisible() + + await component.update({ props: { label, directory: true } }) + await expect(page.getByRole('button', { name: label })).toBeVisible() + + await component.update({ props: { label, directory: true, multiple: true } }) + await expect(page.getByRole('button', { name: label })).toBeVisible() + + await component.update({ props: { label, directory: true, directoryOnly: true } }) + await expect(page.getByRole('button', { name: label })).toBeVisible() +}) + +test('Shows the menu when picking directories is allowed', async ({ mount, page }) => { + await mount(NcFilePicker, { + props: { + directory: true, + }, + }) + + const button = page.getByRole('button', { name: 'Pick file' }) + await expect(button).toBeVisible() + await button.click() + + await expect(page.getByRole('menu', { name: 'Pick file' })).toBeVisible() + await expect(page.getByRole('menuitem', { name: 'Upload file' })).toBeVisible() + await expect(page.getByRole('menuitem', { name: 'Upload folder' })).toBeVisible() +}) + +test.describe('file picking', () => { + test.skip(({ browserName }) => browserName === 'webkit', 'WebKit does not support file pickers in Playwright yet') + + test('picking a single file', async ({ mount, page }) => { + page.on('filechooser', async (fileChooser) => { + expect(fileChooser.isMultiple()).toBe(false) + await fileChooser.setFiles({ name: 'testfile.txt', mimeType: 'text/plain', buffer: Buffer.from('Hello World') }) + }) + + const { promise, resolve } = Promise.withResolvers() + await mount(NcFilePickerEmitStory, { // we need this story as PlayWright can only proxy simple structures like strings but not complex ones like File objects + props: {}, + on: { + pick: resolve, + }, + }) + + const button = page.getByRole('button', { name: 'Pick file' }) + await button.click() + + expect(await promise).toEqual(['testfile.txt']) + }) + + test('picking multiple files', async ({ mount, page }) => { + page.on('filechooser', async (fileChooser) => { + expect(fileChooser.isMultiple()).toBe(true) + await fileChooser.setFiles([ + { name: 'a.txt', mimeType: 'text/plain', buffer: Buffer.from('Hello') }, + { name: 'b.txt', mimeType: 'text/plain', buffer: Buffer.from('Hallo') }, + { name: 'c.txt', mimeType: 'text/plain', buffer: Buffer.from('Hola') }, + ]) + }) + + const { promise, resolve } = Promise.withResolvers() + await mount(NcFilePickerEmitStory, { + props: { + multiple: true, + }, + on: { + pick: resolve, + }, + }) + + const button = page.getByRole('button', { name: 'Pick files' }) + await button.click() + + expect(await promise).toEqual(['a.txt', 'b.txt', 'c.txt']) + }) + + test('picking a directory', async ({ mount, page }) => { + page.on('filechooser', async (fileChooser) => { + expect(fileChooser.isMultiple()).toBe(false) + await fileChooser.setFiles(join(import.meta.dirname, 'folder')) + }) + + const { promise, resolve } = Promise.withResolvers() + await mount(NcFilePickerEmitStory, { + props: { + directory: true, + directoryOnly: true, + }, + on: { + pick: resolve, + }, + }) + + await page.getByRole('button', { name: 'Pick folder' }) + .click() + expect(await promise).toEqual(['folder/.gitkeep']) + }) + + test('picking a directory when having the menu', async ({ mount, page }) => { + page.on('filechooser', async (fileChooser) => { + expect(fileChooser.isMultiple()).toBe(false) + await fileChooser.setFiles(join(import.meta.dirname, 'folder')) + }) + + const { promise, resolve } = Promise.withResolvers() + await mount(NcFilePickerEmitStory, { + props: { + directory: true, + }, + on: { + pick: resolve, + }, + }) + + await page.getByRole('button', { name: 'Pick file' }) + .click() + await page.getByRole('menuitem', { name: 'Upload folder' }) + .click() + + expect(await promise).toEqual(['folder/.gitkeep']) + }) +}) diff --git a/tests/component/components/NcFilePicker/NcFilePickerEmit.story.vue b/tests/component/components/NcFilePicker/NcFilePickerEmit.story.vue new file mode 100644 index 0000000000..a50147210f --- /dev/null +++ b/tests/component/components/NcFilePicker/NcFilePickerEmit.story.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/tests/component/components/NcFilePicker/folder/.gitkeep b/tests/component/components/NcFilePicker/folder/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/mounting-with-default-props-1-chromium-linux.png b/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/mounting-with-default-props-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..75e148f3991f3bb5709df5039982b1a9fbbe966f GIT binary patch literal 1966 zcmV;f2T}NmP)oEPr>-{(B{IR{YigFk{D`Nn>3G8PVnqp^50mPn*C8L$Va#Idi*rOmYm z-{@>@Z%`HN6_3)3z%jjQ-}R-nXd(^vAkZz+36P3o$?%U)7{~g%a^`LH;&M38*U4_K z1cI?7D3eK5r9z|xr{4V$tRbMeySP=v<7y8QcCRm<+7s(;cQ{A_6~KTLPsCq3T+1nL zBBho4tFTX({h-YKM0oqgrmx&w`1h87Nh&cO7g_K}L79iC;BYrKlJQvHzfcZ1xKB_P zYX=yBe(I(3dc9C5|L4j&D6<0?-?g%a!h%$Q-B?-!Wp)PPhGR_DzjQU?Na;zIMk<6K z@T}lfNji@l>Li7{-TNYPxvWD6I}=1VBv+a zi;ID~tj6HNc2c6?=4=utd5d3NaNfFdspO?_+{ zchSnmvx8atnrXiuZUJ3n%1W$}sb2k+`?hGND2cdW}vu@sN{qrs=TJwY>Z z_)f&w4}fBQ{MHG16`rd?)nN7cu|KnsY?Fxf6WYlDewEPBa6sGioVhQ0(kkoLD>W*{ zY+G3jgU1IK8k8dV73Oo7Pu*k+W&%7l(DiTxAX1jS8|76qKY6tvRt{J#L9$qAV%j$8 z&u$)gN}cuXc0)s*=`VAqElKUF*B&ce7p7Ja8o=0ZM-$u3f~ZM_z6yymA-g*i%Gq#X zM-wz=Tg3`RClheSaJ>|vX`geO88Eey&~P&gbZC(&VJCy$jVC}QtFBNi7_;^Z`42bc zBpey9f6{Ln0iY{J@~VYsrSet~w13ER=6Gji`?_oBNv#g3V5`p!uS0c$O|2 zhcp_GTKa1>ATlxjSL=VRJOw%0pIh&O?jBS3vU7y48Eq9&mW!{s!F|J9+RS}geQy3# z%szL0;60Cv-LmDtV;aB?3_)K1!{1(d_rpn>r`Qgw(AYa}|7M!2v3Jk`!Ge)*c7R}td$Fl3^U*;3qj(SD0-MbdC zx@ne^00A-3Z8X3oz{Rv=jY$B4ZttaaVj$m)>03zXBQ$lrcigj>#@aiNTYIWaEi&1S zY%`y1oFGLCZo)b>GjJskDxR5%&NzI%C!yrY1@Fp7ZsaxN!(WeRo3itRP{0{Z22%n1 zvanml(>F=Ll@OcDf8vCsWJIP`kttt>@5j%E-*hJIMTq@DT?N27Q9)R#5iMjEWyjT z;PBB5<6LHY(C|3^HEH+Ee6ba9XnhzqDxqvU{o=ntf zoByFoGHsNO^flXmXS?7p>0C2IIXu?R>Mr=w7^kS~APgrKJpLRrnKHnjpie8wydPaGCSXF=ab=o=3HWkQ`xa}D^ zFVpnZ>znFxQ!cn2qqA&xgcYFsXFy)ZrmN%UJ-P$pojQCOJp7BPM>gm7k05Gy2UZ`r zL!9U!34Xdb`6Sv&80JHTd=>7QO{5Tn16FU=!jsBON}Qi!D6emEE0f0>8)RBEl1qW{I64?*#8x>5jL#O7v@k;BxWE@1xztQeU#E)*I2amUKG}Zgw`Q9tw?mCDDzy{m1iai88 zzk$vaTZbyCW#WpBjSXmQd-M07cy0p@0?%6j$_FS9APCsYWYU}e&q=@oVX%>Dt5aV4 z)eto2^@6#}@{NBj-)000I_L_t&o0Is$b;X1vu0000007*qoM6N<$g04~2 A-2eap literal 0 HcmV?d00001 diff --git a/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-1-chromium-linux.png b/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-1-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..75e148f3991f3bb5709df5039982b1a9fbbe966f GIT binary patch literal 1966 zcmV;f2T}NmP)oEPr>-{(B{IR{YigFk{D`Nn>3G8PVnqp^50mPn*C8L$Va#Idi*rOmYm z-{@>@Z%`HN6_3)3z%jjQ-}R-nXd(^vAkZz+36P3o$?%U)7{~g%a^`LH;&M38*U4_K z1cI?7D3eK5r9z|xr{4V$tRbMeySP=v<7y8QcCRm<+7s(;cQ{A_6~KTLPsCq3T+1nL zBBho4tFTX({h-YKM0oqgrmx&w`1h87Nh&cO7g_K}L79iC;BYrKlJQvHzfcZ1xKB_P zYX=yBe(I(3dc9C5|L4j&D6<0?-?g%a!h%$Q-B?-!Wp)PPhGR_DzjQU?Na;zIMk<6K z@T}lfNji@l>Li7{-TNYPxvWD6I}=1VBv+a zi;ID~tj6HNc2c6?=4=utd5d3NaNfFdspO?_+{ zchSnmvx8atnrXiuZUJ3n%1W$}sb2k+`?hGND2cdW}vu@sN{qrs=TJwY>Z z_)f&w4}fBQ{MHG16`rd?)nN7cu|KnsY?Fxf6WYlDewEPBa6sGioVhQ0(kkoLD>W*{ zY+G3jgU1IK8k8dV73Oo7Pu*k+W&%7l(DiTxAX1jS8|76qKY6tvRt{J#L9$qAV%j$8 z&u$)gN}cuXc0)s*=`VAqElKUF*B&ce7p7Ja8o=0ZM-$u3f~ZM_z6yymA-g*i%Gq#X zM-wz=Tg3`RClheSaJ>|vX`geO88Eey&~P&gbZC(&VJCy$jVC}QtFBNi7_;^Z`42bc zBpey9f6{Ln0iY{J@~VYsrSet~w13ER=6Gji`?_oBNv#g3V5`p!uS0c$O|2 zhcp_GTKa1>ATlxjSL=VRJOw%0pIh&O?jBS3vU7y48Eq9&mW!{s!F|J9+RS}geQy3# z%szL0;60Cv-LmDtV;aB?3_)K1!{1(d_rpn>r`Qgw(AYa}|7M!2v3Jk`!Ge)*c7R}td$Fl3^U*;3qj(SD0-MbdC zx@ne^00A-3Z8X3oz{Rv=jY$B4ZttaaVj$m)>03zXBQ$lrcigj>#@aiNTYIWaEi&1S zY%`y1oFGLCZo)b>GjJskDxR5%&NzI%C!yrY1@Fp7ZsaxN!(WeRo3itRP{0{Z22%n1 zvanml(>F=Ll@OcDf8vCsWJIP`kttt>@5j%E-*hJIMTq@DT?N27Q9)R#5iMjEWyjT z;PBB5<6LHY(C|3^HEH+Ee6ba9XnhzqDxqvU{o=ntf zoByFoGHsNO^flXmXS?7p>0C2IIXu?R>Mr=w7^kS~APgrKJpLRrnKHnjpie8wydPaGCSXF=ab=o=3HWkQ`xa}D^ zFVpnZ>znFxQ!cn2qqA&xgcYFsXFy)ZrmN%UJ-P$pojQCOJp7BPM>gm7k05Gy2UZ`r zL!9U!34Xdb`6Sv&80JHTd=>7QO{5Tn16FU=!jsBON}Qi!D6emEE0f0>8)RBEl1qW{I64?*#8x>5jL#O7v@k;BxWE@1xztQeU#E)*I2amUKG}Zgw`Q9tw?mCDDzy{m1iai88 zzk$vaTZbyCW#WpBjSXmQd-M07cy0p@0?%6j$_FS9APCsYWYU}e&q=@oVX%>Dt5aV4 z)eto2^@6#}@{NBj-)000I_L_t&o0Is$b;X1vu0000007*qoM6N<$g04~2 A-2eap literal 0 HcmV?d00001 diff --git a/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-2-chromium-linux.png b/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-2-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..75e148f3991f3bb5709df5039982b1a9fbbe966f GIT binary patch literal 1966 zcmV;f2T}NmP)oEPr>-{(B{IR{YigFk{D`Nn>3G8PVnqp^50mPn*C8L$Va#Idi*rOmYm z-{@>@Z%`HN6_3)3z%jjQ-}R-nXd(^vAkZz+36P3o$?%U)7{~g%a^`LH;&M38*U4_K z1cI?7D3eK5r9z|xr{4V$tRbMeySP=v<7y8QcCRm<+7s(;cQ{A_6~KTLPsCq3T+1nL zBBho4tFTX({h-YKM0oqgrmx&w`1h87Nh&cO7g_K}L79iC;BYrKlJQvHzfcZ1xKB_P zYX=yBe(I(3dc9C5|L4j&D6<0?-?g%a!h%$Q-B?-!Wp)PPhGR_DzjQU?Na;zIMk<6K z@T}lfNji@l>Li7{-TNYPxvWD6I}=1VBv+a zi;ID~tj6HNc2c6?=4=utd5d3NaNfFdspO?_+{ zchSnmvx8atnrXiuZUJ3n%1W$}sb2k+`?hGND2cdW}vu@sN{qrs=TJwY>Z z_)f&w4}fBQ{MHG16`rd?)nN7cu|KnsY?Fxf6WYlDewEPBa6sGioVhQ0(kkoLD>W*{ zY+G3jgU1IK8k8dV73Oo7Pu*k+W&%7l(DiTxAX1jS8|76qKY6tvRt{J#L9$qAV%j$8 z&u$)gN}cuXc0)s*=`VAqElKUF*B&ce7p7Ja8o=0ZM-$u3f~ZM_z6yymA-g*i%Gq#X zM-wz=Tg3`RClheSaJ>|vX`geO88Eey&~P&gbZC(&VJCy$jVC}QtFBNi7_;^Z`42bc zBpey9f6{Ln0iY{J@~VYsrSet~w13ER=6Gji`?_oBNv#g3V5`p!uS0c$O|2 zhcp_GTKa1>ATlxjSL=VRJOw%0pIh&O?jBS3vU7y48Eq9&mW!{s!F|J9+RS}geQy3# z%szL0;60Cv-LmDtV;aB?3_)K1!{1(d_rpn>r`Qgw(AYa}|7M!2v3Jk`!Ge)*c7R}td$Fl3^U*;3qj(SD0-MbdC zx@ne^00A-3Z8X3oz{Rv=jY$B4ZttaaVj$m)>03zXBQ$lrcigj>#@aiNTYIWaEi&1S zY%`y1oFGLCZo)b>GjJskDxR5%&NzI%C!yrY1@Fp7ZsaxN!(WeRo3itRP{0{Z22%n1 zvanml(>F=Ll@OcDf8vCsWJIP`kttt>@5j%E-*hJIMTq@DT?N27Q9)R#5iMjEWyjT z;PBB5<6LHY(C|3^HEH+Ee6ba9XnhzqDxqvU{o=ntf zoByFoGHsNO^flXmXS?7p>0C2IIXu?R>Mr=w7^kS~APgrKJpLRrnKHnjpie8wydPaGCSXF=ab=o=3HWkQ`xa}D^ zFVpnZ>znFxQ!cn2qqA&xgcYFsXFy)ZrmN%UJ-P$pojQCOJp7BPM>gm7k05Gy2UZ`r zL!9U!34Xdb`6Sv&80JHTd=>7QO{5Tn16FU=!jsBON}Qi!D6emEE0f0>8)RBEl1qW{I64?*#8x>5jL#O7v@k;BxWE@1xztQeU#E)*I2amUKG}Zgw`Q9tw?mCDDzy{m1iai88 zzk$vaTZbyCW#WpBjSXmQd-M07cy0p@0?%6j$_FS9APCsYWYU}e&q=@oVX%>Dt5aV4 z)eto2^@6#}@{NBj-)000I_L_t&o0Is$b;X1vu0000007*qoM6N<$g04~2 A-2eap literal 0 HcmV?d00001 diff --git a/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-3-chromium-linux.png b/tests/component/snapshots/components/NcFilePicker/NcFilePicker.spec.ts-snapshots/setting-the-variant-prop-3-chromium-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..859b4eda1fb1336e633f32700ac93658292ea318 GIT binary patch literal 1928 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 o0RR6x<8VR%000I_L_t&o0QM Date: Wed, 21 Jan 2026 13:08:08 +0100 Subject: [PATCH 2/2] refactor: use `actions` prop instead of slot Signed-off-by: Ferdinand Thiessen --- src/components/NcFilePicker/NcFilePicker.vue | 65 ++++++++++++++++---- src/components/NcFilePicker/index.ts | 7 ++- 2 files changed, 60 insertions(+), 12 deletions(-) diff --git a/src/components/NcFilePicker/NcFilePicker.vue b/src/components/NcFilePicker/NcFilePicker.vue index 666ac83141..3bf1a0dfef 100644 --- a/src/components/NcFilePicker/NcFilePicker.vue +++ b/src/components/NcFilePicker/NcFilePicker.vue @@ -13,9 +13,36 @@ import IconUpload from 'vue-material-design-icons/Upload.vue' import NcActionButton from '../NcActionButton/NcActionButton.vue' import NcActionCaption from '../NcActionCaption/NcActionCaption.vue' import NcActions from '../NcActions/NcActions.vue' +import NcIconSvgWrapper from '../NcIconSvgWrapper/NcIconSvgWrapper.vue' import NcLoadingIcon from '../NcLoadingIcon/NcLoadingIcon.vue' import { t } from '../../l10n.js' +export interface FilePickerItem { + /** + * SVG icon (as string) for the action + */ + iconSvg: string + /** + * Label of the action + */ + label: string + /** + * Callback when the action is clicked + */ + onClick: () => void +} + +export interface FilePickerItemGroup { + /** + * Caption for the action group + */ + caption: string + /** + * Actions within this group + */ + actions: FilePickerItem[] +} + const props = withDefaults(defineProps<{ /** * File types to accept @@ -27,6 +54,11 @@ const props = withDefaults(defineProps<{ */ actionCaption?: string + /** + * Additional actions to be shown within the picker menu + */ + actions?: FilePickerItem[] | FilePickerItemGroup[] + /** * Allow picking a directory */ @@ -71,6 +103,7 @@ const props = withDefaults(defineProps<{ */ variant?: 'primary' | 'secondary' | 'tertiary' }>(), { + actions: () => [], accept: undefined, actionCaption: '', label: undefined, @@ -82,11 +115,6 @@ const emit = defineEmits<{ }>() defineSlots<{ - /** - * Custom NcAction* to be shown within the picker menu - */ - actions?: Slot - /** * Optional custom icon for the picker menu */ @@ -169,8 +197,8 @@ function reset() {