Skip to content

Commit

Permalink
Merge pull request #938 from nextcloud-libraries/feat/icons-and-target
Browse files Browse the repository at this point in the history
  • Loading branch information
skjnldsv authored Aug 24, 2023
2 parents 7a07526 + 8b9b6d0 commit ed9b0b0
Show file tree
Hide file tree
Showing 9 changed files with 126 additions and 73 deletions.
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

0 comments on commit ed9b0b0

Please sign in to comment.