diff options
Diffstat (limited to 'apps/files_versions/src')
-rw-r--r-- | apps/files_versions/src/components/Version.vue | 93 | ||||
-rw-r--r-- | apps/files_versions/src/components/VersionLabelDialog.vue | 123 | ||||
-rw-r--r-- | apps/files_versions/src/components/VersionLabelForm.vue | 99 | ||||
-rw-r--r-- | apps/files_versions/src/css/versions.css | 12 | ||||
-rw-r--r-- | apps/files_versions/src/files_versions_tab.js | 4 | ||||
-rw-r--r-- | apps/files_versions/src/utils/davClient.js | 14 | ||||
-rw-r--r-- | apps/files_versions/src/utils/versions.ts | 6 | ||||
-rw-r--r-- | apps/files_versions/src/views/VersionTab.vue | 89 |
8 files changed, 243 insertions, 197 deletions
diff --git a/apps/files_versions/src/components/Version.vue b/apps/files_versions/src/components/Version.vue index c6d44edaf06..dc36e4134f9 100644 --- a/apps/files_versions/src/components/Version.vue +++ b/apps/files_versions/src/components/Version.vue @@ -5,12 +5,13 @@ <template> <NcListItem class="version" :force-display-actions="true" + :actions-aria-label="t('files_versions', 'Actions for version from {versionHumanExplicitDate}', { versionHumanExplicitDate })" :data-files-versions-version="version.fileVersion" @click="click"> <!-- Icon --> <template #icon> <div v-if="!(loadPreview || previewLoaded)" class="version__image" /> - <img v-else-if="(isCurrent || version.hasPreview) && !previewErrored" + <img v-else-if="version.previewUrl && !previewErrored" :src="version.previewUrl" alt="" decoding="async" @@ -30,18 +31,24 @@ <div class="version__info"> <div v-if="versionLabel" class="version__info__label" + data-cy-files-version-label :title="versionLabel"> {{ versionLabel }} </div> - <div v-if="versionAuthor" class="version__info"> + <div v-if="versionAuthor" + class="version__info" + data-cy-files-version-author-name> <span v-if="versionLabel">•</span> <NcAvatar class="avatar" :user="version.author" - :size="16" - :disable-menu="true" - :disable-tooltip="true" + :size="20" + disable-menu + disable-tooltip :show-user-status="false" /> - <div>{{ versionAuthor }}</div> + <div class="version__info__author_name" + :title="versionAuthor"> + {{ versionAuthor }} + </div> </div> </div> </template> @@ -52,7 +59,7 @@ <NcDateTime class="version__info__date" relative-time="short" :timestamp="version.mtime" /> - <!-- Separate dot to improve alignement --> + <!-- Separate dot to improve alignment --> <span>•</span> <span>{{ humanReadableSize }}</span> </div> @@ -109,33 +116,35 @@ </template> </NcListItem> </template> - <script lang="ts"> import type { PropType } from 'vue' import type { Version } from '../utils/versions' +import { getCurrentUser } from '@nextcloud/auth' +import { Permission, formatFileSize } from '@nextcloud/files' +import { loadState } from '@nextcloud/initial-state' +import { t } from '@nextcloud/l10n' +import { joinPaths } from '@nextcloud/paths' +import { getRootUrl, generateUrl } from '@nextcloud/router' +import { defineComponent } from 'vue' + +import axios from '@nextcloud/axios' +import moment from '@nextcloud/moment' +import logger from '../utils/logger' + import BackupRestore from 'vue-material-design-icons/BackupRestore.vue' import Delete from 'vue-material-design-icons/Delete.vue' import Download from 'vue-material-design-icons/Download.vue' import FileCompare from 'vue-material-design-icons/FileCompare.vue' import ImageOffOutline from 'vue-material-design-icons/ImageOffOutline.vue' -import Pencil from 'vue-material-design-icons/Pencil.vue' - -import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' -import NcActionLink from '@nextcloud/vue/dist/Components/NcActionLink.js' -import NcAvatar from '@nextcloud/vue/dist/Components/NcAvatar.js' -import NcDateTime from '@nextcloud/vue/dist/Components/NcDateTime.js' -import NcListItem from '@nextcloud/vue/dist/Components/NcListItem.js' -import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js' +import Pencil from 'vue-material-design-icons/PencilOutline.vue' -import { getRootUrl, generateOcsUrl } from '@nextcloud/router' -import { joinPaths } from '@nextcloud/paths' -import { loadState } from '@nextcloud/initial-state' -import { Permission, formatFileSize } from '@nextcloud/files' -import { translate as t } from '@nextcloud/l10n' -import { defineComponent } from 'vue' - -import axios from '@nextcloud/axios' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActionLink from '@nextcloud/vue/components/NcActionLink' +import NcAvatar from '@nextcloud/vue/components/NcAvatar' +import NcDateTime from '@nextcloud/vue/components/NcDateTime' +import NcListItem from '@nextcloud/vue/components/NcListItem' +import Tooltip from '@nextcloud/vue/directives/Tooltip' const hasPermission = (permissions: number, permission: number): boolean => (permissions & permission) !== 0 @@ -198,7 +207,7 @@ export default defineComponent({ previewLoaded: false, previewErrored: false, capabilities: loadState('core', 'capabilities', { files: { version_labeling: false, version_deletion: false } }), - versionAuthor: '', + versionAuthor: '' as string | null, } }, @@ -225,6 +234,10 @@ export default defineComponent({ return label }, + versionHumanExplicitDate(): string { + return moment(this.version.mtime).format('LLLL') + }, + downloadURL(): string { if (this.isCurrent) { return getRootUrl() + joinPaths('/remote.php/webdav', this.fileInfo.path, this.fileInfo.name) @@ -259,7 +272,7 @@ export default defineComponent({ const downloadAttribute = this.fileInfo.shareAttributes .find((attribute) => attribute.scope === 'permissions' && attribute.key === 'download') || {} // If the download attribute is set to false, the file is not downloadable - if (downloadAttribute?.enabled === false) { + if (downloadAttribute?.value === false) { return false } } @@ -290,21 +303,26 @@ export default defineComponent({ }, async fetchDisplayName() { - // check to make sure that we have a valid author - in case database did not migrate, null author, etc. - if (this.version.author) { + this.versionAuthor = null + if (!this.version.author) { + return + } + + if (this.version.author === getCurrentUser()?.uid) { + this.versionAuthor = t('files_versions', 'You') + } else { try { - const { data } = await axios.get(generateOcsUrl(`/cloud/users/${this.version.author}`)) - this.versionAuthor = data.ocs.data.displayname - } catch (e) { - // Promise got rejected - default to null author to not try to load author profile - this.versionAuthor = null + const { data } = await axios.post(generateUrl('/displaynames'), { users: [this.version.author] }) + this.versionAuthor = data.users[this.version.author] + } catch (error) { + logger.warn('Could not load user display name', { error }) } } }, click() { if (!this.canView) { - window.location = this.downloadURL + window.location.href = this.downloadURL return } this.$emit('click', { version: this.version }) @@ -334,12 +352,19 @@ export default defineComponent({ gap: 0.5rem; color: var(--color-main-text); font-weight: 500; + overflow: hidden; &__label { font-weight: 700; // Fix overflow on narrow screens overflow: hidden; text-overflow: ellipsis; + min-width: 110px; + } + + &__author_name { + overflow: hidden; + text-overflow: ellipsis; } &__date { diff --git a/apps/files_versions/src/components/VersionLabelDialog.vue b/apps/files_versions/src/components/VersionLabelDialog.vue new file mode 100644 index 00000000000..760780cae61 --- /dev/null +++ b/apps/files_versions/src/components/VersionLabelDialog.vue @@ -0,0 +1,123 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> +<template> + <NcDialog :buttons="dialogButtons" + content-classes="version-label-modal" + is-form + :open="open" + size="normal" + :name="t('files_versions', 'Name this version')" + @update:open="$emit('update:open', $event)" + @submit="setVersionLabel(editedVersionLabel)"> + <NcTextField ref="labelInput" + class="version-label-modal__input" + :label="t('files_versions', 'Version name')" + :placeholder="t('files_versions', 'Version name')" + :value.sync="editedVersionLabel" /> + + <p class="version-label-modal__info"> + {{ t('files_versions', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }} + </p> + </NcDialog> +</template> + +<script lang="ts"> +import { t } from '@nextcloud/l10n' +import { defineComponent } from 'vue' +import svgCheck from '@mdi/svg/svg/check.svg?raw' + +import NcDialog from '@nextcloud/vue/components/NcDialog' +import NcTextField from '@nextcloud/vue/components/NcTextField' + +type Focusable = Vue & { focus: () => void } + +export default defineComponent({ + name: 'VersionLabelDialog', + components: { + NcDialog, + NcTextField, + }, + props: { + open: { + type: Boolean, + default: false, + }, + versionLabel: { + type: String, + default: '', + }, + }, + data() { + return { + editedVersionLabel: '', + } + }, + computed: { + dialogButtons() { + const buttons: unknown[] = [] + if (this.versionLabel.trim() === '') { + // If there is no label just offer a cancel action that just closes the dialog + buttons.push({ + label: t('files_versions', 'Cancel'), + }) + } else { + // If there is already a label set, offer to remove the version label + buttons.push({ + label: t('files_versions', 'Remove version name'), + type: 'error', + nativeType: 'reset', + callback: () => { this.setVersionLabel('') }, + }) + } + return [ + ...buttons, + { + label: t('files_versions', 'Save version name'), + type: 'primary', + nativeType: 'submit', + icon: svgCheck, + }, + ] + }, + }, + watch: { + versionLabel: { + immediate: true, + handler(label) { + this.editedVersionLabel = label ?? '' + }, + }, + open: { + immediate: true, + handler(open) { + if (open) { + this.$nextTick(() => (this.$refs.labelInput as Focusable).focus()) + } + this.editedVersionLabel = this.versionLabel + }, + }, + }, + methods: { + setVersionLabel(label: string) { + this.$emit('label-update', label) + }, + + t, + }, +}) +</script> + +<style scoped lang="scss"> +.version-label-modal { + &__info { + color: var(--color-text-maxcontrast); + margin-block: calc(3 * var(--default-grid-baseline)); + } + + &__input { + margin-block-start: calc(2 * var(--default-grid-baseline)); + } +} +</style> diff --git a/apps/files_versions/src/components/VersionLabelForm.vue b/apps/files_versions/src/components/VersionLabelForm.vue deleted file mode 100644 index a0efcbe47ee..00000000000 --- a/apps/files_versions/src/components/VersionLabelForm.vue +++ /dev/null @@ -1,99 +0,0 @@ -<!-- - - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors - - SPDX-License-Identifier: AGPL-3.0-or-later ---> -<template> - <form class="version-label-modal" - @submit.prevent="setVersionLabel(innerVersionLabel)"> - <label> - <div class="version-label-modal__title">{{ t('files_versions', 'Version name') }}</div> - <NcTextField ref="labelInput" - :value.sync="innerVersionLabel" - :placeholder="t('files_versions', 'Version name')" - :label-outside="true" /> - </label> - - <div class="version-label-modal__info"> - {{ t('files_versions', 'Named versions are persisted, and excluded from automatic cleanups when your storage quota is full.') }} - </div> - - <div class="version-label-modal__actions"> - <NcButton :disabled="innerVersionLabel.trim().length === 0" @click="setVersionLabel('')"> - {{ t('files_versions', 'Remove version name') }} - </NcButton> - <NcButton type="primary" native-type="submit"> - <template #icon> - <Check /> - </template> - {{ t('files_versions', 'Save version name') }} - </NcButton> - </div> - </form> -</template> - -<script lang="ts"> -import Check from 'vue-material-design-icons/Check.vue' -import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' -import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' -import { translate } from '@nextcloud/l10n' - -import { defineComponent } from 'vue' - -export default defineComponent({ - name: 'VersionLabelForm', - components: { - NcButton, - NcTextField, - Check, - }, - props: { - versionLabel: { - type: String, - default: '', - }, - }, - data() { - return { - innerVersionLabel: this.versionLabel, - } - }, - mounted() { - this.$nextTick(() => { - (this.$refs.labelInput as Vue).$el.getElementsByTagName('input')[0].focus() - }) - }, - methods: { - setVersionLabel(label: string) { - this.$emit('label-update', label) - }, - - t: translate, - }, -}) -</script> - -<style scoped lang="scss"> -.version-label-modal { - display: flex; - justify-content: space-between; - flex-direction: column; - height: 250px; - padding: 16px; - - &__title { - margin-bottom: 12px; - font-weight: 600; - } - - &__info { - margin-top: 12px; - color: var(--color-text-maxcontrast); - } - - &__actions { - display: flex; - justify-content: space-between; - margin-top: 64px; - } -} -</style> diff --git a/apps/files_versions/src/css/versions.css b/apps/files_versions/src/css/versions.css index b9914027738..1637394ef48 100644 --- a/apps/files_versions/src/css/versions.css +++ b/apps/files_versions/src/css/versions.css @@ -41,16 +41,16 @@ .versionsTabView img { cursor: pointer; - padding-right: 4px; + padding-inline-end: 4px; } .versionsTabView img.preview { position: relative; top: 6px; - left: 10px; + inset-inline-start: 10px; border: 1px solid var(--color-border-dark); cursor: default; - padding-right: 0; + padding-inline-end: 0; } .versionsTabView .version-container { @@ -63,7 +63,7 @@ } .versionsTabView .version-details { - text-align: left; + text-align: start; } .versionsTabView .version-details > span { @@ -73,7 +73,7 @@ .versionsTabView .revertVersion { cursor: pointer; float: right; - margin-right: 0; + margin-inline-end: 0; } .versionsTabView li.active .downloadVersion { @@ -95,7 +95,7 @@ } .version-container { - padding-left: 5px; + padding-inline-start: 5px; } .version-details { diff --git a/apps/files_versions/src/files_versions_tab.js b/apps/files_versions/src/files_versions_tab.js index 011cde11c04..12f36bad24a 100644 --- a/apps/files_versions/src/files_versions_tab.js +++ b/apps/files_versions/src/files_versions_tab.js @@ -7,14 +7,14 @@ import Vue from 'vue' import { translate as t, translatePlural as n } from '@nextcloud/l10n' import VersionTab from './views/VersionTab.vue' -import VTooltip from 'v-tooltip' +import VTooltipPlugin from 'v-tooltip' // eslint-disable-next-line n/no-missing-import, import/no-unresolved import BackupRestore from '@mdi/svg/svg/backup-restore.svg?raw' Vue.prototype.t = t Vue.prototype.n = n -Vue.use(VTooltip) +Vue.use(VTooltipPlugin) // Init Sharing tab component const View = Vue.extend(VersionTab) diff --git a/apps/files_versions/src/utils/davClient.js b/apps/files_versions/src/utils/davClient.js index 094f9cee0f0..029373e9193 100644 --- a/apps/files_versions/src/utils/davClient.js +++ b/apps/files_versions/src/utils/davClient.js @@ -14,16 +14,16 @@ const client = createClient(remote) // set CSRF token header const setHeaders = (token) => { - client.setHeaders({ - // Add this so the server knows it is an request from the browser - 'X-Requested-With': 'XMLHttpRequest', - // Inject user auth - requesttoken: token ?? '', - }) + client.setHeaders({ + // Add this so the server knows it is an request from the browser + 'X-Requested-With': 'XMLHttpRequest', + // Inject user auth + requesttoken: token ?? '', + }) } // refresh headers when request token changes onRequestTokenUpdate(setHeaders) setHeaders(getRequestToken()) -export default client
\ No newline at end of file +export default client diff --git a/apps/files_versions/src/utils/versions.ts b/apps/files_versions/src/utils/versions.ts index b52f92ef462..6d5933f0bd9 100644 --- a/apps/files_versions/src/utils/versions.ts +++ b/apps/files_versions/src/utils/versions.ts @@ -28,7 +28,6 @@ export interface Version { type: string, // 'file' mtime: number, // Version creation date as a timestamp permissions: string, // Only readable: 'R' - hasPreview: boolean, // Whether the version has a preview previewUrl: string, // Preview URL of the version url: string, // Download URL of the version source: string, // The WebDAV endpoint of the ressource @@ -78,12 +77,12 @@ function formatVersion(version: any, fileInfo: any): Version { let previewUrl = '' if (mtime === fileInfo.mtime) { // Version is the current one - previewUrl = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0', { + previewUrl = generateUrl('/core/preview?fileId={fileId}&c={fileEtag}&x=250&y=250&forceIcon=0&a=0&forceIcon=1&mimeFallback=1', { fileId: fileInfo.id, fileEtag: fileInfo.etag, }) } else { - previewUrl = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}', { + previewUrl = generateUrl('/apps/files_versions/preview?file={file}&version={fileVersion}&mimeFallback=1', { file: joinPaths(fileInfo.path, fileInfo.name), fileVersion: version.basename, }) @@ -102,7 +101,6 @@ function formatVersion(version: any, fileInfo: any): Version { type: version.type, mtime, permissions: 'R', - hasPreview: version.props['has-preview'] === 1, previewUrl, url: joinPaths('/remote.php/dav', version.filename), source: generateRemoteUrl('dav') + encodePath(version.filename), diff --git a/apps/files_versions/src/views/VersionTab.vue b/apps/files_versions/src/views/VersionTab.vue index b656fc75d4f..a643aef439d 100644 --- a/apps/files_versions/src/views/VersionTab.vue +++ b/apps/files_versions/src/views/VersionTab.vue @@ -4,65 +4,65 @@ --> <template> <div class="versions-tab__container"> - <VirtualScrolling :sections="sections" + <VirtualScrolling v-slot="{ visibleSections }" + :sections="sections" :header-height="0"> - <template slot-scope="{visibleSections}"> - <ul data-files-versions-versions-list> - <template v-if="visibleSections.length === 1"> - <Version v-for="(row) of visibleSections[0].rows" - :key="row.items[0].mtime" - :can-view="canView" - :can-compare="canCompare" - :load-preview="isActive" - :version="row.items[0]" - :file-info="fileInfo" - :is-current="row.items[0].mtime === fileInfo.mtime" - :is-first-version="row.items[0].mtime === initialVersionMtime" - @click="openVersion" - @compare="compareVersion" - @restore="handleRestore" - @label-update-request="handleLabelUpdateRequest(row.items[0])" - @delete="handleDelete" /> - </template> - </ul> - </template> + <ul :aria-label="t('files_versions', 'File versions')" data-files-versions-versions-list> + <template v-if="visibleSections.length === 1"> + <Version v-for="(row) of visibleSections[0].rows" + :key="row.items[0].mtime" + :can-view="canView" + :can-compare="canCompare" + :load-preview="isActive" + :version="row.items[0]" + :file-info="fileInfo" + :is-current="row.items[0].mtime === fileInfo.mtime" + :is-first-version="row.items[0].mtime === initialVersionMtime" + @click="openVersion" + @compare="compareVersion" + @restore="handleRestore" + @label-update-request="handleLabelUpdateRequest(row.items[0])" + @delete="handleDelete" /> + </template> + </ul> <NcLoadingIcon v-if="loading" slot="loader" class="files-list-viewer__loader" /> </VirtualScrolling> - <NcModal v-if="showVersionLabelForm" - :title="t('files_versions', 'Name this version')" - @close="showVersionLabelForm = false"> - <VersionLabelForm :version-label="editedVersion.label" @label-update="handleLabelUpdate" /> - </NcModal> + <VersionLabelDialog v-if="editedVersion" + :open.sync="showVersionLabelForm" + :version-label="editedVersion.label" + @label-update="handleLabelUpdate" /> </div> </template> <script> import path from 'path' +import { getCurrentUser } from '@nextcloud/auth' import { showError, showSuccess } from '@nextcloud/dialogs' -import isMobile from '@nextcloud/vue/dist/Mixins/isMobile.js' import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import { getCurrentUser } from '@nextcloud/auth' -import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' -import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' +import { useIsMobile } from '@nextcloud/vue/composables/useIsMobile' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' import { fetchVersions, deleteVersion, restoreVersion, setVersionLabel } from '../utils/versions.ts' import Version from '../components/Version.vue' import VirtualScrolling from '../components/VirtualScrolling.vue' -import VersionLabelForm from '../components/VersionLabelForm.vue' +import VersionLabelDialog from '../components/VersionLabelDialog.vue' export default { name: 'VersionTab', components: { Version, VirtualScrolling, - VersionLabelForm, + VersionLabelDialog, NcLoadingIcon, - NcModal, }, - mixins: [ - isMobile, - ], + + setup() { + return { + isMobile: useIsMobile(), + } + }, + data() { return { fileInfo: null, @@ -71,6 +71,7 @@ export default { versions: [], loading: false, showVersionLabelForm: false, + editedVersion: null, } }, computed: { @@ -179,7 +180,7 @@ export default { /** * Handle restored event from Version.vue * - * @param {import('../utils/versions.ts').Version} version + * @param {import('../utils/versions.ts').Version} version The version to restore */ async handleRestore(version) { // Update local copy of fileInfo as rendering depends on it. @@ -202,7 +203,7 @@ export default { try { await restoreVersion(version) - if (version.label !== '') { + if (version.label) { showSuccess(t('files_versions', `${version.label} restored`)) } else if (version.mtime === this.initialVersionMtime) { showSuccess(t('files_versions', 'Initial version restored')) @@ -219,7 +220,7 @@ export default { /** * Handle label-updated event from Version.vue - * @param {import('../utils/versions.ts').Version} version + * @param {import('../utils/versions.ts').Version} version The version to update */ handleLabelUpdateRequest(version) { this.showVersionLabelForm = true @@ -228,7 +229,7 @@ export default { /** * Handle label-updated event from Version.vue - * @param {string} newLabel + * @param {string} newLabel The new label */ async handleLabelUpdate(newLabel) { const oldLabel = this.editedVersion.label @@ -248,8 +249,7 @@ export default { /** * Handle deleted event from Version.vue * - * @param {import('../utils/versions.ts').Version} version - * @param {string} newName + * @param {import('../utils/versions.ts').Version} version The version to delete */ async handleDelete(version) { const index = this.versions.indexOf(version) @@ -277,13 +277,12 @@ export default { return } - // Versions previews are too small for our use case, so we override hasPreview and previewUrl + // Versions previews are too small for our use case, so we override previewUrl // which makes the viewer render the original file. // We also point to the original filename if the version is the current one. const versions = this.versions.map(version => ({ ...version, filename: version.mtime === this.fileInfo.mtime ? path.join('files', getCurrentUser()?.uid ?? '', this.fileInfo.path, this.fileInfo.name) : version.filename, - hasPreview: false, previewUrl: undefined, })) @@ -294,7 +293,7 @@ export default { }, compareVersion({ version }) { - const versions = this.versions.map(version => ({ ...version, hasPreview: false, previewUrl: undefined })) + const versions = this.versions.map(version => ({ ...version, previewUrl: undefined })) OCA.Viewer.compare(this.viewerFileInfo, versions.find(v => v.source === version.source)) }, |