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
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"extends": ["@nextcloud/eslint-config/typescript"],
"extends": ["@nextcloud/eslint-config/vue3"],
"overrides": [
// https://github.com/mysticatea/eslint-plugin-node/issues/248#issuecomment-1052550467
{
Expand Down
51 changes: 10 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,22 @@ npm i -S @nextcloud/dialogs
```

### Version compatibility
Since version 4.2 this package provides a Vue.js based file picker, so this package depends on `@nextcloud/vue`. So to not introduce style collisions stick with the supported versions:
Since version 4.2 this package provides a Vue.js based file picker, so this package depends on `@nextcloud/vue`.
So to not introduce style collisions stick with the supported versions:

| `@nextcloud/dialogs` | maintained | `@nextcloud/vue` dependency | Nextcloud server version |
|----------------------|------------|-----------------------------|----------------------------|
| 7.x | ✅ | 9.x (Vue 3)¹ | Nextcloud 30 and newer |
| 6.x | ✅ | 8.x | Nextcloud 29 and newer |
| 5.x | ❌ | 8.x | Nextcloud 28, 29, 30 |
| 4.2+ | ❌ | 7.12 | Nextcloud 25, 26, 27, 27.1 |
| 4.1 | ❌ | *any* | *any* |

¹: In version 7.x the `@nextcloud/vue` dependency is moved to `dependencies` so you can also use this library
with an old version of `@nextcloud/vue` in your app dependencies if your app still uses Vue 2.
Note that this might increase the bundled app size.
If your app also already uses `@nextcloud/vue` version 9.x and Vue 3 then the bundle size will not increase.

## Usage

### General
Expand Down Expand Up @@ -62,11 +69,8 @@ showError('This is an error shown without a timeout', { timeout: -1 })
A full list of available options can be found in the [documentation](https://nextcloud-libraries.github.io/nextcloud-dialogs/).

### FilePicker
There are two ways to spawn a FilePicker provided by the library:

#### Use the FilePickerBuilder
This way you do not need to use Vue, but can programatically spawn a FilePicker.
The FilePickerBuilder is included in the main entry point of this library, so you can use it like this:
To spawn the FilePicker provided by the library you have to use the *FilePickerBuilder*.
The *FilePickerBuilder* is included in the main entry point of this library, so you can use it like this:

```js
import { getFilePickerBuilder } from '@nextcloud/dialogs'
Expand All @@ -82,41 +86,6 @@ const filepicker = getFilePickerBuilder('Pick plain text files')
const paths = await filepicker.pick()
```

#### Use the Vue component directly

> [!WARNING]
> The Vue component is deprecated and will no longer be exported in a future version.

We also provide the `@nextcloud/dialogs/filepicker.js` entry point to allow using the Vue component directly:

```vue
<template>
<FilePicker name="Pick some files" :buttons="buttons" />
</template>
<script setup lang="ts">
import {
FilePickerVue as FilePicker,
type IFilePickerButton,
} from '@nextcloud/dialogs/filepicker.js'
import type { Node } from '@nextcloud/files'
import IconShare from 'vue-material-design-icons/Share.vue'

const buttons: IFilePickerButton[] = [
{
label: 'Pick',
callback: (nodes: Node[]) => console.log('Picked', nodes),
type: 'primary'
},
{
label: 'Share',
callback: (nodes: Node[]) => console.log('Share picked files', nodes),
type: 'secondary',
icon: IconShare,
}
]
</script>
```

## Development
### Testing
For testing all components provide `data-testid` attributes as selectors, so the tests are independent from code or styling changes.
Expand Down
34 changes: 17 additions & 17 deletions lib/components/FilePicker/FileList.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('FilePicker FileList', () => {
const consoleWarning = vi.spyOn(console, 'warn')

const wrapper = shallowMount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: false,
allowPickDirectory: false,
Expand All @@ -80,7 +80,7 @@ describe('FilePicker FileList', () => {

it('header checkbox is not shown if multiselect is `false`', () => {
const wrapper = shallowMount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: false,
allowPickDirectory: false,
Expand All @@ -95,7 +95,7 @@ describe('FilePicker FileList', () => {

it('header checkbox is shown if multiselect is `true`', async () => {
const wrapper = shallowMount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
Expand All @@ -111,15 +111,15 @@ describe('FilePicker FileList', () => {
const selectAll = wrapper.find('[data-testid="select-all-checkbox"]')
expect(selectAll.exists()).toBe(true)
// there is an aria label
expect(selectAll.props('ariaLabel')).toBeTruthy()
expect(selectAll.attributes('arialabel')).toBeTruthy()
// no checked
expect(selectAll.props('modelValue')).toBe(false)
expect(selectAll.attributes('modelvalue')).toBe('false')
})

it('header checkbox is checked when all nodes are selected', async () => {
const nodes = [...exampleNodes]
const wrapper = shallowMount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
Expand All @@ -131,14 +131,14 @@ describe('FilePicker FileList', () => {
})

const selectAll = wrapper.find('[data-testid="select-all-checkbox"]')
expect(selectAll.props('modelValue')).toBe(true)
expect(selectAll.attributes('modelvalue')).toBe('true')
})

describe('file list sorting', () => {
it('is sorted initially by name', async () => {
const nodes = [...exampleNodes]
const wrapper = mount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
Expand All @@ -158,18 +158,18 @@ describe('FilePicker FileList', () => {
// all nodes are shown
expect(rows.length).toBe(nodes.length)
// by default favorites are sorted before other files
expect(rows.at(0).attributes('data-filename')).toBe('favorite.txt')
expect(rows.at(0)!.attributes('data-filename')).toBe('favorite.txt')
// folder are sorted first
expect(rows.at(1).attributes('data-filename')).toBe('directory')
expect(rows.at(1)!.attributes('data-filename')).toBe('directory')
// other files are ascending
expect(rows.at(2).attributes('data-filename')).toBe('a-file.txt')
expect(rows.at(3).attributes('data-filename')).toBe('b-file.txt')
expect(rows.at(2)!.attributes('data-filename')).toBe('a-file.txt')
expect(rows.at(3)!.attributes('data-filename')).toBe('b-file.txt')
})

it('can sort descending by name', async () => {
const nodes = [...exampleNodes]
const wrapper = mount(FileList, {
propsData: {
props: {
currentView: 'files',
multiselect: true,
allowPickDirectory: false,
Expand All @@ -193,12 +193,12 @@ describe('FilePicker FileList', () => {
// all nodes are shown
expect(rows.length).toBe(nodes.length)
// by default favorites are sorted before other files
expect(rows.at(0).attributes('data-filename')).toBe('favorite.txt')
expect(rows.at(0)!.attributes('data-filename')).toBe('favorite.txt')
// folder are sorted first
expect(rows.at(1).attributes('data-filename')).toBe('directory')
expect(rows.at(1)!.attributes('data-filename')).toBe('directory')
// other files are descending
expect(rows.at(2).attributes('data-filename')).toBe('b-file.txt')
expect(rows.at(3).attributes('data-filename')).toBe('a-file.txt')
expect(rows.at(2)!.attributes('data-filename')).toBe('b-file.txt')
expect(rows.at(3)!.attributes('data-filename')).toBe('a-file.txt')
})
})
})
10 changes: 5 additions & 5 deletions lib/components/FilePicker/FileList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<th :aria-sort="sortByName" class="row-name">
<div class="header-wrapper">
<span class="file-picker__header-preview" />
<NcButton :wide="true"
type="tertiary"
data-test="file-picker_sort-name"
<NcButton data-test="file-picker_sort-name"
variant="tertiary"
wide
@click="toggleSorting('basename')">
<template #icon>
<IconSortAscending v-if="sortByName === 'ascending'" :size="20" />
Expand All @@ -34,7 +34,7 @@
</div>
</th>
<th :aria-sort="sortBySize" class="row-size">
<NcButton :wide="true" type="tertiary" @click="toggleSorting('size')">
<NcButton variant="tertiary" wide @click="toggleSorting('size')">
<template #icon>
<IconSortAscending v-if="sortBySize === 'ascending'" :size="20" />
<IconSortDescending v-else-if="sortBySize === 'descending'" :size="20" />
Expand All @@ -44,7 +44,7 @@
</NcButton>
</th>
<th :aria-sort="sortByModified" class="row-modified">
<NcButton :wide="true" type="tertiary" @click="toggleSorting('mtime')">
<NcButton variant="tertiary" wide @click="toggleSorting('mtime')">
<template #icon>
<IconSortAscending v-if="sortByModified === 'ascending'" :size="20" />
<IconSortDescending v-else-if="sortByModified === 'descending'" :size="20" />
Expand Down
20 changes: 11 additions & 9 deletions lib/components/FilePicker/FileListRow.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ describe('FilePicker: FileListRow', () => {
const consoleError = vi.spyOn(console, 'error')

const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: true,
selected: false,
showCheckbox: true,
Expand All @@ -50,7 +50,7 @@ describe('FilePicker: FileListRow', () => {

it('shows checkbox based on `showCheckbox` property', async () => {
const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: true,
selected: false,
showCheckbox: true,
Expand All @@ -67,17 +67,19 @@ describe('FilePicker: FileListRow', () => {

it('Click checkbox triggers select', async () => {
const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: false,
selected: false,
showCheckbox: true,
canPick: true,
node,
cropImagePreviews: true,
},
stubs: {
NcCheckboxRadioSwitch: {
template: '<label><input type="checkbox" @click="$emit(\'update:model-value\', true)" ></label>',
global: {
stubs: {
NcCheckboxRadioSwitch: {
template: '<label><input type="checkbox" @click="$emit(\'update:model-value\', true)" ></label>',
},
},
},
})
Expand All @@ -90,7 +92,7 @@ describe('FilePicker: FileListRow', () => {

it('Click element triggers select', async () => {
const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: false,
selected: false,
showCheckbox: true,
Expand All @@ -108,7 +110,7 @@ describe('FilePicker: FileListRow', () => {

it('Click element without checkbox triggers select', async () => {
const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: false,
selected: false,
showCheckbox: false,
Expand All @@ -126,7 +128,7 @@ describe('FilePicker: FileListRow', () => {

it('Enter triggers select', async () => {
const wrapper = shallowMount(FileListRow, {
propsData: {
props: {
allowPickDirectory: false,
selected: false,
showCheckbox: false,
Expand Down
12 changes: 7 additions & 5 deletions lib/components/FilePicker/FileListRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
{{ formatFileSize(node.size || 0) }}
</td>
<td class="row-modified">
<NcDateTime :timestamp="node.mtime" :ignore-seconds="true" />
<NcDateTime :timestamp ignore-seconds />
</td>
</tr>
</template>
Expand Down Expand Up @@ -65,11 +65,13 @@ const props = defineProps<{

const emit = defineEmits<{
/** Emitted when the selected state is changed */
(e: 'update:selected', v: boolean): void
'update:selected': [selected: boolean]
/** Emitted when a directory was not selected but entered */
(e: 'enter-directory', node: INode): void
enterDirectory: [node: INode]
}>()

const timestamp = computed(() => props.node.mtime ?? 0)

/**
* The displayname of the current node (excluding file extension)
*/
Expand Down Expand Up @@ -102,7 +104,7 @@ function toggleSelected() {
*/
function handleClick() {
if (isDirectory.value) {
emit('enter-directory', props.node)
emit('enterDirectory', props.node)
} else {
toggleSelected()
}
Expand All @@ -120,7 +122,7 @@ function handleKeyDown(event: KeyboardEvent) {
</script>

<style scoped lang="scss">
@use './FileList.scss';
@use './FileList';

.file-picker {
&__row {
Expand Down
Loading