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

Share indicator for direct and indirect shares in file list #2877

Merged
merged 2 commits into from
Jan 23, 2020
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 86 additions & 2 deletions apps/files/src/components/AllFilesList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<span class="oc-visually-hidden" v-text="favoritesHeaderText" />
<oc-star id="files-table-header-star" aria-hidden="true" class="uk-display-block uk-disabled" />
</div>
<div></div>
<div class="uk-text-truncate uk-text-meta uk-width-expand" v-translate>Name</div>
<div :class="{ 'uk-visible@s' : !_sidebarOpen, 'uk-hidden' : _sidebarOpen }" class="uk-text-meta uk-width-small" v-translate>Size</div>
<div type="head" :class="{ 'uk-visible@s' : !_sidebarOpen, 'uk-hidden' : _sidebarOpen }" class="uk-text-nowrap uk-text-meta uk-width-small" v-translate>Updated</div>
Expand All @@ -28,6 +29,14 @@
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>
<div class="uk-text-meta uk-text-nowrap uk-width-small" :class="{ 'uk-visible@s' : !_sidebarOpen, 'uk-hidden' : _sidebarOpen }">
{{ item.size | fileSize }}
</div>
Expand Down Expand Up @@ -66,9 +75,14 @@
<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]

export default {
components: {
Expand All @@ -92,6 +106,50 @@ export default {
...mapActions('Files', ['loadFolder', 'setFilterTerm', 'markFavorite',
'setHighlightedFile', 'setPublicLinkPassword']),

$_openSideBar (item, sideBarName) {
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
Expand All @@ -106,7 +164,8 @@ export default {
client: this.$client,
absolutePath: absolutePath,
$gettext: this.$gettext,
routeName: this.$route.name
routeName: this.$route.name,
loadSharesTree: !this.publicPage()
}).then(() => {
const scrollTo = this.$route.query.scrollTo
if (scrollTo && this.activeFiles.length > 0) {
Expand Down Expand Up @@ -157,13 +216,38 @@ export default {
},
computed: {
...mapState(['route']),
...mapGetters('Files', ['loadingFolder', 'activeFiles', 'quota', 'filesTotalSize', 'activeFilesCount', 'currentFolder']),
...mapGetters('Files', ['loadingFolder', 'activeFiles', 'quota', 'filesTotalSize', 'activeFilesCount', 'currentFolder', 'sharesTree']),
...mapGetters(['configuration']),

item () {
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() &&
Expand Down
7 changes: 5 additions & 2 deletions apps/files/src/components/Collaborators/NewCollaborator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
:items="autocompleteResults"
:itemsLoading="autocompleteInProgress"
:placeholder="$_ocCollaborationStatus_autocompletePlacholder"
@update:input="onAutocompleteInput"
@update:input="$_onAutocompleteInput"
:filter="filterRecipients"
:fillOnSelection="false"
id="oc-sharing-autocomplete"
Expand Down Expand Up @@ -76,6 +76,7 @@
</template>

<script>
import _ from 'lodash'
import { mapActions, mapGetters } from 'vuex'
import Mixins from '../../mixins/collaborators'
import { roleToBitmask } from '../../helpers/collaborators'
Expand Down Expand Up @@ -119,6 +120,8 @@ export default {
this.$nextTick(() => {
this.$refs.ocSharingAutocomplete.focus()
})

this.$_onAutocompleteInput = _.debounce(this.$_onAutocompleteInput, 1000)
},

methods: {
Expand All @@ -136,7 +139,7 @@ export default {
this.$emit('close')
},

onAutocompleteInput (value) {
$_onAutocompleteInput (value) {
if (
value.length <
parseInt(this.user.capabilities.files_sharing.search_min_length, 10)
Expand Down
36 changes: 36 additions & 0 deletions apps/files/src/helpers/path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Return all absolute parent paths.
*
* For example if passing in "a/b/c" it will return
* ["a/b", "a", ""]

* If an empty string or "/" is passed in, an empty array is returned.
*
* @param {String} path path to process
* @param {Boolean} includeCurrent whether to include the current path (with leading slash)
* @return {Array.<String>} parent paths
*/
export function getParentPaths (path, includeCurrent = false) {
if (path === '' || path === '/') {
return []
}

if (path.charAt(0) !== '/') {
path = '/' + path
}

const paths = []
const sections = path.split('/')

if (includeCurrent) {
paths.push(path)
}

sections.pop()
while (sections.length > 0) {
paths.push(sections.join('/'))
sections.pop()
}

return paths
}
11 changes: 11 additions & 0 deletions apps/files/src/helpers/shareTypes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Share types
*/
export const shareTypes = {
user: 0,
group: 1,
userGroup: 2,
link: 3,
guest: 4,
remote: 6
}
98 changes: 97 additions & 1 deletion apps/files/src/store/actions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import moment from 'moment'
import _ from 'lodash'
import { getParentPaths } from '../helpers/path'
import { bitmaskToRole, permissionsBitmask } from '../helpers/collaborators'
const { default: PQueue } = require('p-queue')

function _buildFile (file) {
let ext = ''
Expand Down Expand Up @@ -40,6 +43,17 @@ function _buildFile (file) {
permissions: file.fileInfo['{http://owncloud.org/ns}permissions'] || '',
etag: file.fileInfo['{DAV:}getetag'],
sharePermissions: file.fileInfo['{http://open-collaboration-services.org/ns}share-permissions'],
shareTypes: (function () {
let shareTypes = file.fileInfo['{http://owncloud.org/ns}share-types']
if (shareTypes) {
shareTypes = _.chain(shareTypes).filter((xmlvalue) =>
(xmlvalue.namespaceURI === 'http://owncloud.org/ns' && xmlvalue.nodeName.split(':')[1] === 'share-type')
).map((xmlvalue) =>
parseInt(xmlvalue.textContent || xmlvalue.text, 10)
).value()
}
return shareTypes || []
}()),
privateLink: file.fileInfo['{http://owncloud.org/ns}privatelink'],
owner: {
username: file.fileInfo['{http://owncloud.org/ns}owner-id'],
Expand All @@ -62,6 +76,9 @@ function _buildFile (file) {
},
isMounted: function () {
return this.permissions.indexOf('M') >= 0
},
isReceivedShare: function () {
return this.permissions.indexOf('S') >= 0
}
})
}
Expand Down Expand Up @@ -241,7 +258,7 @@ function _buildShare (s, file) {
}

export default {
loadFolder (context, { client, absolutePath, $gettext, routeName }) {
loadFolder (context, { client, absolutePath, $gettext, routeName, loadSharesTree = false }) {
context.commit('UPDATE_FOLDER_LOADING', true)
context.commit('CLEAR_CURRENT_FILES_LIST')

Expand Down Expand Up @@ -278,6 +295,12 @@ export default {
currentFolder: res[0],
files: res.splice(1)
})
if (loadSharesTree) {
context.dispatch('loadSharesTree', {
client: client,
path: absolutePath
})
}
}
}
context.dispatch('resetFileSelection')
Expand Down Expand Up @@ -631,6 +654,79 @@ export default {
resetSearch (context) {
context.commit('SET_SEARCH_TERM', '')
},
/**
* Prune all branches of the shares tree that are
* unrelated to the given path
*/
pruneSharesTreeOutsidePath (context, path) {
context.commit('SHARESTREE_PRUNE_OUTSIDE_PATH', path)
},
/**
* Load shares for each parent of the given path.
* This will add new entries into the shares tree and will
* not remove unrelated existing ones.
*/
loadSharesTree (context, { client, path }) {
context.commit('SHARESTREE_ERROR', null)
// prune shares tree cache for all unrelated paths, keeping only
// existing relevant parent entries
context.dispatch('pruneSharesTreeOutsidePath', path)

const parentPaths = getParentPaths(path, true)
const sharesTree = {}

if (!parentPaths.length) {
return Promise.resolve()
}

// remove last entry which is the root folder
parentPaths.pop()

context.commit('SHARESTREE_LOADING', true)

const shareQueriesQueue = new PQueue({ concurrency: 2 })
const shareQueriesPromises = []
parentPaths.forEach((queryPath) => {
// skip already cached paths
if (context.getters.sharesTree[queryPath]) {
return
}
sharesTree[queryPath] = []
// query the outgoing share information for each of the parent paths
shareQueriesPromises.push(shareQueriesQueue.add(() =>
client.shares.getShares(queryPath, { reshares: true })
.then(data => {
data.forEach(element => {
sharesTree[queryPath].push({ ..._buildShare(element.shareInfo, { type: 'folder' }), outgoing: true })
})
})
.catch(error => {
console.error('SHARESTREE_ERROR', error)
context.commit('SHARESTREE_ERROR', error.message)
context.commit('SHARESTREE_LOADING', false)
})
))
// query the incoming share information for each of the parent paths
shareQueriesPromises.push(shareQueriesQueue.add(() =>
client.shares.getShares(queryPath, { reshares: true, shared_with_me: true })
.then(data => {
data.forEach(element => {
sharesTree[queryPath].push({ ..._buildShare(element.shareInfo, { type: 'folder' }), incoming: true })
})
})
.catch(error => {
console.error('SHARESTREE_ERROR', error)
context.commit('SHARESTREE_ERROR', error.message)
context.commit('SHARESTREE_LOADING', false)
})
))
})

return Promise.all(shareQueriesPromises).then(() => {
context.commit('SHARESTREE_ADD', sharesTree)
context.commit('SHARESTREE_LOADING', false)
})
},
dragOver (context, value) {
context.commit('DRAG_OVER', value)
},
Expand Down
4 changes: 3 additions & 1 deletion apps/files/src/store/getters.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,10 @@ export default {
sharesLoading: state => {
return state.sharesLoading
},
sharesTree: state => state.sharesTree,
sharesTreeLoading: state => state.sharesTreeLoading,
loadingFolder: state => {
return state.loadingFolder
return state.loadingFolder || state.sharesTreeLoading
},
quota: state => {
return state.quota
Expand Down
Loading