Skip to content

Commit a29bb7f

Browse files
authored
Merge pull request #1070 from nextcloud-libraries/refactor-structure
Fix package and add separate filepicker entry point
2 parents eae05f7 + bc789de commit a29bb7f

File tree

9 files changed

+1745
-384
lines changed

9 files changed

+1745
-384
lines changed
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# This workflow is provided via the organization template repository
2+
#
3+
# https://github.com/nextcloud/.github
4+
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
5+
6+
name: Lint stylelint
7+
8+
on: pull_request
9+
10+
permissions:
11+
contents: read
12+
13+
concurrency:
14+
group: lint-stylelint-${{ github.head_ref || github.run_id }}
15+
cancel-in-progress: true
16+
17+
jobs:
18+
lint:
19+
runs-on: ubuntu-latest
20+
21+
name: stylelint
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0
26+
27+
- name: Read package.json node and npm engines version
28+
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
29+
id: versions
30+
with:
31+
fallbackNode: '^20'
32+
fallbackNpm: '^9'
33+
34+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
35+
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
36+
with:
37+
node-version: ${{ steps.versions.outputs.nodeVersion }}
38+
39+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
40+
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
41+
42+
- name: Install dependencies
43+
env:
44+
CYPRESS_INSTALL_BINARY: 0
45+
run: npm ci
46+
47+
- name: Lint
48+
run: npm run stylelint

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ npm i -S @nextcloud/dialogs
1414
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:
1515

1616
`@nextcloud/dialogs` | `@nextcloud/vue` | Nextcloud server version
17-
---|---|---
18-
4.1 | *any* | *any*
19-
4.2+ | 7.12 | Nextcloud 25, 26, 27, 27.1
20-
5.x | 8.x | Nextcloud 28 and newer
17+
-----|-------|-----------------------
18+
5.x | 8.x | Nextcloud 28 and newer
19+
4.2+ | 7.12 | Nextcloud 25, 26, 27, 27.1
20+
4.1 | *any* | *any*
2121

2222
## Usage
2323

@@ -61,6 +61,7 @@ There are two ways to spawn a FilePicker provided by the library:
6161

6262
#### Use the FilePickerBuilder
6363
This way you do not need to use Vue, but can programatically spawn a FilePicker.
64+
The FilePickerBuilder is included in the main entry point of this library, so you can use it like this:
6465

6566
```js
6667
import { getFilePickerBuilder } from '@nextcloud/dialogs'
@@ -77,6 +78,8 @@ const paths = await filepicker.pick()
7778
```
7879

7980
#### Use the Vue component directly
81+
We also provide the `@nextcloud/dialogs/filepicker.js` entry point to allow using the Vue component directly:
82+
8083
```vue
8184
<template>
8285
<FilePicker name="Pick some files" :buttons="buttons" />
@@ -85,7 +88,7 @@ const paths = await filepicker.pick()
8588
import {
8689
FilePickerVue as FilePicker,
8790
type IFilePickerButton,
88-
} from '@nextcloud/dialogs'
91+
} from '@nextcloud/dialogs/filepicker.js'
8992
import type { Node } from '@nextcloud/files'
9093
import IconShare from 'vue-material-design-icons/Share.vue'
9194
@@ -110,8 +113,8 @@ const paths = await filepicker.pick()
110113
For testing all components provide `data-testid` attributes as selectors, so the tests are independent from code or styling changes.
111114

112115
### Test selectors
113-
`data-testid` | Intended purpose
114-
---|---
116+
`data-testid` | Intended purpose
117+
----------------------|-----------------
115118
`select-all-checkbox` | The select all checkbox of the file list
116119
`file-list-row` | A row in the file list (`tr`), can be identified by `data-filename`
117120
`row-checkbox` | Checkbox for selecting a row

lib/filepicker-builder.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
/**
2+
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessen.de>
3+
*
4+
* @author Ferdinand Thiessen <opensource@fthiessen.de>
5+
*
6+
* @license AGPL-3.0-or-later
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Affero General Public License as
10+
* published by the Free Software Foundation, either version 3 of the
11+
* License, or (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU Affero General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU Affero General Public License
19+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
20+
*
21+
*/
22+
23+
import type { IFilePickerButton, IFilePickerButtonFactory, IFilePickerFilter } from './components/types'
24+
import type { Node } from '@nextcloud/files'
25+
26+
import { basename } from 'path'
27+
import { spawnDialog } from './utils/dialogs'
28+
import { t } from './utils/l10n'
29+
30+
import IconMove from '@mdi/svg/svg/folder-move.svg?raw'
31+
import IconCopy from '@mdi/svg/svg/folder-multiple.svg?raw'
32+
33+
/**
34+
* @deprecated
35+
*/
36+
export enum FilePickerType {
37+
Choose = 1,
38+
Move = 2,
39+
Copy = 3,
40+
CopyMove = 4,
41+
Custom = 5,
42+
}
43+
44+
export class FilePicker<IsMultiSelect extends boolean> {
45+
46+
private title: string
47+
private multiSelect: IsMultiSelect
48+
private mimeTypeFilter: string[]
49+
private directoriesAllowed: boolean
50+
private buttons: IFilePickerButton[] | IFilePickerButtonFactory
51+
private path?: string
52+
private filter?: IFilePickerFilter
53+
private container?: string
54+
55+
public constructor(title: string,
56+
multiSelect: IsMultiSelect,
57+
mimeTypeFilter: string[],
58+
directoriesAllowed: boolean,
59+
buttons: IFilePickerButton[] | IFilePickerButtonFactory,
60+
path?: string,
61+
filter?: IFilePickerFilter,
62+
container?: string) {
63+
this.title = title
64+
this.multiSelect = multiSelect
65+
this.mimeTypeFilter = mimeTypeFilter
66+
this.directoriesAllowed = directoriesAllowed
67+
this.path = path
68+
this.filter = filter
69+
this.buttons = buttons
70+
this.container = container
71+
}
72+
73+
/**
74+
* Pick files using the FilePicker
75+
*
76+
* @return Promise with array of picked files or rejected promise on close without picking
77+
*/
78+
public async pick(): Promise<IsMultiSelect extends true ? string[] : string> {
79+
const { FilePickerVue } = await import('./components/FilePicker/index')
80+
81+
return new Promise((resolve, reject) => {
82+
spawnDialog(FilePickerVue, {
83+
allowPickDirectory: this.directoriesAllowed,
84+
buttons: this.buttons,
85+
container: this.container,
86+
name: this.title,
87+
path: this.path,
88+
mimetypeFilter: this.mimeTypeFilter,
89+
multiselect: this.multiSelect,
90+
filterFn: this.filter,
91+
}, (...rest: unknown[]) => {
92+
const [nodes] = rest as [nodes: Node[]]
93+
if (!Array.isArray(nodes) || nodes.length === 0) {
94+
reject(new Error('FilePicker: No nodes selected'))
95+
} else {
96+
if (this.multiSelect) {
97+
resolve((nodes as Node[]).map((node) => node.path) as (IsMultiSelect extends true ? string[] : string))
98+
} else {
99+
resolve(((nodes as Node[])[0]?.path || '/') as (IsMultiSelect extends true ? string[] : string))
100+
}
101+
}
102+
})
103+
})
104+
}
105+
106+
}
107+
108+
export class FilePickerBuilder<IsMultiSelect extends boolean> {
109+
110+
private title: string
111+
private multiSelect = false
112+
private mimeTypeFilter: string[] = []
113+
private directoriesAllowed = false
114+
private path?: string
115+
private filter?: IFilePickerFilter
116+
private buttons: IFilePickerButton[] | IFilePickerButtonFactory = []
117+
private container?: string
118+
119+
/**
120+
* Construct a new FilePicker
121+
*
122+
* @param title Title of the FilePicker
123+
*/
124+
public constructor(title: string) {
125+
this.title = title
126+
}
127+
128+
/**
129+
* Set the container where the FilePicker will be mounted
130+
* By default 'body' is used
131+
*
132+
* @param container The dialog container
133+
*/
134+
public setContainer(container: string) {
135+
this.container = container
136+
return this
137+
}
138+
139+
/**
140+
* Enable or disable picking multiple files
141+
*
142+
* @param ms True to enable picking multiple files, false otherwise
143+
*/
144+
public setMultiSelect<T extends boolean>(ms: T) {
145+
this.multiSelect = ms
146+
return this as unknown as FilePickerBuilder<T extends true ? true : false>
147+
}
148+
149+
/**
150+
* Add allowed MIME type
151+
*
152+
* @param filter MIME type to allow
153+
*/
154+
public addMimeTypeFilter(filter: string) {
155+
this.mimeTypeFilter.push(filter)
156+
return this
157+
}
158+
159+
/**
160+
* Set allowed MIME types
161+
*
162+
* @param filter Array of allowed MIME types
163+
*/
164+
public setMimeTypeFilter(filter: string[]) {
165+
this.mimeTypeFilter = filter
166+
return this
167+
}
168+
169+
/**
170+
* Add a button to the FilePicker
171+
* Note: This overrides any previous `setButtonFactory` call
172+
*
173+
* @param button The button
174+
*/
175+
public addButton(button: IFilePickerButton) {
176+
if (typeof this.buttons === 'function') {
177+
console.warn('FilePicker buttons were set to factory, now overwritten with button object.')
178+
this.buttons = []
179+
}
180+
this.buttons.push(button)
181+
return this
182+
}
183+
184+
/**
185+
* Set the button factory which is used to generate buttons from current view, path and selected nodes
186+
* Note: This overrides any previous `addButton` call
187+
*
188+
* @param factory The button factory
189+
*/
190+
public setButtonFactory(factory: IFilePickerButtonFactory) {
191+
this.buttons = factory
192+
return this
193+
}
194+
195+
/**
196+
* Set FilePicker type based on legacy file picker types
197+
* @param type The legacy filepicker type to emulate
198+
* @deprecated Use `addButton` or `setButtonFactory` instead as with setType you do not know which button was pressed
199+
*/
200+
public setType(type: FilePickerType) {
201+
this.buttons = (nodes, path) => {
202+
const buttons: IFilePickerButton[] = []
203+
const node = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename
204+
const target = node || basename(path)
205+
206+
if (type === FilePickerType.Choose) {
207+
buttons.push({
208+
callback: () => {},
209+
label: node && !this.multiSelect ? t('Choose {file}', { file: node }) : t('Choose'),
210+
type: 'primary',
211+
})
212+
}
213+
if (type === FilePickerType.CopyMove || type === FilePickerType.Copy) {
214+
buttons.push({
215+
callback: () => {},
216+
label: target ? t('Copy to {target}', { target }) : t('Copy'),
217+
type: 'primary',
218+
icon: IconCopy,
219+
})
220+
}
221+
if (type === FilePickerType.Move || type === FilePickerType.CopyMove) {
222+
buttons.push({
223+
callback: () => {},
224+
label: target ? t('Move to {target}', { target }) : t('Move'),
225+
type: type === FilePickerType.Move ? 'primary' : 'secondary',
226+
icon: IconMove,
227+
})
228+
}
229+
return buttons
230+
}
231+
232+
return this
233+
}
234+
235+
/**
236+
* Allow to pick directories besides files
237+
*
238+
* @param allow True to allow picking directories
239+
*/
240+
public allowDirectories(allow = true) {
241+
this.directoriesAllowed = allow
242+
return this
243+
}
244+
245+
/**
246+
* Set starting path of the FilePicker
247+
*
248+
* @param path Path to start from picking
249+
*/
250+
public startAt(path: string) {
251+
this.path = path
252+
return this
253+
}
254+
255+
/**
256+
* Add filter function to filter file list of FilePicker
257+
*
258+
* @param filter Filter function to apply
259+
*/
260+
public setFilter(filter: IFilePickerFilter) {
261+
this.filter = filter
262+
return this
263+
}
264+
265+
/**
266+
* Construct the configured FilePicker
267+
*/
268+
public build() {
269+
return new FilePicker<IsMultiSelect>(
270+
this.title,
271+
this.multiSelect as IsMultiSelect,
272+
this.mimeTypeFilter,
273+
this.directoriesAllowed,
274+
this.buttons,
275+
this.path,
276+
this.filter,
277+
)
278+
}
279+
280+
}
281+
282+
/**
283+
*
284+
* @param title Title of the file picker
285+
*/
286+
export function getFilePickerBuilder(title: string): FilePickerBuilder<boolean> {
287+
return new FilePickerBuilder(title)
288+
}

0 commit comments

Comments
 (0)