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

[stable4] Add icons for filepicker and allow reactive button based on current path and selection #942

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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 21 additions & 9 deletions l10n/messages.pot
Expand Up @@ -22,19 +22,27 @@ msgstr ""
msgid "All files"
msgstr ""

#: lib/legacy.ts:135
#: lib/legacy.ts:155
msgid "Choose"
msgstr ""

#: lib/legacy.ts:141
#: lib/legacy.ts:155
msgid "Choose {file}"
msgstr ""

#: lib/legacy.ts:163
msgid "Copy"
msgstr ""

#: lib/components/FilePicker/FilePicker.vue:242
#: lib/legacy.ts:163
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 @@ -43,27 +51,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/legacy.ts:147
#: lib/legacy.ts:171
msgid "Move"
msgstr ""

#: lib/legacy.ts:171
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 Down Expand Up @@ -94,6 +106,6 @@ msgstr ""
msgid "Unset"
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
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
29 changes: 18 additions & 11 deletions lib/components/FilePicker/FilePicker.vue
Expand Up @@ -44,7 +44,7 @@
</template>

<script setup lang="ts">
import type { IFilePickerButton } 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 All @@ -84,7 +84,7 @@ const props = withDefaults(defineProps<{
/**
* Custom filter function used to filter pickable files
*/
filterFn?: (node: Node) => boolean
filterFn?: IFilePickerFilter

/**
* List of allowed mime types
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
20 changes: 19 additions & 1 deletion lib/components/types.ts
Expand Up @@ -25,11 +25,29 @@ import type { AsyncComponent, Component } from 'vue'

export interface IDialogButton {
label: string,
icon?: Component | AsyncComponent,

/** Callback on button click */
callback: () => void,
/**
* Optional Icon for the button
* Can be a Vue component, async Vue component, or SVG
*/
icon?: Component | AsyncComponent | string,

/**
* Button type
* @see https://nextcloud-vue-components.netlify.app/#/Components/NcButton
*/
type?: 'primary' | 'secondary' | 'error' | 'warning' | 'success'
}

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
*/
export type IFilePickerFilter = (node: Node) => boolean
66 changes: 42 additions & 24 deletions lib/legacy.ts
Expand Up @@ -25,13 +25,17 @@
*/

/// <reference types="@nextcloud/typings" />
import type { IFilePickerButton } from './components/FilePicker/FilePicker.vue'
import type { IFilePickerButton, IFilePickerButtonFactory } from './components/types'
import type { Node } from '@nextcloud/files'
import type { AsyncComponent, Component } from 'vue'

import { basename } from 'path'
import { t } from './utils/l10n'
import { FilePickerVue, FilePickerType } from '.'

import DialogBase from './components/DialogBase.vue'
import IconCopy from '@mdi/svg/svg/folder-multiple.svg'
import IconMove from '@mdi/svg/svg/folder-move.svg'
import Vue from 'vue'

/**
Expand Down Expand Up @@ -129,34 +133,48 @@ export async function filepicker(title: string, callback: (s: string | string[],
sharePermissions: null,
})

const buttons: IFilePickerButton[] = []
if (type === FilePickerType.Choose) {
buttons.push({
label: t('Choose'),
type: 'primary',
callback: legacyCallback(callback, FilePickerType.Choose),
})
} else if (type === FilePickerType.Copy || type === FilePickerType.CopyMove) {
buttons.push({
label: t('Copy'),
callback: legacyCallback(callback, FilePickerType.Copy),
})
}
if (type === FilePickerType.CopyMove || type === FilePickerType.Move) {
buttons.push({
label: t('Move'),
type: 'primary',
callback: legacyCallback(callback, FilePickerType.Move),
})
}
let buttonFn: IFilePickerButtonFactory|IFilePickerButton[]
if (type === FilePickerType.Custom) {
(options.buttons || []).forEach((button) => {
buttons.push({
buttonFn = [] as IFilePickerButton[]
(options.buttons || []).forEach((button: { text: string, defaultButton: boolean, type: FilePickerType }) => {
(buttonFn as IFilePickerButton[]).push({
callback: legacyCallback(callback, button.type),
label: button.text,
type: button.defaultButton ? 'primary' : 'secondary',
})
})
} else {
buttonFn = (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: legacyCallback(callback, FilePickerType.Choose),
label: node && !multiselect ? t('Choose {file}', { file: node }) : t('Choose'),
type: 'primary',
})
}

if (type === FilePickerType.CopyMove || type === FilePickerType.Copy) {
buttons.push({
callback: legacyCallback(callback, FilePickerType.Copy),
label: target ? t('Copy to {target}', { target }) : t('Copy'),
type: 'primary',
icon: IconCopy,
})
}
if (type === FilePickerType.Move || type === FilePickerType.CopyMove) {
buttons.push({
callback: legacyCallback(callback, FilePickerType.Move),
label: target ? t('Move to {target}', { target }) : t('Move'),
type: type === FilePickerType.Move ? 'primary' : 'secondary',
icon: IconMove,
})
}
return buttons
}
}

const filter = {} as any
Expand All @@ -170,7 +188,7 @@ export async function filepicker(title: string, callback: (s: string | string[],
spawnDialog(FilePickerVue, {
...filter,
name: title,
buttons,
buttons: buttonFn,
multiselect,
path,
mimetypeFilter,
Expand Down
9 changes: 9 additions & 0 deletions lib/svg.d.ts
@@ -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
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.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -54,6 +54,7 @@
"vue": "^2.7.14"
},
"dependencies": {
"@mdi/svg": "^7.2.96",
"@nextcloud/files": "^3.0.0-beta.14",
"@nextcloud/l10n": "^2.2.0",
"@nextcloud/router": "^2.1.2",
Expand Down