summaryrefslogtreecommitdiffstats
path: root/apps/files/src
diff options
context:
space:
mode:
authorJohn Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>2021-01-14 13:31:15 +0100
committerJulius Härtl <jus@bitgrid.net>2021-01-28 12:00:18 +0100
commit78e114ed72a01e3c37fe0e955ffe6ef649782575 (patch)
treec5e01ff4546b780dbb8a597e642b877fa01ab48d /apps/files/src
parent497440477492b6e7df8ca1eb6c79eb7100a2fe24 (diff)
downloadnextcloud-server-78e114ed72a01e3c37fe0e955ffe6ef649782575.tar.gz
nextcloud-server-78e114ed72a01e3c37fe0e955ffe6ef649782575.zip
Add template picker
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
Diffstat (limited to 'apps/files/src')
-rw-r--r--apps/files/src/components/TemplatePreview.vue203
-rw-r--r--apps/files/src/templates.js92
-rw-r--r--apps/files/src/utils/davUtils.js42
-rw-r--r--apps/files/src/utils/fileUtils.js53
-rw-r--r--apps/files/src/views/TemplatePicker.vue268
5 files changed, 658 insertions, 0 deletions
diff --git a/apps/files/src/components/TemplatePreview.vue b/apps/files/src/components/TemplatePreview.vue
new file mode 100644
index 00000000000..538e1bcff7b
--- /dev/null
+++ b/apps/files/src/components/TemplatePreview.vue
@@ -0,0 +1,203 @@
+<!--
+ - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - 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/>.
+ -
+ -->
+
+<template>
+ <li class="template-picker__item">
+ <input :id="id"
+ :checked="checked"
+ type="radio"
+ class="radio"
+ name="template-picker"
+ @change="onCheck">
+
+ <label :for="id" class="template-picker__label">
+ <div class="template-picker__preview">
+ <img class="template-picker__image"
+ :class="failedPreview ? 'template-picker__image--failed' : ''"
+ :src="realPreviewUrl"
+ alt=""
+ draggable="false"
+ @error="onFailure">
+ </div>
+
+ <span class="template-picker__title">
+ {{ basename }}
+ </span>
+ </label>
+ </li>
+</template>
+
+<script>
+import { generateUrl } from '@nextcloud/router'
+import { encodeFilePath } from '../utils/fileUtils'
+import { getToken, isPublic } from '../utils/davUtils'
+
+// preview width generation
+const previewWidth = 256
+
+export default {
+ name: 'TemplatePreview',
+ inheritAttrs: false,
+
+ props: {
+ basename: {
+ type: String,
+ required: true,
+ },
+ checked: {
+ type: Boolean,
+ default: false,
+ },
+ fileid: {
+ type: [String, Number],
+ required: true,
+ },
+ filename: {
+ type: String,
+ required: true,
+ },
+ previewUrl: {
+ type: String,
+ default: null,
+ },
+ hasPreview: {
+ type: Boolean,
+ default: true,
+ },
+ mime: {
+ type: String,
+ required: true,
+ },
+ ratio: {
+ type: Number,
+ default: null,
+ },
+ },
+
+ data() {
+ return {
+ failedPreview: false,
+ }
+ },
+
+ computed: {
+ id() {
+ return `template-picker-${this.fileid}`
+ },
+
+ realPreviewUrl() {
+ // If original preview failed, fallback to mime icon
+ if (this.failedPreview && this.mimeIcon) {
+ return generateUrl(this.mimeIcon)
+ }
+
+ if (this.previewUrl) {
+ return this.previewUrl
+ }
+ // TODO: find a nicer standard way of doing this?
+ if (isPublic()) {
+ return generateUrl(`/apps/files_sharing/publicpreview/${getToken()}?fileId=${this.fileid}&file=${encodeFilePath(this.filename)}&x=${previewWidth}&y=${previewWidth}&a=1`)
+ }
+ return generateUrl(`/core/preview?fileId=${this.fileid}&x=${previewWidth}&y=${previewWidth}&a=1`)
+ },
+
+ mimeIcon() {
+ return OC.MimeType.getIconUrl(this.mime)
+ },
+ },
+
+ methods: {
+ onCheck() {
+ this.$emit('check', this.fileid)
+ },
+ onFailure() {
+ this.failedPreview = true
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+
+.template-picker {
+ &__item {
+ display: flex;
+ }
+
+ &__label {
+ display: flex;
+ // Align in the middle of the grid
+ align-items: center;
+ flex: 1 1;
+ flex-direction: column;
+ margin: var(--margin);
+
+ &, * {
+ cursor: pointer;
+ user-select: none;
+ }
+
+ &::before {
+ display: none !important;
+ }
+ }
+
+ &__preview {
+ display: flex;
+ overflow: hidden;
+ // Stretch so all entries are the same width
+ flex: 1 1;
+ width: var(--width);
+ min-height: var(--width);
+ max-height: var(--height);
+ padding: var(--margin);
+ border: var(--border) solid var(--color-border);
+ border-radius: var(--border-radius-large);
+
+ input:checked + label > & {
+ border-color: var(--color-primary);
+ }
+ }
+
+ &__image {
+ max-width: 100%;
+ background-color: var(--color-main-background);
+
+ &--failed {
+ width: calc(var(--margin) * 8);
+ // Center mime icon
+ margin: auto;
+ background-color: transparent !important;
+ }
+ }
+
+ &__title {
+ overflow: hidden;
+ // also count preview border
+ max-width: calc(var(--width) + 2*2px);
+ padding: var(--margin);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+}
+
+</style>
diff --git a/apps/files/src/templates.js b/apps/files/src/templates.js
new file mode 100644
index 00000000000..3cffc30085c
--- /dev/null
+++ b/apps/files/src/templates.js
@@ -0,0 +1,92 @@
+/**
+ * @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 { getLoggerBuilder } from '@nextcloud/logger'
+import { loadState } from '@nextcloud/initial-state'
+import { translate as t, translatePlural as n } from '@nextcloud/l10n'
+import Vue from 'vue'
+
+import TemplatePickerView from './views/TemplatePicker'
+
+// Set up logger
+const logger = getLoggerBuilder()
+ .setApp('files')
+ .detectUser()
+ .build()
+
+// Add translates functions
+Vue.mixin({
+ methods: {
+ t,
+ n,
+ },
+})
+
+// Create document root
+const TemplatePickerRoot = document.createElement('div')
+TemplatePickerRoot.id = 'template-picker'
+document.body.appendChild(TemplatePickerRoot)
+
+// Retrieve and init templates
+const templates = loadState('files', 'templates', [])
+logger.debug('Templates providers', templates)
+
+// Init vue app
+const View = Vue.extend(TemplatePickerView)
+const TemplatePicker = new View({
+ name: 'TemplatePicker',
+ propsData: {
+ logger,
+ },
+})
+TemplatePicker.$mount('#template-picker')
+
+// Init template engine after load
+window.addEventListener('DOMContentLoaded', function() {
+ // Init template files menu
+ templates.forEach((provider, index) => {
+
+ const newTemplatePlugin = {
+ attach(menu) {
+ const fileList = menu.fileList
+
+ // only attach to main file list, public view is not supported yet
+ if (fileList.id !== 'files' && fileList.id !== 'files.public') {
+ return
+ }
+
+ // register the new menu entry
+ menu.addMenuEntry({
+ id: `template-new-${provider.app}-${index}`,
+ displayName: provider.label,
+ templateName: provider.label + provider.extension,
+ iconClass: provider.iconClass || 'icon-file',
+ fileType: 'file',
+ actionHandler(name) {
+ TemplatePicker.open(name, provider)
+ },
+ })
+ },
+ }
+ OC.Plugins.register('OCA.Files.NewFileMenu', newTemplatePlugin)
+ })
+})
diff --git a/apps/files/src/utils/davUtils.js b/apps/files/src/utils/davUtils.js
new file mode 100644
index 00000000000..f64801f08dd
--- /dev/null
+++ b/apps/files/src/utils/davUtils.js
@@ -0,0 +1,42 @@
+/**
+ * @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 { generateRemoteUrl } from '@nextcloud/router'
+import { getCurrentUser } from '@nextcloud/auth'
+
+const getRootPath = function() {
+ if (getCurrentUser()) {
+ return generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
+ } else {
+ return generateRemoteUrl('webdav').replace('/remote.php', '/public.php')
+ }
+}
+
+const isPublic = function() {
+ return !getCurrentUser()
+}
+
+const getToken = function() {
+ return document.getElementById('sharingToken') && document.getElementById('sharingToken').value
+}
+
+export { getRootPath, getToken, isPublic }
diff --git a/apps/files/src/utils/fileUtils.js b/apps/files/src/utils/fileUtils.js
new file mode 100644
index 00000000000..97d1c333566
--- /dev/null
+++ b/apps/files/src/utils/fileUtils.js
@@ -0,0 +1,53 @@
+/**
+ * @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @author John Molakvoæ <skjnldsv@protonmail.com>
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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/>.
+ *
+ */
+
+/**
+ * Get an url encoded path
+ *
+ * @param {String} path the full path
+ * @returns {string} url encoded file path
+ */
+const encodeFilePath = function(path) {
+ const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/')
+ let relativePath = ''
+ pathSections.forEach((section) => {
+ if (section !== '') {
+ relativePath += '/' + encodeURIComponent(section)
+ }
+ })
+ return relativePath
+}
+
+/**
+ * Extract dir and name from file path
+ *
+ * @param {String} path the full path
+ * @returns {String[]} [dirPath, fileName]
+ */
+const extractFilePaths = function(path) {
+ const pathSections = path.split('/')
+ const fileName = pathSections[pathSections.length - 1]
+ const dirPath = pathSections.slice(0, pathSections.length - 1).join('/')
+ return [dirPath, fileName]
+}
+
+export { encodeFilePath, extractFilePaths }
diff --git a/apps/files/src/views/TemplatePicker.vue b/apps/files/src/views/TemplatePicker.vue
new file mode 100644
index 00000000000..84c7c38aba4
--- /dev/null
+++ b/apps/files/src/views/TemplatePicker.vue
@@ -0,0 +1,268 @@
+<!--
+ - @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @author John Molakvoæ <skjnldsv@protonmail.com>
+ -
+ - @license GNU AGPL version 3 or any later version
+ -
+ - 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/>.
+ -
+ -->
+
+<template>
+ <Modal v-if="opened"
+ :clear-view-delay="-1"
+ class="templates-picker"
+ size="large"
+ @close="close">
+ <form class="templates-picker__form"
+ :style="style"
+ @submit.prevent.stop="onSubmit">
+ <h3>{{ t('files', 'Pick a template') }}</h3>
+
+ <!-- Templates list -->
+ <ul class="templates-picker__list">
+ <TemplatePreview
+ v-bind="emptyTemplate"
+ :checked="checked === emptyTemplate.fileid"
+ @check="onCheck" />
+
+ <TemplatePreview
+ v-for="template in provider.templates"
+ :key="template.fileid"
+ v-bind="template"
+ :checked="checked === template.fileid"
+ :ratio="provider.ratio"
+ @check="onCheck" />
+ </ul>
+
+ <!-- Cancel and submit -->
+ <div class="templates-picker__buttons">
+ <button @click="close">
+ {{ t('files', 'Cancel') }}
+ </button>
+ <input type="submit"
+ class="primary"
+ :value="t('files', 'Create')"
+ :aria-label="t('files', 'Create a new file with the ')">
+ </div>
+ </form>
+
+ <EmptyContent class="templates-picker__loading" v-if="loading" icon="icon-loading">
+ {{ t('files', 'Creating file') }}
+ </EmptyContent>
+ </Modal>
+</template>
+
+<script>
+import { generateOcsUrl } from '@nextcloud/router'
+import { showError } from '@nextcloud/dialogs'
+
+import axios from '@nextcloud/axios'
+import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
+import Modal from '@nextcloud/vue/dist/Components/Modal'
+
+import TemplatePreview from '../components/TemplatePreview'
+
+const border = 2
+const margin = 8
+const width = margin * 20
+
+export default {
+ name: 'TemplatePicker',
+
+ components: {
+ EmptyContent,
+ Modal,
+ TemplatePreview,
+ },
+
+ props: {
+ logger: {
+ type: Object,
+ required: true,
+ },
+ },
+
+ data() {
+ return {
+ // Check empty template by default
+ checked: -1,
+ loading: false,
+ name: null,
+ opened: false,
+ provider: null,
+ }
+ },
+
+ computed: {
+ emptyTemplate() {
+ return {
+ basename: t('files', 'Blank'),
+ fileid: -1,
+ filename: this.t('files', 'Blank'),
+ hasPreview: false,
+ mime: this.provider?.mimetypes[0] || this.provider?.mimetypes,
+ }
+ },
+
+ selectedTemplate() {
+ return this.provider.templates.find(template => template.fileid === this.checked)
+ },
+
+ /**
+ * Style css vars bin,d
+ * @returns {Object}
+ */
+ style() {
+ return {
+ '--margin': margin + 'px',
+ '--width': width + 'px',
+ '--border': border + 'px',
+ '--fullwidth': width + 2 * margin + 2 * border + 'px',
+ '--height': this.ratio ? width * this.ratio + 'px' : null,
+ }
+ },
+ },
+
+ methods: {
+ /**
+ * Open the picker
+ * @param {string} name the file name to create
+ * @param {object} provider the template provider picked
+ */
+ open(name, provider) {
+ this.checked = this.emptyTemplate.fileid
+ this.name = name
+ this.opened = true
+ this.provider = provider
+ },
+
+ /**
+ * Close the picker and reset variables
+ */
+ close() {
+ this.checked = this.emptyTemplate.fileid
+ this.loading = false
+ this.name = null
+ this.opened = false
+ this.provider = null
+ },
+
+ /**
+ * Manages the radio template picker change
+ * @param {number} fileid the selected template file id
+ */
+ onCheck(fileid) {
+ this.checked = fileid
+ },
+
+ async onSubmit() {
+ this.loading = true
+ const currentDirectory = this.getCurrentDirectory()
+ const fileList = OCA?.Files?.App?.currentFileList
+
+ try {
+ const response = await axios.post(generateOcsUrl('apps/files/api/v1/templates', 2) + 'create', {
+ filePath: `${currentDirectory}/${this.name}`,
+ templatePath: this.selectedTemplate?.filename,
+ templateType: this.selectedTemplate?.templateType,
+ })
+
+ const fileInfo = response.data.ocs.data
+ this.logger.debug('Created new file', fileInfo)
+
+ // Run default action
+ const fileAction = OCA.Files.fileActions.getDefaultFileAction(fileInfo.mime, 'file', OC.PERMISSION_ALL)
+ fileAction.action(fileInfo.basename, {
+ $file: null,
+ dir: currentDirectory,
+ fileList,
+ fileActions: fileList?.fileActions,
+ })
+
+ // Reload files list
+ fileList?.reload?.() || window.location.reload()
+
+ this.close()
+ } catch (error) {
+ this.logger.error('Error while creating the new file from template', error)
+ showError(this.t('files', 'Unable to create new file from template'))
+ } finally {
+ this.loading = false
+ }
+ },
+
+ /**
+ * Return the current directory, fallback to root
+ * @returns {string}
+ */
+ getCurrentDirectory() {
+ const currentDirInfo = OCA?.Files?.App?.currentFileList?.dirInfo
+ || { path: '/', name: '' }
+
+ // Make sure we don't have double slashes
+ return `${currentDirInfo.path}/${currentDirInfo.name}`.replace(/\/\//gi, '/')
+ },
+ },
+}
+</script>
+
+<style lang="scss" scoped>
+.templates-picker {
+ &__form {
+ padding: calc(var(--margin) * 2);
+ // Will be handled by the buttons
+ padding-bottom: 0;
+ }
+
+ &__list {
+ display: grid;
+ grid-gap: calc(var(--margin) * 2);
+ grid-auto-columns: 1fr;
+ // We want maximum 5 columns. Putting 6 as we don't count the grid gap. So it will always be lower than 6
+ max-width: calc(var(--fullwidth) * 6);
+ grid-template-columns: repeat(auto-fit, minmax(var(--fullwidth), 1fr));
+ // Make sure all rows are the same height
+ grid-auto-rows: 1fr;
+ }
+ &__buttons {
+ display: flex;
+ justify-content: space-between;
+ padding: calc(var(--margin) * 2) var(--margin);
+ position: sticky;
+ // Make sure the templates list doesn't weirdly peak under when scrolled. Happens on some rare occasions
+ bottom: -1px;
+ background-color: var(--color-main-background);
+ }
+
+ // Make sure we're relative for the loading emptycontent on top
+ /deep/ .modal-container {
+ position: relative;
+ overflow-y: auto !important;
+ }
+
+ &__loading {
+ position: absolute;
+ top: 0;
+ left: 0;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ background-color: var(--color-main-background-translucent);
+ }
+}
+
+</style>