Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add icons for filepicker and allow reactive button based on current path and selection #938

Merged
merged 3 commits into from
Aug 24, 2023
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
31 changes: 21 additions & 10 deletions l10n/messages.pot
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,27 @@ msgstr ""
msgid "All files"
msgstr ""

#: lib/filepicker.ts:182
#: lib/filepicker.ts:188
msgid "Choose"
msgstr ""

#: lib/filepicker.ts:170
#: lib/filepicker.ts:188
msgid "Choose {file}"
msgstr ""

#: lib/filepicker.ts:195
msgid "Copy"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:242
#: lib/filepicker.ts:195
msgid "Copy to {target}"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:249
msgid "Could not create the new folder"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:152
#: lib/components/FilePicker/FilePicker.vue:159
#: lib/components/FilePicker/FilePickerNavigation.vue:65
msgid "Favorites"
msgstr ""
Expand All @@ -39,28 +47,31 @@ msgstr ""
msgid "File name cannot be empty."
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:228
#: lib/components/FilePicker/FilePicker.vue:235
msgid "Files and folders you mark as favorite will show up here."
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:226
#: lib/components/FilePicker/FilePicker.vue:233
msgid "Files and folders you recently modified will show up here."
msgstr ""

#: lib/components/FilePicker/FileList.vue:39
msgid "Modified"
msgstr ""

#: lib/filepicker.ts:176
#: lib/filepicker.ts:190
#: lib/filepicker.ts:203
msgid "Move"
msgstr ""

#: lib/filepicker.ts:203
msgid "Move to {target}"
msgstr ""

#: lib/components/FilePicker/FileList.vue:19
msgid "Name"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:152
#: lib/components/FilePicker/FilePicker.vue:159
#: lib/components/FilePicker/FilePickerNavigation.vue:61
msgid "Recent"
msgstr ""
Expand All @@ -77,6 +88,6 @@ msgstr ""
msgid "Undo"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:224
#: lib/components/FilePicker/FilePicker.vue:231
msgid "Upload some content or sync with your devices!"
msgstr ""
5 changes: 3 additions & 2 deletions lib/components/DialogButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
<NcButton :aria-label="props.label" :type="props.type" @click="handleClick">
{{ props.label }}
<template v-if="props.icon !== undefined" #icon>
<component :is="props.icon" :size="20" />
<NcIconSvgWrapper v-if="typeof props.icon === 'string'" :svg="props.icon" />
<component :is="props.icon" v-else :size="20" />
</template>
</NcButton>
</template>

<script setup lang="ts">
import type { IDialogButton } from './types'
import { NcButton } from '@nextcloud/vue'
import { NcButton, NcIconSvgWrapper } from '@nextcloud/vue'

// with vue 3.3:
// const props = defineProps<IDialogButton>()
Expand Down
27 changes: 17 additions & 10 deletions lib/components/FilePicker/FilePicker.vue
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
</template>

<script setup lang="ts">
import type { IFilePickerButton, IFilePickerFilter } from '../types'
import type { IFilePickerButton, IFilePickerButtonFactory, IFilePickerFilter } from '../types'
import type { Node } from '@nextcloud/files'

import IconFile from 'vue-material-design-icons/File.vue'
Expand All @@ -64,7 +64,7 @@ import { t } from '../../utils/l10n'

