diff --git a/apps/files/src/components/AllFilesList.vue b/apps/files/src/components/AllFilesList.vue index 0874d58ee1b..232d7c3ad4f 100644 --- a/apps/files/src/components/AllFilesList.vue +++ b/apps/files/src/components/AllFilesList.vue @@ -29,14 +29,7 @@ class="uk-margin-small-left" /> </div> - <div> - <oc-button v-if="$_isUserShare(item)" class="file-row-share-indicator uk-text-middle" :aria-label="$_shareUserIconLabel(item)" @click="$_openSideBar(item, 'files-sharing')" variation="raw"> - <oc-icon name="group" class="uk-text-middle" size="small" :variation="$_shareUserIconVariation(item)"/> - </oc-button> - <oc-button v-if="$_isLinkShare(item)" class="file-row-share-indicator uk-text-middle" :aria-label="$_shareLinkIconLabel(item)" @click="$_openSideBar(item, 'file-link')" variation="raw"> - <oc-icon name="link" class="uk-text-middle" size="small" :variation="$_shareLinkIconVariation(item)"/> - </oc-button> - </div> + <StatusIndicators :item="item" :currentFolderPath="currentFolder.path" @click="$_openSideBar" /> <div class="uk-text-meta uk-text-nowrap uk-width-small" :class="{ 'uk-visible@s' : !_sidebarOpen, 'uk-hidden' : _sidebarOpen }"> {{ item.size | fileSize }} </div> @@ -75,18 +68,16 @@ <script> import FileList from './FileList.vue' import { mapGetters, mapActions, mapState } from 'vuex' -import { shareTypes } from '../helpers/shareTypes' -import { getParentPaths } from '../helpers/path' import Mixins from '../mixins' import FileActions from '../fileactions' -import intersection from 'lodash/intersection' -const userShareTypes = [shareTypes.user, shareTypes.group, shareTypes.guest, shareTypes.remote] +const StatusIndicators = () => import('./FilesLists/StatusIndicators/StatusIndicators.vue') export default { components: { - FileList + FileList, + StatusIndicators }, mixins: [ Mixins, @@ -110,46 +101,6 @@ export default { this.$emit('sideBarOpen', item, sideBarName) }, - $_isDirectUserShare (item) { - return (intersection(userShareTypes, item.shareTypes).length > 0) - }, - - $_isIndirectUserShare (item) { - return (item.isReceivedShare() || intersection(userShareTypes, this.$_shareTypesIndirect).length > 0) - }, - - $_isDirectLinkShare (item) { - return (item.shareTypes.indexOf(shareTypes.link) >= 0) - }, - - $_isIndirectLinkShare (item) { - return (this.$_shareTypesIndirect.indexOf(shareTypes.link) >= 0) - }, - - $_isUserShare (item) { - return this.$_isDirectUserShare(item) || this.$_isIndirectUserShare(item) - }, - - $_isLinkShare (item) { - return this.$_isDirectLinkShare(item) || this.$_isIndirectLinkShare(item) - }, - - $_shareUserIconVariation (item) { - return this.$_isDirectUserShare(item) ? 'active' : 'passive' - }, - - $_shareLinkIconVariation (item) { - return this.$_isDirectLinkShare(item) ? 'active' : 'passive' - }, - - $_shareUserIconLabel (item) { - return this.$_isDirectUserShare(item) ? this.$gettext('Directly shared with collaborators') : this.$gettext('Shared with collaborators through one of the parent folders') - }, - - $_shareLinkIconLabel (item) { - return this.$_isDirectLinkShare(item) ? this.$gettext('Directly shared with links') : this.$gettext('Shared with links through one of the parent folders') - }, - $_ocFilesFolder_getFolder () { this.setFilterTerm('') let absolutePath @@ -223,31 +174,6 @@ export default { return this.$route.params.item }, - $_shareTypesIndirect () { - const parentPaths = getParentPaths(this.currentFolder.path, true) - if (parentPaths.length === 0) { - return [] - } - - // remove root entry - parentPaths.pop() - - const shareTypes = {} - parentPaths.forEach((parentPath) => { - // TODO: optimize for performance by skipping once we got all known types - const shares = this.sharesTree[parentPath] - if (shares) { - shares.forEach((share) => { - // note: no distinction between incoming and outgoing shares as we display the same - // indirect indicator for them - shareTypes[share.info.share_type] = true - }) - } - }) - - return Object.keys(shareTypes).map(shareType => parseInt(shareType, 10)) - }, - quotaVisible () { return ( !this.publicPage() && diff --git a/apps/files/src/components/FilesLists/StatusIndicators/DefaultIndicators.vue b/apps/files/src/components/FilesLists/StatusIndicators/DefaultIndicators.vue new file mode 100644 index 00000000000..3e2970150dc --- /dev/null +++ b/apps/files/src/components/FilesLists/StatusIndicators/DefaultIndicators.vue @@ -0,0 +1,142 @@ +<template> + <div> + <oc-button + v-for="(indicator, index) in indicators" + :key="index" + class="file-row-share-indicator uk-text-middle" + :class="{ 'uk-margin-xsmall-left' : index > 0 }" + :aria-label="indicator.label" + @click="indicator.handler(item, indicator.id)" + variation="raw" + > + <oc-icon + :name="indicator.icon" + class="uk-text-middle" + size="small" + :variation="indicator.status" + /> + </oc-button> + </div> +</template> + +<script> +import intersection from 'lodash/intersection' +import { shareTypes } from '../../../helpers/shareTypes' +import { getParentPaths } from '../../../helpers/path' + +const userShareTypes = [shareTypes.user, shareTypes.group, shareTypes.guest, shareTypes.remote] + +export default { + name: 'StatusIndicators', + + props: { + item: { + type: Object, + required: true + }, + currentFolderPath: { + type: String, + required: true + } + }, + + computed: { + indicators () { + const indicators = [] + + if (this.isUserShare(this.item)) { + indicators.push({ + id: 'files-sharing', + label: this.shareUserIconLabel(this.item), + icon: 'group', + status: this.shareUserIconVariation(this.item), + handler: this.indicatorHandler + }) + } + + if (this.isLinkShare(this.item)) { + indicators.push({ + id: 'file-link', + label: this.shareLinkIconLabel(this.item), + icon: 'link', + status: this.shareLinkIconVariation(this.item), + handler: this.indicatorHandler + }) + } + + return indicators + }, + + shareTypesIndirect () { + const parentPaths = getParentPaths(this.currentFolderPath, true) + if (parentPaths.length === 0) { + return [] + } + + // remove root entry + parentPaths.pop() + + const shareTypes = {} + parentPaths.forEach((parentPath) => { + // TODO: optimize for performance by skipping once we got all known types + const shares = this.sharesTree[parentPath] + if (shares) { + shares.forEach((share) => { + // note: no distinction between incoming and outgoing shares as we display the same + // indirect indicator for them + shareTypes[share.info.share_type] = true + }) + } + }) + + return Object.keys(shareTypes).map(shareType => parseInt(shareType, 10)) + } + }, + + methods: { + isDirectUserShare (item) { + return (intersection(userShareTypes, item.shareTypes).length > 0) + }, + + isIndirectUserShare (item) { + return (item.isReceivedShare() || intersection(userShareTypes, this.shareTypesIndirect).length > 0) + }, + + isDirectLinkShare (item) { + return (item.shareTypes.indexOf(shareTypes.link) >= 0) + }, + + isIndirectLinkShare () { + return (this.shareTypesIndirect.indexOf(shareTypes.link) >= 0) + }, + + isUserShare (item) { + return this.isDirectUserShare(item) || this.isIndirectUserShare(item) + }, + + isLinkShare (item) { + return this.isDirectLinkShare(item) || this.isIndirectLinkShare(item) + }, + + shareUserIconVariation (item) { + return this.isDirectUserShare(item) ? 'active' : 'passive' + }, + + shareLinkIconVariation (item) { + return this.isDirectLinkShare(item) ? 'active' : 'passive' + }, + + shareUserIconLabel (item) { + return this.isDirectUserShare(item) ? this.$gettext('Directly shared with collaborators') : this.$gettext('Shared with collaborators through one of the parent folders') + }, + + shareLinkIconLabel (item) { + return this.isDirectLinkShare(item) ? this.$gettext('Directly shared with links') : this.$gettext('Shared with links through one of the parent folders') + }, + + indicatorHandler (item, sideBarName) { + this.$emit('click', item, sideBarName) + } + } +} +</script> diff --git a/apps/files/src/components/FilesLists/StatusIndicators/StatusIndicators.vue b/apps/files/src/components/FilesLists/StatusIndicators/StatusIndicators.vue new file mode 100644 index 00000000000..6571c5aacbc --- /dev/null +++ b/apps/files/src/components/FilesLists/StatusIndicators/StatusIndicators.vue @@ -0,0 +1,64 @@ +<template> + <div class="uk-flex uk-flex-middle"> + <DefaultIndicators + v-if="displayDefaultIndicators" + :item="item" + :currentFolderPath="currentFolderPath" + :class="{ 'uk-margin-xsmall-right' : customIndicators }" + @click="openSidebar" + /> + <template v-if="customIndicators"> + <component + v-for="(indicator, index) in customIndicators" + :is="indicator" + :key="index" + :item="item" + :currentFolderPath="currentFolderPath" + /> + </template> + </div> +</template> + +<script> +import { mapGetters } from 'vuex' + +const DefaultIndicators = () => import('./DefaultIndicators.vue') + +export default { + name: 'StatusIndicators', + + components: { + DefaultIndicators + }, + + props: { + item: { + type: Object, + required: true + }, + currentFolderPath: { + type: String, + required: true + } + }, + + computed: { + ...mapGetters(['configuration', 'customFilesListIndicators']), + + displayDefaultIndicators () { + return !this.configuration.theme.filesList.hideDefaultStatusIndicators + }, + + customIndicators () { + return this.customFilesListIndicators + } + }, + + methods: { + // TODO: Adjust to send the event via store + openSidebar (item, indicatorId) { + this.$emit('click', item, indicatorId) + } + } +} +</script> diff --git a/changelog/unreleased/2895 b/changelog/unreleased/2895 new file mode 100644 index 00000000000..9d88279153f --- /dev/null +++ b/changelog/unreleased/2895 @@ -0,0 +1,8 @@ +Enhancement: Add status indicator extension point + +We've added the ability for the extension to inject custom status indicator into files list. +New indicators will then appear next to the default one. +We've also added ability to disable default indicators. + +https://github.com/owncloud/phoenix/issues/2895 +https://github.com/owncloud/phoenix/pull/2928 diff --git a/src/store/apps.js b/src/store/apps.js index 311dc34e86f..ddba6dba3df 100644 --- a/src/store/apps.js +++ b/src/store/apps.js @@ -8,6 +8,7 @@ const state = { extensions: {}, newFileHandlers: [], fileSideBars: [], + customFilesListIndicators: [], meta: {} } @@ -118,6 +119,15 @@ const mutations = { }) state.fileSideBars = list } + + if (appInfo.filesListIndicators) { + const indicators = state.customFilesListIndicators + appInfo.filesListIndicators.forEach(indicator => { + indicators.push(indicator) + }) + state.customFilesListIndicators = indicators + } + if (!appInfo.id) return // name: use id as fallback display name // icon: use empty box as fallback icon @@ -164,7 +174,8 @@ const getters = { }, fileSideBars: state => { return state.fileSideBars - } + }, + customFilesListIndicators: state => state.customFilesListIndicators } export default { diff --git a/src/store/config.js b/src/store/config.js index 0ded00196cd..d9f6c8ccb50 100644 --- a/src/store/config.js +++ b/src/store/config.js @@ -23,6 +23,9 @@ const state = { }, logo: { favicon: '' + }, + filesList: { + hideDefaultStatusIndicators: false } } } diff --git a/themes/owncloud.json b/themes/owncloud.json index 37193e6592d..e097e5a49cf 100644 --- a/themes/owncloud.json +++ b/themes/owncloud.json @@ -5,5 +5,8 @@ }, "logo": { "favicon": "themes/owncloud/favicon.jpg" + }, + "filesList": { + "hideDefaultStatusIndicators": false } }