Skip to content

Commit

Permalink
Merge pull request #1070 from nextcloud-libraries/refactor-structure
Browse files Browse the repository at this point in the history
Fix package and add separate filepicker entry point
  • Loading branch information
susnux authored Oct 17, 2023
2 parents eae05f7 + bc789de commit a29bb7f
Show file tree
Hide file tree
Showing 9 changed files with 1,745 additions and 384 deletions.
48 changes: 48 additions & 0 deletions .github/workflows/lint-stylelint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This workflow is provided via the organization template repository
#
# https://github.com/nextcloud/.github
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization

name: Lint stylelint

on: pull_request

permissions:
contents: read

concurrency:
group: lint-stylelint-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest

name: stylelint

steps:
- name: Checkout
uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v3.6.0

- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@8205673bab74a63eb9b8093402fd9e0e018663a1 # v2.2
id: versions
with:
fallbackNode: '^20'
fallbackNpm: '^9'

- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"

- name: Install dependencies
env:
CYPRESS_INSTALL_BINARY: 0
run: npm ci

- name: Lint
run: npm run stylelint
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ npm i -S @nextcloud/dialogs
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` | `@nextcloud/vue` | Nextcloud server version
---|---|---
4.1 | *any* | *any*
4.2+ | 7.12 | Nextcloud 25, 26, 27, 27.1
5.x | 8.x | Nextcloud 28 and newer
-----|-------|-----------------------
5.x | 8.x | Nextcloud 28 and newer
4.2+ | 7.12 | Nextcloud 25, 26, 27, 27.1
4.1 | *any* | *any*

## Usage

Expand Down Expand Up @@ -61,6 +61,7 @@ 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:

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

#### Use the Vue component directly
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" />
Expand All @@ -85,7 +88,7 @@ const paths = await filepicker.pick()
import {
FilePickerVue as FilePicker,
type IFilePickerButton,
} from '@nextcloud/dialogs'
} from '@nextcloud/dialogs/filepicker.js'
import type { Node } from '@nextcloud/files'
import IconShare from 'vue-material-design-icons/Share.vue'
Expand All @@ -110,8 +113,8 @@ const paths = await filepicker.pick()
For testing all components provide `data-testid` attributes as selectors, so the tests are independent from code or styling changes.

### Test selectors
`data-testid` | Intended purpose
---|---
`data-testid` | Intended purpose
----------------------|-----------------
`select-all-checkbox` | The select all checkbox of the file list
`file-list-row` | A row in the file list (`tr`), can be identified by `data-filename`
`row-checkbox` | Checkbox for selecting a row
Expand Down
288 changes: 288 additions & 0 deletions lib/filepicker-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
/**
* @copyright Copyright (c) 2023 Ferdinand Thiessen <[email protected]>
*
* @author Ferdinand Thiessen <[email protected]>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

import type { IFilePickerButton, IFilePickerButtonFactory, IFilePickerFilter } from './components/types'
import type { Node } from '@nextcloud/files'

import { basename } from 'path'
import { spawnDialog } from './utils/dialogs'
import { t } from './utils/l10n'

import IconMove from '@mdi/svg/svg/folder-move.svg?raw'
import IconCopy from '@mdi/svg/svg/folder-multiple.svg?raw'

/**
* @deprecated
*/
export enum FilePickerType {
Choose = 1,
Move = 2,
Copy = 3,
CopyMove = 4,
Custom = 5,
}

export class FilePicker<IsMultiSelect extends boolean> {

private title: string
private multiSelect: IsMultiSelect
private mimeTypeFilter: string[]
private directoriesAllowed: boolean
private buttons: IFilePickerButton[] | IFilePickerButtonFactory
private path?: string
private filter?: IFilePickerFilter
private container?: string

public constructor(title: string,
multiSelect: IsMultiSelect,
mimeTypeFilter: string[],
directoriesAllowed: boolean,
buttons: IFilePickerButton[] | IFilePickerButtonFactory,
path?: string,
filter?: IFilePickerFilter,
container?: string) {
this.title = title
this.multiSelect = multiSelect
this.mimeTypeFilter = mimeTypeFilter
this.directoriesAllowed = directoriesAllowed
this.path = path
this.filter = filter
this.buttons = buttons
this.container = container
}

/**
* Pick files using the FilePicker
*
* @return Promise with array of picked files or rejected promise on close without picking
*/
public async pick(): Promise<IsMultiSelect extends true ? string[] : string> {
const { FilePickerVue } = await import('./components/FilePicker/index')

return new Promise((resolve, reject) => {
spawnDialog(FilePickerVue, {
allowPickDirectory: this.directoriesAllowed,
buttons: this.buttons,
container: this.container,
name: this.title,
path: this.path,
mimetypeFilter: this.mimeTypeFilter,
multiselect: this.multiSelect,
filterFn: this.filter,
}, (...rest: unknown[]) => {
const [nodes] = rest as [nodes: Node[]]
if (!Array.isArray(nodes) || nodes.length === 0) {
reject(new Error('FilePicker: No nodes selected'))
} else {
if (this.multiSelect) {
resolve((nodes as Node[]).map((node) => node.path) as (IsMultiSelect extends true ? string[] : string))
} else {
resolve(((nodes as Node[])[0]?.path || '/') as (IsMultiSelect extends true ? string[] : string))
}
}
})
})
}

}

export class FilePickerBuilder<IsMultiSelect extends boolean> {

private title: string
private multiSelect = false
private mimeTypeFilter: string[] = []
private directoriesAllowed = false
private path?: string
private filter?: IFilePickerFilter
private buttons: IFilePickerButton[] | IFilePickerButtonFactory = []
private container?: string

/**
* Construct a new FilePicker
*
* @param title Title of the FilePicker
*/
public constructor(title: string) {
this.title = title
}

/**
* Set the container where the FilePicker will be mounted
* By default 'body' is used
*
* @param container The dialog container
*/
public setContainer(container: string) {
this.container = container
return this
}

/**
* Enable or disable picking multiple files
*
* @param ms True to enable picking multiple files, false otherwise
*/
public setMultiSelect<T extends boolean>(ms: T) {
this.multiSelect = ms
return this as unknown as FilePickerBuilder<T extends true ? true : false>
}

/**
* Add allowed MIME type
*
* @param filter MIME type to allow
*/
public addMimeTypeFilter(filter: string) {
this.mimeTypeFilter.push(filter)
return this
}

/**
* Set allowed MIME types
*
* @param filter Array of allowed MIME types
*/
public setMimeTypeFilter(filter: string[]) {
this.mimeTypeFilter = filter
return this
}

/**
* Add a button to the FilePicker
* Note: This overrides any previous `setButtonFactory` call
*
* @param button The button
*/
public addButton(button: IFilePickerButton) {
if (typeof this.buttons === 'function') {
console.warn('FilePicker buttons were set to factory, now overwritten with button object.')
this.buttons = []
}
this.buttons.push(button)
return this
}

/**
* Set the button factory which is used to generate buttons from current view, path and selected nodes
* Note: This overrides any previous `addButton` call
*
* @param factory The button factory
*/
public setButtonFactory(factory: IFilePickerButtonFactory) {
this.buttons = factory
return this
}

/**
* Set FilePicker type based on legacy file picker types
* @param type The legacy filepicker type to emulate
* @deprecated Use `addButton` or `setButtonFactory` instead as with setType you do not know which button was pressed
*/
public setType(type: FilePickerType) {
this.buttons = (nodes, path) => {
const buttons: IFilePickerButton[] = []
const node = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename
const target = node || basename(path)

if (type === FilePickerType.Choose) {
buttons.push({
callback: () => {},
label: node && !this.multiSelect ? t('Choose {file}', { file: node }) : t('Choose'),
type: 'primary',
})
}
if (type === FilePickerType.CopyMove || type === FilePickerType.Copy) {
buttons.push({
callback: () => {},
label: target ? t('Copy to {target}', { target }) : t('Copy'),
type: 'primary',
icon: IconCopy,
})
}
if (type === FilePickerType.Move || type === FilePickerType.CopyMove) {
buttons.push({
callback: () => {},
label: target ? t('Move to {target}', { target }) : t('Move'),
type: type === FilePickerType.Move ? 'primary' : 'secondary',
icon: IconMove,
})
}
return buttons
}

return this
}

/**
* Allow to pick directories besides files
*
* @param allow True to allow picking directories
*/
public allowDirectories(allow = true) {
this.directoriesAllowed = allow
return this
}

/**
* Set starting path of the FilePicker
*
* @param path Path to start from picking
*/
public startAt(path: string) {
this.path = path
return this
}

/**
* Add filter function to filter file list of FilePicker
*
* @param filter Filter function to apply
*/
public setFilter(filter: IFilePickerFilter) {
this.filter = filter
return this
}

/**
* Construct the configured FilePicker
*/
public build() {
return new FilePicker<IsMultiSelect>(
this.title,
this.multiSelect as IsMultiSelect,
this.mimeTypeFilter,
this.directoriesAllowed,
this.buttons,
this.path,
this.filter,
)
}

}

/**
*
* @param title Title of the file picker
*/
export function getFilePickerBuilder(title: string): FilePickerBuilder<boolean> {
return new FilePickerBuilder(title)
}
Loading

0 comments on commit a29bb7f

Please sign in to comment.