const props = withDefaults(defineProps<{
/** Buttons to be displayed */
buttons: IFilePickerButton[]
buttons: IFilePickerButton[] | IFilePickerButtonFactory

/** The name of file picker dialog (heading) */
name: string
Expand Down Expand Up @@ -115,7 +115,7 @@ const props = withDefaults(defineProps<{
})

const emit = defineEmits<{
(e: 'close'): void
(e: 'close', v?: Node[]): void
}>()

/**
Expand All @@ -133,13 +133,20 @@ const dialogProps = computed(() => ({
/**
* Map buttons to Dialog buttons by wrapping the callback function to pass the selected files
*/
const dialogButtons = computed(() => [...props.buttons].map(button => ({
...button,
callback: async () => {
const nodes = selectedFiles.value.length === 0 && props.allowPickDirectory ? [await getFile(currentPath.value)] : selectedFiles.value as Node[]
return button.callback(nodes)
},
})))
const dialogButtons = computed(() => {
const buttons = typeof props.buttons === 'function'
? props.buttons(selectedFiles.value as Node[], currentPath.value, currentView.value)
: props.buttons

return buttons.map((button) => ({
...button,
callback: async () => {
const nodes = selectedFiles.value.length === 0 && props.allowPickDirectory ? [await getFile(currentPath.value)] : selectedFiles.value as Node[]
button.callback(nodes)
emit('close', selectedFiles.value as Node[])
},
} as IFilePickerButton))
})

/**
* Name of the currently active view
Expand Down
6 changes: 4 additions & 2 deletions lib/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ export interface IDialogButton {
callback: () => void,
/**
* Optional Icon for the button
* Can be a Vue component or async component
* Can be a Vue component, async Vue component, or SVG
*/
icon?: Component | AsyncComponent,
icon?: Component | AsyncComponent | string,

/**
* Button type
Expand All @@ -58,6 +58,8 @@ export interface IFilePickerButton extends Omit<IDialogButton, 'callback'> {
callback: (nodes: Node[]) => void
}

export type IFilePickerButtonFactory = (selectedNodes: Node[], currentPath: string, currentView: string) => IFilePickerButton[]

/**
* Type of filter functions to filter the FilePicker's file list
*/
Expand Down
108 changes: 62 additions & 46 deletions lib/filepicker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,17 @@
*
*/

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

import { basename } from 'path'
import { spawnDialog } from './utils/dialogs'
import { FilePickerVue } from './components/FilePicker/index'
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
*/
Expand All @@ -44,15 +48,15 @@ export class FilePicker<IsMultiSelect extends boolean> {
private multiSelect: IsMultiSelect
private mimeTypeFilter: string[]
private directoriesAllowed: boolean
private buttons: IFilePickerButton[]
private buttons: IFilePickerButton[] | IFilePickerButtonFactory
private path?: string
private filter?: IFilePickerFilter

public constructor(title: string,
multiSelect: IsMultiSelect,
mimeTypeFilter: string[],
directoriesAllowed: boolean,
buttons: IFilePickerButton[],
buttons: IFilePickerButton[] | IFilePickerButtonFactory,
path?: string,
filter?: IFilePickerFilter) {
this.title = title
Expand All @@ -71,27 +75,22 @@ export class FilePicker<IsMultiSelect extends boolean> {
*/
public async pick(): Promise<IsMultiSelect extends true ? string[] : string> {
return new Promise((resolve, reject) => {
const buttons = this.buttons.map((button) => ({
...button,
callback: (nodes: Node[]) => {
button.callback(nodes)
if (this.multiSelect) {
resolve(nodes.map((node) => node.path) as (IsMultiSelect extends true ? string[] : string))
} else {
resolve((nodes[0]?.path || '/') as (IsMultiSelect extends true ? string[] : string))
}
},
}))

spawnDialog(FilePickerVue, {
allowPickDirectory: this.directoriesAllowed,
buttons,
buttons: this.buttons,
name: this.title,
path: this.path,
mimetypeFilter: this.mimeTypeFilter,
multiselect: this.multiSelect,
filterFn: this.filter,
}, reject)
}, (...nodes: unknown[]) => {
if (!nodes) reject(new Error('Nothing selected'))
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))
}
})
})
}

Expand All @@ -105,7 +104,7 @@ export class FilePickerBuilder<IsMultiSelect extends boolean> {
private directoriesAllowed = false
private path?: string
private filter?: IFilePickerFilter
private buttons: IFilePickerButton[] = []
private buttons: IFilePickerButton[] | IFilePickerButtonFactory = []

/**
* Construct a new FilePicker
Expand Down Expand Up @@ -148,48 +147,65 @@ export class FilePickerBuilder<IsMultiSelect extends boolean> {

/**
* 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` instead as with setType you do not know which button was pressed
* @deprecated Use `addButton` or `setButtonFactory` instead as with setType you do not know which button was pressed
*/
public setType(type: FilePickerType) {
this.buttons = []

if (type === FilePickerType.CopyMove || type === FilePickerType.Copy) {
this.buttons.push({
callback: () => {},
label: t('Copy'),
type: 'primary',
})
} else if (type === FilePickerType.Move) {
this.buttons.push({
callback: () => {},
label: t('Move'),
type: 'primary',
})
} else if (type === FilePickerType.Choose) {
this.buttons.push({
callback: () => {},
label: t('Choose'),
type: 'primary',
})
}
this.buttons = (nodes, path) => {
const buttons: IFilePickerButton[] = []
const node = nodes?.[0]?.attributes?.displayName || nodes?.[0]?.basename
const target = node || basename(path)

if (type === FilePickerType.CopyMove) {
this.buttons.push({
callback: () => {},
label: t('Move'),
type: 'secondary',
})
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
Expand Down
9 changes: 9 additions & 0 deletions lib/svg.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare module '*.svg' {
const content: string
export default content
}

declare module '*.svg?raw' {
const content: string
export default content
}
6 changes: 3 additions & 3 deletions lib/utils/dialogs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import Vue from 'vue'
* @param props Properties to pass to the dialog
* @param onClose Callback when the dialog is closed
*/
export const spawnDialog = (dialog: Component | AsyncComponent, props: any, onClose: () => void = () => {}) => {
export const spawnDialog = (dialog: Component | AsyncComponent, props: any, onClose: (...rest: unknown[]) => void = () => {}) => {
const el = document.createElement('div')

const container: HTMLElement = document.querySelector(props?.container) || document.body
Expand All @@ -43,8 +43,8 @@ export const spawnDialog = (dialog: Component | AsyncComponent, props: any, onCl
h(dialog, {
props,
on: {
close: () => {
onClose()
close: (...rest: unknown[]) => {
onClose(rest)
vue.$destroy()
},
},
Expand Down
6 changes: 6 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading