Merge pull request #17263 from nextcloud/enhancement/eslint
Add eslint global nextcloud config
This commit is contained in:
commit
7dc5bbae39
242 changed files with 13405 additions and 10393 deletions
12
.eslintrc.js
Normal file
12
.eslintrc.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
module.exports = {
|
||||
globals: {
|
||||
__webpack_nonce__: true,
|
||||
__webpack_public_path__: true,
|
||||
_: true,
|
||||
$: true,
|
||||
moment: true,
|
||||
escapeHTML: true,
|
||||
oc_userconfig: true
|
||||
},
|
||||
extends: ['nextcloud']
|
||||
}
|
20
Makefile
20
Makefile
|
@ -1,6 +1,7 @@
|
|||
all: clean dev-setup build-js-production
|
||||
|
||||
dev-setup: clean-dev npm-init
|
||||
# Dev env management
|
||||
dev-setup: clean clean-dev npm-init
|
||||
|
||||
npm-init:
|
||||
npm install
|
||||
|
@ -8,6 +9,7 @@ npm-init:
|
|||
npm-update:
|
||||
npm update
|
||||
|
||||
# Building
|
||||
build-js:
|
||||
npm run dev
|
||||
|
||||
|
@ -17,9 +19,14 @@ build-js-production:
|
|||
watch-js:
|
||||
npm run watch
|
||||
|
||||
clean-dev:
|
||||
rm -rf node_modules
|
||||
# Linting
|
||||
lint-fix:
|
||||
npm run lint:fix
|
||||
|
||||
lint-fix-watch:
|
||||
npm run lint:fix-watch
|
||||
|
||||
# Cleaning
|
||||
clean:
|
||||
rm -rf apps/accessibility/js/
|
||||
rm -rf apps/comments/js/
|
||||
|
@ -27,12 +34,15 @@ clean:
|
|||
rm -rf apps/files_trashbin/js/
|
||||
rm -rf apps/files_versions/js/
|
||||
rm -rf apps/oauth2/js/
|
||||
rm -rf apps/settings/js/vue-*
|
||||
rm -rf apps/systemtags/js/systemtags.*
|
||||
rm -rf apps/twofactor_backupcodes/js
|
||||
rm -rf apps/updatenotification/js/updatenotification.*
|
||||
rm -rf apps/workflowengine/js/
|
||||
rm -rf core/js/dist
|
||||
rm -rf settings/js/vue-*
|
||||
|
||||
clean-dev:
|
||||
rm -rf node_modules
|
||||
|
||||
clean-git: clean
|
||||
git checkout -- apps/accessibility/js/
|
||||
|
@ -41,9 +51,9 @@ clean-git: clean
|
|||
git checkout -- apps/files_trashbin/js/
|
||||
git checkout -- apps/files_versions/js/
|
||||
git checkout -- apps/oauth2/js/
|
||||
git checkout -- apps/settings/js/vue-*
|
||||
git checkout -- apps/systemtags/js/systemtags.*
|
||||
git checkout -- apps/twofactor_backupcodes/js
|
||||
git checkout -- apps/updatenotification/js/updatenotification.*
|
||||
git checkout -- apps/workflowengine/js/
|
||||
git checkout -- core/js/dist
|
||||
git checkout -- settings/js/vue-*
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 'tab'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always']
|
||||
}
|
||||
};
|
Binary file not shown.
Binary file not shown.
|
@ -1,60 +1,56 @@
|
|||
<template>
|
||||
<div id="accessibility" class="section">
|
||||
<h2>{{t('accessibility', 'Accessibility')}}</h2>
|
||||
<h2>{{ t('accessibility', 'Accessibility') }}</h2>
|
||||
<p v-html="description" />
|
||||
<p v-html="descriptionDetail" />
|
||||
|
||||
<div class="preview-list">
|
||||
<preview :preview="highcontrast"
|
||||
:key="highcontrast.id" :selected="selected.highcontrast"
|
||||
v-on:select="selectHighContrast"></preview>
|
||||
<preview v-for="preview in themes" :preview="preview"
|
||||
:key="preview.id" :selected="selected.theme"
|
||||
v-on:select="selectTheme"></preview>
|
||||
<preview v-for="preview in fonts" :preview="preview"
|
||||
:key="preview.id" :selected="selected.font"
|
||||
v-on:select="selectFont"></preview>
|
||||
<ItemPreview :key="highcontrast.id"
|
||||
:preview="highcontrast"
|
||||
:selected="selected.highcontrast"
|
||||
@select="selectHighContrast" />
|
||||
<ItemPreview v-for="preview in themes"
|
||||
:key="preview.id"
|
||||
:preview="preview"
|
||||
:selected="selected.theme"
|
||||
@select="selectTheme" />
|
||||
<ItemPreview v-for="preview in fonts"
|
||||
:key="preview.id"
|
||||
:preview="preview"
|
||||
:selected="selected.font"
|
||||
@select="selectFont" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import preview from './components/itemPreview';
|
||||
import axios from 'nextcloud-axios';
|
||||
import ItemPreview from './components/ItemPreview'
|
||||
import axios from 'nextcloud-axios'
|
||||
|
||||
export default {
|
||||
name: 'Accessibility',
|
||||
components: { preview },
|
||||
beforeMount() {
|
||||
// importing server data into the app
|
||||
const serverDataElmt = document.getElementById('serverData');
|
||||
if (serverDataElmt !== null) {
|
||||
this.serverData = JSON.parse(
|
||||
document.getElementById('serverData').dataset.server
|
||||
);
|
||||
}
|
||||
},
|
||||
components: { ItemPreview },
|
||||
data() {
|
||||
return {
|
||||
serverData: []
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
themes() {
|
||||
return this.serverData.themes;
|
||||
return this.serverData.themes
|
||||
},
|
||||
highcontrast() {
|
||||
return this.serverData.highcontrast;
|
||||
return this.serverData.highcontrast
|
||||
},
|
||||
fonts() {
|
||||
return this.serverData.fonts;
|
||||
return this.serverData.fonts
|
||||
},
|
||||
selected() {
|
||||
return {
|
||||
theme: this.serverData.selected.theme,
|
||||
highcontrast: this.serverData.selected.highcontrast,
|
||||
font: this.serverData.selected.font
|
||||
};
|
||||
}
|
||||
},
|
||||
description() {
|
||||
// using the `t` replace method escape html, we have to do it manually :/
|
||||
|
@ -66,7 +62,7 @@ export default {
|
|||
We aim to be compliant with the {guidelines} 2.1 on AA level,
|
||||
with the high contrast theme even on AAA level.`
|
||||
)
|
||||
.replace('{guidelines}', this.guidelinesLink)
|
||||
.replace('{guidelines}', this.guidelinesLink)
|
||||
},
|
||||
guidelinesLink() {
|
||||
return `<a target="_blank" href="https://www.w3.org/WAI/standards-guidelines/wcag/" rel="noreferrer nofollow">${t('accessibility', 'Web Content Accessibility Guidelines')}</a>`
|
||||
|
@ -77,8 +73,8 @@ export default {
|
|||
`If you find any issues, don’t hesitate to report them on {issuetracker}.
|
||||
And if you want to get involved, come join {designteam}!`
|
||||
)
|
||||
.replace('{issuetracker}', this.issuetrackerLink)
|
||||
.replace('{designteam}', this.designteamLink)
|
||||
.replace('{issuetracker}', this.issuetrackerLink)
|
||||
.replace('{designteam}', this.designteamLink)
|
||||
},
|
||||
issuetrackerLink() {
|
||||
return `<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">${t('accessibility', 'our issue tracker')}</a>`
|
||||
|
@ -87,19 +83,28 @@ export default {
|
|||
return `<a target="_blank" href="https://nextcloud.com/design" rel="noreferrer nofollow">${t('accessibility', 'our design team')}</a>`
|
||||
}
|
||||
},
|
||||
beforeMount() {
|
||||
// importing server data into the app
|
||||
const serverDataElmt = document.getElementById('serverData')
|
||||
if (serverDataElmt !== null) {
|
||||
this.serverData = JSON.parse(
|
||||
document.getElementById('serverData').dataset.server
|
||||
)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectHighContrast(id) {
|
||||
this.selectItem('highcontrast', id);
|
||||
this.selectItem('highcontrast', id)
|
||||
},
|
||||
selectTheme(id, idSelectedBefore) {
|
||||
this.selectItem('theme', id);
|
||||
document.body.classList.remove(idSelectedBefore);
|
||||
this.selectItem('theme', id)
|
||||
document.body.classList.remove(idSelectedBefore)
|
||||
if (id) {
|
||||
document.body.classList.add(id);
|
||||
document.body.classList.add(id)
|
||||
}
|
||||
},
|
||||
selectFont(id) {
|
||||
this.selectItem('font', id);
|
||||
this.selectItem('font', id)
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -110,43 +115,40 @@ export default {
|
|||
* @param {string} id the data of the change
|
||||
*/
|
||||
selectItem(type, id) {
|
||||
axios.post(
|
||||
OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type,
|
||||
{ value: id }
|
||||
)
|
||||
axios.post(OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type, { value: id })
|
||||
.then(response => {
|
||||
this.serverData.selected[type] = id;
|
||||
this.serverData.selected[type] = id
|
||||
|
||||
// Remove old link
|
||||
let link = document.querySelector('link[rel=stylesheet][href*=accessibility][href*=user-]');
|
||||
let link = document.querySelector('link[rel=stylesheet][href*=accessibility][href*=user-]')
|
||||
if (!link) {
|
||||
// insert new css
|
||||
let link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime();
|
||||
document.head.appendChild(link);
|
||||
let link = document.createElement('link')
|
||||
link.rel = 'stylesheet'
|
||||
link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime()
|
||||
document.head.appendChild(link)
|
||||
} else {
|
||||
// compare arrays
|
||||
if (
|
||||
JSON.stringify(Object.values(this.selected)) ===
|
||||
JSON.stringify([false, false])
|
||||
JSON.stringify(Object.values(this.selected))
|
||||
=== JSON.stringify([false, false])
|
||||
) {
|
||||
// if nothing is selected, blindly remove the css
|
||||
link.remove();
|
||||
link.remove()
|
||||
} else {
|
||||
// force update
|
||||
link.href =
|
||||
link.href.split('?')[0] +
|
||||
'?v=' +
|
||||
new Date().getTime();
|
||||
link.href
|
||||
= link.href.split('?')[0]
|
||||
+ '?v='
|
||||
+ new Date().getTime()
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err, err.response);
|
||||
OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.'));
|
||||
});
|
||||
console.error(err, err.response)
|
||||
OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.'))
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
</script>
|
40
apps/accessibility/src/components/ItemPreview.vue
Normal file
40
apps/accessibility/src/components/ItemPreview.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<template>
|
||||
<div :class="{preview: true}">
|
||||
<div class="preview-image" :style="{backgroundImage: 'url(' + preview.img + ')'}" />
|
||||
<div class="preview-description">
|
||||
<h3>{{ preview.title }}</h3>
|
||||
<p>{{ preview.text }}</p>
|
||||
<input :id="'accessibility-' + preview.id"
|
||||
v-model="checked"
|
||||
type="checkbox"
|
||||
class="checkbox">
|
||||
<label :for="'accessibility-' + preview.id">{{ t('accessibility', 'Enable') }} {{ preview.title.toLowerCase() }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ItemPreview',
|
||||
props: {
|
||||
preview: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
selected: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
checked: {
|
||||
get() {
|
||||
return this.selected === this.preview.id
|
||||
},
|
||||
set(checked) {
|
||||
this.$emit('select', checked ? this.preview.id : false, this.selected)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,28 +0,0 @@
|
|||
<template>
|
||||
<div :class="{preview: true}">
|
||||
<div class="preview-image" :style="{backgroundImage: 'url(' + preview.img + ')'}"></div>
|
||||
<div class="preview-description">
|
||||
<h3>{{preview.title}}</h3>
|
||||
<p>{{preview.text}}</p>
|
||||
<input type="checkbox" class="checkbox" :id="'accessibility-' + preview.id" v-model="checked" />
|
||||
<label :for="'accessibility-' + preview.id">{{t('accessibility', 'Enable')}} {{preview.title.toLowerCase()}}</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'itemPreview',
|
||||
props: ['preview', 'selected'],
|
||||
computed: {
|
||||
checked: {
|
||||
get() {
|
||||
return this.selected === this.preview.id;
|
||||
},
|
||||
set(checked) {
|
||||
this.$emit('select', checked ? this.preview.id : false, this.selected);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,12 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import Vue from 'vue'
|
||||
import App from './Accessibility.vue'
|
||||
|
||||
/* global t */
|
||||
// bind to window
|
||||
Vue.prototype.OC = OC;
|
||||
Vue.prototype.t = t;
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.t = t
|
||||
|
||||
new Vue({
|
||||
export default new Vue({
|
||||
el: '#accessibility',
|
||||
render: h => h(App)
|
||||
});
|
||||
})
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* Copyright (c) 2016
|
||||
*
|
||||
|
@ -18,18 +18,18 @@
|
|||
* @param {jQuery} $el jQuery handle for this activity
|
||||
* @param {string} view The view that displayes this activity
|
||||
*/
|
||||
prepareModelForDisplay: function (model, $el, view) {
|
||||
prepareModelForDisplay: function(model, $el, view) {
|
||||
if (model.get('app') !== 'comments' || model.get('type') !== 'comments') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (view === 'ActivityTabView') {
|
||||
$el.addClass('comment');
|
||||
$el.addClass('comment')
|
||||
if (model.get('message') && this._isLong(model.get('message'))) {
|
||||
$el.addClass('collapsed');
|
||||
var $overlay = $('<div>').addClass('message-overlay');
|
||||
$el.find('.activitymessage').after($overlay);
|
||||
$el.on('click', this._onClickCollapsedComment);
|
||||
$el.addClass('collapsed')
|
||||
var $overlay = $('<div>').addClass('message-overlay')
|
||||
$el.find('.activitymessage').after($overlay)
|
||||
$el.on('click', this._onClickCollapsedComment)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -38,22 +38,21 @@
|
|||
* Copy of CommentsTabView._onClickComment()
|
||||
*/
|
||||
_onClickCollapsedComment: function(ev) {
|
||||
var $row = $(ev.target);
|
||||
var $row = $(ev.target)
|
||||
if (!$row.is('.comment')) {
|
||||
$row = $row.closest('.comment');
|
||||
$row = $row.closest('.comment')
|
||||
}
|
||||
$row.removeClass('collapsed');
|
||||
$row.removeClass('collapsed')
|
||||
},
|
||||
|
||||
/*
|
||||
* Copy of CommentsTabView._isLong()
|
||||
*/
|
||||
_isLong: function(message) {
|
||||
return message.length > 250 || (message.match(/\n/g) || []).length > 1;
|
||||
return message.length > 250 || (message.match(/\n/g) || []).length > 1
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
})()
|
||||
|
||||
})();
|
||||
|
||||
OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin);
|
||||
OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin)
|
||||
|
|
|
@ -13,8 +13,7 @@
|
|||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.Comments = {};
|
||||
OCA.Comments = {}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
})()
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Copyright (c) 2016
|
||||
*
|
||||
|
@ -20,148 +21,147 @@
|
|||
var CommentCollection = OC.Backbone.Collection.extend(
|
||||
/** @lends OCA.Comments.CommentCollection.prototype */ {
|
||||
|
||||
sync: OC.Backbone.davSync,
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
model: OCA.Comments.CommentModel,
|
||||
model: OCA.Comments.CommentModel,
|
||||
|
||||
/**
|
||||
/**
|
||||
* Object type
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
_objectType: 'files',
|
||||
_objectType: 'files',
|
||||
|
||||
/**
|
||||
/**
|
||||
* Object id
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
_objectId: null,
|
||||
_objectId: null,
|
||||
|
||||
/**
|
||||
/**
|
||||
* True if there are no more page results left to fetch
|
||||
*
|
||||
* @type bool
|
||||
*/
|
||||
_endReached: false,
|
||||
_endReached: false,
|
||||
|
||||
/**
|
||||
/**
|
||||
* Number of comments to fetch per page
|
||||
*
|
||||
* @type int
|
||||
*/
|
||||
_limit : 20,
|
||||
_limit: 20,
|
||||
|
||||
/**
|
||||
/**
|
||||
* Initializes the collection
|
||||
*
|
||||
* @param {string} [options.objectType] object type
|
||||
* @param {string} [options.objectId] object id
|
||||
*/
|
||||
initialize: function(models, options) {
|
||||
options = options || {};
|
||||
if (options.objectType) {
|
||||
this._objectType = options.objectType;
|
||||
}
|
||||
if (options.objectId) {
|
||||
this._objectId = options.objectId;
|
||||
}
|
||||
},
|
||||
initialize: function(models, options) {
|
||||
options = options || {}
|
||||
if (options.objectType) {
|
||||
this._objectType = options.objectType
|
||||
}
|
||||
if (options.objectId) {
|
||||
this._objectId = options.objectId
|
||||
}
|
||||
},
|
||||
|
||||
url: function() {
|
||||
return OC.linkToRemote('dav') + '/comments/' +
|
||||
encodeURIComponent(this._objectType) + '/' +
|
||||
encodeURIComponent(this._objectId) + '/';
|
||||
},
|
||||
url: function() {
|
||||
return OC.linkToRemote('dav') + '/comments/'
|
||||
+ encodeURIComponent(this._objectType) + '/'
|
||||
+ encodeURIComponent(this._objectId) + '/'
|
||||
},
|
||||
|
||||
setObjectId: function(objectId) {
|
||||
this._objectId = objectId;
|
||||
},
|
||||
setObjectId: function(objectId) {
|
||||
this._objectId = objectId
|
||||
},
|
||||
|
||||
hasMoreResults: function() {
|
||||
return !this._endReached;
|
||||
},
|
||||
hasMoreResults: function() {
|
||||
return !this._endReached
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this._endReached = false;
|
||||
this._summaryModel = null;
|
||||
return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
|
||||
},
|
||||
reset: function() {
|
||||
this._endReached = false
|
||||
this._summaryModel = null
|
||||
return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Fetch the next set of results
|
||||
*/
|
||||
fetchNext: function(options) {
|
||||
var self = this;
|
||||
if (!this.hasMoreResults()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var body = '<?xml version="1.0" encoding="utf-8" ?>\n' +
|
||||
'<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n' +
|
||||
// load one more so we know there is more
|
||||
' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n' +
|
||||
' <oc:offset>' + this.length + '</oc:offset>\n' +
|
||||
'</oc:filter-comments>\n';
|
||||
|
||||
options = options || {};
|
||||
var success = options.success;
|
||||
options = _.extend({
|
||||
remove: false,
|
||||
parse: true,
|
||||
data: body,
|
||||
davProperties: CommentCollection.prototype.model.prototype.davProperties,
|
||||
success: function(resp) {
|
||||
if (resp.length <= self._limit) {
|
||||
// no new entries, end reached
|
||||
self._endReached = true;
|
||||
} else {
|
||||
// remove last entry, for next page load
|
||||
resp = _.initial(resp);
|
||||
}
|
||||
if (!self.set(resp, options)) {
|
||||
return false;
|
||||
}
|
||||
if (success) {
|
||||
success.apply(null, arguments);
|
||||
}
|
||||
self.trigger('sync', 'REPORT', self, options);
|
||||
fetchNext: function(options) {
|
||||
var self = this
|
||||
if (!this.hasMoreResults()) {
|
||||
return null
|
||||
}
|
||||
}, options);
|
||||
|
||||
return this.sync('REPORT', this, options);
|
||||
},
|
||||
var body = '<?xml version="1.0" encoding="utf-8" ?>\n'
|
||||
+ '<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n'
|
||||
// load one more so we know there is more
|
||||
+ ' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n'
|
||||
+ ' <oc:offset>' + this.length + '</oc:offset>\n'
|
||||
+ '</oc:filter-comments>\n'
|
||||
|
||||
/**
|
||||
options = options || {}
|
||||
var success = options.success
|
||||
options = _.extend({
|
||||
remove: false,
|
||||
parse: true,
|
||||
data: body,
|
||||
davProperties: CommentCollection.prototype.model.prototype.davProperties,
|
||||
success: function(resp) {
|
||||
if (resp.length <= self._limit) {
|
||||
// no new entries, end reached
|
||||
self._endReached = true
|
||||
} else {
|
||||
// remove last entry, for next page load
|
||||
resp = _.initial(resp)
|
||||
}
|
||||
if (!self.set(resp, options)) {
|
||||
return false
|
||||
}
|
||||
if (success) {
|
||||
success.apply(null, arguments)
|
||||
}
|
||||
self.trigger('sync', 'REPORT', self, options)
|
||||
}
|
||||
}, options)
|
||||
|
||||
return this.sync('REPORT', this, options)
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns the matching summary model
|
||||
*
|
||||
* @return {OCA.Comments.CommentSummaryModel} summary model
|
||||
* @returns {OCA.Comments.CommentSummaryModel} summary model
|
||||
*/
|
||||
getSummaryModel: function() {
|
||||
if (!this._summaryModel) {
|
||||
this._summaryModel = new OCA.Comments.CommentSummaryModel({
|
||||
id: this._objectId,
|
||||
objectType: this._objectType
|
||||
});
|
||||
}
|
||||
return this._summaryModel;
|
||||
},
|
||||
getSummaryModel: function() {
|
||||
if (!this._summaryModel) {
|
||||
this._summaryModel = new OCA.Comments.CommentSummaryModel({
|
||||
id: this._objectId,
|
||||
objectType: this._objectType
|
||||
})
|
||||
}
|
||||
return this._summaryModel
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Updates the read marker for this comment thread
|
||||
*
|
||||
* @param {Date} [date] optional date, defaults to now
|
||||
* @param {Object} [options] backbone options
|
||||
*/
|
||||
updateReadMarker: function(date, options) {
|
||||
options = options || {};
|
||||
updateReadMarker: function(date, options) {
|
||||
options = options || {}
|
||||
|
||||
return this.getSummaryModel().save({
|
||||
readMarker: (date || new Date()).toUTCString()
|
||||
}, options);
|
||||
}
|
||||
});
|
||||
|
||||
OCA.Comments.CommentCollection = CommentCollection;
|
||||
})(OC, OCA);
|
||||
return this.getSummaryModel().save({
|
||||
readMarker: (date || new Date()).toUTCString()
|
||||
}, options)
|
||||
}
|
||||
})
|
||||
|
||||
OCA.Comments.CommentCollection = CommentCollection
|
||||
})(OC, OCA)
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
_.extend(OC.Files.Client, {
|
||||
PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
|
||||
PROPERTY_MESSAGE: '{' + OC.Files.Client.NS_OWNCLOUD + '}message',
|
||||
PROPERTY_MESSAGE: '{' + OC.Files.Client.NS_OWNCLOUD + '}message',
|
||||
PROPERTY_ACTORTYPE: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorType',
|
||||
PROPERTY_ACTORID: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorId',
|
||||
PROPERTY_ISUNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}isUnread',
|
||||
|
@ -21,7 +21,7 @@
|
|||
PROPERTY_ACTORDISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorDisplayName',
|
||||
PROPERTY_CREATIONDATETIME: '{' + OC.Files.Client.NS_OWNCLOUD + '}creationDateTime',
|
||||
PROPERTY_MENTIONS: '{' + OC.Files.Client.NS_OWNCLOUD + '}mentions'
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* @class OCA.Comments.CommentModel
|
||||
|
@ -32,62 +32,62 @@
|
|||
*/
|
||||
var CommentModel = OC.Backbone.Model.extend(
|
||||
/** @lends OCA.Comments.CommentModel.prototype */ {
|
||||
sync: OC.Backbone.davSync,
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
defaults: {
|
||||
actorType: 'users',
|
||||
objectType: 'files'
|
||||
},
|
||||
defaults: {
|
||||
actorType: 'users',
|
||||
objectType: 'files'
|
||||
},
|
||||
|
||||
davProperties: {
|
||||
'id': OC.Files.Client.PROPERTY_FILEID,
|
||||
'message': OC.Files.Client.PROPERTY_MESSAGE,
|
||||
'actorType': OC.Files.Client.PROPERTY_ACTORTYPE,
|
||||
'actorId': OC.Files.Client.PROPERTY_ACTORID,
|
||||
'actorDisplayName': OC.Files.Client.PROPERTY_ACTORDISPLAYNAME,
|
||||
'creationDateTime': OC.Files.Client.PROPERTY_CREATIONDATETIME,
|
||||
'objectType': OC.Files.Client.PROPERTY_OBJECTTYPE,
|
||||
'objectId': OC.Files.Client.PROPERTY_OBJECTID,
|
||||
'isUnread': OC.Files.Client.PROPERTY_ISUNREAD,
|
||||
'mentions': OC.Files.Client.PROPERTY_MENTIONS
|
||||
},
|
||||
davProperties: {
|
||||
'id': OC.Files.Client.PROPERTY_FILEID,
|
||||
'message': OC.Files.Client.PROPERTY_MESSAGE,
|
||||
'actorType': OC.Files.Client.PROPERTY_ACTORTYPE,
|
||||
'actorId': OC.Files.Client.PROPERTY_ACTORID,
|
||||
'actorDisplayName': OC.Files.Client.PROPERTY_ACTORDISPLAYNAME,
|
||||
'creationDateTime': OC.Files.Client.PROPERTY_CREATIONDATETIME,
|
||||
'objectType': OC.Files.Client.PROPERTY_OBJECTTYPE,
|
||||
'objectId': OC.Files.Client.PROPERTY_OBJECTID,
|
||||
'isUnread': OC.Files.Client.PROPERTY_ISUNREAD,
|
||||
'mentions': OC.Files.Client.PROPERTY_MENTIONS
|
||||
},
|
||||
|
||||
parse: function(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
message: data.message,
|
||||
actorType: data.actorType,
|
||||
actorId: data.actorId,
|
||||
actorDisplayName: data.actorDisplayName,
|
||||
creationDateTime: data.creationDateTime,
|
||||
objectType: data.objectType,
|
||||
objectId: data.objectId,
|
||||
isUnread: (data.isUnread === 'true'),
|
||||
mentions: this._parseMentions(data.mentions)
|
||||
};
|
||||
},
|
||||
|
||||
_parseMentions: function(mentions) {
|
||||
if(_.isUndefined(mentions)) {
|
||||
return {};
|
||||
}
|
||||
var result = {};
|
||||
for(var i in mentions) {
|
||||
var mention = mentions[i];
|
||||
if(_.isUndefined(mention.localName) || mention.localName !== 'mention') {
|
||||
continue;
|
||||
parse: function(data) {
|
||||
return {
|
||||
id: data.id,
|
||||
message: data.message,
|
||||
actorType: data.actorType,
|
||||
actorId: data.actorId,
|
||||
actorDisplayName: data.actorDisplayName,
|
||||
creationDateTime: data.creationDateTime,
|
||||
objectType: data.objectType,
|
||||
objectId: data.objectId,
|
||||
isUnread: (data.isUnread === 'true'),
|
||||
mentions: this._parseMentions(data.mentions)
|
||||
}
|
||||
result[i] = {};
|
||||
for (var child = mention.firstChild; child; child = child.nextSibling) {
|
||||
if(_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
|
||||
continue;
|
||||
},
|
||||
|
||||
_parseMentions: function(mentions) {
|
||||
if (_.isUndefined(mentions)) {
|
||||
return {}
|
||||
}
|
||||
var result = {}
|
||||
for (var i in mentions) {
|
||||
var mention = mentions[i]
|
||||
if (_.isUndefined(mention.localName) || mention.localName !== 'mention') {
|
||||
continue
|
||||
}
|
||||
result[i] = {}
|
||||
for (var child = mention.firstChild; child; child = child.nextSibling) {
|
||||
if (_.isUndefined(child.localName) || !child.localName.startsWith('mention')) {
|
||||
continue
|
||||
}
|
||||
result[i][child.localName] = child.textContent
|
||||
}
|
||||
result[i][child.localName] = child.textContent;
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result;
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Comments.CommentModel = CommentModel;
|
||||
})(OC, OCA);
|
||||
OCA.Comments.CommentModel = CommentModel
|
||||
})(OC, OCA)
|
||||
|
|
|
@ -15,4 +15,4 @@ import './vendor/At.js/dist/js/jquery.atwho.min'
|
|||
import './style/autocomplete.scss'
|
||||
import './style/comments.scss'
|
||||
|
||||
window.OCA.Comments = OCA.Comments;
|
||||
window.OCA.Comments = OCA.Comments
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/* global Handlebars */
|
||||
(function() {
|
||||
|
||||
/**
|
||||
|
@ -23,7 +22,7 @@
|
|||
_scopes: [
|
||||
{
|
||||
name: 'edit',
|
||||
displayName: t('comments', 'Edit comment'),
|
||||
displayName: t('comments', 'Edit comment'),
|
||||
iconClass: 'icon-rename'
|
||||
},
|
||||
{
|
||||
|
@ -45,14 +44,14 @@
|
|||
* @param {Object} event event object
|
||||
*/
|
||||
_onClickAction: function(event) {
|
||||
var $target = $(event.currentTarget);
|
||||
var $target = $(event.currentTarget)
|
||||
if (!$target.hasClass('menuitem')) {
|
||||
$target = $target.closest('.menuitem');
|
||||
$target = $target.closest('.menuitem')
|
||||
}
|
||||
|
||||
OC.hideMenus();
|
||||
OC.hideMenus()
|
||||
|
||||
this.trigger('select:menu-item-clicked', event, $target.data('action'));
|
||||
this.trigger('select:menu-item-clicked', event, $target.data('action'))
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -61,49 +60,49 @@
|
|||
render: function() {
|
||||
this.$el.html(OCA.Comments.Templates['commentsmodifymenu']({
|
||||
items: this._scopes
|
||||
}));
|
||||
}))
|
||||
},
|
||||
|
||||
/**
|
||||
* Displays the menu
|
||||
* @param {Event} context the click event
|
||||
*/
|
||||
show: function(context) {
|
||||
this._context = context;
|
||||
this._context = context
|
||||
|
||||
for(var i in this._scopes) {
|
||||
this._scopes[i].active = false;
|
||||
for (var i in this._scopes) {
|
||||
this._scopes[i].active = false
|
||||
}
|
||||
|
||||
|
||||
var $el = $(context.target);
|
||||
var offsetIcon = $el.offset();
|
||||
var offsetContainer = $el.closest('.authorRow').offset();
|
||||
var $el = $(context.target)
|
||||
var offsetIcon = $el.offset()
|
||||
var offsetContainer = $el.closest('.authorRow').offset()
|
||||
|
||||
// adding some extra top offset to push the menu below the button.
|
||||
var position = {
|
||||
top: offsetIcon.top - offsetContainer.top + 48,
|
||||
left: '',
|
||||
right: ''
|
||||
};
|
||||
}
|
||||
|
||||
position.left = offsetIcon.left - offsetContainer.left;
|
||||
position.left = offsetIcon.left - offsetContainer.left
|
||||
|
||||
if (position.left > 200) {
|
||||
// we need to position the menu to the right.
|
||||
position.left = '';
|
||||
position.right = this.$el.closest('.comment').find('.date').width();
|
||||
this.$el.removeClass('menu-left').addClass('menu-right');
|
||||
position.left = ''
|
||||
position.right = this.$el.closest('.comment').find('.date').width()
|
||||
this.$el.removeClass('menu-left').addClass('menu-right')
|
||||
} else {
|
||||
this.$el.removeClass('menu-right').addClass('menu-left');
|
||||
this.$el.removeClass('menu-right').addClass('menu-left')
|
||||
}
|
||||
this.$el.css(position);
|
||||
this.render();
|
||||
this.$el.removeClass('hidden');
|
||||
this.$el.css(position)
|
||||
this.render()
|
||||
this.$el.removeClass('hidden')
|
||||
|
||||
OC.showMenu(null, this.$el);
|
||||
OC.showMenu(null, this.$el)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Comments = OCA.Comments || {};
|
||||
OCA.Comments.CommentsModifyMenu = CommentsModifyMenu;
|
||||
})(OC, OCA);
|
||||
OCA.Comments = OCA.Comments || {}
|
||||
OCA.Comments.CommentsModifyMenu = CommentsModifyMenu
|
||||
})(OC, OCA)
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@
|
|||
|
||||
_.extend(OC.Files.Client, {
|
||||
PROPERTY_READMARKER: '{' + OC.Files.Client.NS_OWNCLOUD + '}readMarker'
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* @class OCA.Comments.CommentSummaryModel
|
||||
|
@ -24,45 +24,47 @@
|
|||
*/
|
||||
var CommentSummaryModel = OC.Backbone.Model.extend(
|
||||
/** @lends OCA.Comments.CommentSummaryModel.prototype */ {
|
||||
sync: OC.Backbone.davSync,
|
||||
sync: OC.Backbone.davSync,
|
||||
|
||||
/**
|
||||
/**
|
||||
* Object type
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
_objectType: 'files',
|
||||
_objectType: 'files',
|
||||
|
||||
/**
|
||||
/**
|
||||
* Object id
|
||||
*
|
||||
* @type string
|
||||
*/
|
||||
_objectId: null,
|
||||
_objectId: null,
|
||||
|
||||
davProperties: {
|
||||
'readMarker': OC.Files.Client.PROPERTY_READMARKER
|
||||
},
|
||||
davProperties: {
|
||||
'readMarker': OC.Files.Client.PROPERTY_READMARKER
|
||||
},
|
||||
|
||||
/**
|
||||
* Initializes the summary model
|
||||
*
|
||||
* @param {string} [options.objectType] object type
|
||||
* @param {string} [options.objectId] object id
|
||||
*/
|
||||
initialize: function(attrs, options) {
|
||||
options = options || {};
|
||||
if (options.objectType) {
|
||||
this._objectType = options.objectType;
|
||||
/**
|
||||
* Initializes the summary model
|
||||
*
|
||||
* @param {any} [attrs] ignored
|
||||
* @param {Object} [options] destructuring object
|
||||
* @param {string} [options.objectType] object type
|
||||
* @param {string} [options.objectId] object id
|
||||
*/
|
||||
initialize: function(attrs, options) {
|
||||
options = options || {}
|
||||
if (options.objectType) {
|
||||
this._objectType = options.objectType
|
||||
}
|
||||
},
|
||||
|
||||
url: function() {
|
||||
return OC.linkToRemote('dav') + '/comments/'
|
||||
+ encodeURIComponent(this._objectType) + '/'
|
||||
+ encodeURIComponent(this.id) + '/'
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
url: function() {
|
||||
return OC.linkToRemote('dav') + '/comments/' +
|
||||
encodeURIComponent(this._objectType) + '/' +
|
||||
encodeURIComponent(this.id) + '/';
|
||||
}
|
||||
});
|
||||
|
||||
OCA.Comments.CommentSummaryModel = CommentSummaryModel;
|
||||
})(OC, OCA);
|
||||
OCA.Comments.CommentSummaryModel = CommentSummaryModel
|
||||
})(OC, OCA)
|
||||
|
|
|
@ -8,20 +8,18 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/* global Handlebars */
|
||||
|
||||
(function() {
|
||||
|
||||
_.extend(OC.Files.Client, {
|
||||
PROPERTY_COMMENTS_UNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}comments-unread'
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Comments = _.extend({}, OCA.Comments);
|
||||
OCA.Comments = _.extend({}, OCA.Comments)
|
||||
if (!OCA.Comments) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.Comments = {};
|
||||
OCA.Comments = {}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,43 +36,43 @@
|
|||
count: count,
|
||||
countMessage: n('comments', '%n unread comment', '%n unread comments', count),
|
||||
iconUrl: OC.imagePath('core', 'actions/comment')
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
attach: function(fileList) {
|
||||
var self = this;
|
||||
var self = this
|
||||
if (this.ignoreLists.indexOf(fileList.id) >= 0) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView'));
|
||||
fileList.registerTabView(new OCA.Comments.CommentsTabView('commentsTabView'))
|
||||
|
||||
var oldGetWebdavProperties = fileList._getWebdavProperties;
|
||||
var oldGetWebdavProperties = fileList._getWebdavProperties
|
||||
fileList._getWebdavProperties = function() {
|
||||
var props = oldGetWebdavProperties.apply(this, arguments);
|
||||
props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD);
|
||||
return props;
|
||||
};
|
||||
var props = oldGetWebdavProperties.apply(this, arguments)
|
||||
props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD)
|
||||
return props
|
||||
}
|
||||
|
||||
fileList.filesClient.addFileInfoParser(function(response) {
|
||||
var data = {};
|
||||
var props = response.propStat[0].properties;
|
||||
var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD];
|
||||
var data = {}
|
||||
var props = response.propStat[0].properties
|
||||
var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD]
|
||||
if (!_.isUndefined(commentsUnread) && commentsUnread !== '') {
|
||||
data.commentsUnread = parseInt(commentsUnread, 10);
|
||||
data.commentsUnread = parseInt(commentsUnread, 10)
|
||||
}
|
||||
return data;
|
||||
});
|
||||
return data
|
||||
})
|
||||
|
||||
fileList.$el.addClass('has-comments');
|
||||
var oldCreateRow = fileList._createRow;
|
||||
fileList.$el.addClass('has-comments')
|
||||
var oldCreateRow = fileList._createRow
|
||||
fileList._createRow = function(fileData) {
|
||||
var $tr = oldCreateRow.apply(this, arguments);
|
||||
var $tr = oldCreateRow.apply(this, arguments)
|
||||
if (fileData.commentsUnread) {
|
||||
$tr.attr('data-comments-unread', fileData.commentsUnread);
|
||||
$tr.attr('data-comments-unread', fileData.commentsUnread)
|
||||
}
|
||||
return $tr;
|
||||
};
|
||||
return $tr
|
||||
}
|
||||
|
||||
// register "comment" action for reading comments
|
||||
fileList.fileActions.registerAction({
|
||||
|
@ -94,35 +92,35 @@
|
|||
permissions: OC.PERMISSION_READ,
|
||||
type: OCA.Files.FileActions.TYPE_INLINE,
|
||||
render: function(actionSpec, isDefault, context) {
|
||||
var $file = context.$file;
|
||||
var unreadComments = $file.data('comments-unread');
|
||||
var $file = context.$file
|
||||
var unreadComments = $file.data('comments-unread')
|
||||
if (unreadComments) {
|
||||
var $actionLink = $(self._formatCommentCount(unreadComments));
|
||||
context.$file.find('a.name>span.fileactions').append($actionLink);
|
||||
return $actionLink;
|
||||
var $actionLink = $(self._formatCommentCount(unreadComments))
|
||||
context.$file.find('a.name>span.fileactions').append($actionLink)
|
||||
return $actionLink
|
||||
}
|
||||
return '';
|
||||
return ''
|
||||
},
|
||||
actionHandler: function(fileName, context) {
|
||||
context.$file.find('.action-comment').tooltip('hide');
|
||||
context.$file.find('.action-comment').tooltip('hide')
|
||||
// open sidebar in comments section
|
||||
context.fileList.showDetailsView(fileName, 'commentsTabView');
|
||||
context.fileList.showDetailsView(fileName, 'commentsTabView')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// add attribute to "elementToFile"
|
||||
var oldElementToFile = fileList.elementToFile;
|
||||
var oldElementToFile = fileList.elementToFile
|
||||
fileList.elementToFile = function($el) {
|
||||
var fileInfo = oldElementToFile.apply(this, arguments);
|
||||
var commentsUnread = $el.data('comments-unread');
|
||||
var fileInfo = oldElementToFile.apply(this, arguments)
|
||||
var commentsUnread = $el.data('comments-unread')
|
||||
if (commentsUnread) {
|
||||
fileInfo.commentsUnread = commentsUnread;
|
||||
fileInfo.commentsUnread = commentsUnread
|
||||
}
|
||||
return fileInfo;
|
||||
};
|
||||
return fileInfo
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
||||
})()
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin);
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Comments.FilesPlugin)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Copyright (c) 2014
|
||||
*
|
||||
|
@ -8,15 +9,15 @@
|
|||
*
|
||||
*/
|
||||
(function(OC, OCA, $) {
|
||||
"use strict";
|
||||
'use strict'
|
||||
|
||||
/**
|
||||
* Construct a new FileActions instance
|
||||
* @constructs Files
|
||||
*/
|
||||
var Comment = function() {
|
||||
this.initialize();
|
||||
};
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
Comment.prototype = {
|
||||
|
||||
|
@ -27,25 +28,25 @@
|
|||
*/
|
||||
initialize: function() {
|
||||
|
||||
var self = this;
|
||||
var self = this
|
||||
|
||||
this.fileAppLoaded = function() {
|
||||
return !!OCA.Files && !!OCA.Files.App;
|
||||
};
|
||||
return !!OCA.Files && !!OCA.Files.App
|
||||
}
|
||||
function inFileList($row, result) {
|
||||
return false;
|
||||
return false
|
||||
|
||||
if (! self.fileAppLoaded()) {
|
||||
return false;
|
||||
if (!self.fileAppLoaded()) {
|
||||
return false
|
||||
}
|
||||
var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,'');
|
||||
var resultDir = OC.dirname(result.path);
|
||||
return dir === resultDir && self.fileList.inList(result.name);
|
||||
var dir = self.fileList.getCurrentDirectory().replace(/\/+$/, '')
|
||||
var resultDir = OC.dirname(result.path)
|
||||
return dir === resultDir && self.fileList.inList(result.name)
|
||||
}
|
||||
function hideNoFilterResults() {
|
||||
var $nofilterresults = $('.nofilterresults');
|
||||
if ( ! $nofilterresults.hasClass('hidden') ) {
|
||||
$nofilterresults.addClass('hidden');
|
||||
var $nofilterresults = $('.nofilterresults')
|
||||
if (!$nofilterresults.hasClass('hidden')) {
|
||||
$nofilterresults.addClass('hidden')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,73 +65,73 @@
|
|||
*/
|
||||
this.renderCommentResult = function($row, result) {
|
||||
if (inFileList($row, result)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
hideNoFilterResults();
|
||||
/*render preview icon, show path beneath filename,
|
||||
hideNoFilterResults()
|
||||
/* render preview icon, show path beneath filename,
|
||||
show size and last modified date on the right */
|
||||
this.updateLegacyMimetype(result);
|
||||
this.updateLegacyMimetype(result)
|
||||
|
||||
var $pathDiv = $('<div>').addClass('path').text(result.path);
|
||||
var $pathDiv = $('<div>').addClass('path').text(result.path)
|
||||
|
||||
var $avatar = $('<div>');
|
||||
var $avatar = $('<div>')
|
||||
$avatar.addClass('avatar')
|
||||
.css('display', 'inline-block')
|
||||
.css('vertical-align', 'middle')
|
||||
.css('margin', '0 5px 2px 3px');
|
||||
.css('margin', '0 5px 2px 3px')
|
||||
|
||||
if (result.authorName) {
|
||||
$avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName);
|
||||
$avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName)
|
||||
} else {
|
||||
$avatar.avatar(result.authorId, 21);
|
||||
$avatar.avatar(result.authorId, 21)
|
||||
}
|
||||
|
||||
$row.find('td.info div.name').after($pathDiv).text(result.comment).prepend($('<span>').addClass('path').css('margin-right', '5px').text(result.authorName)).prepend($avatar);
|
||||
$row.find('td.result a').attr('href', result.link);
|
||||
$row.find('td.info div.name').after($pathDiv).text(result.comment).prepend($('<span>').addClass('path').css('margin-right', '5px').text(result.authorName)).prepend($avatar)
|
||||
$row.find('td.result a').attr('href', result.link)
|
||||
|
||||
$row.find('td.icon')
|
||||
.css('background-image', 'url(' + OC.imagePath('core', 'actions/comment') + ')')
|
||||
.css('opacity', '.4');
|
||||
var dir = OC.dirname(result.path);
|
||||
.css('opacity', '.4')
|
||||
var dir = OC.dirname(result.path)
|
||||
// "result.path" does not include a leading "/", so "OC.dirname"
|
||||
// returns the path itself for files or folders in the root.
|
||||
if (dir === result.path) {
|
||||
dir = '/';
|
||||
dir = '/'
|
||||
}
|
||||
$row.find('td.info a').attr('href',
|
||||
OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', {dir: dir, scrollto: result.fileName})
|
||||
);
|
||||
OC.generateUrl('/apps/files/?dir={dir}&scrollto={scrollto}', { dir: dir, scrollto: result.fileName })
|
||||
)
|
||||
|
||||
return $row;
|
||||
};
|
||||
return $row
|
||||
}
|
||||
|
||||
this.handleCommentClick = function($row, result, event) {
|
||||
if (self.fileAppLoaded() && self.fileList.id === 'files') {
|
||||
self.fileList.changeDirectory(OC.dirname(result.path));
|
||||
self.fileList.scrollTo(result.name);
|
||||
return false;
|
||||
self.fileList.changeDirectory(OC.dirname(result.path))
|
||||
self.fileList.scrollTo(result.name)
|
||||
return false
|
||||
} else {
|
||||
return true;
|
||||
return true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.updateLegacyMimetype = function (result) {
|
||||
this.updateLegacyMimetype = function(result) {
|
||||
// backward compatibility:
|
||||
if (!result.mime && result.mime_type) {
|
||||
result.mime = result.mime_type;
|
||||
result.mime = result.mime_type
|
||||
}
|
||||
};
|
||||
this.setFileList = function (fileList) {
|
||||
this.fileList = fileList;
|
||||
};
|
||||
}
|
||||
this.setFileList = function(fileList) {
|
||||
this.fileList = fileList
|
||||
}
|
||||
|
||||
OC.Plugins.register('OCA.Search.Core', this);
|
||||
OC.Plugins.register('OCA.Search.Core', this)
|
||||
},
|
||||
attach: function(search) {
|
||||
search.setRenderer('comment', this.renderCommentResult.bind(this));
|
||||
search.setHandler('comment', this.handleCommentClick.bind(this));
|
||||
search.setRenderer('comment', this.renderCommentResult.bind(this))
|
||||
search.setHandler('comment', this.handleCommentClick.bind(this))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
OCA.Search.comment = new Comment();
|
||||
})(OC, OCA, $);
|
||||
OCA.Search.comment = new Comment()
|
||||
})(OC, OCA, $)
|
||||
|
|
|
@ -727,11 +727,11 @@ OC.Uploader.prototype = _.extend({
|
|||
*
|
||||
* @param {array} selection of files to upload
|
||||
* @param {object} callbacks - object with several callback methods
|
||||
* @param {function} callbacks.onNoConflicts
|
||||
* @param {function} callbacks.onSkipConflicts
|
||||
* @param {function} callbacks.onReplaceConflicts
|
||||
* @param {function} callbacks.onChooseConflicts
|
||||
* @param {function} callbacks.onCancel
|
||||
* @param {Function} callbacks.onNoConflicts
|
||||
* @param {Function} callbacks.onSkipConflicts
|
||||
* @param {Function} callbacks.onReplaceConflicts
|
||||
* @param {Function} callbacks.onChooseConflicts
|
||||
* @param {Function} callbacks.onCancel
|
||||
*/
|
||||
checkExistingFiles: function (selection, callbacks) {
|
||||
var fileList = this.fileList;
|
||||
|
|
|
@ -336,7 +336,7 @@
|
|||
* - JS periodically checks for this cookie and then knows when the download has started to call the callback
|
||||
*
|
||||
* @param {string} url download URL
|
||||
* @param {function} callback function to call once the download has started
|
||||
* @param {Function} callback function to call once the download has started
|
||||
*/
|
||||
handleDownload: function(url, callback) {
|
||||
var randomToken = Math.random().toString(36).substring(2),
|
||||
|
|
|
@ -125,7 +125,7 @@ OCA.Files_External.StatusManager = {
|
|||
|
||||
/**
|
||||
* Function to get external mount point list from the files_external API
|
||||
* @param {function} afterCallback function to be executed
|
||||
* @param {Function} afterCallback function to be executed
|
||||
*/
|
||||
|
||||
getMountPointList: function (afterCallback) {
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es6: true
|
||||
},
|
||||
globals: {
|
||||
t: true,
|
||||
n: true,
|
||||
OC: true,
|
||||
OCA: true
|
||||
},
|
||||
extends: 'eslint:recommended',
|
||||
parserOptions: {
|
||||
sourceType: 'module'
|
||||
},
|
||||
rules: {
|
||||
indent: ['error', 'tab'],
|
||||
'linebreak-style': ['error', 'unix'],
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'always']
|
||||
}
|
||||
};
|
|
@ -12,7 +12,7 @@ if (!OCA.Sharing) {
|
|||
/**
|
||||
* @namespace OCA.Sharing
|
||||
*/
|
||||
OCA.Sharing = {};
|
||||
OCA.Sharing = {}
|
||||
}
|
||||
/**
|
||||
* @namespace
|
||||
|
@ -25,7 +25,7 @@ OCA.Sharing.App = {
|
|||
|
||||
initSharingIn: function($el) {
|
||||
if (this._inFileList) {
|
||||
return this._inFileList;
|
||||
return this._inFileList
|
||||
}
|
||||
|
||||
this._inFileList = new OCA.Sharing.FileList(
|
||||
|
@ -40,19 +40,19 @@ OCA.Sharing.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this._extendFileList(this._inFileList);
|
||||
this._inFileList.appName = t('files_sharing', 'Shared with you');
|
||||
this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' +
|
||||
'<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>' +
|
||||
'<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>');
|
||||
return this._inFileList;
|
||||
this._extendFileList(this._inFileList)
|
||||
this._inFileList.appName = t('files_sharing', 'Shared with you')
|
||||
this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
|
||||
+ '<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>'
|
||||
+ '<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>')
|
||||
return this._inFileList
|
||||
},
|
||||
|
||||
initSharingOut: function($el) {
|
||||
if (this._outFileList) {
|
||||
return this._outFileList;
|
||||
return this._outFileList
|
||||
}
|
||||
this._outFileList = new OCA.Sharing.FileList(
|
||||
$el,
|
||||
|
@ -66,19 +66,19 @@ OCA.Sharing.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this._extendFileList(this._outFileList);
|
||||
this._outFileList.appName = t('files_sharing', 'Shared with others');
|
||||
this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' +
|
||||
'<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>' +
|
||||
'<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>');
|
||||
return this._outFileList;
|
||||
this._extendFileList(this._outFileList)
|
||||
this._outFileList.appName = t('files_sharing', 'Shared with others')
|
||||
this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
|
||||
+ '<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>'
|
||||
+ '<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>')
|
||||
return this._outFileList
|
||||
},
|
||||
|
||||
initSharingLinks: function($el) {
|
||||
if (this._linkFileList) {
|
||||
return this._linkFileList;
|
||||
return this._linkFileList
|
||||
}
|
||||
this._linkFileList = new OCA.Sharing.FileList(
|
||||
$el,
|
||||
|
@ -92,19 +92,19 @@ OCA.Sharing.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this._extendFileList(this._linkFileList);
|
||||
this._linkFileList.appName = t('files_sharing', 'Shared by link');
|
||||
this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>' +
|
||||
'<h2>' + t('files_sharing', 'No shared links') + '</h2>' +
|
||||
'<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>');
|
||||
return this._linkFileList;
|
||||
this._extendFileList(this._linkFileList)
|
||||
this._linkFileList.appName = t('files_sharing', 'Shared by link')
|
||||
this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>'
|
||||
+ '<h2>' + t('files_sharing', 'No shared links') + '</h2>'
|
||||
+ '<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>')
|
||||
return this._linkFileList
|
||||
},
|
||||
|
||||
initSharingDeleted: function($el) {
|
||||
if (this._deletedFileList) {
|
||||
return this._deletedFileList;
|
||||
return this._deletedFileList
|
||||
}
|
||||
this._deletedFileList = new OCA.Sharing.FileList(
|
||||
$el,
|
||||
|
@ -119,19 +119,19 @@ OCA.Sharing.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this._extendFileList(this._deletedFileList);
|
||||
this._deletedFileList.appName = t('files_sharing', 'Deleted shares');
|
||||
this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' +
|
||||
'<h2>' + t('files_sharing', 'No deleted shares') + '</h2>' +
|
||||
'<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>');
|
||||
return this._deletedFileList;
|
||||
this._extendFileList(this._deletedFileList)
|
||||
this._deletedFileList.appName = t('files_sharing', 'Deleted shares')
|
||||
this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
|
||||
+ '<h2>' + t('files_sharing', 'No deleted shares') + '</h2>'
|
||||
+ '<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>')
|
||||
return this._deletedFileList
|
||||
},
|
||||
|
||||
initShareingOverview: function($el) {
|
||||
if (this._overviewFileList) {
|
||||
return this._overviewFileList;
|
||||
return this._overviewFileList
|
||||
}
|
||||
this._overviewFileList = new OCA.Sharing.FileList(
|
||||
$el,
|
||||
|
@ -144,43 +144,43 @@ OCA.Sharing.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
this._extendFileList(this._overviewFileList);
|
||||
this._overviewFileList.appName = t('files_sharing', 'Shares');
|
||||
this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' +
|
||||
'<h2>' + t('files_sharing', 'No shares') + '</h2>' +
|
||||
'<p>' + t('files_sharing', 'Shares will show up here') + '</p>');
|
||||
return this._overviewFileList;
|
||||
this._extendFileList(this._overviewFileList)
|
||||
this._overviewFileList.appName = t('files_sharing', 'Shares')
|
||||
this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
|
||||
+ '<h2>' + t('files_sharing', 'No shares') + '</h2>'
|
||||
+ '<p>' + t('files_sharing', 'Shares will show up here') + '</p>')
|
||||
return this._overviewFileList
|
||||
},
|
||||
|
||||
removeSharingIn: function() {
|
||||
if (this._inFileList) {
|
||||
this._inFileList.$fileList.empty();
|
||||
this._inFileList.$fileList.empty()
|
||||
}
|
||||
},
|
||||
|
||||
removeSharingOut: function() {
|
||||
if (this._outFileList) {
|
||||
this._outFileList.$fileList.empty();
|
||||
this._outFileList.$fileList.empty()
|
||||
}
|
||||
},
|
||||
|
||||
removeSharingLinks: function() {
|
||||
if (this._linkFileList) {
|
||||
this._linkFileList.$fileList.empty();
|
||||
this._linkFileList.$fileList.empty()
|
||||
}
|
||||
},
|
||||
|
||||
removeSharingDeleted: function() {
|
||||
if (this._deletedFileList) {
|
||||
this._deletedFileList.$fileList.empty();
|
||||
this._deletedFileList.$fileList.empty()
|
||||
}
|
||||
},
|
||||
|
||||
removeSharingOverview: function() {
|
||||
if (this._overviewFileList) {
|
||||
this._overviewFileList.$fileList.empty();
|
||||
this._overviewFileList.$fileList.empty()
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -188,46 +188,46 @@ OCA.Sharing.App = {
|
|||
* Destroy the app
|
||||
*/
|
||||
destroy: function() {
|
||||
OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated);
|
||||
OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated);
|
||||
this.removeSharingIn();
|
||||
this.removeSharingOut();
|
||||
this.removeSharingLinks();
|
||||
this._inFileList = null;
|
||||
this._outFileList = null;
|
||||
this._linkFileList = null;
|
||||
this._overviewFileList = null;
|
||||
delete this._globalActionsInitialized;
|
||||
OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated)
|
||||
OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated)
|
||||
this.removeSharingIn()
|
||||
this.removeSharingOut()
|
||||
this.removeSharingLinks()
|
||||
this._inFileList = null
|
||||
this._outFileList = null
|
||||
this._linkFileList = null
|
||||
this._overviewFileList = null
|
||||
delete this._globalActionsInitialized
|
||||
},
|
||||
|
||||
_createFileActions: function() {
|
||||
// inherit file actions from the files app
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
var fileActions = new OCA.Files.FileActions()
|
||||
// note: not merging the legacy actions because legacy apps are not
|
||||
// compatible with the sharing overview and need to be adapted first
|
||||
fileActions.registerDefaultActions();
|
||||
fileActions.merge(OCA.Files.fileActions);
|
||||
fileActions.registerDefaultActions()
|
||||
fileActions.merge(OCA.Files.fileActions)
|
||||
|
||||
if (!this._globalActionsInitialized) {
|
||||
// in case actions are registered later
|
||||
this._onActionsUpdated = _.bind(this._onActionsUpdated, this);
|
||||
OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated);
|
||||
OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated);
|
||||
this._globalActionsInitialized = true;
|
||||
this._onActionsUpdated = _.bind(this._onActionsUpdated, this)
|
||||
OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated)
|
||||
OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated)
|
||||
this._globalActionsInitialized = true
|
||||
}
|
||||
|
||||
// when the user clicks on a folder, redirect to the corresponding
|
||||
// folder in the files app instead of opening it directly
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
|
||||
OCA.Files.App.setActiveView('files', {silent: true});
|
||||
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
|
||||
});
|
||||
fileActions.setDefault('dir', 'Open');
|
||||
return fileActions;
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
|
||||
OCA.Files.App.setActiveView('files', { silent: true })
|
||||
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true)
|
||||
})
|
||||
fileActions.setDefault('dir', 'Open')
|
||||
return fileActions
|
||||
},
|
||||
|
||||
_restoreShareAction: function() {
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
var fileActions = new OCA.Files.FileActions()
|
||||
fileActions.registerAction({
|
||||
name: 'Restore',
|
||||
displayName: '',
|
||||
|
@ -237,70 +237,70 @@ OCA.Sharing.App = {
|
|||
iconClass: 'icon-history',
|
||||
type: OCA.Files.FileActions.TYPE_INLINE,
|
||||
actionHandler: function(fileName, context) {
|
||||
var shareId = context.$file.data('shareId');
|
||||
var shareId = context.$file.data('shareId')
|
||||
$.post(OC.linkToOCS('apps/files_sharing/api/v1/deletedshares', 2) + shareId)
|
||||
.success(function(result) {
|
||||
context.fileList.remove(context.fileInfoModel.attributes.name);
|
||||
}).fail(function() {
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'));
|
||||
});
|
||||
.success(function(result) {
|
||||
context.fileList.remove(context.fileInfoModel.attributes.name)
|
||||
}).fail(function() {
|
||||
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'))
|
||||
})
|
||||
}
|
||||
});
|
||||
return fileActions;
|
||||
})
|
||||
return fileActions
|
||||
},
|
||||
|
||||
_onActionsUpdated: function(ev) {
|
||||
_.each([this._inFileList, this._outFileList, this._linkFileList], function(list) {
|
||||
if (!list) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (ev.action) {
|
||||
list.fileActions.registerAction(ev.action);
|
||||
list.fileActions.registerAction(ev.action)
|
||||
} else if (ev.defaultAction) {
|
||||
list.fileActions.setDefault(
|
||||
ev.defaultAction.mime,
|
||||
ev.defaultAction.name
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
_extendFileList: function(fileList) {
|
||||
// remove size column from summary
|
||||
fileList.fileSummary.$el.find('.filesize').remove();
|
||||
fileList.fileSummary.$el.find('.filesize').remove()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#app-content-sharingin').on('show', function(e) {
|
||||
OCA.Sharing.App.initSharingIn($(e.target));
|
||||
});
|
||||
OCA.Sharing.App.initSharingIn($(e.target))
|
||||
})
|
||||
$('#app-content-sharingin').on('hide', function() {
|
||||
OCA.Sharing.App.removeSharingIn();
|
||||
});
|
||||
OCA.Sharing.App.removeSharingIn()
|
||||
})
|
||||
$('#app-content-sharingout').on('show', function(e) {
|
||||
OCA.Sharing.App.initSharingOut($(e.target));
|
||||
});
|
||||
OCA.Sharing.App.initSharingOut($(e.target))
|
||||
})
|
||||
$('#app-content-sharingout').on('hide', function() {
|
||||
OCA.Sharing.App.removeSharingOut();
|
||||
});
|
||||
OCA.Sharing.App.removeSharingOut()
|
||||
})
|
||||
$('#app-content-sharinglinks').on('show', function(e) {
|
||||
OCA.Sharing.App.initSharingLinks($(e.target));
|
||||
});
|
||||
OCA.Sharing.App.initSharingLinks($(e.target))
|
||||
})
|
||||
$('#app-content-sharinglinks').on('hide', function() {
|
||||
OCA.Sharing.App.removeSharingLinks();
|
||||
});
|
||||
OCA.Sharing.App.removeSharingLinks()
|
||||
})
|
||||
$('#app-content-deletedshares').on('show', function(e) {
|
||||
OCA.Sharing.App.initSharingDeleted($(e.target));
|
||||
});
|
||||
OCA.Sharing.App.initSharingDeleted($(e.target))
|
||||
})
|
||||
$('#app-content-deletedshares').on('hide', function() {
|
||||
OCA.Sharing.App.removeSharingDeleted();
|
||||
});
|
||||
OCA.Sharing.App.removeSharingDeleted()
|
||||
})
|
||||
$('#app-content-shareoverview').on('show', function(e) {
|
||||
OCA.Sharing.App.initShareingOverview($(e.target));
|
||||
});
|
||||
OCA.Sharing.App.initShareingOverview($(e.target))
|
||||
})
|
||||
$('#app-content-shareoverview').on('hide', function() {
|
||||
OCA.Sharing.App.removeSharingOverview();
|
||||
});
|
||||
});
|
||||
OCA.Sharing.App.removeSharingOverview()
|
||||
})
|
||||
})
|
||||
|
|
BIN
apps/files_sharing/js/dist/additionalScripts.js
vendored
BIN
apps/files_sharing/js/dist/additionalScripts.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/additionalScripts.js.map
vendored
BIN
apps/files_sharing/js/dist/additionalScripts.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/collaboration.js
vendored
BIN
apps/files_sharing/js/dist/collaboration.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/collaboration.js.map
vendored
BIN
apps/files_sharing/js/dist/collaboration.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.4.js
vendored
BIN
apps/files_sharing/js/dist/files_sharing.4.js
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.4.js.map
vendored
BIN
apps/files_sharing/js/dist/files_sharing.4.js.map
vendored
Binary file not shown.
BIN
apps/files_sharing/js/dist/files_sharing.js.map
vendored
BIN
apps/files_sharing/js/dist/files_sharing.js.map
vendored
Binary file not shown.
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
|
@ -25,428 +26,423 @@
|
|||
* @param {boolean} [options.linksOnly] true to return only link shares
|
||||
*/
|
||||
var FileList = function($el, options) {
|
||||
this.initialize($el, options);
|
||||
};
|
||||
this.initialize($el, options)
|
||||
}
|
||||
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
|
||||
/** @lends OCA.Sharing.FileList.prototype */ {
|
||||
appName: 'Shares',
|
||||
appName: 'Shares',
|
||||
|
||||
/**
|
||||
/**
|
||||
* Whether the list shows the files shared with the user (true) or
|
||||
* the files that the user shared with others (false).
|
||||
*/
|
||||
_sharedWithUser: false,
|
||||
_linksOnly: false,
|
||||
_showDeleted: false,
|
||||
_clientSideSort: true,
|
||||
_allowSelection: false,
|
||||
_isOverview: false,
|
||||
_sharedWithUser: false,
|
||||
_linksOnly: false,
|
||||
_showDeleted: false,
|
||||
_clientSideSort: true,
|
||||
_allowSelection: false,
|
||||
_isOverview: false,
|
||||
|
||||
/**
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initialize: function($el, options) {
|
||||
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
initialize: function($el, options) {
|
||||
OCA.Files.FileList.prototype.initialize.apply(this, arguments)
|
||||
if (this.initialized) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: consolidate both options
|
||||
if (options && options.sharedWithUser) {
|
||||
this._sharedWithUser = true;
|
||||
}
|
||||
if (options && options.linksOnly) {
|
||||
this._linksOnly = true;
|
||||
}
|
||||
if (options && options.showDeleted) {
|
||||
this._showDeleted = true;
|
||||
}
|
||||
if (options && options.isOverview) {
|
||||
this._isOverview = true;
|
||||
}
|
||||
},
|
||||
// TODO: consolidate both options
|
||||
if (options && options.sharedWithUser) {
|
||||
this._sharedWithUser = true
|
||||
}
|
||||
if (options && options.linksOnly) {
|
||||
this._linksOnly = true
|
||||
}
|
||||
if (options && options.showDeleted) {
|
||||
this._showDeleted = true
|
||||
}
|
||||
if (options && options.isOverview) {
|
||||
this._isOverview = true
|
||||
}
|
||||
},
|
||||
|
||||
_renderRow: function() {
|
||||
_renderRow: function() {
|
||||
// HACK: needed to call the overridden _renderRow
|
||||
// this is because at the time this class is created
|
||||
// the overriding hasn't been done yet...
|
||||
return OCA.Files.FileList.prototype._renderRow.apply(this, arguments);
|
||||
},
|
||||
return OCA.Files.FileList.prototype._renderRow.apply(this, arguments)
|
||||
},
|
||||
|
||||
_createRow: function(fileData) {
|
||||
_createRow: function(fileData) {
|
||||
// TODO: hook earlier and render the whole row here
|
||||
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
|
||||
$tr.find('.filesize').remove();
|
||||
$tr.find('td.date').before($tr.children('td:first'));
|
||||
$tr.find('td.filename input:checkbox').remove();
|
||||
$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','));
|
||||
if (this._sharedWithUser) {
|
||||
$tr.attr('data-share-owner', fileData.shareOwner);
|
||||
$tr.attr('data-mounttype', 'shared-root');
|
||||
var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE;
|
||||
$tr.attr('data-permissions', permission);
|
||||
}
|
||||
if (this._showDeleted) {
|
||||
var permission = fileData.permissions;
|
||||
$tr.attr('data-share-permissions', permission);
|
||||
}
|
||||
|
||||
// add row with expiration date for link only shares - influenced by _createRow of filelist
|
||||
if (this._linksOnly) {
|
||||
var expirationTimestamp = 0;
|
||||
if(fileData.shares && fileData.shares[0].expiration !== null) {
|
||||
expirationTimestamp = moment(fileData.shares[0].expiration).valueOf();
|
||||
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
|
||||
$tr.find('.filesize').remove()
|
||||
$tr.find('td.date').before($tr.children('td:first'))
|
||||
$tr.find('td.filename input:checkbox').remove()
|
||||
$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','))
|
||||
if (this._sharedWithUser) {
|
||||
$tr.attr('data-share-owner', fileData.shareOwner)
|
||||
$tr.attr('data-mounttype', 'shared-root')
|
||||
var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE
|
||||
$tr.attr('data-permissions', permission)
|
||||
}
|
||||
$tr.attr('data-expiration', expirationTimestamp);
|
||||
|
||||
// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
|
||||
// difference in days multiplied by 5 - brightest shade for expiry dates in more than 32 days (160/5)
|
||||
var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5);
|
||||
// ensure that the brightest color is still readable
|
||||
if (modifiedColor >= 160) {
|
||||
modifiedColor = 160;
|
||||
if (this._showDeleted) {
|
||||
var permission = fileData.permissions
|
||||
$tr.attr('data-share-permissions', permission)
|
||||
}
|
||||
|
||||
var formatted;
|
||||
var text;
|
||||
if (expirationTimestamp > 0) {
|
||||
formatted = OC.Util.formatDate(expirationTimestamp);
|
||||
text = OC.Util.relativeModifiedDate(expirationTimestamp);
|
||||
} else {
|
||||
formatted = t('files_sharing', 'No expiration date set');
|
||||
text = '';
|
||||
modifiedColor = 160;
|
||||
}
|
||||
td = $('<td></td>').attr({"class": "date"});
|
||||
td.append($('<span></span>').attr({
|
||||
"class": "modified",
|
||||
"title": formatted,
|
||||
"style": 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')'
|
||||
// add row with expiration date for link only shares - influenced by _createRow of filelist
|
||||
if (this._linksOnly) {
|
||||
var expirationTimestamp = 0
|
||||
if (fileData.shares && fileData.shares[0].expiration !== null) {
|
||||
expirationTimestamp = moment(fileData.shares[0].expiration).valueOf()
|
||||
}
|
||||
$tr.attr('data-expiration', expirationTimestamp)
|
||||
|
||||
// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours)
|
||||
// difference in days multiplied by 5 - brightest shade for expiry dates in more than 32 days (160/5)
|
||||
var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5)
|
||||
// ensure that the brightest color is still readable
|
||||
if (modifiedColor >= 160) {
|
||||
modifiedColor = 160
|
||||
}
|
||||
|
||||
var formatted
|
||||
var text
|
||||
if (expirationTimestamp > 0) {
|
||||
formatted = OC.Util.formatDate(expirationTimestamp)
|
||||
text = OC.Util.relativeModifiedDate(expirationTimestamp)
|
||||
} else {
|
||||
formatted = t('files_sharing', 'No expiration date set')
|
||||
text = ''
|
||||
modifiedColor = 160
|
||||
}
|
||||
td = $('<td></td>').attr({ 'class': 'date' })
|
||||
td.append($('<span></span>').attr({
|
||||
'class': 'modified',
|
||||
'title': formatted,
|
||||
'style': 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')'
|
||||
}).text(text)
|
||||
.tooltip({placement: 'top'})
|
||||
);
|
||||
.tooltip({ placement: 'top' })
|
||||
)
|
||||
|
||||
$tr.append(td);
|
||||
}
|
||||
return $tr;
|
||||
},
|
||||
$tr.append(td)
|
||||
}
|
||||
return $tr
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Set whether the list should contain outgoing shares
|
||||
* or incoming shares.
|
||||
*
|
||||
* @param state true for incoming shares, false otherwise
|
||||
*/
|
||||
setSharedWithUser: function(state) {
|
||||
this._sharedWithUser = !!state;
|
||||
},
|
||||
setSharedWithUser: function(state) {
|
||||
this._sharedWithUser = !!state
|
||||
},
|
||||
|
||||
updateEmptyContent: function() {
|
||||
var dir = this.getCurrentDirectory();
|
||||
if (dir === '/') {
|
||||
updateEmptyContent: function() {
|
||||
var dir = this.getCurrentDirectory()
|
||||
if (dir === '/') {
|
||||
// root has special permissions
|
||||
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty);
|
||||
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty);
|
||||
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty)
|
||||
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty)
|
||||
|
||||
// hide expiration date header for non link only shares
|
||||
if (!this._linksOnly) {
|
||||
this.$el.find('th.column-expiration').addClass('hidden');
|
||||
// hide expiration date header for non link only shares
|
||||
if (!this._linksOnly) {
|
||||
this.$el.find('th.column-expiration').addClass('hidden')
|
||||
}
|
||||
} else {
|
||||
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments)
|
||||
}
|
||||
}
|
||||
else {
|
||||
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
getDirectoryPermissions: function() {
|
||||
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
|
||||
},
|
||||
getDirectoryPermissions: function() {
|
||||
return OC.PERMISSION_READ | OC.PERMISSION_DELETE
|
||||
},
|
||||
|
||||
updateStorageStatistics: function() {
|
||||
updateStorageStatistics: function() {
|
||||
// no op because it doesn't have
|
||||
// storage info like free space / used space
|
||||
},
|
||||
},
|
||||
|
||||
updateRow: function($tr, fileInfo, options) {
|
||||
updateRow: function($tr, fileInfo, options) {
|
||||
// no-op, suppress re-rendering
|
||||
return $tr;
|
||||
},
|
||||
return $tr
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.showMask();
|
||||
if (this._reloadCall) {
|
||||
this._reloadCall.abort();
|
||||
}
|
||||
|
||||
// there is only root
|
||||
this._setCurrentDir('/', false);
|
||||
|
||||
var promises = [];
|
||||
|
||||
var deletedShares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1', 2) + 'deletedshares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true');
|
||||
},
|
||||
};
|
||||
|
||||
var shares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
shared_with_me: this._sharedWithUser !== false,
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true');
|
||||
},
|
||||
};
|
||||
|
||||
var remoteShares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true');
|
||||
},
|
||||
};
|
||||
|
||||
// Add the proper ajax requests to the list and run them
|
||||
// and make sure we have 2 promises
|
||||
if (this._showDeleted) {
|
||||
promises.push($.ajax(deletedShares));
|
||||
} else {
|
||||
promises.push($.ajax(shares));
|
||||
|
||||
if (this._sharedWithUser !== false || this._isOverview) {
|
||||
promises.push($.ajax(remoteShares));
|
||||
reload: function() {
|
||||
this.showMask()
|
||||
if (this._reloadCall) {
|
||||
this._reloadCall.abort()
|
||||
}
|
||||
if (this._isOverview) {
|
||||
shares.data.shared_with_me = !shares.data.shared_with_me;
|
||||
promises.push($.ajax(shares));
|
||||
|
||||
// there is only root
|
||||
this._setCurrentDir('/', false)
|
||||
|
||||
var promises = []
|
||||
|
||||
var deletedShares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1', 2) + 'deletedshares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._reloadCall = $.when.apply($, promises);
|
||||
var callBack = this.reloadCallback.bind(this);
|
||||
return this._reloadCall.then(callBack, callBack);
|
||||
},
|
||||
var shares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
shared_with_me: this._sharedWithUser !== false,
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
reloadCallback: function(shares, remoteShares, additionalShares) {
|
||||
delete this._reloadCall;
|
||||
this.hideMask();
|
||||
var remoteShares = {
|
||||
url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
|
||||
/* jshint camelcase: false */
|
||||
data: {
|
||||
format: 'json',
|
||||
include_tags: true
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
|
||||
}
|
||||
}
|
||||
|
||||
this.$el.find('#headerSharedWith').text(
|
||||
t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
|
||||
);
|
||||
// Add the proper ajax requests to the list and run them
|
||||
// and make sure we have 2 promises
|
||||
if (this._showDeleted) {
|
||||
promises.push($.ajax(deletedShares))
|
||||
} else {
|
||||
promises.push($.ajax(shares))
|
||||
|
||||
var files = [];
|
||||
if (this._sharedWithUser !== false || this._isOverview) {
|
||||
promises.push($.ajax(remoteShares))
|
||||
}
|
||||
if (this._isOverview) {
|
||||
shares.data.shared_with_me = !shares.data.shared_with_me
|
||||
promises.push($.ajax(shares))
|
||||
}
|
||||
}
|
||||
|
||||
// make sure to use the same format
|
||||
if (shares[0] && shares[0].ocs) {
|
||||
shares = shares[0];
|
||||
}
|
||||
if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
|
||||
remoteShares = remoteShares[0];
|
||||
}
|
||||
if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
|
||||
additionalShares = additionalShares[0];
|
||||
}
|
||||
this._reloadCall = $.when.apply($, promises)
|
||||
var callBack = this.reloadCallback.bind(this)
|
||||
return this._reloadCall.then(callBack, callBack)
|
||||
},
|
||||
|
||||
if (shares.ocs && shares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser));
|
||||
}
|
||||
reloadCallback: function(shares, remoteShares, additionalShares) {
|
||||
delete this._reloadCall
|
||||
this.hideMask()
|
||||
|
||||
if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data));
|
||||
}
|
||||
this.$el.find('#headerSharedWith').text(
|
||||
t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
|
||||
)
|
||||
|
||||
if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser));
|
||||
}
|
||||
var files = []
|
||||
|
||||
// make sure to use the same format
|
||||
if (shares[0] && shares[0].ocs) {
|
||||
shares = shares[0]
|
||||
}
|
||||
if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
|
||||
remoteShares = remoteShares[0]
|
||||
}
|
||||
if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
|
||||
additionalShares = additionalShares[0]
|
||||
}
|
||||
|
||||
this.setFiles(files);
|
||||
return true;
|
||||
},
|
||||
if (shares.ocs && shares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser))
|
||||
}
|
||||
|
||||
_makeFilesFromRemoteShares: function(data) {
|
||||
var files = data;
|
||||
if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data))
|
||||
}
|
||||
|
||||
files = _.chain(files)
|
||||
if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) {
|
||||
files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser))
|
||||
}
|
||||
|
||||
this.setFiles(files)
|
||||
return true
|
||||
},
|
||||
|
||||
_makeFilesFromRemoteShares: function(data) {
|
||||
var files = data
|
||||
|
||||
files = _.chain(files)
|
||||
// convert share data to file data
|
||||
.map(function(share) {
|
||||
var file = {
|
||||
shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ""),
|
||||
name: OC.basename(share.mountpoint),
|
||||
mtime: share.mtime * 1000,
|
||||
mimetype: share.mimetype,
|
||||
type: share.type,
|
||||
id: share.file_id,
|
||||
path: OC.dirname(share.mountpoint),
|
||||
permissions: share.permissions,
|
||||
tags: share.tags || []
|
||||
};
|
||||
.map(function(share) {
|
||||
var file = {
|
||||
shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ''),
|
||||
name: OC.basename(share.mountpoint),
|
||||
mtime: share.mtime * 1000,
|
||||
mimetype: share.mimetype,
|
||||
type: share.type,
|
||||
id: share.file_id,
|
||||
path: OC.dirname(share.mountpoint),
|
||||
permissions: share.permissions,
|
||||
tags: share.tags || []
|
||||
}
|
||||
|
||||
file.shares = [{
|
||||
id: share.id,
|
||||
type: OC.Share.SHARE_TYPE_REMOTE
|
||||
}];
|
||||
return file;
|
||||
})
|
||||
.value();
|
||||
return files;
|
||||
},
|
||||
file.shares = [{
|
||||
id: share.id,
|
||||
type: OC.Share.SHARE_TYPE_REMOTE
|
||||
}]
|
||||
return file
|
||||
})
|
||||
.value()
|
||||
return files
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Converts the OCS API share response data to a file info
|
||||
* list
|
||||
* @param {Array} data OCS API share array
|
||||
* @param {bool} sharedWithUser
|
||||
* @return {Array.<OCA.Sharing.SharedFileInfo>} array of shared file info
|
||||
* @returns {Array.<OCA.Sharing.SharedFileInfo>} array of shared file info
|
||||
*/
|
||||
_makeFilesFromShares: function(data, sharedWithUser) {
|
||||
_makeFilesFromShares: function(data, sharedWithUser) {
|
||||
/* jshint camelcase: false */
|
||||
var files = data;
|
||||
var files = data
|
||||
|
||||
if (this._linksOnly) {
|
||||
files = _.filter(data, function(share) {
|
||||
return share.share_type === OC.Share.SHARE_TYPE_LINK;
|
||||
});
|
||||
}
|
||||
if (this._linksOnly) {
|
||||
files = _.filter(data, function(share) {
|
||||
return share.share_type === OC.Share.SHARE_TYPE_LINK
|
||||
})
|
||||
}
|
||||
|
||||
// OCS API uses non-camelcased names
|
||||
files = _.chain(files)
|
||||
// OCS API uses non-camelcased names
|
||||
files = _.chain(files)
|
||||
// convert share data to file data
|
||||
.map(function(share) {
|
||||
.map(function(share) {
|
||||
// TODO: use OC.Files.FileInfo
|
||||
var file = {
|
||||
id: share.file_source,
|
||||
icon: OC.MimeType.getIconUrl(share.mimetype),
|
||||
mimetype: share.mimetype,
|
||||
tags: share.tags || []
|
||||
};
|
||||
if (share.item_type === 'folder') {
|
||||
file.type = 'dir';
|
||||
file.mimetype = 'httpd/unix-directory';
|
||||
}
|
||||
else {
|
||||
file.type = 'file';
|
||||
}
|
||||
file.share = {
|
||||
id: share.id,
|
||||
type: share.share_type,
|
||||
target: share.share_with,
|
||||
stime: share.stime * 1000,
|
||||
expiration: share.expiration,
|
||||
};
|
||||
if (sharedWithUser) {
|
||||
file.shareOwner = share.displayname_owner;
|
||||
file.shareOwnerId = share.uid_owner;
|
||||
file.name = OC.basename(share.file_target);
|
||||
file.path = OC.dirname(share.file_target);
|
||||
file.permissions = share.permissions;
|
||||
if (file.path) {
|
||||
file.extraData = share.file_target;
|
||||
var file = {
|
||||
id: share.file_source,
|
||||
icon: OC.MimeType.getIconUrl(share.mimetype),
|
||||
mimetype: share.mimetype,
|
||||
tags: share.tags || []
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (share.share_type !== OC.Share.SHARE_TYPE_LINK) {
|
||||
file.share.targetDisplayName = share.share_with_displayname;
|
||||
file.share.targetShareWithId = share.share_with;
|
||||
if (share.item_type === 'folder') {
|
||||
file.type = 'dir'
|
||||
file.mimetype = 'httpd/unix-directory'
|
||||
} else {
|
||||
file.type = 'file'
|
||||
}
|
||||
file.name = OC.basename(share.path);
|
||||
file.path = OC.dirname(share.path);
|
||||
file.permissions = OC.PERMISSION_ALL;
|
||||
if (file.path) {
|
||||
file.extraData = share.path;
|
||||
file.share = {
|
||||
id: share.id,
|
||||
type: share.share_type,
|
||||
target: share.share_with,
|
||||
stime: share.stime * 1000,
|
||||
expiration: share.expiration
|
||||
}
|
||||
}
|
||||
return file;
|
||||
})
|
||||
if (sharedWithUser) {
|
||||
file.shareOwner = share.displayname_owner
|
||||
file.shareOwnerId = share.uid_owner
|
||||
file.name = OC.basename(share.file_target)
|
||||
file.path = OC.dirname(share.file_target)
|
||||
file.permissions = share.permissions
|
||||
if (file.path) {
|
||||
file.extraData = share.file_target
|
||||
}
|
||||
} else {
|
||||
if (share.share_type !== OC.Share.SHARE_TYPE_LINK) {
|
||||
file.share.targetDisplayName = share.share_with_displayname
|
||||
file.share.targetShareWithId = share.share_with
|
||||
}
|
||||
file.name = OC.basename(share.path)
|
||||
file.path = OC.dirname(share.path)
|
||||
file.permissions = OC.PERMISSION_ALL
|
||||
if (file.path) {
|
||||
file.extraData = share.path
|
||||
}
|
||||
}
|
||||
return file
|
||||
})
|
||||
// Group all files and have a "shares" array with
|
||||
// the share info for each file.
|
||||
//
|
||||
// This uses a hash memo to cumulate share information
|
||||
// inside the same file object (by file id).
|
||||
.reduce(function(memo, file) {
|
||||
var data = memo[file.id];
|
||||
var recipient = file.share.targetDisplayName;
|
||||
var recipientId = file.share.targetShareWithId;
|
||||
if (!data) {
|
||||
data = memo[file.id] = file;
|
||||
data.shares = [file.share];
|
||||
// using a hash to make them unique,
|
||||
// this is only a list to be displayed
|
||||
data.recipients = {};
|
||||
data.recipientData = {};
|
||||
// share types
|
||||
data.shareTypes = {};
|
||||
// counter is cheaper than calling _.keys().length
|
||||
data.recipientsCount = 0;
|
||||
data.mtime = file.share.stime;
|
||||
}
|
||||
else {
|
||||
.reduce(function(memo, file) {
|
||||
var data = memo[file.id]
|
||||
var recipient = file.share.targetDisplayName
|
||||
var recipientId = file.share.targetShareWithId
|
||||
if (!data) {
|
||||
data = memo[file.id] = file
|
||||
data.shares = [file.share]
|
||||
// using a hash to make them unique,
|
||||
// this is only a list to be displayed
|
||||
data.recipients = {}
|
||||
data.recipientData = {}
|
||||
// share types
|
||||
data.shareTypes = {}
|
||||
// counter is cheaper than calling _.keys().length
|
||||
data.recipientsCount = 0
|
||||
data.mtime = file.share.stime
|
||||
} else {
|
||||
// always take the most recent stime
|
||||
if (file.share.stime > data.mtime) {
|
||||
data.mtime = file.share.stime;
|
||||
if (file.share.stime > data.mtime) {
|
||||
data.mtime = file.share.stime
|
||||
}
|
||||
data.shares.push(file.share)
|
||||
}
|
||||
data.shares.push(file.share);
|
||||
}
|
||||
|
||||
if (recipient) {
|
||||
if (recipient) {
|
||||
// limit counterparts for output
|
||||
if (data.recipientsCount < 4) {
|
||||
if (data.recipientsCount < 4) {
|
||||
// only store the first ones, they will be the only ones
|
||||
// displayed
|
||||
data.recipients[recipient] = true;
|
||||
data.recipientData[data.recipientsCount] = {
|
||||
'shareWith': recipientId,
|
||||
'shareWithDisplayName': recipient
|
||||
};
|
||||
data.recipients[recipient] = true
|
||||
data.recipientData[data.recipientsCount] = {
|
||||
'shareWith': recipientId,
|
||||
'shareWithDisplayName': recipient
|
||||
}
|
||||
}
|
||||
data.recipientsCount++
|
||||
}
|
||||
data.recipientsCount++;
|
||||
}
|
||||
|
||||
data.shareTypes[file.share.type] = true;
|
||||
data.shareTypes[file.share.type] = true
|
||||
|
||||
delete file.share;
|
||||
return memo;
|
||||
}, {})
|
||||
delete file.share
|
||||
return memo
|
||||
}, {})
|
||||
// Retrieve only the values of the returned hash
|
||||
.values()
|
||||
.values()
|
||||
// Clean up
|
||||
.each(function(data) {
|
||||
.each(function(data) {
|
||||
// convert the recipients map to a flat
|
||||
// array of sorted names
|
||||
data.mountType = 'shared';
|
||||
delete data.recipientsCount;
|
||||
if (sharedWithUser) {
|
||||
data.mountType = 'shared'
|
||||
delete data.recipientsCount
|
||||
if (sharedWithUser) {
|
||||
// only for outgoing shares
|
||||
delete data.shareTypes;
|
||||
} else {
|
||||
data.shareTypes = _.keys(data.shareTypes);
|
||||
}
|
||||
})
|
||||
delete data.shareTypes
|
||||
} else {
|
||||
data.shareTypes = _.keys(data.shareTypes)
|
||||
}
|
||||
})
|
||||
// Finish the chain by getting the result
|
||||
.value();
|
||||
.value()
|
||||
|
||||
// Sort by expected sort comparator
|
||||
return files.sort(this._sortComparator);
|
||||
},
|
||||
});
|
||||
// Sort by expected sort comparator
|
||||
return files.sort(this._sortComparator)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Share info attributes.
|
||||
|
@ -486,5 +482,5 @@
|
|||
* passing to HTML data attributes with jQuery)
|
||||
*/
|
||||
|
||||
OCA.Sharing.FileList = FileList;
|
||||
})();
|
||||
OCA.Sharing.FileList = FileList
|
||||
})()
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
|
||||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
|
||||
import './share'
|
||||
import './sharetabview'
|
||||
import './sharebreadcrumbview'
|
||||
|
@ -10,4 +7,9 @@ import './style/sharebreadcrumb.scss'
|
|||
|
||||
import './collaborationresourceshandler.js'
|
||||
|
||||
window.OCA.Sharing = OCA.Sharing;
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
|
||||
window.OCA.Sharing = OCA.Sharing
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
|
@ -20,21 +20,23 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { Tooltip, PopoverMenu } from 'nextcloud-vue';
|
||||
import ClickOutside from 'vue-click-outside';
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import { Tooltip, PopoverMenu } from 'nextcloud-vue'
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
|
||||
Vue.prototype.t = t;
|
||||
Vue.component('PopoverMenu', PopoverMenu);
|
||||
Vue.directive('ClickOutside', ClickOutside);
|
||||
import View from './views/CollaborationView'
|
||||
|
||||
Vue.prototype.t = t
|
||||
Tooltip.options.defaultHtml = false
|
||||
Vue.directive('Tooltip', Tooltip);
|
||||
Vue.use(Vuex);
|
||||
|
||||
import View from './views/CollaborationView';
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
Vue.component('PopoverMenu', PopoverMenu)
|
||||
Vue.directive('ClickOutside', ClickOutside)
|
||||
Vue.directive('Tooltip', Tooltip)
|
||||
Vue.use(Vuex)
|
||||
|
||||
export {
|
||||
Vue,
|
||||
View
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
|
||||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
|
||||
window.OCP.Collaboration.registerType('file', {
|
||||
action: () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function (f) {
|
||||
const client = OC.Files.getClient();
|
||||
OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function(f) {
|
||||
const client = OC.Files.getClient()
|
||||
client.getFileInfo(f).then((status, fileInfo) => {
|
||||
resolve(fileInfo.id);
|
||||
resolve(fileInfo.id)
|
||||
}).fail(() => {
|
||||
reject();
|
||||
});
|
||||
}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true });
|
||||
});
|
||||
reject(new Error('Cannot get fileinfo'))
|
||||
})
|
||||
}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true })
|
||||
})
|
||||
},
|
||||
typeString: t('files_sharing', 'Link to a file'),
|
||||
typeIconClass: 'icon-files-dark'
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
|
||||
import '../js/app'
|
||||
import '../js/sharedfilelist'
|
||||
|
||||
import '../js/app';
|
||||
import '../js/sharedfilelist';
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Copyright (c) 2014
|
||||
*
|
||||
|
@ -14,10 +15,10 @@
|
|||
PROPERTY_SHARE_TYPES: '{' + OC.Files.Client.NS_OWNCLOUD + '}share-types',
|
||||
PROPERTY_OWNER_ID: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-id',
|
||||
PROPERTY_OWNER_DISPLAY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-display-name'
|
||||
});
|
||||
})
|
||||
|
||||
if (!OCA.Sharing) {
|
||||
OCA.Sharing = {};
|
||||
OCA.Sharing = {}
|
||||
}
|
||||
/**
|
||||
* @namespace
|
||||
|
@ -34,131 +35,130 @@
|
|||
attach: function(fileList) {
|
||||
// core sharing is disabled/not loaded
|
||||
if (!OC.Share) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
var fileActions = fileList.fileActions;
|
||||
var oldCreateRow = fileList._createRow;
|
||||
var fileActions = fileList.fileActions
|
||||
var oldCreateRow = fileList._createRow
|
||||
fileList._createRow = function(fileData) {
|
||||
|
||||
var tr = oldCreateRow.apply(this, arguments);
|
||||
var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData);
|
||||
|
||||
var tr = oldCreateRow.apply(this, arguments)
|
||||
var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData)
|
||||
|
||||
if (fileData.permissions === 0) {
|
||||
// no permission, disabling sidebar
|
||||
delete fileActions.actions.all.Comment;
|
||||
delete fileActions.actions.all.Details;
|
||||
delete fileActions.actions.all.Goto;
|
||||
delete fileActions.actions.all.Comment
|
||||
delete fileActions.actions.all.Details
|
||||
delete fileActions.actions.all.Goto
|
||||
}
|
||||
tr.attr('data-share-permissions', sharePermissions);
|
||||
tr.attr('data-share-permissions', sharePermissions)
|
||||
if (fileData.shareOwner) {
|
||||
tr.attr('data-share-owner', fileData.shareOwner);
|
||||
tr.attr('data-share-owner-id', fileData.shareOwnerId);
|
||||
tr.attr('data-share-owner', fileData.shareOwner)
|
||||
tr.attr('data-share-owner-id', fileData.shareOwnerId)
|
||||
// user should always be able to rename a mount point
|
||||
if (fileData.mountType === 'shared-root') {
|
||||
tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE);
|
||||
tr.attr('data-permissions', fileData.permissions | OC.PERMISSION_UPDATE)
|
||||
}
|
||||
}
|
||||
if (fileData.recipientData && !_.isEmpty(fileData.recipientData)) {
|
||||
tr.attr('data-share-recipient-data', JSON.stringify(fileData.recipientData));
|
||||
tr.attr('data-share-recipient-data', JSON.stringify(fileData.recipientData))
|
||||
}
|
||||
if (fileData.shareTypes) {
|
||||
tr.attr('data-share-types', fileData.shareTypes.join(','));
|
||||
tr.attr('data-share-types', fileData.shareTypes.join(','))
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
return tr
|
||||
}
|
||||
|
||||
var oldElementToFile = fileList.elementToFile;
|
||||
var oldElementToFile = fileList.elementToFile
|
||||
fileList.elementToFile = function($el) {
|
||||
var fileInfo = oldElementToFile.apply(this, arguments);
|
||||
fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined;
|
||||
fileInfo.shareOwner = $el.attr('data-share-owner') || undefined;
|
||||
fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined;
|
||||
var fileInfo = oldElementToFile.apply(this, arguments)
|
||||
fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined
|
||||
fileInfo.shareOwner = $el.attr('data-share-owner') || undefined
|
||||
fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined
|
||||
|
||||
if( $el.attr('data-share-types')){
|
||||
fileInfo.shareTypes = $el.attr('data-share-types').split(',');
|
||||
if ($el.attr('data-share-types')) {
|
||||
fileInfo.shareTypes = $el.attr('data-share-types').split(',')
|
||||
}
|
||||
|
||||
if( $el.attr('data-expiration')){
|
||||
var expirationTimestamp = parseInt($el.attr('data-expiration'));
|
||||
fileInfo.shares = [];
|
||||
fileInfo.shares.push({expiration: expirationTimestamp});
|
||||
if ($el.attr('data-expiration')) {
|
||||
var expirationTimestamp = parseInt($el.attr('data-expiration'))
|
||||
fileInfo.shares = []
|
||||
fileInfo.shares.push({ expiration: expirationTimestamp })
|
||||
}
|
||||
|
||||
return fileInfo;
|
||||
};
|
||||
return fileInfo
|
||||
}
|
||||
|
||||
var oldGetWebdavProperties = fileList._getWebdavProperties;
|
||||
var oldGetWebdavProperties = fileList._getWebdavProperties
|
||||
fileList._getWebdavProperties = function() {
|
||||
var props = oldGetWebdavProperties.apply(this, arguments);
|
||||
props.push(OC.Files.Client.PROPERTY_OWNER_ID);
|
||||
props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME);
|
||||
props.push(OC.Files.Client.PROPERTY_SHARE_TYPES);
|
||||
return props;
|
||||
};
|
||||
var props = oldGetWebdavProperties.apply(this, arguments)
|
||||
props.push(OC.Files.Client.PROPERTY_OWNER_ID)
|
||||
props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME)
|
||||
props.push(OC.Files.Client.PROPERTY_SHARE_TYPES)
|
||||
return props
|
||||
}
|
||||
|
||||
fileList.filesClient.addFileInfoParser(function(response) {
|
||||
var data = {};
|
||||
var props = response.propStat[0].properties;
|
||||
var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS];
|
||||
var data = {}
|
||||
var props = response.propStat[0].properties
|
||||
var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS]
|
||||
|
||||
if (permissionsProp && permissionsProp.indexOf('S') >= 0) {
|
||||
data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME];
|
||||
data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID];
|
||||
data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME]
|
||||
data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID]
|
||||
}
|
||||
|
||||
var shareTypesProp = props[OC.Files.Client.PROPERTY_SHARE_TYPES];
|
||||
var shareTypesProp = props[OC.Files.Client.PROPERTY_SHARE_TYPES]
|
||||
if (shareTypesProp) {
|
||||
data.shareTypes = _.chain(shareTypesProp).filter(function(xmlvalue) {
|
||||
return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'share-type');
|
||||
return (xmlvalue.namespaceURI === OC.Files.Client.NS_OWNCLOUD && xmlvalue.nodeName.split(':')[1] === 'share-type')
|
||||
}).map(function(xmlvalue) {
|
||||
return parseInt(xmlvalue.textContent || xmlvalue.text, 10);
|
||||
}).value();
|
||||
return parseInt(xmlvalue.textContent || xmlvalue.text, 10)
|
||||
}).value()
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
return data
|
||||
})
|
||||
|
||||
// use delegate to catch the case with multiple file lists
|
||||
fileList.$el.on('fileActionsReady', function(ev){
|
||||
var $files = ev.$files;
|
||||
fileList.$el.on('fileActionsReady', function(ev) {
|
||||
var $files = ev.$files
|
||||
|
||||
_.each($files, function(file) {
|
||||
var $tr = $(file);
|
||||
var shareTypes = $tr.attr('data-share-types') || '';
|
||||
var shareOwner = $tr.attr('data-share-owner');
|
||||
var $tr = $(file)
|
||||
var shareTypes = $tr.attr('data-share-types') || ''
|
||||
var shareOwner = $tr.attr('data-share-owner')
|
||||
if (shareTypes || shareOwner) {
|
||||
var hasLink = false;
|
||||
var hasShares = false;
|
||||
var hasLink = false
|
||||
var hasShares = false
|
||||
_.each(shareTypes.split(',') || [], function(shareType) {
|
||||
shareType = parseInt(shareType, 10);
|
||||
shareType = parseInt(shareType, 10)
|
||||
if (shareType === OC.Share.SHARE_TYPE_LINK) {
|
||||
hasLink = true;
|
||||
hasLink = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
hasLink = true;
|
||||
hasLink = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_USER) {
|
||||
hasShares = true;
|
||||
hasShares = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_GROUP) {
|
||||
hasShares = true;
|
||||
hasShares = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_REMOTE) {
|
||||
hasShares = true;
|
||||
hasShares = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_CIRCLE) {
|
||||
hasShares = true;
|
||||
hasShares = true
|
||||
} else if (shareType === OC.Share.SHARE_TYPE_ROOM) {
|
||||
hasShares = true;
|
||||
hasShares = true
|
||||
}
|
||||
});
|
||||
OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink);
|
||||
})
|
||||
OCA.Sharing.Util._updateFileActionIcon($tr, hasShares, hasLink)
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
fileList.$el.on('changeDirectory', function() {
|
||||
OCA.Sharing.sharesLoaded = false;
|
||||
});
|
||||
OCA.Sharing.sharesLoaded = false
|
||||
})
|
||||
|
||||
fileActions.registerAction({
|
||||
name: 'Share',
|
||||
|
@ -193,40 +193,40 @@
|
|||
type: OCA.Files.FileActions.TYPE_INLINE,
|
||||
actionHandler: function(fileName, context) {
|
||||
// do not open sidebar if permission is set and equal to 0
|
||||
var permissions = parseInt(context.$file.data('share-permissions'), 10);
|
||||
var permissions = parseInt(context.$file.data('share-permissions'), 10)
|
||||
if (isNaN(permissions) || permissions > 0) {
|
||||
fileList.showDetailsView(fileName, 'shareTabView');
|
||||
fileList.showDetailsView(fileName, 'shareTabView')
|
||||
}
|
||||
},
|
||||
render: function(actionSpec, isDefault, context) {
|
||||
var permissions = parseInt(context.$file.data('permissions'), 10);
|
||||
var permissions = parseInt(context.$file.data('permissions'), 10)
|
||||
// if no share permissions but share owner exists, still show the link
|
||||
if ((permissions & OC.PERMISSION_SHARE) !== 0 || context.$file.attr('data-share-owner')) {
|
||||
return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context);
|
||||
return fileActions._defaultRenderAction.call(fileActions, actionSpec, isDefault, context)
|
||||
}
|
||||
// don't render anything
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
var shareTab = new OCA.Sharing.ShareTabView('shareTabView', {order: -20});
|
||||
var shareTab = new OCA.Sharing.ShareTabView('shareTabView', { order: -20 })
|
||||
// detect changes and change the matching list entry
|
||||
shareTab.on('sharesChanged', function(shareModel) {
|
||||
var fileInfoModel = shareModel.fileInfoModel;
|
||||
var $tr = fileList.findFileEl(fileInfoModel.get('name'));
|
||||
var fileInfoModel = shareModel.fileInfoModel
|
||||
var $tr = fileList.findFileEl(fileInfoModel.get('name'))
|
||||
|
||||
// We count email shares as link share
|
||||
var hasLinkShares = shareModel.hasLinkShares();
|
||||
shareModel.get('shares').forEach(function (share) {
|
||||
var hasLinkShares = shareModel.hasLinkShares()
|
||||
shareModel.get('shares').forEach(function(share) {
|
||||
if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) {
|
||||
hasLinkShares = true;
|
||||
hasLinkShares = true
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel);
|
||||
OCA.Sharing.Util._updateFileListDataAttributes(fileList, $tr, shareModel)
|
||||
if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
|
||||
// remove icon, if applicable
|
||||
OC.Share.markFileAsShared($tr, false, false);
|
||||
OC.Share.markFileAsShared($tr, false, false)
|
||||
}
|
||||
|
||||
// FIXME: this is too convoluted. We need to get rid of the above updates
|
||||
|
@ -237,12 +237,12 @@
|
|||
// we need to modify the model
|
||||
// (FIXME: yes, this is hacky)
|
||||
icon: $tr.attr('data-icon')
|
||||
});
|
||||
});
|
||||
fileList.registerTabView(shareTab);
|
||||
})
|
||||
})
|
||||
fileList.registerTabView(shareTab)
|
||||
|
||||
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({shareTab: shareTab});
|
||||
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView);
|
||||
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
|
||||
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -252,18 +252,17 @@
|
|||
// files app current cannot show recipients on load, so we don't update the
|
||||
// icon when changed for consistency
|
||||
if (fileList.id === 'files') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname');
|
||||
var recipients = _.pluck(shareModel.get('shares'), 'share_with_displayname')
|
||||
// note: we only update the data attribute because updateIcon()
|
||||
if (recipients.length) {
|
||||
var recipientData = _.mapObject(shareModel.get('shares'), function (share) {
|
||||
return {shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname};
|
||||
});
|
||||
$tr.attr('data-share-recipient-data', JSON.stringify(recipientData));
|
||||
}
|
||||
else {
|
||||
$tr.removeAttr('data-share-recipient-data');
|
||||
var recipientData = _.mapObject(shareModel.get('shares'), function(share) {
|
||||
return { shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname }
|
||||
})
|
||||
$tr.attr('data-share-recipient-data', JSON.stringify(recipientData))
|
||||
} else {
|
||||
$tr.removeAttr('data-share-recipient-data')
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -274,16 +273,16 @@
|
|||
* @param {boolean} hasUserShares true if a user share exists
|
||||
* @param {boolean} hasLinkShares true if a link share exists
|
||||
*
|
||||
* @return {boolean} true if the icon was set, false otherwise
|
||||
* @returns {boolean} true if the icon was set, false otherwise
|
||||
*/
|
||||
_updateFileActionIcon: function($tr, hasUserShares, hasLinkShares) {
|
||||
// if the statuses are loaded already, use them for the icon
|
||||
// (needed when scrolling to the next page)
|
||||
if (hasUserShares || hasLinkShares || $tr.attr('data-share-recipient-data') || $tr.attr('data-share-owner')) {
|
||||
OC.Share.markFileAsShared($tr, true, hasLinkShares);
|
||||
return true;
|
||||
OC.Share.markFileAsShared($tr, true, hasLinkShares)
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -291,9 +290,9 @@
|
|||
* @returns {String}
|
||||
*/
|
||||
getSharePermissions: function(fileData) {
|
||||
return fileData.sharePermissions;
|
||||
return fileData.sharePermissions
|
||||
}
|
||||
};
|
||||
})();
|
||||
}
|
||||
})()
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Sharing.Util);
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Sharing.Util)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
/* global Handlebars, OC */
|
||||
|
||||
/**
|
||||
* @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
*
|
||||
|
@ -23,7 +21,7 @@
|
|||
*/
|
||||
|
||||
(function() {
|
||||
'use strict';
|
||||
'use strict'
|
||||
|
||||
var BreadCrumbView = OC.Backbone.View.extend({
|
||||
tagName: 'span',
|
||||
|
@ -36,68 +34,68 @@
|
|||
_shareTab: undefined,
|
||||
|
||||
initialize: function(options) {
|
||||
this._shareTab = options.shareTab;
|
||||
this._shareTab = options.shareTab
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
this._dirInfo = data.dirInfo || null;
|
||||
this._dirInfo = data.dirInfo || null
|
||||
|
||||
if (this._dirInfo !== null && (this._dirInfo.path !== '/' || this._dirInfo.name !== '')) {
|
||||
var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0;
|
||||
this.$el.removeClass('shared icon-public icon-shared');
|
||||
var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0
|
||||
this.$el.removeClass('shared icon-public icon-shared')
|
||||
if (isShared) {
|
||||
this.$el.addClass('shared');
|
||||
this.$el.addClass('shared')
|
||||
if (data.dirInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) !== -1) {
|
||||
this.$el.addClass('icon-public');
|
||||
this.$el.addClass('icon-public')
|
||||
} else {
|
||||
this.$el.addClass('icon-shared');
|
||||
this.$el.addClass('icon-shared')
|
||||
}
|
||||
} else {
|
||||
this.$el.addClass('icon-shared');
|
||||
this.$el.addClass('icon-shared')
|
||||
}
|
||||
this.$el.show();
|
||||
this.delegateEvents();
|
||||
this.$el.show()
|
||||
this.delegateEvents()
|
||||
} else {
|
||||
this.$el.removeClass('shared icon-public icon-shared');
|
||||
this.$el.hide();
|
||||
this.$el.removeClass('shared icon-public icon-shared')
|
||||
this.$el.hide()
|
||||
}
|
||||
|
||||
return this;
|
||||
return this
|
||||
},
|
||||
_onClick: function(e) {
|
||||
e.preventDefault();
|
||||
e.preventDefault()
|
||||
|
||||
var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo);
|
||||
var self = this;
|
||||
var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo)
|
||||
var self = this
|
||||
fileInfoModel.on('change', function() {
|
||||
self.render({
|
||||
dirInfo: self._dirInfo
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
this._shareTab.on('sharesChanged', function(shareModel) {
|
||||
var shareTypes = [];
|
||||
var shares = shareModel.getSharesWithCurrentItem();
|
||||
var shareTypes = []
|
||||
var shares = shareModel.getSharesWithCurrentItem()
|
||||
|
||||
for(var i = 0; i < shares.length; i++) {
|
||||
for (var i = 0; i < shares.length; i++) {
|
||||
if (shareTypes.indexOf(shares[i].share_type) === -1) {
|
||||
shareTypes.push(shares[i].share_type);
|
||||
shareTypes.push(shares[i].share_type)
|
||||
}
|
||||
}
|
||||
|
||||
if (shareModel.hasLinkShares()) {
|
||||
shareTypes.push(OC.Share.SHARE_TYPE_LINK);
|
||||
shareTypes.push(OC.Share.SHARE_TYPE_LINK)
|
||||
}
|
||||
|
||||
// Since the dirInfo isn't updated we need to do this dark hackery
|
||||
self._dirInfo.shareTypes = shareTypes;
|
||||
self._dirInfo.shareTypes = shareTypes
|
||||
|
||||
self.render({
|
||||
dirInfo: self._dirInfo
|
||||
});
|
||||
});
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView');
|
||||
})
|
||||
})
|
||||
OCA.Files.App.fileList.showDetailsView(fileInfoModel, 'shareTabView')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Sharing.ShareBreadCrumbView = BreadCrumbView;
|
||||
})();
|
||||
OCA.Sharing.ShareBreadCrumbView = BreadCrumbView
|
||||
})()
|
||||
|
|
|
@ -11,77 +11,77 @@
|
|||
/* @global Handlebars */
|
||||
|
||||
(function() {
|
||||
var TEMPLATE =
|
||||
'<div>' +
|
||||
'<div class="dialogContainer"></div>' +
|
||||
'<div id="collaborationResources"></div>' +
|
||||
'</div>';
|
||||
var TEMPLATE
|
||||
= '<div>'
|
||||
+ '<div class="dialogContainer"></div>'
|
||||
+ '<div id="collaborationResources"></div>'
|
||||
+ '</div>'
|
||||
|
||||
/**
|
||||
* @memberof OCA.Sharing
|
||||
*/
|
||||
var ShareTabView = OCA.Files.DetailTabView.extend(
|
||||
/** @lends OCA.Sharing.ShareTabView.prototype */ {
|
||||
id: 'shareTabView',
|
||||
className: 'tab shareTabView',
|
||||
id: 'shareTabView',
|
||||
className: 'tab shareTabView',
|
||||
|
||||
initialize: function(name, options) {
|
||||
OCA.Files.DetailTabView.prototype.initialize.call(this, name, options);
|
||||
OC.Plugins.attach('OCA.Sharing.ShareTabView', this);
|
||||
},
|
||||
initialize: function(name, options) {
|
||||
OCA.Files.DetailTabView.prototype.initialize.call(this, name, options)
|
||||
OC.Plugins.attach('OCA.Sharing.ShareTabView', this)
|
||||
},
|
||||
|
||||
template: function(params) {
|
||||
return TEMPLATE;
|
||||
},
|
||||
template: function(params) {
|
||||
return TEMPLATE
|
||||
},
|
||||
|
||||
getLabel: function() {
|
||||
return t('files_sharing', 'Sharing');
|
||||
},
|
||||
getLabel: function() {
|
||||
return t('files_sharing', 'Sharing')
|
||||
},
|
||||
|
||||
getIcon: function() {
|
||||
return 'icon-shared';
|
||||
},
|
||||
getIcon: function() {
|
||||
return 'icon-shared'
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Renders this details view
|
||||
*/
|
||||
render: function() {
|
||||
var self = this;
|
||||
if (this._dialog) {
|
||||
render: function() {
|
||||
var self = this
|
||||
if (this._dialog) {
|
||||
// remove/destroy older instance
|
||||
this._dialog.model.off();
|
||||
this._dialog.remove();
|
||||
this._dialog = null;
|
||||
}
|
||||
|
||||
if (this.model) {
|
||||
this.$el.html(this.template());
|
||||
|
||||
if (_.isUndefined(this.model.get('sharePermissions'))) {
|
||||
this.model.set('sharePermissions', OCA.Sharing.Util.getSharePermissions(this.model.attributes));
|
||||
this._dialog.model.off()
|
||||
this._dialog.remove()
|
||||
this._dialog = null
|
||||
}
|
||||
|
||||
// TODO: the model should read these directly off the passed fileInfoModel
|
||||
var attributes = {
|
||||
itemType: this.model.isDirectory() ? 'folder' : 'file',
|
||||
itemSource: this.model.get('id'),
|
||||
possiblePermissions: this.model.get('sharePermissions')
|
||||
};
|
||||
var configModel = new OC.Share.ShareConfigModel();
|
||||
var shareModel = new OC.Share.ShareItemModel(attributes, {
|
||||
configModel: configModel,
|
||||
fileInfoModel: this.model
|
||||
});
|
||||
this._dialog = new OC.Share.ShareDialogView({
|
||||
configModel: configModel,
|
||||
model: shareModel
|
||||
});
|
||||
this.$el.find('.dialogContainer').append(this._dialog.$el);
|
||||
this._dialog.render();
|
||||
this._dialog.model.fetch();
|
||||
this._dialog.model.on('change', function() {
|
||||
self.trigger('sharesChanged', shareModel);
|
||||
});
|
||||
if (this.model) {
|
||||
this.$el.html(this.template())
|
||||
|
||||
if (_.isUndefined(this.model.get('sharePermissions'))) {
|
||||
this.model.set('sharePermissions', OCA.Sharing.Util.getSharePermissions(this.model.attributes))
|
||||
}
|
||||
|
||||
// TODO: the model should read these directly off the passed fileInfoModel
|
||||
var attributes = {
|
||||
itemType: this.model.isDirectory() ? 'folder' : 'file',
|
||||
itemSource: this.model.get('id'),
|
||||
possiblePermissions: this.model.get('sharePermissions')
|
||||
}
|
||||
var configModel = new OC.Share.ShareConfigModel()
|
||||
var shareModel = new OC.Share.ShareItemModel(attributes, {
|
||||
configModel: configModel,
|
||||
fileInfoModel: this.model
|
||||
})
|
||||
this._dialog = new OC.Share.ShareDialogView({
|
||||
configModel: configModel,
|
||||
model: shareModel
|
||||
})
|
||||
this.$el.find('.dialogContainer').append(this._dialog.$el)
|
||||
this._dialog.render()
|
||||
this._dialog.model.fetch()
|
||||
this._dialog.model.on('change', function() {
|
||||
self.trigger('sharesChanged', shareModel)
|
||||
})
|
||||
|
||||
import('./collaborationresources').then((Resources) => {
|
||||
var vm = new Resources.Vue({
|
||||
|
@ -89,20 +89,19 @@
|
|||
render: h => h(Resources.View),
|
||||
data: {
|
||||
model: this.model.toJSON()
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
this.model.on('change', () => { vm.data = this.model.toJSON() })
|
||||
|
||||
})
|
||||
|
||||
} else {
|
||||
this.$el.empty();
|
||||
} else {
|
||||
this.$el.empty()
|
||||
// TODO: render placeholder text?
|
||||
}
|
||||
this.trigger('rendered')
|
||||
}
|
||||
this.trigger('rendered');
|
||||
}
|
||||
});
|
||||
|
||||
OCA.Sharing.ShareTabView = ShareTabView;
|
||||
})();
|
||||
})
|
||||
|
||||
OCA.Sharing.ShareTabView = ShareTabView
|
||||
})()
|
||||
|
|
|
@ -21,7 +21,10 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<collection-list v-if="fileId" type="file" :id="fileId" :name="filename"></collection-list>
|
||||
<CollectionList v-if="fileId"
|
||||
:id="fileId"
|
||||
type="file"
|
||||
:name="filename" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -29,22 +32,22 @@ import { CollectionList } from 'nextcloud-vue-collections'
|
|||
|
||||
export default {
|
||||
name: 'CollaborationView',
|
||||
components: {
|
||||
CollectionList
|
||||
},
|
||||
computed: {
|
||||
fileId() {
|
||||
if (this.$root.model && this.$root.model.id) {
|
||||
return '' + this.$root.model.id;
|
||||
return '' + this.$root.model.id
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
},
|
||||
filename() {
|
||||
if (this.$root.model && this.$root.model.name) {
|
||||
return '' + this.$root.model.name;
|
||||
return '' + this.$root.model.name
|
||||
}
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CollectionList
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* Copyright (c) 2014
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
|
@ -11,7 +11,7 @@
|
|||
/**
|
||||
* @namespace OCA.Trashbin
|
||||
*/
|
||||
OCA.Trashbin = {};
|
||||
OCA.Trashbin = {}
|
||||
/**
|
||||
* @namespace OCA.Trashbin.App
|
||||
*/
|
||||
|
@ -20,19 +20,19 @@ OCA.Trashbin.App = {
|
|||
/** @type {OC.Files.Client} */
|
||||
client: null,
|
||||
|
||||
initialize: function ($el) {
|
||||
initialize: function($el) {
|
||||
if (this._initialized) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this._initialized = true;
|
||||
this._initialized = true
|
||||
|
||||
this.client = new OC.Files.Client({
|
||||
host: OC.getHost(),
|
||||
port: OC.getPort(),
|
||||
root: OC.linkToRemoteBase('dav') + '/trashbin/' + OC.getCurrentUser().uid,
|
||||
useHTTPS: OC.getProtocol() === 'https'
|
||||
});
|
||||
var urlParams = OC.Util.History.parseUrlQuery();
|
||||
})
|
||||
var urlParams = OC.Util.History.parseUrlQuery()
|
||||
this.fileList = new OCA.Trashbin.FileList(
|
||||
$('#app-content-trashbin'), {
|
||||
fileActions: this._createFileActions(),
|
||||
|
@ -43,12 +43,12 @@ OCA.Trashbin.App = {
|
|||
{
|
||||
name: 'restore',
|
||||
displayName: t('files_trashbin', 'Restore'),
|
||||
iconClass: 'icon-history',
|
||||
iconClass: 'icon-history'
|
||||
},
|
||||
{
|
||||
name: 'delete',
|
||||
displayName: t('files_trashbin', 'Delete permanently'),
|
||||
iconClass: 'icon-delete',
|
||||
iconClass: 'icon-delete'
|
||||
}
|
||||
],
|
||||
client: this.client,
|
||||
|
@ -57,18 +57,18 @@ OCA.Trashbin.App = {
|
|||
// if handling the event with the file list already created.
|
||||
shown: true
|
||||
}
|
||||
);
|
||||
)
|
||||
},
|
||||
|
||||
_createFileActions: function () {
|
||||
var client = this.client;
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
|
||||
var dir = context.fileList.getCurrentDirectory();
|
||||
context.fileList.changeDirectory(OC.joinPaths(dir, filename));
|
||||
});
|
||||
_createFileActions: function() {
|
||||
var client = this.client
|
||||
var fileActions = new OCA.Files.FileActions()
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
|
||||
var dir = context.fileList.getCurrentDirectory()
|
||||
context.fileList.changeDirectory(OC.joinPaths(dir, filename))
|
||||
})
|
||||
|
||||
fileActions.setDefault('dir', 'Open');
|
||||
fileActions.setDefault('dir', 'Open')
|
||||
|
||||
fileActions.registerAction({
|
||||
name: 'Restore',
|
||||
|
@ -77,21 +77,21 @@ OCA.Trashbin.App = {
|
|||
mime: 'all',
|
||||
permissions: OC.PERMISSION_READ,
|
||||
iconClass: 'icon-history',
|
||||
actionHandler: function (filename, context) {
|
||||
var fileList = context.fileList;
|
||||
var tr = fileList.findFileEl(filename);
|
||||
fileList.showFileBusyState(tr, true);
|
||||
var dir = context.fileList.getCurrentDirectory();
|
||||
actionHandler: function(filename, context) {
|
||||
var fileList = context.fileList
|
||||
var tr = fileList.findFileEl(filename)
|
||||
fileList.showFileBusyState(tr, true)
|
||||
var dir = context.fileList.getCurrentDirectory()
|
||||
client.move(OC.joinPaths('trash', dir, filename), OC.joinPaths('restore', filename), true)
|
||||
.then(
|
||||
fileList._removeCallback.bind(fileList, [filename]),
|
||||
function () {
|
||||
fileList.showFileBusyState(tr, false);
|
||||
OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'));
|
||||
function() {
|
||||
fileList.showFileBusyState(tr, false)
|
||||
OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'))
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
fileActions.registerAction({
|
||||
name: 'Delete',
|
||||
|
@ -99,39 +99,38 @@ OCA.Trashbin.App = {
|
|||
mime: 'all',
|
||||
permissions: OC.PERMISSION_READ,
|
||||
iconClass: 'icon-delete',
|
||||
render: function (actionSpec, isDefault, context) {
|
||||
var $actionLink = fileActions._makeActionLink(actionSpec, context);
|
||||
$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'));
|
||||
$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'));
|
||||
context.$file.find('td:last').append($actionLink);
|
||||
return $actionLink;
|
||||
render: function(actionSpec, isDefault, context) {
|
||||
var $actionLink = fileActions._makeActionLink(actionSpec, context)
|
||||
$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'))
|
||||
$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'))
|
||||
context.$file.find('td:last').append($actionLink)
|
||||
return $actionLink
|
||||
},
|
||||
actionHandler: function (filename, context) {
|
||||
var fileList = context.fileList;
|
||||
$('.tipsy').remove();
|
||||
var tr = fileList.findFileEl(filename);
|
||||
fileList.showFileBusyState(tr, true);
|
||||
var dir = context.fileList.getCurrentDirectory();
|
||||
actionHandler: function(filename, context) {
|
||||
var fileList = context.fileList
|
||||
$('.tipsy').remove()
|
||||
var tr = fileList.findFileEl(filename)
|
||||
fileList.showFileBusyState(tr, true)
|
||||
var dir = context.fileList.getCurrentDirectory()
|
||||
client.remove(OC.joinPaths('trash', dir, filename))
|
||||
.then(
|
||||
fileList._removeCallback.bind(fileList, [filename]),
|
||||
function () {
|
||||
fileList.showFileBusyState(tr, false);
|
||||
OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'));
|
||||
function() {
|
||||
fileList.showFileBusyState(tr, false)
|
||||
OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'))
|
||||
}
|
||||
);
|
||||
)
|
||||
}
|
||||
});
|
||||
return fileActions;
|
||||
})
|
||||
return fileActions
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
$('#app-content-trashbin').one('show', function () {
|
||||
var App = OCA.Trashbin.App;
|
||||
App.initialize($('#app-content-trashbin'));
|
||||
$(document).ready(function() {
|
||||
$('#app-content-trashbin').one('show', function() {
|
||||
var App = OCA.Trashbin.App
|
||||
App.initialize($('#app-content-trashbin'))
|
||||
// force breadcrumb init
|
||||
// App.fileList.changeDirectory(App.fileList.getCurrentDirectory(), false, true);
|
||||
});
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable */
|
||||
/*
|
||||
* Copyright (c) 2014
|
||||
*
|
||||
|
@ -8,25 +9,25 @@
|
|||
*
|
||||
*/
|
||||
(function() {
|
||||
var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/);
|
||||
var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename';
|
||||
var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time';
|
||||
var TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location';
|
||||
var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/)
|
||||
var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename'
|
||||
var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time'
|
||||
var TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location'
|
||||
|
||||
/**
|
||||
* Convert a file name in the format filename.d12345 to the real file name.
|
||||
* This will use basename.
|
||||
* The name will not be changed if it has no ".d12345" suffix.
|
||||
* @param {String} name file name
|
||||
* @return {String} converted file name
|
||||
* @returns {String} converted file name
|
||||
*/
|
||||
function getDeletedFileName(name) {
|
||||
name = OC.basename(name);
|
||||
var match = DELETED_REGEXP.exec(name);
|
||||
name = OC.basename(name)
|
||||
var match = DELETED_REGEXP.exec(name)
|
||||
if (match && match.length > 1) {
|
||||
name = match[1];
|
||||
name = match[1]
|
||||
}
|
||||
return name;
|
||||
return name
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,283 +40,282 @@
|
|||
* @param [options] map of options
|
||||
*/
|
||||
var FileList = function($el, options) {
|
||||
this.client = options.client;
|
||||
this.initialize($el, options);
|
||||
};
|
||||
this.client = options.client
|
||||
this.initialize($el, options)
|
||||
}
|
||||
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
|
||||
/** @lends OCA.Trashbin.FileList.prototype */ {
|
||||
id: 'trashbin',
|
||||
appName: t('files_trashbin', 'Deleted files'),
|
||||
/** @type {OC.Files.Client} */
|
||||
client: null,
|
||||
id: 'trashbin',
|
||||
appName: t('files_trashbin', 'Deleted files'),
|
||||
/** @type {OC.Files.Client} */
|
||||
client: null,
|
||||
|
||||
/**
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initialize: function() {
|
||||
this.client.addFileInfoParser(function(response, data) {
|
||||
var props = response.propStat[0].properties;
|
||||
var path = props[TRASHBIN_ORIGINAL_LOCATION];
|
||||
return {
|
||||
displayName: props[FILENAME_PROP],
|
||||
mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
|
||||
hasPreview: true,
|
||||
path: path,
|
||||
extraData: path
|
||||
}
|
||||
});
|
||||
initialize: function() {
|
||||
this.client.addFileInfoParser(function(response, data) {
|
||||
var props = response.propStat[0].properties
|
||||
var path = props[TRASHBIN_ORIGINAL_LOCATION]
|
||||
return {
|
||||
displayName: props[FILENAME_PROP],
|
||||
mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
|
||||
hasPreview: true,
|
||||
path: path,
|
||||
extraData: path
|
||||
}
|
||||
})
|
||||
|
||||
var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments);
|
||||
this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this));
|
||||
var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments)
|
||||
this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this))
|
||||
|
||||
this.setSort('mtime', 'desc');
|
||||
/**
|
||||
this.setSort('mtime', 'desc')
|
||||
/**
|
||||
* Override crumb making to add "Deleted Files" entry
|
||||
* and convert files with ".d" extensions to a more
|
||||
* user friendly name.
|
||||
*/
|
||||
this.breadcrumb._makeCrumbs = function() {
|
||||
var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments);
|
||||
for (var i = 1; i < parts.length; i++) {
|
||||
parts[i].name = getDeletedFileName(parts[i].name);
|
||||
this.breadcrumb._makeCrumbs = function() {
|
||||
var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments)
|
||||
for (var i = 1; i < parts.length; i++) {
|
||||
parts[i].name = getDeletedFileName(parts[i].name)
|
||||
}
|
||||
return parts
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
OC.Plugins.attach('OCA.Trashbin.FileList', this);
|
||||
return result;
|
||||
},
|
||||
OC.Plugins.attach('OCA.Trashbin.FileList', this)
|
||||
return result
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Override to only return read permissions
|
||||
*/
|
||||
getDirectoryPermissions: function() {
|
||||
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
|
||||
},
|
||||
getDirectoryPermissions: function() {
|
||||
return OC.PERMISSION_READ | OC.PERMISSION_DELETE
|
||||
},
|
||||
|
||||
_setCurrentDir: function(targetDir) {
|
||||
OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments);
|
||||
_setCurrentDir: function(targetDir) {
|
||||
OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments)
|
||||
|
||||
var baseDir = OC.basename(targetDir);
|
||||
if (baseDir !== '') {
|
||||
this.setPageTitle(getDeletedFileName(baseDir));
|
||||
}
|
||||
},
|
||||
|
||||
_createRow: function() {
|
||||
// FIXME: MEGAHACK until we find a better solution
|
||||
var tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
|
||||
tr.find('td.filesize').remove();
|
||||
return tr;
|
||||
},
|
||||
|
||||
getAjaxUrl: function(action, params) {
|
||||
var q = '';
|
||||
if (params) {
|
||||
q = '?' + OC.buildQueryString(params);
|
||||
}
|
||||
return OC.filePath('files_trashbin', 'ajax', action + '.php') + q;
|
||||
},
|
||||
|
||||
setupUploadEvents: function() {
|
||||
// override and do nothing
|
||||
},
|
||||
|
||||
linkTo: function(dir){
|
||||
return OC.linkTo('files', 'index.php')+"?view=trashbin&dir="+ encodeURIComponent(dir).replace(/%2F/g, '/');
|
||||
},
|
||||
|
||||
elementToFile: function($el) {
|
||||
var fileInfo = OCA.Files.FileList.prototype.elementToFile($el);
|
||||
if (this.getCurrentDirectory() === '/') {
|
||||
fileInfo.displayName = getDeletedFileName(fileInfo.name);
|
||||
}
|
||||
// no size available
|
||||
delete fileInfo.size;
|
||||
return fileInfo;
|
||||
},
|
||||
|
||||
updateEmptyContent: function(){
|
||||
var exists = this.$fileList.find('tr:first').exists();
|
||||
this.$el.find('#emptycontent').toggleClass('hidden', exists);
|
||||
this.$el.find('#filestable th').toggleClass('hidden', !exists);
|
||||
},
|
||||
|
||||
_removeCallback: function(files) {
|
||||
var $el;
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
$el = this.remove(OC.basename(files[i]), {updateSummary: false});
|
||||
this.fileSummary.remove({type: $el.attr('data-type'), size: $el.attr('data-size')});
|
||||
}
|
||||
this.fileSummary.update();
|
||||
this.updateEmptyContent();
|
||||
},
|
||||
|
||||
_onClickRestoreSelected: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var files = _.pluck(this.getSelectedFiles(), 'name');
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var tr = this.findFileEl(files[i]);
|
||||
this.showFileBusyState(tr, true);
|
||||
}
|
||||
|
||||
this.fileMultiSelectMenu.toggleLoading('restore', true);
|
||||
var restorePromises = files.map(function(file) {
|
||||
return self.client.move(OC.joinPaths('trash', self.getCurrentDirectory(), file), OC.joinPaths('restore', file), true)
|
||||
.then(
|
||||
function() {
|
||||
self._removeCallback([file]);
|
||||
}
|
||||
);
|
||||
});
|
||||
return Promise.all(restorePromises).then(
|
||||
function() {
|
||||
self.fileMultiSelectMenu.toggleLoading('restore', false);
|
||||
},
|
||||
function() {
|
||||
OC.Notification.show(t('files_trashbin', 'Error while restoring files from trashbin'));
|
||||
var baseDir = OC.basename(targetDir)
|
||||
if (baseDir !== '') {
|
||||
this.setPageTitle(getDeletedFileName(baseDir))
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
_onClickDeleteSelected: function(event) {
|
||||
event.preventDefault();
|
||||
var self = this;
|
||||
var allFiles = this.$el.find('.select-all').is(':checked');
|
||||
var files = _.pluck(this.getSelectedFiles(), 'name');
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var tr = this.findFileEl(files[i]);
|
||||
this.showFileBusyState(tr, true);
|
||||
}
|
||||
_createRow: function() {
|
||||
// FIXME: MEGAHACK until we find a better solution
|
||||
var tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
|
||||
tr.find('td.filesize').remove()
|
||||
return tr
|
||||
},
|
||||
|
||||
if (allFiles) {
|
||||
return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory()))
|
||||
.then(
|
||||
function() {
|
||||
self.hideMask();
|
||||
self.setFiles([]);
|
||||
},
|
||||
function() {
|
||||
OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin'));
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.fileMultiSelectMenu.toggleLoading('delete', true);
|
||||
var deletePromises = files.map(function(file) {
|
||||
return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file))
|
||||
getAjaxUrl: function(action, params) {
|
||||
var q = ''
|
||||
if (params) {
|
||||
q = '?' + OC.buildQueryString(params)
|
||||
}
|
||||
return OC.filePath('files_trashbin', 'ajax', action + '.php') + q
|
||||
},
|
||||
|
||||
setupUploadEvents: function() {
|
||||
// override and do nothing
|
||||
},
|
||||
|
||||
linkTo: function(dir) {
|
||||
return OC.linkTo('files', 'index.php') + '?view=trashbin&dir=' + encodeURIComponent(dir).replace(/%2F/g, '/')
|
||||
},
|
||||
|
||||
elementToFile: function($el) {
|
||||
var fileInfo = OCA.Files.FileList.prototype.elementToFile($el)
|
||||
if (this.getCurrentDirectory() === '/') {
|
||||
fileInfo.displayName = getDeletedFileName(fileInfo.name)
|
||||
}
|
||||
// no size available
|
||||
delete fileInfo.size
|
||||
return fileInfo
|
||||
},
|
||||
|
||||
updateEmptyContent: function() {
|
||||
var exists = this.$fileList.find('tr:first').exists()
|
||||
this.$el.find('#emptycontent').toggleClass('hidden', exists)
|
||||
this.$el.find('#filestable th').toggleClass('hidden', !exists)
|
||||
},
|
||||
|
||||
_removeCallback: function(files) {
|
||||
var $el
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
$el = this.remove(OC.basename(files[i]), { updateSummary: false })
|
||||
this.fileSummary.remove({ type: $el.attr('data-type'), size: $el.attr('data-size') })
|
||||
}
|
||||
this.fileSummary.update()
|
||||
this.updateEmptyContent()
|
||||
},
|
||||
|
||||
_onClickRestoreSelected: function(event) {
|
||||
event.preventDefault()
|
||||
var self = this
|
||||
var files = _.pluck(this.getSelectedFiles(), 'name')
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var tr = this.findFileEl(files[i])
|
||||
this.showFileBusyState(tr, true)
|
||||
}
|
||||
|
||||
this.fileMultiSelectMenu.toggleLoading('restore', true)
|
||||
var restorePromises = files.map(function(file) {
|
||||
return self.client.move(OC.joinPaths('trash', self.getCurrentDirectory(), file), OC.joinPaths('restore', file), true)
|
||||
.then(
|
||||
function() {
|
||||
self._removeCallback([file]);
|
||||
self._removeCallback([file])
|
||||
}
|
||||
);
|
||||
});
|
||||
return Promise.all(deletePromises).then(
|
||||
)
|
||||
})
|
||||
return Promise.all(restorePromises).then(
|
||||
function() {
|
||||
self.fileMultiSelectMenu.toggleLoading('delete', false);
|
||||
self.fileMultiSelectMenu.toggleLoading('restore', false)
|
||||
},
|
||||
function() {
|
||||
OC.Notification.show(t('files_trashbin', 'Error while removing files from trashbin'));
|
||||
OC.Notification.show(t('files_trashbin', 'Error while restoring files from trashbin'))
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
_onClickFile: function(event) {
|
||||
var mime = $(this).parent().parent().data('mime');
|
||||
if (mime !== 'httpd/unix-directory') {
|
||||
event.preventDefault();
|
||||
}
|
||||
return OCA.Files.FileList.prototype._onClickFile.apply(this, arguments);
|
||||
},
|
||||
_onClickDeleteSelected: function(event) {
|
||||
event.preventDefault()
|
||||
var self = this
|
||||
var allFiles = this.$el.find('.select-all').is(':checked')
|
||||
var files = _.pluck(this.getSelectedFiles(), 'name')
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
var tr = this.findFileEl(files[i])
|
||||
this.showFileBusyState(tr, true)
|
||||
}
|
||||
|
||||
generatePreviewUrl: function(urlSpec) {
|
||||
return OC.generateUrl('/apps/files_trashbin/preview?') + $.param(urlSpec);
|
||||
},
|
||||
if (allFiles) {
|
||||
return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory()))
|
||||
.then(
|
||||
function() {
|
||||
self.hideMask()
|
||||
self.setFiles([])
|
||||
},
|
||||
function() {
|
||||
OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin'))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.fileMultiSelectMenu.toggleLoading('delete', true)
|
||||
var deletePromises = files.map(function(file) {
|
||||
return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file))
|
||||
.then(
|
||||
function() {
|
||||
self._removeCallback([file])
|
||||
}
|
||||
)
|
||||
})
|
||||
return Promise.all(deletePromises).then(
|
||||
function() {
|
||||
self.fileMultiSelectMenu.toggleLoading('delete', false)
|
||||
},
|
||||
function() {
|
||||
OC.Notification.show(t('files_trashbin', 'Error while removing files from trashbin'))
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
getDownloadUrl: function() {
|
||||
_onClickFile: function(event) {
|
||||
var mime = $(this).parent().parent().data('mime')
|
||||
if (mime !== 'httpd/unix-directory') {
|
||||
event.preventDefault()
|
||||
}
|
||||
return OCA.Files.FileList.prototype._onClickFile.apply(this, arguments)
|
||||
},
|
||||
|
||||
generatePreviewUrl: function(urlSpec) {
|
||||
return OC.generateUrl('/apps/files_trashbin/preview?') + $.param(urlSpec)
|
||||
},
|
||||
|
||||
getDownloadUrl: function() {
|
||||
// no downloads
|
||||
return '#';
|
||||
},
|
||||
return '#'
|
||||
},
|
||||
|
||||
updateStorageStatistics: function() {
|
||||
updateStorageStatistics: function() {
|
||||
// no op because the trashbin doesn't have
|
||||
// storage info like free space / used space
|
||||
},
|
||||
},
|
||||
|
||||
isSelectedDeletable: function() {
|
||||
return true;
|
||||
},
|
||||
isSelectedDeletable: function() {
|
||||
return true
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Returns list of webdav properties to request
|
||||
*/
|
||||
_getWebdavProperties: function() {
|
||||
return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties());
|
||||
},
|
||||
_getWebdavProperties: function() {
|
||||
return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties())
|
||||
},
|
||||
|
||||
/**
|
||||
/**
|
||||
* Reloads the file list using ajax call
|
||||
*
|
||||
* @return ajax call object
|
||||
* @returns ajax call object
|
||||
*/
|
||||
reload: function() {
|
||||
this._selectedFiles = {};
|
||||
this._selectionSummary.clear();
|
||||
this.$el.find('.select-all').prop('checked', false);
|
||||
this.showMask();
|
||||
if (this._reloadCall) {
|
||||
this._reloadCall.abort();
|
||||
}
|
||||
this._reloadCall = this.client.getFolderContents(
|
||||
'trash/' + this.getCurrentDirectory(), {
|
||||
includeParent: false,
|
||||
properties: this._getWebdavProperties()
|
||||
reload: function() {
|
||||
this._selectedFiles = {}
|
||||
this._selectionSummary.clear()
|
||||
this.$el.find('.select-all').prop('checked', false)
|
||||
this.showMask()
|
||||
if (this._reloadCall) {
|
||||
this._reloadCall.abort()
|
||||
}
|
||||
);
|
||||
var callBack = this.reloadCallback.bind(this);
|
||||
return this._reloadCall.then(callBack, callBack);
|
||||
},
|
||||
reloadCallback: function(status, result) {
|
||||
delete this._reloadCall;
|
||||
this.hideMask();
|
||||
this._reloadCall = this.client.getFolderContents(
|
||||
'trash/' + this.getCurrentDirectory(), {
|
||||
includeParent: false,
|
||||
properties: this._getWebdavProperties()
|
||||
}
|
||||
)
|
||||
var callBack = this.reloadCallback.bind(this)
|
||||
return this._reloadCall.then(callBack, callBack)
|
||||
},
|
||||
reloadCallback: function(status, result) {
|
||||
delete this._reloadCall
|
||||
this.hideMask()
|
||||
|
||||
if (status === 401) {
|
||||
return false;
|
||||
}
|
||||
if (status === 401) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Firewall Blocked request?
|
||||
if (status === 403) {
|
||||
// Firewall Blocked request?
|
||||
if (status === 403) {
|
||||
// Go home
|
||||
this.changeDirectory('/');
|
||||
OC.Notification.show(t('files', 'This operation is forbidden'));
|
||||
return false;
|
||||
}
|
||||
this.changeDirectory('/')
|
||||
OC.Notification.show(t('files', 'This operation is forbidden'))
|
||||
return false
|
||||
}
|
||||
|
||||
// Did share service die or something else fail?
|
||||
if (status === 500) {
|
||||
// Did share service die or something else fail?
|
||||
if (status === 500) {
|
||||
// Go home
|
||||
this.changeDirectory('/');
|
||||
OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'));
|
||||
return false;
|
||||
}
|
||||
this.changeDirectory('/')
|
||||
OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'))
|
||||
return false
|
||||
}
|
||||
|
||||
if (status === 404) {
|
||||
if (status === 404) {
|
||||
// go back home
|
||||
this.changeDirectory('/');
|
||||
return false;
|
||||
}
|
||||
// aborted ?
|
||||
if (status === 0){
|
||||
return true;
|
||||
this.changeDirectory('/')
|
||||
return false
|
||||
}
|
||||
// aborted ?
|
||||
if (status === 0) {
|
||||
return true
|
||||
}
|
||||
|
||||
this.setFiles(result)
|
||||
return true
|
||||
}
|
||||
|
||||
this.setFiles(result);
|
||||
return true;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
OCA.Trashbin.FileList = FileList;
|
||||
})();
|
||||
})
|
||||
|
||||
OCA.Trashbin.FileList = FileList
|
||||
})()
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -9,7 +9,7 @@
|
|||
*/
|
||||
|
||||
(function() {
|
||||
OCA.Versions = OCA.Versions || {};
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
|
@ -22,13 +22,12 @@
|
|||
*/
|
||||
attach: function(fileList) {
|
||||
if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', {order: -10}));
|
||||
fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', { order: -10 }))
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util);
|
||||
}
|
||||
})()
|
||||
|
||||
OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util)
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
(function () {
|
||||
(function() {
|
||||
/**
|
||||
* @memberof OCA.Versions
|
||||
*/
|
||||
|
@ -25,24 +25,24 @@
|
|||
|
||||
_client: null,
|
||||
|
||||
setFileInfo: function (fileInfo) {
|
||||
this._fileInfo = fileInfo;
|
||||
setFileInfo: function(fileInfo) {
|
||||
this._fileInfo = fileInfo
|
||||
},
|
||||
|
||||
getFileInfo: function () {
|
||||
return this._fileInfo;
|
||||
getFileInfo: function() {
|
||||
return this._fileInfo
|
||||
},
|
||||
|
||||
setCurrentUser: function(user) {
|
||||
this._currentUser = user;
|
||||
this._currentUser = user
|
||||
},
|
||||
|
||||
getCurrentUser: function() {
|
||||
return this._currentUser || OC.getCurrentUser().uid;
|
||||
return this._currentUser || OC.getCurrentUser().uid
|
||||
},
|
||||
|
||||
setClient: function(client) {
|
||||
this._client = client;
|
||||
this._client = client
|
||||
},
|
||||
|
||||
getClient: function() {
|
||||
|
@ -50,35 +50,34 @@
|
|||
host: OC.getHost(),
|
||||
root: OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser(),
|
||||
useHTTPS: OC.getProtocol() === 'https'
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
url: function () {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id');
|
||||
url: function() {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id')
|
||||
},
|
||||
|
||||
parse: function(result) {
|
||||
var fullPath = this._fileInfo.getFullPath();
|
||||
var fileId = this._fileInfo.get('id');
|
||||
var name = this._fileInfo.get('name');
|
||||
var user = this.getCurrentUser();
|
||||
var client = this.getClient();
|
||||
var fullPath = this._fileInfo.getFullPath()
|
||||
var fileId = this._fileInfo.get('id')
|
||||
var name = this._fileInfo.get('name')
|
||||
var user = this.getCurrentUser()
|
||||
var client = this.getClient()
|
||||
return _.map(result, function(version) {
|
||||
version.fullPath = fullPath;
|
||||
version.fileId = fileId;
|
||||
version.name = name;
|
||||
version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10);
|
||||
version.id = OC.basename(version.href);
|
||||
version.size = parseInt(version.size, 10);
|
||||
version.user = user;
|
||||
version.client = client;
|
||||
return version;
|
||||
});
|
||||
version.fullPath = fullPath
|
||||
version.fileId = fileId
|
||||
version.name = name
|
||||
version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10)
|
||||
version.id = OC.basename(version.href)
|
||||
version.size = parseInt(version.size, 10)
|
||||
version.user = user
|
||||
version.client = client
|
||||
return version
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {};
|
||||
|
||||
OCA.Versions.VersionCollection = VersionCollection;
|
||||
})();
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionCollection = VersionCollection
|
||||
})()
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
/* global moment */
|
||||
|
||||
(function () {
|
||||
(function() {
|
||||
/**
|
||||
* @memberof OCA.Versions
|
||||
*/
|
||||
|
@ -20,53 +18,55 @@
|
|||
davProperties: {
|
||||
'size': '{DAV:}getcontentlength',
|
||||
'mimetype': '{DAV:}getcontenttype',
|
||||
'timestamp': '{DAV:}getlastmodified',
|
||||
'timestamp': '{DAV:}getlastmodified'
|
||||
},
|
||||
|
||||
/**
|
||||
* Restores the original file to this revision
|
||||
*
|
||||
* @param {Object} [options] options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
revert: function (options) {
|
||||
options = options ? _.clone(options) : {};
|
||||
var model = this;
|
||||
revert: function(options) {
|
||||
options = options ? _.clone(options) : {}
|
||||
var model = this
|
||||
|
||||
var client = this.get('client');
|
||||
var client = this.get('client')
|
||||
|
||||
return client.move('/versions/' + this.get('fileId') + '/' + this.get('id'), '/restore/target', true)
|
||||
.done(function () {
|
||||
.done(function() {
|
||||
if (options.success) {
|
||||
options.success.call(options.context, model, {}, options);
|
||||
options.success.call(options.context, model, {}, options)
|
||||
}
|
||||
model.trigger('revert', model, options);
|
||||
model.trigger('revert', model, options)
|
||||
})
|
||||
.fail(function () {
|
||||
.fail(function() {
|
||||
if (options.error) {
|
||||
options.error.call(options.context, model, {}, options);
|
||||
options.error.call(options.context, model, {}, options)
|
||||
}
|
||||
model.trigger('error', model, {}, options);
|
||||
});
|
||||
model.trigger('error', model, {}, options)
|
||||
})
|
||||
},
|
||||
|
||||
getFullPath: function () {
|
||||
return this.get('fullPath');
|
||||
getFullPath: function() {
|
||||
return this.get('fullPath')
|
||||
},
|
||||
|
||||
getPreviewUrl: function () {
|
||||
var url = OC.generateUrl('/apps/files_versions/preview');
|
||||
getPreviewUrl: function() {
|
||||
var url = OC.generateUrl('/apps/files_versions/preview')
|
||||
var params = {
|
||||
file: this.get('fullPath'),
|
||||
version: this.get('id')
|
||||
};
|
||||
return url + '?' + OC.buildQueryString(params);
|
||||
}
|
||||
return url + '?' + OC.buildQueryString(params)
|
||||
},
|
||||
|
||||
getDownloadUrl: function () {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id');
|
||||
getDownloadUrl: function() {
|
||||
return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id')
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {};
|
||||
|
||||
OCA.Versions.VersionModel = VersionModel;
|
||||
})();
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionModel = VersionModel
|
||||
})()
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import ItemTemplate from './templates/item.handlebars';
|
||||
import ItemTemplate from './templates/item.handlebars'
|
||||
import Template from './templates/template.handlebars';
|
||||
|
||||
(function() {
|
||||
|
@ -28,137 +28,137 @@ import Template from './templates/template.handlebars';
|
|||
},
|
||||
|
||||
initialize: function() {
|
||||
OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments);
|
||||
this.collection = new OCA.Versions.VersionCollection();
|
||||
this.collection.on('request', this._onRequest, this);
|
||||
this.collection.on('sync', this._onEndRequest, this);
|
||||
this.collection.on('update', this._onUpdate, this);
|
||||
this.collection.on('error', this._onError, this);
|
||||
this.collection.on('add', this._onAddModel, this);
|
||||
OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments)
|
||||
this.collection = new OCA.Versions.VersionCollection()
|
||||
this.collection.on('request', this._onRequest, this)
|
||||
this.collection.on('sync', this._onEndRequest, this)
|
||||
this.collection.on('update', this._onUpdate, this)
|
||||
this.collection.on('error', this._onError, this)
|
||||
this.collection.on('add', this._onAddModel, this)
|
||||
},
|
||||
|
||||
getLabel: function() {
|
||||
return t('files_versions', 'Versions');
|
||||
return t('files_versions', 'Versions')
|
||||
},
|
||||
|
||||
getIcon: function() {
|
||||
return 'icon-history';
|
||||
return 'icon-history'
|
||||
},
|
||||
|
||||
nextPage: function() {
|
||||
if (this._loading) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
this.collection.fetch();
|
||||
this.collection.fetch()
|
||||
},
|
||||
|
||||
_onClickRevertVersion: function(ev) {
|
||||
var self = this;
|
||||
var $target = $(ev.target);
|
||||
var fileInfoModel = this.collection.getFileInfo();
|
||||
var revision;
|
||||
var self = this
|
||||
var $target = $(ev.target)
|
||||
var fileInfoModel = this.collection.getFileInfo()
|
||||
var revision
|
||||
if (!$target.is('li')) {
|
||||
$target = $target.closest('li');
|
||||
$target = $target.closest('li')
|
||||
}
|
||||
|
||||
ev.preventDefault();
|
||||
revision = $target.attr('data-revision');
|
||||
ev.preventDefault()
|
||||
revision = $target.attr('data-revision')
|
||||
|
||||
var versionModel = this.collection.get(revision);
|
||||
var versionModel = this.collection.get(revision)
|
||||
versionModel.revert({
|
||||
success: function() {
|
||||
// reset and re-fetch the updated collection
|
||||
self.$versionsContainer.empty();
|
||||
self.collection.setFileInfo(fileInfoModel);
|
||||
self.collection.reset([], {silent: true});
|
||||
self.collection.fetch();
|
||||
self.$versionsContainer.empty()
|
||||
self.collection.setFileInfo(fileInfoModel)
|
||||
self.collection.reset([], { silent: true })
|
||||
self.collection.fetch()
|
||||
|
||||
self.$el.find('.versions').removeClass('hidden');
|
||||
self.$el.find('.versions').removeClass('hidden')
|
||||
|
||||
// update original model
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false);
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false)
|
||||
fileInfoModel.set({
|
||||
size: versionModel.get('size'),
|
||||
mtime: versionModel.get('timestamp') * 1000,
|
||||
// temp dummy, until we can do a PROPFIND
|
||||
etag: versionModel.get('id') + versionModel.get('timestamp')
|
||||
});
|
||||
})
|
||||
},
|
||||
|
||||
error: function() {
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false);
|
||||
self.$el.find('.versions').removeClass('hidden');
|
||||
self._toggleLoading(false);
|
||||
fileInfoModel.trigger('busy', fileInfoModel, false)
|
||||
self.$el.find('.versions').removeClass('hidden')
|
||||
self._toggleLoading(false)
|
||||
OC.Notification.show(t('files_version', 'Failed to revert {file} to revision {timestamp}.',
|
||||
{
|
||||
file: versionModel.getFullPath(),
|
||||
timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000)
|
||||
}),
|
||||
{
|
||||
type: 'error'
|
||||
}
|
||||
);
|
||||
{
|
||||
type: 'error'
|
||||
}
|
||||
)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// spinner
|
||||
this._toggleLoading(true);
|
||||
fileInfoModel.trigger('busy', fileInfoModel, true);
|
||||
this._toggleLoading(true)
|
||||
fileInfoModel.trigger('busy', fileInfoModel, true)
|
||||
},
|
||||
|
||||
_toggleLoading: function(state) {
|
||||
this._loading = state;
|
||||
this.$el.find('.loading').toggleClass('hidden', !state);
|
||||
this._loading = state
|
||||
this.$el.find('.loading').toggleClass('hidden', !state)
|
||||
},
|
||||
|
||||
_onRequest: function() {
|
||||
this._toggleLoading(true);
|
||||
this._toggleLoading(true)
|
||||
},
|
||||
|
||||
_onEndRequest: function() {
|
||||
this._toggleLoading(false);
|
||||
this.$el.find('.empty').toggleClass('hidden', !!this.collection.length);
|
||||
this._toggleLoading(false)
|
||||
this.$el.find('.empty').toggleClass('hidden', !!this.collection.length)
|
||||
},
|
||||
|
||||
_onAddModel: function(model) {
|
||||
var $el = $(this.itemTemplate(this._formatItem(model)));
|
||||
this.$versionsContainer.append($el);
|
||||
$el.find('.has-tooltip').tooltip();
|
||||
var $el = $(this.itemTemplate(this._formatItem(model)))
|
||||
this.$versionsContainer.append($el)
|
||||
$el.find('.has-tooltip').tooltip()
|
||||
},
|
||||
|
||||
template: function(data) {
|
||||
return Template(data);
|
||||
return Template(data)
|
||||
},
|
||||
|
||||
itemTemplate: function(data) {
|
||||
return ItemTemplate(data);
|
||||
return ItemTemplate(data)
|
||||
},
|
||||
|
||||
setFileInfo: function(fileInfo) {
|
||||
if (fileInfo) {
|
||||
this.render();
|
||||
this.collection.setFileInfo(fileInfo);
|
||||
this.collection.reset([], {silent: true});
|
||||
this.nextPage();
|
||||
this.render()
|
||||
this.collection.setFileInfo(fileInfo)
|
||||
this.collection.reset([], { silent: true })
|
||||
this.nextPage()
|
||||
} else {
|
||||
this.render();
|
||||
this.collection.reset();
|
||||
this.render()
|
||||
this.collection.reset()
|
||||
}
|
||||
},
|
||||
|
||||
_formatItem: function(version) {
|
||||
var timestamp = version.get('timestamp') * 1000;
|
||||
var size = version.has('size') ? version.get('size') : 0;
|
||||
var preview = OC.MimeType.getIconUrl(version.get('mimetype'));
|
||||
var img = new Image();
|
||||
img.onload = function () {
|
||||
$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl());
|
||||
};
|
||||
img.src = version.getPreviewUrl();
|
||||
var timestamp = version.get('timestamp') * 1000
|
||||
var size = version.has('size') ? version.get('size') : 0
|
||||
var preview = OC.MimeType.getIconUrl(version.get('mimetype'))
|
||||
var img = new Image()
|
||||
img.onload = function() {
|
||||
$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl())
|
||||
}
|
||||
img.src = version.getPreviewUrl()
|
||||
|
||||
return _.extend({
|
||||
versionId: version.get('id'),
|
||||
|
@ -175,7 +175,7 @@ import Template from './templates/template.handlebars';
|
|||
previewUrl: preview,
|
||||
revertLabel: t('files_versions', 'Restore'),
|
||||
canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0
|
||||
}, version.attributes);
|
||||
}, version.attributes)
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -183,27 +183,27 @@ import Template from './templates/template.handlebars';
|
|||
*/
|
||||
render: function() {
|
||||
this.$el.html(this.template({
|
||||
emptyResultLabel: t('files_versions', 'No other versions available'),
|
||||
}));
|
||||
this.$el.find('.has-tooltip').tooltip();
|
||||
this.$versionsContainer = this.$el.find('ul.versions');
|
||||
this.delegateEvents();
|
||||
emptyResultLabel: t('files_versions', 'No other versions available')
|
||||
}))
|
||||
this.$el.find('.has-tooltip').tooltip()
|
||||
this.$versionsContainer = this.$el.find('ul.versions')
|
||||
this.delegateEvents()
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns true for files, false for folders.
|
||||
*
|
||||
* @return {bool} true for files, false for folders
|
||||
* @param {FileInfo} fileInfo fileInfo
|
||||
* @returns {bool} true for files, false for folders
|
||||
*/
|
||||
canDisplay: function(fileInfo) {
|
||||
if (!fileInfo) {
|
||||
return false;
|
||||
return false
|
||||
}
|
||||
return !fileInfo.isDirectory();
|
||||
return !fileInfo.isDirectory()
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
OCA.Versions = OCA.Versions || {};
|
||||
OCA.Versions = OCA.Versions || {}
|
||||
|
||||
OCA.Versions.VersionsTabView = VersionsTabView;
|
||||
})();
|
||||
OCA.Versions.VersionsTabView = VersionsTabView
|
||||
})()
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -22,52 +22,71 @@
|
|||
<template>
|
||||
<div id="oauth2" class="section">
|
||||
<h2>{{ t('oauth2', 'OAuth 2.0 clients') }}</h2>
|
||||
<p class="settings-hint">{{ t('oauth2', 'OAuth 2.0 allows external services to request access to {instanceName}.', { instanceName: OC.theme.name}) }}</p>
|
||||
<table class="grid" v-if="clients.length > 0">
|
||||
<p class="settings-hint">
|
||||
{{ t('oauth2', 'OAuth 2.0 allows external services to request access to {instanceName}.', { instanceName: OC.theme.name}) }}
|
||||
</p>
|
||||
<table v-if="clients.length > 0" class="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th id="headerName" scope="col">{{ t('oauth2', 'Name') }}</th>
|
||||
<th id="headerRedirectUri" scope="col">{{ t('oauth2', 'Redirection URI') }}</th>
|
||||
<th id="headerClientIdentifier" scope="col">{{ t('oauth2', 'Client Identifier') }}</th>
|
||||
<th id="headerSecret" scope="col">{{ t('oauth2', 'Secret') }}</th>
|
||||
<th id="headerRemove"> </th>
|
||||
<th id="headerName" scope="col">
|
||||
{{ t('oauth2', 'Name') }}
|
||||
</th>
|
||||
<th id="headerRedirectUri" scope="col">
|
||||
{{ t('oauth2', 'Redirection URI') }}
|
||||
</th>
|
||||
<th id="headerClientIdentifier" scope="col">
|
||||
{{ t('oauth2', 'Client Identifier') }}
|
||||
</th>
|
||||
<th id="headerSecret" scope="col">
|
||||
{{ t('oauth2', 'Secret') }}
|
||||
</th>
|
||||
<th id="headerRemove">
|
||||
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<OAuthItem v-for="client in clients"
|
||||
:key="client.id"
|
||||
:client="client"
|
||||
@delete="deleteClient"
|
||||
/>
|
||||
@delete="deleteClient" />
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<br/>
|
||||
<br>
|
||||
<h3>{{ t('oauth2', 'Add client') }}</h3>
|
||||
<span v-if="newClient.error" class="msg error">{{newClient.errorMsg}}</span>
|
||||
<span v-if="newClient.error" class="msg error">{{ newClient.errorMsg }}</span>
|
||||
<form @submit.prevent="addClient">
|
||||
<input type="text" id="name" name="name" :placeholder="t('oauth2', 'Name')" v-model="newClient.name">
|
||||
<input type="url" id="redirectUri" name="redirectUri" :placeholder="t('oauth2', 'Redirection URI')" v-model="newClient.redirectUri">
|
||||
<input id="name"
|
||||
v-model="newClient.name"
|
||||
type="text"
|
||||
name="name"
|
||||
:placeholder="t('oauth2', 'Name')">
|
||||
<input id="redirectUri"
|
||||
v-model="newClient.redirectUri"
|
||||
type="url"
|
||||
name="redirectUri"
|
||||
:placeholder="t('oauth2', 'Redirection URI')">
|
||||
<input type="submit" class="button" :value="t('oauth2', 'Add')">
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Axios from 'nextcloud-axios'
|
||||
import OAuthItem from './components/OAuthItem';
|
||||
import axios from 'nextcloud-axios'
|
||||
import OAuthItem from './components/OAuthItem'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
OAuthItem
|
||||
},
|
||||
props: {
|
||||
clients: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
components: {
|
||||
OAuthItem
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
newClient: {
|
||||
|
@ -76,34 +95,34 @@ export default {
|
|||
errorMsg: '',
|
||||
error: false
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteClient(id) {
|
||||
Axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', {id: id}))
|
||||
axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', { id: id }))
|
||||
.then((response) => {
|
||||
this.clients = this.clients.filter(client => client.id !== id);
|
||||
});
|
||||
this.clients = this.clients.filter(client => client.id !== id)
|
||||
})
|
||||
},
|
||||
addClient() {
|
||||
this.newClient.error = false;
|
||||
this.newClient.error = false
|
||||
|
||||
Axios.post(
|
||||
axios.post(
|
||||
OC.generateUrl('apps/oauth2/clients'),
|
||||
{
|
||||
name: this.newClient.name,
|
||||
redirectUri: this.newClient.redirectUri
|
||||
}
|
||||
).then(response => {
|
||||
this.clients.push(response.data);
|
||||
this.clients.push(response.data)
|
||||
|
||||
this.newClient.name = '';
|
||||
this.newClient.redirectUri = '';
|
||||
this.newClient.name = ''
|
||||
this.newClient.redirectUri = ''
|
||||
}).catch(reason => {
|
||||
this.newClient.error = true;
|
||||
this.newClient.errorMsg = reason.response.data.message;
|
||||
});
|
||||
this.newClient.error = true
|
||||
this.newClient.errorMsg = reason.response.data.message
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -21,11 +21,13 @@
|
|||
-->
|
||||
<template>
|
||||
<tr>
|
||||
<td>{{name}}</td>
|
||||
<td>{{redirectUri}}</td>
|
||||
<td><code>{{clientId}}</code></td>
|
||||
<td><code>{{renderedSecret}}</code><a class='icon-toggle has-tooltip' :title="t('oauth2', 'Show client secret')" @click="toggleSecret"></a></td>
|
||||
<td class="action-column"><span><a class="icon-delete has-tooltip" :title="t('oauth2', 'Delete')" @click="$emit('delete', id)"></a></span></td>
|
||||
<td>{{ name }}</td>
|
||||
<td>{{ redirectUri }}</td>
|
||||
<td><code>{{ clientId }}</code></td>
|
||||
<td><code>{{ renderedSecret }}</code><a class="icon-toggle has-tooltip" :title="t('oauth2', 'Show client secret')" @click="toggleSecret" /></td>
|
||||
<td class="action-column">
|
||||
<span><a class="icon-delete has-tooltip" :title="t('oauth2', 'Delete')" @click="$emit('delete', id)" /></span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
|
@ -39,27 +41,27 @@ export default {
|
|||
}
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
id: this.client.id,
|
||||
name: this.client.name,
|
||||
redirectUri: this.client.redirectUri,
|
||||
clientId: this.client.clientId,
|
||||
clientSecret: this.client.clientSecret,
|
||||
renderSecret: false,
|
||||
};
|
||||
return {
|
||||
id: this.client.id,
|
||||
name: this.client.name,
|
||||
redirectUri: this.client.redirectUri,
|
||||
clientId: this.client.clientId,
|
||||
clientSecret: this.client.clientSecret,
|
||||
renderSecret: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
renderedSecret: function() {
|
||||
if (this.renderSecret) {
|
||||
return this.clientSecret;
|
||||
return this.clientSecret
|
||||
} else {
|
||||
return '****';
|
||||
return '****'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleSecret() {
|
||||
this.renderSecret = !this.renderSecret;
|
||||
this.renderSecret = !this.renderSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,18 +20,19 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import App from './App.vue';
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import { loadState } from 'nextcloud-initial-state'
|
||||
|
||||
Vue.prototype.t = t;
|
||||
Vue.prototype.OC = OC;
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.OC = OC
|
||||
|
||||
const clients = loadState('oauth2', 'clients');
|
||||
const clients = loadState('oauth2', 'clients')
|
||||
|
||||
const View = Vue.extend(App)
|
||||
new View({
|
||||
const oauth = new View({
|
||||
propsData: {
|
||||
clients
|
||||
}
|
||||
}).$mount('#oauth2');
|
||||
})
|
||||
oauth.$mount('#oauth2')
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -21,7 +21,7 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -29,9 +29,9 @@ export default {
|
|||
name: 'App',
|
||||
beforeMount: function() {
|
||||
// importing server data into the store
|
||||
const serverDataElmt = document.getElementById('serverData');
|
||||
const serverDataElmt = document.getElementById('serverData')
|
||||
if (serverDataElmt !== null) {
|
||||
this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server));
|
||||
this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,14 +4,14 @@
|
|||
{{ t('settings', 'Two-factor authentication can be enforced for all users and specific groups. If they do not have a two-factor provider configured, they will be unable to log into the system.') }}
|
||||
</p>
|
||||
<p v-if="loading">
|
||||
<span class="icon-loading-small two-factor-loading"></span>
|
||||
<span class="icon-loading-small two-factor-loading" />
|
||||
<span>{{ t('settings', 'Enforce two-factor authentication') }}</span>
|
||||
</p>
|
||||
<p v-else>
|
||||
<input type="checkbox"
|
||||
id="two-factor-enforced"
|
||||
class="checkbox"
|
||||
v-model="enforced">
|
||||
<input id="two-factor-enforced"
|
||||
v-model="enforced"
|
||||
type="checkbox"
|
||||
class="checkbox">
|
||||
<label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label>
|
||||
</p>
|
||||
<template v-if="enforced">
|
||||
|
@ -22,32 +22,30 @@
|
|||
</p>
|
||||
<p>
|
||||
<Multiselect v-model="enforcedGroups"
|
||||
:options="groups"
|
||||
:placeholder="t('settings', 'Enforced groups')"
|
||||
:disabled="loading"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
@search-change="searchGroup"
|
||||
:loading="loadingGroups"
|
||||
:show-no-options="false"
|
||||
:close-on-select="false">
|
||||
</Multiselect>
|
||||
:options="groups"
|
||||
:placeholder="t('settings', 'Enforced groups')"
|
||||
:disabled="loading"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:loading="loadingGroups"
|
||||
:show-no-options="false"
|
||||
:close-on-select="false"
|
||||
@search-change="searchGroup" />
|
||||
</p>
|
||||
<p>
|
||||
{{ t('settings', 'Two-factor authentication is not enforced for members of the following groups.') }}
|
||||
</p>
|
||||
<p>
|
||||
<Multiselect v-model="excludedGroups"
|
||||
:options="groups"
|
||||
:placeholder="t('settings', 'Excluded groups')"
|
||||
:disabled="loading"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
@search-change="searchGroup"
|
||||
:loading="loadingGroups"
|
||||
:show-no-options="false"
|
||||
:close-on-select="false">
|
||||
</Multiselect>
|
||||
:options="groups"
|
||||
:placeholder="t('settings', 'Excluded groups')"
|
||||
:disabled="loading"
|
||||
:multiple="true"
|
||||
:searchable="true"
|
||||
:loading="loadingGroups"
|
||||
:show-no-options="false"
|
||||
:close-on-select="false"
|
||||
@search-change="searchGroup" />
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
|
@ -57,10 +55,10 @@
|
|||
</p>
|
||||
</template>
|
||||
<p>
|
||||
<button class="button primary"
|
||||
v-if="dirty"
|
||||
v-on:click="saveChanges"
|
||||
:disabled="loading">
|
||||
<button v-if="dirty"
|
||||
class="button primary"
|
||||
:disabled="loading"
|
||||
@click="saveChanges">
|
||||
{{ t('settings', 'Save changes') }}
|
||||
</button>
|
||||
</p>
|
||||
|
@ -68,94 +66,93 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Axios from 'nextcloud-axios'
|
||||
import { mapState } from 'vuex'
|
||||
import {Multiselect} from 'nextcloud-vue'
|
||||
import _ from 'lodash'
|
||||
import axios from 'nextcloud-axios'
|
||||
import { Multiselect } from 'nextcloud-vue'
|
||||
import _ from 'lodash'
|
||||
|
||||
export default {
|
||||
name: "AdminTwoFactor",
|
||||
components: {
|
||||
Multiselect
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
loading: false,
|
||||
dirty: false,
|
||||
groups: [],
|
||||
loadingGroups: false,
|
||||
export default {
|
||||
name: 'AdminTwoFactor',
|
||||
components: {
|
||||
Multiselect
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
dirty: false,
|
||||
groups: [],
|
||||
loadingGroups: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enforced: {
|
||||
get: function() {
|
||||
return this.$store.state.enforced
|
||||
},
|
||||
set: function(val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setEnforced', val)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enforced: {
|
||||
get: function () {
|
||||
return this.$store.state.enforced
|
||||
},
|
||||
set: function (val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setEnforced', val)
|
||||
}
|
||||
},
|
||||
enforcedGroups: {
|
||||
get: function () {
|
||||
return this.$store.state.enforcedGroups
|
||||
},
|
||||
set: function (val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setEnforcedGroups', val)
|
||||
}
|
||||
},
|
||||
excludedGroups: {
|
||||
get: function () {
|
||||
return this.$store.state.excludedGroups
|
||||
},
|
||||
set: function (val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setExcludedGroups', val)
|
||||
}
|
||||
enforcedGroups: {
|
||||
get: function() {
|
||||
return this.$store.state.enforcedGroups
|
||||
},
|
||||
set: function(val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setEnforcedGroups', val)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
// Groups are loaded dynamically, but the assigned ones *should*
|
||||
// be valid groups, so let's add them as initial state
|
||||
this.groups = _.sortedUniq(_.uniq(this.enforcedGroups.concat(this.excludedGroups)))
|
||||
|
||||
// Populate the groups with a first set so the dropdown is not empty
|
||||
// when opening the page the first time
|
||||
this.searchGroup('')
|
||||
},
|
||||
methods: {
|
||||
searchGroup: _.debounce(function (query) {
|
||||
this.loadingGroups = true
|
||||
Axios.get(OC.linkToOCS(`cloud/groups?offset=0&search=${encodeURIComponent(query)}&limit=20`, 2))
|
||||
.then(res => res.data.ocs)
|
||||
.then(ocs => ocs.data.groups)
|
||||
.then(groups => this.groups = _.sortedUniq(_.uniq(this.groups.concat(groups))))
|
||||
.catch(err => console.error('could not search groups', err))
|
||||
.then(() => this.loadingGroups = false)
|
||||
}, 500),
|
||||
|
||||
saveChanges () {
|
||||
this.loading = true
|
||||
|
||||
const data = {
|
||||
enforced: this.enforced,
|
||||
enforcedGroups: this.enforcedGroups,
|
||||
excludedGroups: this.excludedGroups,
|
||||
}
|
||||
Axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data)
|
||||
.then(resp => resp.data)
|
||||
.then(state => {
|
||||
this.state = state
|
||||
this.dirty = false
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('could not save changes', err)
|
||||
})
|
||||
.then(() => this.loading = false)
|
||||
excludedGroups: {
|
||||
get: function() {
|
||||
return this.$store.state.excludedGroups
|
||||
},
|
||||
set: function(val) {
|
||||
this.dirty = true
|
||||
this.$store.commit('setExcludedGroups', val)
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Groups are loaded dynamically, but the assigned ones *should*
|
||||
// be valid groups, so let's add them as initial state
|
||||
this.groups = _.sortedUniq(_.uniq(this.enforcedGroups.concat(this.excludedGroups)))
|
||||
|
||||
// Populate the groups with a first set so the dropdown is not empty
|
||||
// when opening the page the first time
|
||||
this.searchGroup('')
|
||||
},
|
||||
methods: {
|
||||
searchGroup: _.debounce(function(query) {
|
||||
this.loadingGroups = true
|
||||
axios.get(OC.linkToOCS(`cloud/groups?offset=0&search=${encodeURIComponent(query)}&limit=20`, 2))
|
||||
.then(res => res.data.ocs)
|
||||
.then(ocs => ocs.data.groups)
|
||||
.then(groups => { this.groups = _.sortedUniq(_.uniq(this.groups.concat(groups))) })
|
||||
.catch(err => console.error('could not search groups', err))
|
||||
.then(() => { this.loadingGroups = false })
|
||||
}, 500),
|
||||
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
|
||||
const data = {
|
||||
enforced: this.enforced,
|
||||
enforcedGroups: this.enforcedGroups,
|
||||
excludedGroups: this.excludedGroups
|
||||
}
|
||||
axios.put(OC.generateUrl('/settings/api/admin/twofactorauth'), data)
|
||||
.then(resp => resp.data)
|
||||
.then(state => {
|
||||
this.state = state
|
||||
this.dirty = false
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('could not save changes', err)
|
||||
})
|
||||
.then(() => { this.loading = false })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
|
326
apps/settings/src/components/AppDetails.vue
Normal file
326
apps/settings/src/components/AppDetails.vue
Normal file
|
@ -0,0 +1,326 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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>
|
||||
<div id="app-details-view" style="padding: 20px;">
|
||||
<h2>
|
||||
<div v-if="!app.preview" class="icon-settings-dark" />
|
||||
<svg v-if="app.previewAsIcon && app.preview"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32">
|
||||
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
|
||||
<image x="0"
|
||||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
:filter="filterUrl"
|
||||
:xlink:href="app.preview"
|
||||
class="app-icon" />
|
||||
</svg>
|
||||
{{ app.name }}
|
||||
</h2>
|
||||
<img v-if="app.screenshot" :src="app.screenshot" width="100%">
|
||||
<div v-if="app.level === 300 || app.level === 200 || hasRating" class="app-level">
|
||||
<span v-if="app.level === 300"
|
||||
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
|
||||
class="supported icon-checkmark-color">
|
||||
{{ t('settings', 'Supported') }}</span>
|
||||
<span v-if="app.level === 200"
|
||||
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')"
|
||||
class="official icon-checkmark">
|
||||
{{ t('settings', 'Official') }}</span>
|
||||
<AppScore v-if="hasRating" :score="app.appstoreData.ratingOverall" />
|
||||
</div>
|
||||
|
||||
<div v-if="author" class="app-author">
|
||||
{{ t('settings', 'by') }}
|
||||
<span v-for="(a, index) in author" :key="index">
|
||||
<a v-if="a['@attributes'] && a['@attributes']['homepage']" :href="a['@attributes']['homepage']">{{ a['@value'] }}</a><span v-else-if="a['@value']">{{ a['@value'] }}</span><span v-else>{{ a }}</span><span v-if="index+1 < author.length">, </span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="licence" class="app-licence">
|
||||
{{ licence }}
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="actions-buttons">
|
||||
<input v-if="app.update"
|
||||
class="update primary"
|
||||
type="button"
|
||||
:value="t('settings', 'Update to {version}', {version: app.update})"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click="update(app.id)">
|
||||
<input v-if="app.canUnInstall"
|
||||
class="uninstall"
|
||||
type="button"
|
||||
:value="t('settings', 'Remove')"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click="remove(app.id)">
|
||||
<input v-if="app.active"
|
||||
class="enable"
|
||||
type="button"
|
||||
:value="t('settings','Disable')"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click="disable(app.id)">
|
||||
<input v-if="!app.active && (app.canInstall || app.isCompatible)"
|
||||
v-tooltip.auto="enableButtonTooltip"
|
||||
class="enable primary"
|
||||
type="button"
|
||||
:value="enableButtonText"
|
||||
:disabled="!app.canInstall || installing || loading(app.id)"
|
||||
@click="enable(app.id)">
|
||||
<input v-else-if="!app.active"
|
||||
v-tooltip.auto="forceEnableButtonTooltip"
|
||||
class="enable force"
|
||||
type="button"
|
||||
:value="forceEnableButtonText"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click="forceEnable(app.id)">
|
||||
</div>
|
||||
<div class="app-groups">
|
||||
<div v-if="app.active && canLimitToGroups(app)" class="groups-enable">
|
||||
<input :id="prefix('groups_enable', app.id)"
|
||||
v-model="groupCheckedAppsData"
|
||||
type="checkbox"
|
||||
:value="app.id"
|
||||
class="groups-enable__checkbox checkbox"
|
||||
@change="setGroupLimit">
|
||||
<label :for="prefix('groups_enable', app.id)">{{ t('settings', 'Limit to groups') }}</label>
|
||||
<input type="hidden"
|
||||
class="group_select"
|
||||
:title="t('settings', 'All')"
|
||||
value="">
|
||||
<Multiselect v-if="isLimitedToGroups(app)"
|
||||
:options="groups"
|
||||
:value="appGroups"
|
||||
:options-limit="5"
|
||||
:placeholder="t('settings', 'Limit app usage to groups')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:tag-width="60"
|
||||
@select="addGroupLimitation"
|
||||
@remove="removeGroupLimitation"
|
||||
@search-change="asyncFindGroup">
|
||||
<span slot="noResult">{{ t('settings', 'No results') }}</span>
|
||||
</Multiselect>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="app-dependencies">
|
||||
<li v-if="app.missingMinOwnCloudVersion">
|
||||
{{ t('settings', 'This app has no minimum Nextcloud version assigned. This will be an error in the future.') }}
|
||||
</li>
|
||||
<li v-if="app.missingMaxOwnCloudVersion">
|
||||
{{ t('settings', 'This app has no maximum Nextcloud version assigned. This will be an error in the future.') }}
|
||||
</li>
|
||||
<li v-if="!app.canInstall">
|
||||
{{ t('settings', 'This app cannot be installed because the following dependencies are not fulfilled:') }}
|
||||
<ul class="missing-dependencies">
|
||||
<li v-for="(dep, index) in app.missingDependencies" :key="index">
|
||||
{{ dep }}
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="documentation">
|
||||
<a v-if="!app.internal"
|
||||
class="appslink"
|
||||
:href="appstoreUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'View in store') }} ↗</a>
|
||||
|
||||
<a v-if="app.website"
|
||||
class="appslink"
|
||||
:href="app.website"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'Visit website') }} ↗</a>
|
||||
<a v-if="app.bugs"
|
||||
class="appslink"
|
||||
:href="app.bugs"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'Report a bug') }} ↗</a>
|
||||
|
||||
<a v-if="app.documentation && app.documentation.user"
|
||||
class="appslink"
|
||||
:href="app.documentation.user"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'User documentation') }} ↗</a>
|
||||
<a v-if="app.documentation && app.documentation.admin"
|
||||
class="appslink"
|
||||
:href="app.documentation.admin"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'Admin documentation') }} ↗</a>
|
||||
<a v-if="app.documentation && app.documentation.developer"
|
||||
class="appslink"
|
||||
:href="app.documentation.developer"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener">{{ t('settings', 'Developer documentation') }} ↗</a>
|
||||
</p>
|
||||
|
||||
<div class="app-description" v-html="renderMarkdown" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Multiselect } from 'nextcloud-vue'
|
||||
import marked from 'marked'
|
||||
import dompurify from 'dompurify'
|
||||
|
||||
import AppScore from './AppList/AppScore'
|
||||
import AppManagement from './AppManagement'
|
||||
import PrefixMixin from './PrefixMixin'
|
||||
import SvgFilterMixin from './SvgFilterMixin'
|
||||
|
||||
export default {
|
||||
name: 'AppDetails',
|
||||
components: {
|
||||
Multiselect,
|
||||
AppScore
|
||||
},
|
||||
mixins: [AppManagement, PrefixMixin, SvgFilterMixin],
|
||||
props: ['category', 'app'],
|
||||
data() {
|
||||
return {
|
||||
groupCheckedAppsData: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appstoreUrl() {
|
||||
return `https://apps.nextcloud.com/apps/${this.app.id}`
|
||||
},
|
||||
licence() {
|
||||
if (this.app.licence) {
|
||||
return t('settings', '{license}-licensed', { license: ('' + this.app.licence).toUpperCase() })
|
||||
}
|
||||
return null
|
||||
},
|
||||
hasRating() {
|
||||
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
|
||||
},
|
||||
author() {
|
||||
if (typeof this.app.author === 'string') {
|
||||
return [
|
||||
{
|
||||
'@value': this.app.author
|
||||
}
|
||||
]
|
||||
}
|
||||
if (this.app.author['@value']) {
|
||||
return [this.app.author]
|
||||
}
|
||||
return this.app.author
|
||||
},
|
||||
appGroups() {
|
||||
return this.app.groups.map(group => { return { id: group, name: group } })
|
||||
},
|
||||
groups() {
|
||||
return this.$store.getters.getGroups
|
||||
.filter(group => group.id !== 'disabled')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
},
|
||||
renderMarkdown() {
|
||||
var renderer = new marked.Renderer()
|
||||
renderer.link = function(href, title, text) {
|
||||
try {
|
||||
var prot = decodeURIComponent(unescape(href))
|
||||
.replace(/[^\w:]/g, '')
|
||||
.toLowerCase()
|
||||
} catch (e) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (prot.indexOf('http:') !== 0 && prot.indexOf('https:') !== 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
var out = '<a href="' + href + '" rel="noreferrer noopener"'
|
||||
if (title) {
|
||||
out += ' title="' + title + '"'
|
||||
}
|
||||
out += '>' + text + '</a>'
|
||||
return out
|
||||
}
|
||||
renderer.image = function(href, title, text) {
|
||||
if (text) {
|
||||
return text
|
||||
}
|
||||
return title
|
||||
}
|
||||
renderer.blockquote = function(quote) {
|
||||
return quote
|
||||
}
|
||||
return dompurify.sanitize(
|
||||
marked(this.app.description.trim(), {
|
||||
renderer: renderer,
|
||||
gfm: false,
|
||||
highlight: false,
|
||||
tables: false,
|
||||
breaks: false,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false
|
||||
}),
|
||||
{
|
||||
SAFE_FOR_JQUERY: true,
|
||||
ALLOWED_TAGS: [
|
||||
'strong',
|
||||
'p',
|
||||
'a',
|
||||
'ul',
|
||||
'ol',
|
||||
'li',
|
||||
'em',
|
||||
'del',
|
||||
'blockquote'
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.app.groups.length > 0) {
|
||||
this.groupCheckedAppsData = true
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.force {
|
||||
background: var(--color-main-background);
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
.force:hover,
|
||||
.force:active {
|
||||
background: var(--color-error);
|
||||
border-color: var(--color-error) !important;
|
||||
color: var(--color-main-background);
|
||||
}
|
||||
</style>
|
201
apps/settings/src/components/AppList.vue
Normal file
201
apps/settings/src/components/AppList.vue
Normal file
|
@ -0,0 +1,201 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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>
|
||||
<div id="app-content-inner">
|
||||
<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
|
||||
<template v-if="useListView">
|
||||
<transition-group name="app-list" tag="div" class="apps-list-container">
|
||||
<AppItem v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category" />
|
||||
</transition-group>
|
||||
</template>
|
||||
<transition-group v-if="useBundleView"
|
||||
name="app-list"
|
||||
tag="div"
|
||||
class="apps-list-container">
|
||||
<template v-for="bundle in bundles">
|
||||
<div :key="bundle.id" class="apps-header">
|
||||
<div class="app-image" />
|
||||
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
|
||||
<div class="app-version" />
|
||||
<div class="app-level" />
|
||||
<div class="app-groups" />
|
||||
<div class="actions">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<AppItem v-for="app in bundleApps(bundle.id)"
|
||||
:key="bundle.id + app.id"
|
||||
:app="app"
|
||||
:category="category" />
|
||||
</template>
|
||||
</transition-group>
|
||||
<template v-if="useAppStoreView">
|
||||
<AppItem v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category"
|
||||
:list-view="false" />
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="apps-list-search" class="apps-list installed">
|
||||
<div class="apps-list-container">
|
||||
<template v-if="search !== '' && searchApps.length > 0">
|
||||
<div class="section">
|
||||
<div />
|
||||
<td colspan="5">
|
||||
<h2>{{ t('settings', 'Results from other categories') }}</h2>
|
||||
</td>
|
||||
</div>
|
||||
<AppItem v-for="app in searchApps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category"
|
||||
:list-view="true" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
|
||||
<div id="app-list-empty-icon" class="icon-settings-dark" />
|
||||
<h2>{{ t('settings', 'No apps found for your version') }}</h2>
|
||||
</div>
|
||||
|
||||
<div id="searchresults" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppItem from './AppList/AppItem'
|
||||
import PrefixMixin from './PrefixMixin'
|
||||
|
||||
export default {
|
||||
name: 'AppList',
|
||||
components: {
|
||||
AppItem
|
||||
},
|
||||
mixins: [PrefixMixin],
|
||||
props: ['category', 'app', 'search'],
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.getters.loading('list')
|
||||
},
|
||||
apps() {
|
||||
let apps = this.$store.getters.getAllApps
|
||||
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
|
||||
.sort(function(a, b) {
|
||||
const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
|
||||
const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
|
||||
return OC.Util.naturalSortCompare(sortStringA, sortStringB)
|
||||
})
|
||||
|
||||
if (this.category === 'installed') {
|
||||
return apps.filter(app => app.installed)
|
||||
}
|
||||
if (this.category === 'enabled') {
|
||||
return apps.filter(app => app.active && app.installed)
|
||||
}
|
||||
if (this.category === 'disabled') {
|
||||
return apps.filter(app => !app.active && app.installed)
|
||||
}
|
||||
if (this.category === 'app-bundles') {
|
||||
return apps.filter(app => app.bundles)
|
||||
}
|
||||
if (this.category === 'updates') {
|
||||
return apps.filter(app => app.update)
|
||||
}
|
||||
// filter app store categories
|
||||
return apps.filter(app => {
|
||||
return app.appstore && app.category !== undefined
|
||||
&& (app.category === this.category || app.category.indexOf(this.category) > -1)
|
||||
})
|
||||
},
|
||||
bundles() {
|
||||
return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
|
||||
},
|
||||
bundleApps() {
|
||||
return function(bundle) {
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => app.bundleId === bundle)
|
||||
}
|
||||
},
|
||||
searchApps() {
|
||||
if (this.search === '') {
|
||||
return []
|
||||
}
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => {
|
||||
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
|
||||
return (!this.apps.find(_app => _app.id === app.id))
|
||||
}
|
||||
return false
|
||||
})
|
||||
},
|
||||
useAppStoreView() {
|
||||
return !this.useListView && !this.useBundleView
|
||||
},
|
||||
useListView() {
|
||||
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates')
|
||||
},
|
||||
useBundleView() {
|
||||
return (this.category === 'app-bundles')
|
||||
},
|
||||
allBundlesEnabled() {
|
||||
let self = this
|
||||
return function(id) {
|
||||
return self.bundleApps(id).filter(app => !app.active).length === 0
|
||||
}
|
||||
},
|
||||
bundleToggleText() {
|
||||
let self = this
|
||||
return function(id) {
|
||||
if (self.allBundlesEnabled(id)) {
|
||||
return t('settings', 'Disable all')
|
||||
}
|
||||
return t('settings', 'Enable all')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleBundle(id) {
|
||||
if (this.allBundlesEnabled(id)) {
|
||||
return this.disableBundle(id)
|
||||
}
|
||||
return this.enableBundle(id)
|
||||
},
|
||||
enableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id)
|
||||
this.$store.dispatch('enableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { console.error(error); OC.Notification.show(error) })
|
||||
},
|
||||
disableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id)
|
||||
this.$store.dispatch('disableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
179
apps/settings/src/components/AppList/AppItem.vue
Normal file
179
apps/settings/src/components/AppList/AppItem.vue
Normal file
|
@ -0,0 +1,179 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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>
|
||||
<div class="section" :class="{ selected: isSelected }" @click="showAppDetails">
|
||||
<div class="app-image app-image-icon" @click="showAppDetails">
|
||||
<div v-if="(listView && !app.preview) || (!listView && !app.screenshot)" class="icon-settings-dark" />
|
||||
|
||||
<svg v-if="listView && app.preview"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32">
|
||||
<defs><filter :id="filterId"><feColorMatrix in="SourceGraphic" type="matrix" values="-1 0 0 0 1 0 -1 0 0 1 0 0 -1 0 1 0 0 0 1 0" /></filter></defs>
|
||||
<image x="0"
|
||||
y="0"
|
||||
width="32"
|
||||
height="32"
|
||||
preserveAspectRatio="xMinYMin meet"
|
||||
:filter="filterUrl"
|
||||
:xlink:href="app.preview"
|
||||
class="app-icon" />
|
||||
</svg>
|
||||
|
||||
<img v-if="!listView && app.screenshot" :src="app.screenshot" width="100%">
|
||||
</div>
|
||||
<div class="app-name" @click="showAppDetails">
|
||||
{{ app.name }}
|
||||
</div>
|
||||
<div v-if="!listView" class="app-summary">
|
||||
{{ app.summary }}
|
||||
</div>
|
||||
<div v-if="listView" class="app-version">
|
||||
<span v-if="app.version">{{ app.version }}</span>
|
||||
<span v-else-if="app.appstoreData.releases[0].version">{{ app.appstoreData.releases[0].version }}</span>
|
||||
</div>
|
||||
|
||||
<div class="app-level">
|
||||
<span v-if="app.level === 300"
|
||||
v-tooltip.auto="t('settings', 'This app is supported via your current Nextcloud subscription.')"
|
||||
class="supported icon-checkmark-color">
|
||||
{{ t('settings', 'Supported') }}</span>
|
||||
<span v-if="app.level === 200"
|
||||
v-tooltip.auto="t('settings', 'Official apps are developed by and within the community. They offer central functionality and are ready for production use.')"
|
||||
class="official icon-checkmark">
|
||||
{{ t('settings', 'Official') }}</span>
|
||||
<AppScore v-if="hasRating && !listView" :score="app.score" />
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<div v-if="app.error" class="warning">
|
||||
{{ app.error }}
|
||||
</div>
|
||||
<div v-if="loading(app.id)" class="icon icon-loading-small" />
|
||||
<input v-if="app.update"
|
||||
class="update primary"
|
||||
type="button"
|
||||
:value="t('settings', 'Update to {update}', {update:app.update})"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click.stop="update(app.id)">
|
||||
<input v-if="app.canUnInstall"
|
||||
class="uninstall"
|
||||
type="button"
|
||||
:value="t('settings', 'Remove')"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click.stop="remove(app.id)">
|
||||
<input v-if="app.active"
|
||||
class="enable"
|
||||
type="button"
|
||||
:value="t('settings','Disable')"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click.stop="disable(app.id)">
|
||||
<input v-if="!app.active && (app.canInstall || app.isCompatible)"
|
||||
v-tooltip.auto="enableButtonTooltip"
|
||||
class="enable"
|
||||
type="button"
|
||||
:value="enableButtonText"
|
||||
:disabled="!app.canInstall || installing || loading(app.id)"
|
||||
@click.stop="enable(app.id)">
|
||||
<input v-else-if="!app.active"
|
||||
v-tooltip.auto="forceEnableButtonTooltip"
|
||||
class="enable force"
|
||||
type="button"
|
||||
:value="forceEnableButtonText"
|
||||
:disabled="installing || loading(app.id)"
|
||||
@click.stop="forceEnable(app.id)">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppScore from './AppScore'
|
||||
import AppManagement from '../AppManagement'
|
||||
import SvgFilterMixin from '../SvgFilterMixin'
|
||||
|
||||
export default {
|
||||
name: 'AppItem',
|
||||
components: {
|
||||
AppScore
|
||||
},
|
||||
mixins: [AppManagement, SvgFilterMixin],
|
||||
props: {
|
||||
app: {},
|
||||
category: {},
|
||||
listView: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isSelected: false,
|
||||
scrolled: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
hasRating() {
|
||||
return this.app.appstoreData && this.app.appstoreData.ratingNumOverall > 5
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route.params.id': function(id) {
|
||||
this.isSelected = (this.app.id === id)
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.isSelected = (this.app.id === this.$route.params.id)
|
||||
},
|
||||
watchers: {
|
||||
|
||||
},
|
||||
methods: {
|
||||
showAppDetails(event) {
|
||||
if (event.currentTarget.tagName === 'INPUT' || event.currentTarget.tagName === 'A') {
|
||||
return
|
||||
}
|
||||
this.$router.push({
|
||||
name: 'apps-details',
|
||||
params: { category: this.category, id: this.app.id }
|
||||
})
|
||||
},
|
||||
prefix(prefix, content) {
|
||||
return prefix + '_' + content
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.force {
|
||||
background: var(--color-main-background);
|
||||
border-color: var(--color-error);
|
||||
color: var(--color-error);
|
||||
}
|
||||
.force:hover,
|
||||
.force:active {
|
||||
background: var(--color-error);
|
||||
border-color: var(--color-error) !important;
|
||||
color: var(--color-main-background);
|
||||
}
|
||||
</style>
|
38
apps/settings/src/components/AppList/AppScore.vue
Normal file
38
apps/settings/src/components/AppList/AppScore.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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>
|
||||
<img :src="scoreImage" class="app-score-image">
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'AppScore',
|
||||
props: ['score'],
|
||||
computed: {
|
||||
scoreImage() {
|
||||
let score = Math.round(this.score * 10)
|
||||
let imageName = 'rating/s' + score + '.svg'
|
||||
return OC.imagePath('core', imageName)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
138
apps/settings/src/components/AppManagement.vue
Normal file
138
apps/settings/src/components/AppManagement.vue
Normal file
|
@ -0,0 +1,138 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
computed: {
|
||||
appGroups() {
|
||||
return this.app.groups.map(group => { return { id: group, name: group } })
|
||||
},
|
||||
loading() {
|
||||
let self = this
|
||||
return function(id) {
|
||||
return self.$store.getters.loading(id)
|
||||
}
|
||||
},
|
||||
installing() {
|
||||
return this.$store.getters.loading('install')
|
||||
},
|
||||
enableButtonText() {
|
||||
if (this.app.needsDownload) {
|
||||
return t('settings', 'Download and enable')
|
||||
}
|
||||
return t('settings', 'Enable')
|
||||
},
|
||||
forceEnableButtonText() {
|
||||
if (this.app.needsDownload) {
|
||||
return t('settings', 'Enable untested app')
|
||||
}
|
||||
return t('settings', 'Enable untested app')
|
||||
},
|
||||
enableButtonTooltip() {
|
||||
if (this.app.needsDownload) {
|
||||
return t('settings', 'The app will be downloaded from the app store')
|
||||
}
|
||||
return false
|
||||
},
|
||||
forceEnableButtonTooltip() {
|
||||
const base = t('settings', 'This app is not marked as compatible with your Nextcloud version. If you continue you will still be able to install the app. Note that the app might not work as expected.')
|
||||
if (this.app.needsDownload) {
|
||||
return base + ' ' + t('settings', 'The app will be downloaded from the app store')
|
||||
}
|
||||
return base
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.app.groups.length > 0) {
|
||||
this.groupCheckedAppsData = true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
asyncFindGroup(query) {
|
||||
return this.$store.dispatch('getGroups', { search: query, limit: 5, offset: 0 })
|
||||
},
|
||||
isLimitedToGroups(app) {
|
||||
if (this.app.groups.length || this.groupCheckedAppsData) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
setGroupLimit: function() {
|
||||
if (!this.groupCheckedAppsData) {
|
||||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: [] })
|
||||
}
|
||||
},
|
||||
canLimitToGroups(app) {
|
||||
if ((app.types && app.types.includes('filesystem'))
|
||||
|| app.types.includes('prelogin')
|
||||
|| app.types.includes('authentication')
|
||||
|| app.types.includes('logging')
|
||||
|| app.types.includes('prevent_group_restriction')) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
addGroupLimitation(group) {
|
||||
let groups = this.app.groups.concat([]).concat([group.id])
|
||||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: groups })
|
||||
},
|
||||
removeGroupLimitation(group) {
|
||||
let currentGroups = this.app.groups.concat([])
|
||||
let index = currentGroups.indexOf(group.id)
|
||||
if (index > -1) {
|
||||
currentGroups.splice(index, 1)
|
||||
}
|
||||
this.$store.dispatch('enableApp', { appId: this.app.id, groups: currentGroups })
|
||||
},
|
||||
forceEnable(appId) {
|
||||
this.$store.dispatch('forceEnableApp', { appId: appId, groups: [] })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
},
|
||||
enable(appId) {
|
||||
this.$store.dispatch('enableApp', { appId: appId, groups: [] })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
},
|
||||
disable(appId) {
|
||||
this.$store.dispatch('disableApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
},
|
||||
remove(appId) {
|
||||
this.$store.dispatch('uninstallApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
},
|
||||
install(appId) {
|
||||
this.$store.dispatch('enableApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
},
|
||||
update(appId) {
|
||||
this.$store.dispatch('updateApp', { appId: appId })
|
||||
.then((response) => { OC.Settings.Apps.rebuildNavigation() })
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -23,31 +23,29 @@
|
|||
<tr :data-id="token.id"
|
||||
:class="wiping">
|
||||
<td class="client">
|
||||
<div :class="iconName.icon"></div>
|
||||
<div :class="iconName.icon" />
|
||||
</td>
|
||||
<td class="token-name">
|
||||
<input v-if="token.canRename && renaming"
|
||||
type="text"
|
||||
ref="input"
|
||||
v-model="newName"
|
||||
@keyup.enter="rename"
|
||||
@blur="cancelRename"
|
||||
@keyup.esc="cancelRename">
|
||||
<span v-else>{{iconName.name}}</span>
|
||||
<span v-if="wiping"
|
||||
class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
|
||||
ref="input"
|
||||
v-model="newName"
|
||||
type="text"
|
||||
@keyup.enter="rename"
|
||||
@blur="cancelRename"
|
||||
@keyup.esc="cancelRename">
|
||||
<span v-else>{{ iconName.name }}</span>
|
||||
<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="last-activity" v-tooltip="lastActivity">{{lastActivityRelative}}</span>
|
||||
<span v-tooltip="lastActivity" class="last-activity">{{ lastActivityRelative }}</span>
|
||||
</td>
|
||||
<td class="more">
|
||||
<Actions v-if="!token.current"
|
||||
:actions="actions"
|
||||
:open.sync="actionOpen"
|
||||
v-tooltip.auto="{
|
||||
content: t('settings', 'Device settings'),
|
||||
container: 'body'
|
||||
}">
|
||||
}"
|
||||
:open.sync="actionOpen">
|
||||
<ActionCheckbox v-if="token.type === 1"
|
||||
:checked="token.scope.filesystem"
|
||||
@change.stop.prevent="$emit('toggleScope', token, 'filesystem', !token.scope.filesystem)">
|
||||
|
@ -91,7 +89,7 @@ import {
|
|||
Actions,
|
||||
ActionButton,
|
||||
ActionCheckbox
|
||||
} from 'nextcloud-vue';
|
||||
} from 'nextcloud-vue'
|
||||
|
||||
const userAgentMap = {
|
||||
ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
|
||||
|
@ -106,18 +104,18 @@ const userAgentMap = {
|
|||
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
|
||||
androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
|
||||
iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
|
||||
ipad: /\(iPad\; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
|
||||
iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/,
|
||||
androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/,
|
||||
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud\-Talk.*$/,
|
||||
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud\-Talk.*$/,
|
||||
ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
|
||||
iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)-iOS.*$/,
|
||||
androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud-android.*$/,
|
||||
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
|
||||
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
|
||||
// DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
|
||||
davDroid: /DAV(droid|x5)\/([0-9.]+)/,
|
||||
// Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
|
||||
webPirate: /(Sailfish).*WebPirate\/(\d+)/,
|
||||
// Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
|
||||
sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
|
||||
};
|
||||
}
|
||||
const nameMap = {
|
||||
ie: t('setting', 'Internet Explorer'),
|
||||
edge: t('setting', 'Edge'),
|
||||
|
@ -134,7 +132,7 @@ const nameMap = {
|
|||
davDroid: 'DAVdroid',
|
||||
webPirate: 'WebPirate',
|
||||
sailfishBrowser: 'SailfishBrowser'
|
||||
};
|
||||
}
|
||||
const iconMap = {
|
||||
ie: 'icon-desktop',
|
||||
edge: 'icon-desktop',
|
||||
|
@ -151,10 +149,10 @@ const iconMap = {
|
|||
davDroid: 'icon-phone',
|
||||
webPirate: 'icon-link',
|
||||
sailfishBrowser: 'icon-link'
|
||||
};
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "AuthToken",
|
||||
name: 'AuthToken',
|
||||
components: {
|
||||
Actions,
|
||||
ActionButton,
|
||||
|
@ -163,91 +161,93 @@ export default {
|
|||
props: {
|
||||
token: {
|
||||
type: Object,
|
||||
required: true,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lastActivityRelative () {
|
||||
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000);
|
||||
},
|
||||
lastActivity () {
|
||||
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL');
|
||||
},
|
||||
iconName () {
|
||||
// pretty format sync client user agent
|
||||
let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
|
||||
|
||||
let icon = '';
|
||||
if (matches) {
|
||||
this.token.name = t('settings', 'Sync client - {os}', {
|
||||
os: matches[1],
|
||||
version: matches[2]
|
||||
});
|
||||
icon = 'icon-desktop';
|
||||
}
|
||||
|
||||
// preserve title for cases where we format it further
|
||||
const title = this.token.name;
|
||||
let name = this.token.name;
|
||||
for (let client in userAgentMap) {
|
||||
if (matches = title.match(userAgentMap[client])) {
|
||||
if (matches[2] && matches[1]) { // version number and os
|
||||
name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1];
|
||||
} else if (matches[1]) { // only version number
|
||||
name = nameMap[client] + ' ' + matches[1];
|
||||
} else {
|
||||
name = nameMap[client];
|
||||
}
|
||||
|
||||
icon = iconMap[client];
|
||||
}
|
||||
}
|
||||
if (this.token.current) {
|
||||
name = t('settings', 'This session');
|
||||
}
|
||||
|
||||
return {
|
||||
icon,
|
||||
name,
|
||||
};
|
||||
},
|
||||
wiping() {
|
||||
return this.token.type === 2;
|
||||
}
|
||||
},
|
||||
data () {
|
||||
data() {
|
||||
return {
|
||||
showMore: this.token.canScope || this.token.canDelete,
|
||||
renaming: false,
|
||||
newName: '',
|
||||
actionOpen: false,
|
||||
};
|
||||
actionOpen: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lastActivityRelative() {
|
||||
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000)
|
||||
},
|
||||
lastActivity() {
|
||||
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL')
|
||||
},
|
||||
iconName() {
|
||||
// pretty format sync client user agent
|
||||
let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/)
|
||||
|
||||
let icon = ''
|
||||
if (matches) {
|
||||
/* eslint-disable-next-line */
|
||||
this.token.name = t('settings', 'Sync client - {os}', {
|
||||
os: matches[1],
|
||||
version: matches[2]
|
||||
})
|
||||
icon = 'icon-desktop'
|
||||
}
|
||||
|
||||
// preserve title for cases where we format it further
|
||||
const title = this.token.name
|
||||
let name = this.token.name
|
||||
for (let client in userAgentMap) {
|
||||
const matches = title.match(userAgentMap[client])
|
||||
if (matches) {
|
||||
if (matches[2] && matches[1]) { // version number and os
|
||||
name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1]
|
||||
} else if (matches[1]) { // only version number
|
||||
name = nameMap[client] + ' ' + matches[1]
|
||||
} else {
|
||||
name = nameMap[client]
|
||||
}
|
||||
|
||||
icon = iconMap[client]
|
||||
}
|
||||
}
|
||||
if (this.token.current) {
|
||||
name = t('settings', 'This session')
|
||||
}
|
||||
|
||||
return {
|
||||
icon,
|
||||
name
|
||||
}
|
||||
},
|
||||
wiping() {
|
||||
return this.token.type === 2
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
startRename() {
|
||||
// Close action (popover menu)
|
||||
this.actionOpen = false;
|
||||
this.actionOpen = false
|
||||
|
||||
this.newName = this.token.name;
|
||||
this.renaming = true;
|
||||
this.newName = this.token.name
|
||||
this.renaming = true
|
||||
this.$nextTick(() => {
|
||||
this.$refs.input.select();
|
||||
});
|
||||
this.$refs.input.select()
|
||||
})
|
||||
},
|
||||
cancelRename() {
|
||||
this.renaming = false;
|
||||
this.renaming = false
|
||||
},
|
||||
revoke() {
|
||||
this.actionOpen = false;
|
||||
this.actionOpen = false
|
||||
this.$emit('delete', this.token)
|
||||
},
|
||||
rename() {
|
||||
this.renaming = false;
|
||||
this.$emit('rename', this.token, this.newName);
|
||||
this.renaming = false
|
||||
this.$emit('rename', this.token, this.newName)
|
||||
},
|
||||
wipe() {
|
||||
this.actionOpen = false;
|
||||
this.$emit('wipe', this.token);
|
||||
this.actionOpen = false
|
||||
this.$emit('wipe', this.token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,67 +22,67 @@
|
|||
<template>
|
||||
<table id="app-tokens-table">
|
||||
<thead v-if="tokens.length">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ t('settings', 'Device') }}</th>
|
||||
<th>{{ t('settings', 'Last activity') }}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{{ t('settings', 'Device') }}</th>
|
||||
<th>{{ t('settings', 'Last activity') }}</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="token-list">
|
||||
<AuthToken v-for="token in sortedTokens"
|
||||
:key="token.id"
|
||||
:token="token"
|
||||
@toggleScope="toggleScope"
|
||||
@rename="rename"
|
||||
@delete="onDelete"
|
||||
@wipe="onWipe" />
|
||||
<AuthToken v-for="token in sortedTokens"
|
||||
:key="token.id"
|
||||
:token="token"
|
||||
@toggleScope="toggleScope"
|
||||
@rename="rename"
|
||||
@delete="onDelete"
|
||||
@wipe="onWipe" />
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AuthToken from './AuthToken';
|
||||
import AuthToken from './AuthToken'
|
||||
|
||||
export default {
|
||||
name: 'AuthTokenList',
|
||||
components: {
|
||||
AuthToken
|
||||
export default {
|
||||
name: 'AuthTokenList',
|
||||
components: {
|
||||
AuthToken
|
||||
},
|
||||
props: {
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sortedTokens() {
|
||||
return this.tokens.slice().sort((t1, t2) => {
|
||||
var ts1 = parseInt(t1.lastActivity, 10)
|
||||
var ts2 = parseInt(t2.lastActivity, 10)
|
||||
return ts2 - ts1
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleScope(token, scope, value) {
|
||||
// Just pass it on
|
||||
this.$emit('toggleScope', token, scope, value)
|
||||
},
|
||||
props: {
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
rename(token, newName) {
|
||||
// Just pass it on
|
||||
this.$emit('rename', token, newName)
|
||||
},
|
||||
computed: {
|
||||
sortedTokens () {
|
||||
return this.tokens.sort((t1, t2) => {
|
||||
var ts1 = parseInt(t1.lastActivity, 10);
|
||||
var ts2 = parseInt(t2.lastActivity, 10);
|
||||
return ts2 - ts1;
|
||||
})
|
||||
}
|
||||
onDelete(token) {
|
||||
// Just pass it on
|
||||
this.$emit('delete', token)
|
||||
},
|
||||
methods: {
|
||||
toggleScope (token, scope, value) {
|
||||
// Just pass it on
|
||||
this.$emit('toggleScope', token, scope, value);
|
||||
},
|
||||
rename (token, newName) {
|
||||
// Just pass it on
|
||||
this.$emit('rename', token, newName);
|
||||
},
|
||||
onDelete (token) {
|
||||
// Just pass it on
|
||||
this.$emit('delete', token);
|
||||
},
|
||||
onWipe(token) {
|
||||
// Just pass it on
|
||||
this.$emit('wipe', token);
|
||||
}
|
||||
onWipe(token) {
|
||||
// Just pass it on
|
||||
this.$emit('wipe', token)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -22,155 +22,159 @@
|
|||
<template>
|
||||
<div id="security" class="section">
|
||||
<h2>{{ t('settings', 'Devices & sessions') }}</h2>
|
||||
<p class="settings-hint hidden-when-empty">{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}</p>
|
||||
<p class="settings-hint hidden-when-empty">
|
||||
{{ t('settings', 'Web, desktop and mobile clients currently logged in to your account.') }}
|
||||
</p>
|
||||
<AuthTokenList :tokens="tokens"
|
||||
@toggleScope="toggleTokenScope"
|
||||
@rename="rename"
|
||||
@delete="deleteToken"
|
||||
@wipe="wipeToken" />
|
||||
@toggleScope="toggleTokenScope"
|
||||
@rename="rename"
|
||||
@delete="deleteToken"
|
||||
@wipe="wipeToken" />
|
||||
<AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Axios from 'nextcloud-axios';
|
||||
import confirmPassword from 'nextcloud-password-confirmation';
|
||||
import axios from 'nextcloud-axios'
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
|
||||
import AuthTokenList from './AuthTokenList';
|
||||
import AuthTokenSetupDialogue from './AuthTokenSetupDialogue';
|
||||
import AuthTokenList from './AuthTokenList'
|
||||
import AuthTokenSetupDialogue from './AuthTokenSetupDialogue'
|
||||
|
||||
const confirm = () => {
|
||||
return new Promise(res => {
|
||||
OC.dialogs.confirm(
|
||||
t('core', 'Do you really want to wipe your data from this device?'),
|
||||
t('core', 'Confirm wipe'),
|
||||
res,
|
||||
true
|
||||
)
|
||||
})
|
||||
}
|
||||
const confirm = () => {
|
||||
return new Promise(resolve => {
|
||||
OC.dialogs.confirm(
|
||||
t('core', 'Do you really want to wipe your data from this device?'),
|
||||
t('core', 'Confirm wipe'),
|
||||
resolve,
|
||||
true
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tap into a promise without losing the value
|
||||
*/
|
||||
const tap = cb => val => {
|
||||
cb(val);
|
||||
return val;
|
||||
};
|
||||
/**
|
||||
* Tap into a promise without losing the value
|
||||
* @param {Function} cb the callback
|
||||
* @returns {any} val the value
|
||||
*/
|
||||
const tap = cb => val => {
|
||||
cb(val)
|
||||
return val
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "AuthTokenSection",
|
||||
props: {
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
canCreateToken: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
export default {
|
||||
name: 'AuthTokenSection',
|
||||
components: {
|
||||
AuthTokenSetupDialogue,
|
||||
AuthTokenList
|
||||
},
|
||||
props: {
|
||||
tokens: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
canCreateToken: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
baseUrl: OC.generateUrl('/settings/personal/authtokens')
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addNewToken(name) {
|
||||
console.debug('creating a new app token', name)
|
||||
|
||||
const data = {
|
||||
name
|
||||
}
|
||||
return axios.post(this.baseUrl, data)
|
||||
.then(resp => resp.data)
|
||||
.then(tap(() => console.debug('app token created')))
|
||||
.then(tap(data => this.tokens.push(data.deviceToken)))
|
||||
.catch(err => {
|
||||
console.error.bind('could not create app password', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while creating device token'))
|
||||
throw err
|
||||
})
|
||||
},
|
||||
components: {
|
||||
AuthTokenSetupDialogue,
|
||||
AuthTokenList
|
||||
toggleTokenScope(token, scope, value) {
|
||||
console.debug('updating app token scope', token.id, scope, value)
|
||||
|
||||
const oldVal = token.scope[scope]
|
||||
token.scope[scope] = value
|
||||
|
||||
return this.updateToken(token)
|
||||
.then(tap(() => console.debug('app token scope updated')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not update app token scope', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while updating device token scope'))
|
||||
|
||||
// Restore
|
||||
token.scope[scope] = oldVal
|
||||
|
||||
throw err
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
baseUrl: OC.generateUrl('/settings/personal/authtokens'),
|
||||
}
|
||||
rename(token, newName) {
|
||||
console.debug('renaming app token', token.id, token.name, newName)
|
||||
|
||||
const oldName = token.name
|
||||
token.name = newName
|
||||
|
||||
return this.updateToken(token)
|
||||
.then(tap(() => console.debug('app token name updated')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not update app token name', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while updating device token name'))
|
||||
|
||||
// Restore
|
||||
token.name = oldName
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
addNewToken (name) {
|
||||
console.debug('creating a new app token', name);
|
||||
updateToken(token) {
|
||||
return axios.put(this.baseUrl + '/' + token.id, token)
|
||||
.then(resp => resp.data)
|
||||
},
|
||||
deleteToken(token) {
|
||||
console.debug('deleting app token', token)
|
||||
|
||||
const data = {
|
||||
name,
|
||||
};
|
||||
return Axios.post(this.baseUrl, data)
|
||||
.then(resp => resp.data)
|
||||
.then(tap(() => console.debug('app token created')))
|
||||
.then(tap(data => this.tokens.push(data.deviceToken)))
|
||||
.catch(err => {
|
||||
console.error.bind('could not create app password', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while creating device token'));
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
toggleTokenScope (token, scope, value) {
|
||||
console.debug('updating app token scope', token.id, scope, value);
|
||||
this.tokens = this.tokens.filter(t => t !== token)
|
||||
|
||||
const oldVal = token.scope[scope];
|
||||
token.scope[scope] = value;
|
||||
return axios.delete(this.baseUrl + '/' + token.id)
|
||||
.then(resp => resp.data)
|
||||
.then(tap(() => console.debug('app token deleted')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not delete app token', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while deleting the token'))
|
||||
|
||||
return this.updateToken(token)
|
||||
.then(tap(() => console.debug('app token scope updated')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not update app token scope', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while updating device token scope'));
|
||||
// Restore
|
||||
this.tokens.push(token)
|
||||
})
|
||||
},
|
||||
async wipeToken(token) {
|
||||
console.debug('wiping app token', token)
|
||||
|
||||
// Restore
|
||||
token.scope[scope] = oldVal;
|
||||
try {
|
||||
await confirmPassword()
|
||||
|
||||
throw err;
|
||||
})
|
||||
},
|
||||
rename (token, newName) {
|
||||
console.debug('renaming app token', token.id, token.name, newName);
|
||||
|
||||
const oldName = token.name;
|
||||
token.name = newName;
|
||||
|
||||
return this.updateToken(token)
|
||||
.then(tap(() => console.debug('app token name updated')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not update app token name', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while updating device token name'));
|
||||
|
||||
// Restore
|
||||
token.name = oldName;
|
||||
})
|
||||
},
|
||||
updateToken (token) {
|
||||
return Axios.put(this.baseUrl + '/' + token.id, token)
|
||||
.then(resp => resp.data)
|
||||
},
|
||||
deleteToken (token) {
|
||||
console.debug('deleting app token', token);
|
||||
|
||||
this.tokens = this.tokens.filter(t => t !== token);
|
||||
|
||||
return Axios.delete(this.baseUrl + '/' + token.id)
|
||||
.then(resp => resp.data)
|
||||
.then(tap(() => console.debug('app token deleted')))
|
||||
.catch(err => {
|
||||
console.error.bind('could not delete app token', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while deleting the token'));
|
||||
|
||||
// Restore
|
||||
this.tokens.push(token);
|
||||
})
|
||||
},
|
||||
async wipeToken(token) {
|
||||
console.debug('wiping app token', token);
|
||||
|
||||
try {
|
||||
await confirmPassword()
|
||||
|
||||
if (!(await confirm())) {
|
||||
console.debug('wipe aborted by user')
|
||||
return;
|
||||
}
|
||||
await Axios.post(this.baseUrl + '/wipe/' + token.id)
|
||||
console.debug('app token marked for wipe')
|
||||
|
||||
token.type = 2;
|
||||
} catch (err) {
|
||||
console.error('could not wipe app token', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while wiping the device with the token'));
|
||||
if (!(await confirm())) {
|
||||
console.debug('wipe aborted by user')
|
||||
return
|
||||
}
|
||||
await axios.post(this.baseUrl + '/wipe/' + token.id)
|
||||
console.debug('app token marked for wipe')
|
||||
|
||||
token.type = 2
|
||||
} catch (err) {
|
||||
console.error('could not wipe app token', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while wiping the device with the token'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -22,13 +22,14 @@
|
|||
<template>
|
||||
<div v-if="!adding">
|
||||
<input v-model="deviceName"
|
||||
type="text"
|
||||
@keydown.enter="submit"
|
||||
:disabled="loading"
|
||||
:placeholder="t('settings', 'App name')">
|
||||
type="text"
|
||||
:disabled="loading"
|
||||
:placeholder="t('settings', 'App name')"
|
||||
@keydown.enter="submit">
|
||||
<button class="button"
|
||||
:disabled="loading"
|
||||
@click="submit">{{ t('settings', 'Create new app password') }}
|
||||
:disabled="loading"
|
||||
@click="submit">
|
||||
{{ t('settings', 'Create new app password') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
@ -37,142 +38,142 @@
|
|||
<div class="app-password-row">
|
||||
<span class="app-password-label">{{ t('settings', 'Username') }}</span>
|
||||
<input :value="loginName"
|
||||
type="text"
|
||||
class="monospaced"
|
||||
readonly="readonly"
|
||||
@focus="selectInput"/>
|
||||
type="text"
|
||||
class="monospaced"
|
||||
readonly="readonly"
|
||||
@focus="selectInput">
|
||||
</div>
|
||||
<div class="app-password-row">
|
||||
<span class="app-password-label">{{ t('settings', 'Password') }}</span>
|
||||
<input :value="appPassword"
|
||||
type="text"
|
||||
class="monospaced"
|
||||
ref="appPassword"
|
||||
readonly="readonly"
|
||||
@focus="selectInput"/>
|
||||
<a class="icon icon-clippy"
|
||||
ref="clipboardButton"
|
||||
v-tooltip="copyTooltipOptions"
|
||||
@mouseover="hoveringCopyButton = true"
|
||||
@mouseleave="hoveringCopyButton = false"
|
||||
v-clipboard:copy="appPassword"
|
||||
v-clipboard:success="onCopyPassword"
|
||||
v-clipboard:error="onCopyPasswordFailed"></a>
|
||||
<input ref="appPassword"
|
||||
:value="appPassword"
|
||||
type="text"
|
||||
class="monospaced"
|
||||
readonly="readonly"
|
||||
@focus="selectInput">
|
||||
<a ref="clipboardButton"
|
||||
v-tooltip="copyTooltipOptions"
|
||||
v-clipboard:copy="appPassword"
|
||||
v-clipboard:success="onCopyPassword"
|
||||
v-clipboard:error="onCopyPasswordFailed"
|
||||
class="icon icon-clippy"
|
||||
@mouseover="hoveringCopyButton = true"
|
||||
@mouseleave="hoveringCopyButton = false" />
|
||||
<button class="button"
|
||||
@click="reset">
|
||||
@click="reset">
|
||||
{{ t('settings', 'Done') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="app-password-row">
|
||||
<span class="app-password-label"></span>
|
||||
<span class="app-password-label" />
|
||||
<a v-if="!showQR"
|
||||
@click="showQR = true">
|
||||
@click="showQR = true">
|
||||
{{ t('settings', 'Show QR code for mobile apps') }}
|
||||
</a>
|
||||
<QR v-else
|
||||
:value="qrUrl"></QR>
|
||||
:value="qrUrl" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import QR from '@chenfengyuan/vue-qrcode';
|
||||
import confirmPassword from 'nextcloud-password-confirmation';
|
||||
import QR from '@chenfengyuan/vue-qrcode'
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
|
||||
export default {
|
||||
name: 'AuthTokenSetupDialogue',
|
||||
components: {
|
||||
QR,
|
||||
},
|
||||
props: {
|
||||
add: {
|
||||
type: Function,
|
||||
required: true,
|
||||
export default {
|
||||
name: 'AuthTokenSetupDialogue',
|
||||
components: {
|
||||
QR
|
||||
},
|
||||
props: {
|
||||
add: {
|
||||
type: Function,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
adding: false,
|
||||
loading: false,
|
||||
deviceName: '',
|
||||
appPassword: '',
|
||||
loginName: '',
|
||||
passwordCopied: false,
|
||||
showQR: false,
|
||||
qrUrl: '',
|
||||
hoveringCopyButton: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
copyTooltipOptions() {
|
||||
const base = {
|
||||
hideOnTargetClick: false,
|
||||
trigger: 'manual'
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
adding: false,
|
||||
loading: false,
|
||||
deviceName: '',
|
||||
appPassword: '',
|
||||
loginName: '',
|
||||
passwordCopied: false,
|
||||
showQR: false,
|
||||
qrUrl: '',
|
||||
hoveringCopyButton: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
copyTooltipOptions() {
|
||||
const base = {
|
||||
hideOnTargetClick: false,
|
||||
trigger: 'manual',
|
||||
};
|
||||
|
||||
if (this.passwordCopied) {
|
||||
return {
|
||||
...base,
|
||||
content:t('core', 'Copied!'),
|
||||
show: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...base,
|
||||
content: t('core', 'Copy'),
|
||||
show: this.hoveringCopyButton,
|
||||
}
|
||||
if (this.passwordCopied) {
|
||||
return {
|
||||
...base,
|
||||
content: t('core', 'Copied!'),
|
||||
show: true
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...base,
|
||||
content: t('core', 'Copy'),
|
||||
show: this.hoveringCopyButton
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectInput(e) {
|
||||
e.currentTarget.select()
|
||||
},
|
||||
methods: {
|
||||
selectInput (e) {
|
||||
e.currentTarget.select();
|
||||
},
|
||||
submit: function () {
|
||||
confirmPassword()
|
||||
.then(() => {
|
||||
this.loading = true;
|
||||
return this.add(this.deviceName)
|
||||
submit: function() {
|
||||
confirmPassword()
|
||||
.then(() => {
|
||||
this.loading = true
|
||||
return this.add(this.deviceName)
|
||||
})
|
||||
.then(token => {
|
||||
this.adding = true
|
||||
this.loginName = token.loginName
|
||||
this.appPassword = token.token
|
||||
|
||||
const server = window.location.protocol + '//' + window.location.host + OC.getRootPath()
|
||||
this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}`
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.appPassword.select()
|
||||
})
|
||||
.then(token => {
|
||||
this.adding = true;
|
||||
this.loginName = token.loginName;
|
||||
this.appPassword = token.token;
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('could not create a new app password', err)
|
||||
OC.Notification.showTemporary(t('core', 'Error while creating device token'))
|
||||
|
||||
const server = window.location.protocol + '//' + window.location.host + OC.getRootPath();
|
||||
this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}`;
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.$refs.appPassword.select();
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('could not create a new app password', err);
|
||||
OC.Notification.showTemporary(t('core', 'Error while creating device token'));
|
||||
|
||||
this.reset();
|
||||
});
|
||||
},
|
||||
onCopyPassword() {
|
||||
this.passwordCopied = true;
|
||||
this.$refs.clipboardButton.blur();
|
||||
setTimeout(() => this.passwordCopied = false, 3000);
|
||||
},
|
||||
onCopyPasswordFailed() {
|
||||
OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'));
|
||||
},
|
||||
reset () {
|
||||
this.adding = false;
|
||||
this.loading = false;
|
||||
this.showQR = false;
|
||||
this.qrUrl = '';
|
||||
this.deviceName = '';
|
||||
this.appPassword = '';
|
||||
this.loginName = '';
|
||||
}
|
||||
this.reset()
|
||||
})
|
||||
},
|
||||
onCopyPassword() {
|
||||
this.passwordCopied = true
|
||||
this.$refs.clipboardButton.blur()
|
||||
setTimeout(() => { this.passwordCopied = false }, 3000)
|
||||
},
|
||||
onCopyPasswordFailed() {
|
||||
OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'))
|
||||
},
|
||||
reset() {
|
||||
this.adding = false
|
||||
this.loading = false
|
||||
this.showQR = false
|
||||
this.qrUrl = ''
|
||||
this.deviceName = ''
|
||||
this.appPassword = ''
|
||||
this.loginName = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
32
apps/settings/src/components/PrefixMixin.vue
Normal file
32
apps/settings/src/components/PrefixMixin.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PrefixMixin',
|
||||
methods: {
|
||||
prefix(prefix, content) {
|
||||
return prefix + '_' + content
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
40
apps/settings/src/components/SvgFilterMixin.vue
Normal file
40
apps/settings/src/components/SvgFilterMixin.vue
Normal file
|
@ -0,0 +1,40 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @author Julius Härtl <jus@bitgrid.net>
|
||||
-
|
||||
- @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/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SvgFilterMixin',
|
||||
data() {
|
||||
return {
|
||||
filterId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterUrl() {
|
||||
return `url(#${this.filterId})`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
|
||||
}
|
||||
}
|
||||
</script>
|
553
apps/settings/src/components/UserList.vue
Normal file
553
apps/settings/src/components/UserList.vue
Normal file
|
@ -0,0 +1,553 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 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>
|
||||
<div id="app-content" class="user-list-grid" @scroll.passive="onScroll">
|
||||
<div id="grid-header" class="row" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
|
||||
<div id="headerAvatar" class="avatar" />
|
||||
<div id="headerName" class="name">
|
||||
{{ t('settings', 'Username') }}
|
||||
</div>
|
||||
<div id="headerDisplayName" class="displayName">
|
||||
{{ t('settings', 'Display name') }}
|
||||
</div>
|
||||
<div id="headerPassword" class="password">
|
||||
{{ t('settings', 'Password') }}
|
||||
</div>
|
||||
<div id="headerAddress" class="mailAddress">
|
||||
{{ t('settings', 'Email') }}
|
||||
</div>
|
||||
<div id="headerGroups" class="groups">
|
||||
{{ t('settings', 'Groups') }}
|
||||
</div>
|
||||
<div v-if="subAdminsGroups.length>0 && settings.isAdmin"
|
||||
id="headerSubAdmins"
|
||||
class="subadmins">
|
||||
{{ t('settings', 'Group admin for') }}
|
||||
</div>
|
||||
<div id="headerQuota" class="quota">
|
||||
{{ t('settings', 'Quota') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showLanguages"
|
||||
id="headerLanguages"
|
||||
class="languages">
|
||||
{{ t('settings', 'Language') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath"
|
||||
class="headerStorageLocation storageLocation">
|
||||
{{ t('settings', 'Storage location') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showUserBackend"
|
||||
class="headerUserBackend userBackend">
|
||||
{{ t('settings', 'User backend') }}
|
||||
</div>
|
||||
<div v-if="showConfig.showLastLogin"
|
||||
class="headerLastLogin lastLogin">
|
||||
{{ t('settings', 'Last login') }}
|
||||
</div>
|
||||
<div class="userActions" />
|
||||
</div>
|
||||
|
||||
<form v-show="showConfig.showNewUserForm"
|
||||
id="new-user"
|
||||
class="row"
|
||||
:disabled="loading.all"
|
||||
:class="{'sticky': scrolled && showConfig.showNewUserForm}"
|
||||
@submit.prevent="createUser">
|
||||
<div :class="loading.all?'icon-loading-small':'icon-add'" />
|
||||
<div class="name">
|
||||
<input id="newusername"
|
||||
ref="newusername"
|
||||
v-model="newUser.id"
|
||||
type="text"
|
||||
required
|
||||
:placeholder="settings.newUserGenerateUserID
|
||||
? t('settings', 'Will be autogenerated')
|
||||
: t('settings', 'Username')"
|
||||
name="username"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
pattern="[a-zA-Z0-9 _\.@\-']+"
|
||||
:disabled="settings.newUserGenerateUserID">
|
||||
</div>
|
||||
<div class="displayName">
|
||||
<input id="newdisplayname"
|
||||
v-model="newUser.displayName"
|
||||
type="text"
|
||||
:placeholder="t('settings', 'Display name')"
|
||||
name="displayname"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
autocorrect="off">
|
||||
</div>
|
||||
<div class="password">
|
||||
<input id="newuserpassword"
|
||||
ref="newuserpassword"
|
||||
v-model="newUser.password"
|
||||
type="password"
|
||||
:required="newUser.mailAddress===''"
|
||||
:placeholder="t('settings', 'Password')"
|
||||
name="password"
|
||||
autocomplete="new-password"
|
||||
autocapitalize="none"
|
||||
autocorrect="off"
|
||||
:minlength="minPasswordLength">
|
||||
</div>
|
||||
<div class="mailAddress">
|
||||
<input id="newemail"
|
||||
v-model="newUser.mailAddress"
|
||||
type="email"
|
||||
:required="newUser.password==='' || settings.newUserRequireEmail"
|
||||
:placeholder="t('settings', 'Email')"
|
||||
name="email"
|
||||
autocomplete="off"
|
||||
autocapitalize="none"
|
||||
autocorrect="off">
|
||||
</div>
|
||||
<div class="groups">
|
||||
<!-- hidden input trick for vanilla html5 form validation -->
|
||||
<input v-if="!settings.isAdmin"
|
||||
id="newgroups"
|
||||
type="text"
|
||||
:value="newUser.groups"
|
||||
tabindex="-1"
|
||||
:required="!settings.isAdmin"
|
||||
:class="{'icon-loading-small': loading.groups}">
|
||||
<Multiselect v-model="newUser.groups"
|
||||
:options="canAddGroups"
|
||||
:disabled="loading.groups||loading.all"
|
||||
tag-placeholder="create"
|
||||
:placeholder="t('settings', 'Add user in group')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:multiple="true"
|
||||
:taggable="true"
|
||||
:close-on-select="false"
|
||||
:tag-width="60"
|
||||
@tag="createGroup">
|
||||
<!-- If user is not admin, he is a subadmin.
|
||||
Subadmins can't create users outside their groups
|
||||
Therefore, empty select is forbidden -->
|
||||
<span slot="noResult">{{ t('settings', 'No results') }}</span>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins">
|
||||
<Multiselect v-model="newUser.subAdminsGroups"
|
||||
:options="subAdminsGroups"
|
||||
:placeholder="t('settings', 'Set user as admin for')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:tag-width="60">
|
||||
<span slot="noResult">{{ t('settings', 'No results') }}</span>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div class="quota">
|
||||
<Multiselect v-model="newUser.quota"
|
||||
:options="quotaOptions"
|
||||
:placeholder="t('settings', 'Select user quota')"
|
||||
label="label"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:allow-empty="false"
|
||||
:taggable="true"
|
||||
@tag="validateQuota" />
|
||||
</div>
|
||||
<div v-if="showConfig.showLanguages" class="languages">
|
||||
<Multiselect v-model="newUser.language"
|
||||
:options="languages"
|
||||
:placeholder="t('settings', 'Default language')"
|
||||
label="name"
|
||||
track-by="code"
|
||||
class="multiselect-vue"
|
||||
:allow-empty="false"
|
||||
group-values="languages"
|
||||
group-label="label" />
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath" class="storageLocation" />
|
||||
<div v-if="showConfig.showUserBackend" class="userBackend" />
|
||||
<div v-if="showConfig.showLastLogin" class="lastLogin" />
|
||||
<div class="userActions">
|
||||
<input id="newsubmit"
|
||||
type="submit"
|
||||
class="button primary icon-checkmark-white has-tooltip"
|
||||
value=""
|
||||
:title="t('settings', 'Add a new user')">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<user-row v-for="(user, key) in filteredUsers"
|
||||
:key="key"
|
||||
:user="user"
|
||||
:settings="settings"
|
||||
:show-config="showConfig"
|
||||
:groups="groups"
|
||||
:sub-admins-groups="subAdminsGroups"
|
||||
:quota-options="quotaOptions"
|
||||
:languages="languages"
|
||||
:external-actions="externalActions" />
|
||||
<InfiniteLoading ref="infiniteLoading" @infinite="infiniteHandler">
|
||||
<div slot="spinner">
|
||||
<div class="users-icon-loading icon-loading" />
|
||||
</div>
|
||||
<div slot="no-more">
|
||||
<div class="users-list-end" />
|
||||
</div>
|
||||
<div slot="no-results">
|
||||
<div id="emptycontent">
|
||||
<div class="icon-contacts-dark" />
|
||||
<h2>{{ t('settings', 'No users in here') }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</InfiniteLoading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import userRow from './userList/UserRow'
|
||||
import { Multiselect } from 'nextcloud-vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
import Vue from 'vue'
|
||||
|
||||
const unlimitedQuota = {
|
||||
id: 'none',
|
||||
label: t('settings', 'Unlimited')
|
||||
}
|
||||
const defaultQuota = {
|
||||
id: 'default',
|
||||
label: t('settings', 'Default quota')
|
||||
}
|
||||
const newUser = {
|
||||
id: '',
|
||||
displayName: '',
|
||||
password: '',
|
||||
mailAddress: '',
|
||||
groups: [],
|
||||
subAdminsGroups: [],
|
||||
quota: defaultQuota,
|
||||
language: {
|
||||
code: 'en',
|
||||
name: t('settings', 'Default language')
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'UserList',
|
||||
components: {
|
||||
userRow,
|
||||
Multiselect,
|
||||
InfiniteLoading
|
||||
},
|
||||
props: {
|
||||
users: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
showConfig: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
selectedGroup: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
externalActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
unlimitedQuota,
|
||||
defaultQuota,
|
||||
loading: {
|
||||
all: false,
|
||||
groups: false
|
||||
},
|
||||
scrolled: false,
|
||||
searchQuery: '',
|
||||
newUser: Object.assign({}, newUser)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
settings() {
|
||||
return this.$store.getters.getServerData
|
||||
},
|
||||
filteredUsers() {
|
||||
if (this.selectedGroup === 'disabled') {
|
||||
return this.users.filter(user => user.enabled === false)
|
||||
}
|
||||
if (!this.settings.isAdmin) {
|
||||
// we don't want subadmins to edit themselves
|
||||
return this.users.filter(user => user.enabled !== false && user.id !== OC.getCurrentUser().uid)
|
||||
}
|
||||
return this.users.filter(user => user.enabled !== false)
|
||||
},
|
||||
groups() {
|
||||
// data provided php side + remove the disabled group
|
||||
return this.$store.getters.getGroups
|
||||
.filter(group => group.id !== 'disabled')
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
},
|
||||
canAddGroups() {
|
||||
// disabled if no permission to add new users to group
|
||||
return this.groups.map(group => {
|
||||
// clone object because we don't want
|
||||
// to edit the original groups
|
||||
group = Object.assign({}, group)
|
||||
group.$isDisabled = group.canAdd === false
|
||||
return group
|
||||
})
|
||||
},
|
||||
subAdminsGroups() {
|
||||
// data provided php side
|
||||
return this.$store.getters.getSubadminGroups
|
||||
},
|
||||
quotaOptions() {
|
||||
// convert the preset array into objects
|
||||
let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({ id: cur, label: cur }), [])
|
||||
// add default presets
|
||||
quotaPreset.unshift(this.unlimitedQuota)
|
||||
quotaPreset.unshift(this.defaultQuota)
|
||||
return quotaPreset
|
||||
},
|
||||
minPasswordLength() {
|
||||
return this.$store.getters.getPasswordPolicyMinLength
|
||||
},
|
||||
usersOffset() {
|
||||
return this.$store.getters.getUsersOffset
|
||||
},
|
||||
usersLimit() {
|
||||
return this.$store.getters.getUsersLimit
|
||||
},
|
||||
usersCount() {
|
||||
return this.users.length
|
||||
},
|
||||
|
||||
/* LANGUAGES */
|
||||
languages() {
|
||||
return [
|
||||
{
|
||||
label: t('settings', 'Common languages'),
|
||||
languages: this.settings.languages.commonlanguages
|
||||
},
|
||||
{
|
||||
label: t('settings', 'All languages'),
|
||||
languages: this.settings.languages.languages
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// watch url change and group select
|
||||
selectedGroup: function(val, old) {
|
||||
// if selected is the disabled group but it's empty
|
||||
this.redirectIfDisabled()
|
||||
this.$store.commit('resetUsers')
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
this.setNewUserDefaultGroup(val)
|
||||
},
|
||||
|
||||
// make sure the infiniteLoading state is changed if we manually
|
||||
// add/remove data from the store
|
||||
usersCount: function(val, old) {
|
||||
// deleting the last user, reset the list
|
||||
if (val === 0 && old === 1) {
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
// adding the first user, warn the infiniteLoader that
|
||||
// the list is not empty anymore (we don't fetch the newly
|
||||
// added user as we already have all the info we need)
|
||||
} else if (val === 1 && old === 0) {
|
||||
this.$refs.infiniteLoading.stateChanger.loaded()
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (!this.settings.canChangePassword) {
|
||||
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset and init new user form
|
||||
*/
|
||||
this.resetForm()
|
||||
|
||||
/**
|
||||
* Register search
|
||||
*/
|
||||
this.userSearch = new OCA.Search(this.search, this.resetSearch)
|
||||
|
||||
/**
|
||||
* If disabled group but empty, redirect
|
||||
*/
|
||||
this.redirectIfDisabled()
|
||||
},
|
||||
methods: {
|
||||
onScroll(event) {
|
||||
this.scrolled = event.target.scrollTo > 0
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate quota string to make sure it's a valid human file size
|
||||
*
|
||||
* @param {string} quota Quota in readable format '5 GB'
|
||||
* @returns {Object}
|
||||
*/
|
||||
validateQuota(quota) {
|
||||
// only used for new presets sent through @Tag
|
||||
let validQuota = OC.Util.computerFileSize(quota)
|
||||
if (validQuota !== null && validQuota >= 0) {
|
||||
// unify format output
|
||||
quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
|
||||
this.newUser.quota = { id: quota, label: quota }
|
||||
return this.newUser.quota
|
||||
}
|
||||
// Default is unlimited
|
||||
this.newUser.quota = this.quotaOptions[0]
|
||||
return this.quotaOptions[0]
|
||||
},
|
||||
|
||||
infiniteHandler($state) {
|
||||
this.$store.dispatch('getUsers', {
|
||||
offset: this.usersOffset,
|
||||
limit: this.usersLimit,
|
||||
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : '',
|
||||
search: this.searchQuery
|
||||
})
|
||||
.then((response) => { response ? $state.loaded() : $state.complete() })
|
||||
},
|
||||
|
||||
/* SEARCH */
|
||||
search(query) {
|
||||
this.searchQuery = query
|
||||
this.$store.commit('resetUsers')
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
},
|
||||
resetSearch() {
|
||||
this.search('')
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
// revert form to original state
|
||||
this.newUser = Object.assign({}, newUser)
|
||||
|
||||
/**
|
||||
* Init default language from server data. The use of this.settings
|
||||
* requires a computed variable, which break the v-model binding of the form,
|
||||
* this is a much easier solution than getter and setter on a computed var
|
||||
*/
|
||||
if (this.settings.defaultLanguage) {
|
||||
Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage)
|
||||
}
|
||||
|
||||
/**
|
||||
* In case the user directly loaded the user list within a group
|
||||
* the watch won't be triggered. We need to initialize it.
|
||||
*/
|
||||
this.setNewUserDefaultGroup(this.selectedGroup)
|
||||
|
||||
this.loading.all = false
|
||||
},
|
||||
createUser() {
|
||||
this.loading.all = true
|
||||
this.$store.dispatch('addUser', {
|
||||
userid: this.newUser.id,
|
||||
password: this.newUser.password,
|
||||
displayName: this.newUser.displayName,
|
||||
email: this.newUser.mailAddress,
|
||||
groups: this.newUser.groups.map(group => group.id),
|
||||
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
|
||||
quota: this.newUser.quota.id,
|
||||
language: this.newUser.language.code
|
||||
})
|
||||
.then(() => {
|
||||
this.resetForm()
|
||||
this.$refs.newusername.focus()
|
||||
})
|
||||
.catch((error) => {
|
||||
this.loading.all = false
|
||||
if (error.response && error.response.data && error.response.data.ocs && error.response.data.ocs.meta) {
|
||||
const statuscode = error.response.data.ocs.meta.statuscode
|
||||
if (statuscode === 102) {
|
||||
// wrong username
|
||||
this.$refs.newusername.focus()
|
||||
} else if (statuscode === 107) {
|
||||
// wrong password
|
||||
this.$refs.newuserpassword.focus()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
setNewUserDefaultGroup(value) {
|
||||
if (value && value.length > 0) {
|
||||
// setting new user default group to the current selected one
|
||||
let currentGroup = this.groups.find(group => group.id === value)
|
||||
if (currentGroup) {
|
||||
this.newUser.groups = [currentGroup]
|
||||
return
|
||||
}
|
||||
}
|
||||
// fallback, empty selected group
|
||||
this.newUser.groups = []
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new group
|
||||
*
|
||||
* @param {string} gid Group id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createGroup(gid) {
|
||||
this.loading.groups = true
|
||||
this.$store.dispatch('addGroup', gid)
|
||||
.then((group) => {
|
||||
this.newUser.groups.push(this.groups.find(group => group.id === gid))
|
||||
this.loading.groups = false
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading.groups = false
|
||||
})
|
||||
return this.$store.getters.getGroups[this.groups.length]
|
||||
},
|
||||
|
||||
/**
|
||||
* If the selected group is the disabled group but the count is 0
|
||||
* redirect to the all users page.
|
||||
* * we only check for 0 because we don't have the count on ldap
|
||||
* * and we therefore set the usercount to -1 in this specific case
|
||||
*/
|
||||
redirectIfDisabled() {
|
||||
const allGroups = this.$store.getters.getGroups
|
||||
if (this.selectedGroup === 'disabled'
|
||||
&& allGroups.findIndex(group => group.id === 'disabled' && group.usercount === 0) > -1) {
|
||||
// disabled group is empty, redirection to all users
|
||||
this.$router.push({ name: 'users' })
|
||||
this.$refs.infiniteLoading.stateChanger.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -25,160 +25,177 @@
|
|||
<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
|
||||
<template v-if="useListView">
|
||||
<transition-group name="app-list" tag="div" class="apps-list-container">
|
||||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" />
|
||||
<AppItem v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category" />
|
||||
</transition-group>
|
||||
</template>
|
||||
<template v-for="bundle in bundles" v-if="useBundleView && bundleApps(bundle.id).length > 0">
|
||||
<transition-group name="app-list" tag="div" class="apps-list-container">
|
||||
|
||||
<div class="apps-header" :key="bundle.id">
|
||||
<div class="app-image"></div>
|
||||
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" v-on:click="toggleBundle(bundle.id)"></h2>
|
||||
<div class="app-version"></div>
|
||||
<div class="app-level"></div>
|
||||
<div class="app-groups"></div>
|
||||
<div class="actions"> </div>
|
||||
<transition-group v-if="useBundleView"
|
||||
name="app-list"
|
||||
tag="div"
|
||||
class="apps-list-container">
|
||||
<template v-for="bundle in bundles">
|
||||
<div :key="bundle.id" class="apps-header">
|
||||
<div class="app-image" />
|
||||
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
|
||||
<div class="app-version" />
|
||||
<div class="app-level" />
|
||||
<div class="app-groups" />
|
||||
<div class="actions">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<app-item v-for="app in bundleApps(bundle.id)" :key="bundle.id + app.id" :app="app" :category="category"/>
|
||||
</transition-group>
|
||||
</template>
|
||||
<AppItem v-for="app in bundleApps(bundle.id)"
|
||||
:key="bundle.id + app.id"
|
||||
:app="app"
|
||||
:category="category" />
|
||||
</template>
|
||||
</transition-group>
|
||||
<template v-if="useAppStoreView">
|
||||
<app-item v-for="app in apps" :key="app.id" :app="app" :category="category" :list-view="false" />
|
||||
<AppItem v-for="app in apps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category"
|
||||
:list-view="false" />
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="apps-list-search" class="apps-list installed">
|
||||
<div class="apps-list-container">
|
||||
<template v-if="search !== '' && searchApps.length > 0">
|
||||
<div class="section">
|
||||
<div></div>
|
||||
<div />
|
||||
<td colspan="5">
|
||||
<h2>{{ t('settings', 'Results from other categories') }}</h2>
|
||||
</td>
|
||||
</div>
|
||||
<app-item v-for="app in searchApps" :key="app.id" :app="app" :category="category" :list-view="true" />
|
||||
<AppItem v-for="app in searchApps"
|
||||
:key="app.id"
|
||||
:app="app"
|
||||
:category="category"
|
||||
:list-view="true" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0">
|
||||
<div id="app-list-empty-icon" class="icon-settings-dark"></div>
|
||||
<h2>{{ t('settings', 'No apps found for your version')}}</h2>
|
||||
<div v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0" id="apps-list-empty" class="emptycontent emptycontent-search">
|
||||
<div id="app-list-empty-icon" class="icon-settings-dark" />
|
||||
<h2>{{ t('settings', 'No apps found for your version') }}</h2>
|
||||
</div>
|
||||
|
||||
<div id="searchresults"></div>
|
||||
<div id="searchresults" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import appItem from './appList/appItem';
|
||||
import prefix from './prefixMixin';
|
||||
import AppItem from './AppList/AppItem'
|
||||
import PrefixMixin from './PrefixMixin'
|
||||
|
||||
export default {
|
||||
name: 'appList',
|
||||
mixins: [prefix],
|
||||
props: ['category', 'app', 'search'],
|
||||
name: 'AppList',
|
||||
components: {
|
||||
appItem
|
||||
AppItem
|
||||
},
|
||||
mixins: [PrefixMixin],
|
||||
props: ['category', 'app', 'search'],
|
||||
computed: {
|
||||
loading() {
|
||||
return this.$store.getters.loading('list');
|
||||
return this.$store.getters.loading('list')
|
||||
},
|
||||
apps() {
|
||||
let apps = this.$store.getters.getAllApps
|
||||
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
|
||||
.sort(function (a, b) {
|
||||
const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name;
|
||||
const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name;
|
||||
return OC.Util.naturalSortCompare(sortStringA, sortStringB);
|
||||
});
|
||||
.sort(function(a, b) {
|
||||
const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
|
||||
const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
|
||||
return OC.Util.naturalSortCompare(sortStringA, sortStringB)
|
||||
})
|
||||
|
||||
if (this.category === 'installed') {
|
||||
return apps.filter(app => app.installed);
|
||||
return apps.filter(app => app.installed)
|
||||
}
|
||||
if (this.category === 'enabled') {
|
||||
return apps.filter(app => app.active && app.installed);
|
||||
return apps.filter(app => app.active && app.installed)
|
||||
}
|
||||
if (this.category === 'disabled') {
|
||||
return apps.filter(app => !app.active && app.installed);
|
||||
return apps.filter(app => !app.active && app.installed)
|
||||
}
|
||||
if (this.category === 'app-bundles') {
|
||||
return apps.filter(app => app.bundles);
|
||||
return apps.filter(app => app.bundles)
|
||||
}
|
||||
if (this.category === 'updates') {
|
||||
return apps.filter(app => app.update);
|
||||
return apps.filter(app => app.update)
|
||||
}
|
||||
// filter app store categories
|
||||
return apps.filter(app => {
|
||||
return app.appstore && app.category !== undefined &&
|
||||
(app.category === this.category || app.category.indexOf(this.category) > -1);
|
||||
});
|
||||
return app.appstore && app.category !== undefined
|
||||
&& (app.category === this.category || app.category.indexOf(this.category) > -1)
|
||||
})
|
||||
},
|
||||
bundles() {
|
||||
return this.$store.getters.getServerData.bundles;
|
||||
return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
|
||||
},
|
||||
bundleApps() {
|
||||
return function(bundle) {
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => app.bundleId === bundle);
|
||||
.filter(app => app.bundleId === bundle)
|
||||
}
|
||||
},
|
||||
searchApps() {
|
||||
if (this.search === '') {
|
||||
return [];
|
||||
return []
|
||||
}
|
||||
return this.$store.getters.getAllApps
|
||||
.filter(app => {
|
||||
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) {
|
||||
return (!this.apps.find(_app => _app.id === app.id));
|
||||
return (!this.apps.find(_app => _app.id === app.id))
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return false
|
||||
})
|
||||
},
|
||||
useAppStoreView() {
|
||||
return !this.useListView && !this.useBundleView;
|
||||
return !this.useListView && !this.useBundleView
|
||||
},
|
||||
useListView() {
|
||||
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates');
|
||||
return (this.category === 'installed' || this.category === 'enabled' || this.category === 'disabled' || this.category === 'updates')
|
||||
},
|
||||
useBundleView() {
|
||||
return (this.category === 'app-bundles');
|
||||
return (this.category === 'app-bundles')
|
||||
},
|
||||
allBundlesEnabled() {
|
||||
let self = this;
|
||||
let self = this
|
||||
return function(id) {
|
||||
return self.bundleApps(id).filter(app => !app.active).length === 0;
|
||||
return self.bundleApps(id).filter(app => !app.active).length === 0
|
||||
}
|
||||
},
|
||||
bundleToggleText() {
|
||||
let self = this;
|
||||
let self = this
|
||||
return function(id) {
|
||||
if (self.allBundlesEnabled(id)) {
|
||||
return t('settings', 'Disable all');
|
||||
return t('settings', 'Disable all')
|
||||
}
|
||||
return t('settings', 'Enable all');
|
||||
return t('settings', 'Enable all')
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
toggleBundle(id) {
|
||||
if (this.allBundlesEnabled(id)) {
|
||||
return this.disableBundle(id);
|
||||
return this.disableBundle(id)
|
||||
}
|
||||
return this.enableBundle(id);
|
||||
return this.enableBundle(id)
|
||||
},
|
||||
enableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id);
|
||||
let apps = this.bundleApps(id).map(app => app.id)
|
||||
this.$store.dispatch('enableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { console.log(error); OC.Notification.show(error)});
|
||||
.catch((error) => { console.error(error); OC.Notification.show(error) })
|
||||
},
|
||||
disableBundle(id) {
|
||||
let apps = this.bundleApps(id).map(app => app.id);
|
||||
let apps = this.bundleApps(id).map(app => app.id)
|
||||
this.$store.dispatch('disableApp', { appId: apps, groups: [] })
|
||||
.catch((error) => { OC.Notification.show(error)});
|
||||
.catch((error) => { OC.Notification.show(error) })
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -21,18 +21,18 @@
|
|||
-->
|
||||
|
||||
<template>
|
||||
<img :src="scoreImage" class="app-score-image" />
|
||||
<img :src="scoreImage" class="app-score-image">
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'appScore',
|
||||
props: ['score'],
|
||||
computed: {
|
||||
scoreImage() {
|
||||
let score = Math.round( this.score * 10 );
|
||||
let imageName = 'rating/s' + score + '.svg';
|
||||
return OC.imagePath('core', imageName);
|
||||
}
|
||||
export default {
|
||||
name: 'AppScore',
|
||||
props: ['score'],
|
||||
computed: {
|
||||
scoreImage() {
|
||||
let score = Math.round(this.score * 10)
|
||||
let imageName = 'rating/s' + score + '.svg'
|
||||
return OC.imagePath('core', imageName)
|
||||
}
|
||||
};
|
||||
</script>
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -21,12 +21,12 @@
|
|||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'prefixMixin',
|
||||
methods: {
|
||||
prefix (prefix, content) {
|
||||
return prefix + '_' + content;
|
||||
},
|
||||
export default {
|
||||
name: 'PrefixMixin',
|
||||
methods: {
|
||||
prefix(prefix, content) {
|
||||
return prefix + '_' + content
|
||||
}
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -21,20 +21,20 @@
|
|||
-->
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'svgFilterMixin',
|
||||
mounted() {
|
||||
this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100 )) + new Date().getSeconds() + new Date().getMilliseconds();
|
||||
},
|
||||
computed: {
|
||||
filterUrl () {
|
||||
return `url(#${this.filterId})`;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filterId: '',
|
||||
};
|
||||
},
|
||||
export default {
|
||||
name: 'SvgFilterMixin',
|
||||
data() {
|
||||
return {
|
||||
filterId: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
filterUrl() {
|
||||
return `url(#${this.filterId})`
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
|
||||
}
|
||||
</script>
|
||||
}
|
||||
</script>
|
||||
|
|
706
apps/settings/src/components/userList/UserRow.vue
Normal file
706
apps/settings/src/components/userList/UserRow.vue
Normal file
|
@ -0,0 +1,706 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 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>
|
||||
<!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
|
||||
<div v-if="Object.keys(user).length ===1" class="row" :data-id="user.id">
|
||||
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
|
||||
<img v-if="!loading.delete && !loading.disable && !loading.wipe"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
:src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
|
||||
</div>
|
||||
<div class="name">
|
||||
{{ user.id }}
|
||||
</div>
|
||||
<div class="obfuscated">
|
||||
{{ t('settings','You do not have permissions to see the details of this user') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- User full data -->
|
||||
<div v-else
|
||||
class="row"
|
||||
:class="{'disabled': loading.delete || loading.disable}"
|
||||
:data-id="user.id">
|
||||
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
|
||||
<img v-if="!loading.delete && !loading.disable && !loading.wipe"
|
||||
alt=""
|
||||
width="32"
|
||||
height="32"
|
||||
:src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'">
|
||||
</div>
|
||||
<!-- dirty hack to ellipsis on two lines -->
|
||||
<div class="name">
|
||||
{{ user.id }}
|
||||
</div>
|
||||
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" @submit.prevent="updateDisplayName">
|
||||
<template v-if="user.backendCapabilities.setDisplayName">
|
||||
<input v-if="user.backendCapabilities.setDisplayName"
|
||||
:id="'displayName'+user.id+rand"
|
||||
ref="displayName"
|
||||
type="text"
|
||||
:disabled="loading.displayName||loading.all"
|
||||
:value="user.displayname"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false">
|
||||
<input v-if="user.backendCapabilities.setDisplayName"
|
||||
type="submit"
|
||||
class="icon-confirm"
|
||||
value="">
|
||||
</template>
|
||||
<div v-else v-tooltip.auto="t('settings', 'The backend does not support changing the display name')" class="name">
|
||||
{{ user.displayname }}
|
||||
</div>
|
||||
</form>
|
||||
<form v-if="settings.canChangePassword && user.backendCapabilities.setPassword"
|
||||
class="password"
|
||||
:class="{'icon-loading-small': loading.password}"
|
||||
@submit.prevent="updatePassword">
|
||||
<input :id="'password'+user.id+rand"
|
||||
ref="password"
|
||||
type="password"
|
||||
required
|
||||
:disabled="loading.password||loading.all"
|
||||
:minlength="minPasswordLength"
|
||||
value=""
|
||||
:placeholder="t('settings', 'New password')"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false">
|
||||
<input type="submit" class="icon-confirm" value="">
|
||||
</form>
|
||||
<div v-else />
|
||||
<form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" @submit.prevent="updateEmail">
|
||||
<input :id="'mailAddress'+user.id+rand"
|
||||
ref="mailAddress"
|
||||
type="email"
|
||||
:disabled="loading.mailAddress||loading.all"
|
||||
:value="user.email"
|
||||
autocomplete="new-password"
|
||||
autocorrect="off"
|
||||
autocapitalize="off"
|
||||
spellcheck="false">
|
||||
<input type="submit" class="icon-confirm" value="">
|
||||
</form>
|
||||
<div class="groups" :class="{'icon-loading-small': loading.groups}">
|
||||
<Multiselect :value="userGroups"
|
||||
:options="availableGroups"
|
||||
:disabled="loading.groups||loading.all"
|
||||
tag-placeholder="create"
|
||||
:placeholder="t('settings', 'Add user in group')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:limit="2"
|
||||
:multiple="true"
|
||||
:taggable="settings.isAdmin"
|
||||
:close-on-select="false"
|
||||
:tag-width="60"
|
||||
@tag="createGroup"
|
||||
@select="addUserGroup"
|
||||
@remove="removeUserGroup">
|
||||
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userGroups)" class="multiselect__limit">+{{ userGroups.length-2 }}</span>
|
||||
<span slot="noResult">{{ t('settings', 'No results') }}</span>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div v-if="subAdminsGroups.length>0 && settings.isAdmin" class="subadmins" :class="{'icon-loading-small': loading.subadmins}">
|
||||
<Multiselect :value="userSubAdminsGroups"
|
||||
:options="subAdminsGroups"
|
||||
:disabled="loading.subadmins||loading.all"
|
||||
:placeholder="t('settings', 'Set user as admin for')"
|
||||
label="name"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:limit="2"
|
||||
:multiple="true"
|
||||
:close-on-select="false"
|
||||
:tag-width="60"
|
||||
@select="addUserSubAdmin"
|
||||
@remove="removeUserSubAdmin">
|
||||
<span slot="limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)" class="multiselect__limit">+{{ userSubAdminsGroups.length-2 }}</span>
|
||||
<span slot="noResult">{{ t('settings', 'No results') }}</span>
|
||||
</Multiselect>
|
||||
</div>
|
||||
<div v-tooltip.auto="usedSpace" class="quota" :class="{'icon-loading-small': loading.quota}">
|
||||
<Multiselect :value="userQuota"
|
||||
:options="quotaOptions"
|
||||
:disabled="loading.quota||loading.all"
|
||||
tag-placeholder="create"
|
||||
:placeholder="t('settings', 'Select user quota')"
|
||||
label="label"
|
||||
track-by="id"
|
||||
class="multiselect-vue"
|
||||
:allow-empty="false"
|
||||
:taggable="true"
|
||||
@tag="validateQuota"
|
||||
@input="setUserQuota" />
|
||||
<progress class="quota-user-progress"
|
||||
:class="{'warn':usedQuota>80}"
|
||||
:value="usedQuota"
|
||||
max="100" />
|
||||
</div>
|
||||
<div v-if="showConfig.showLanguages"
|
||||
class="languages"
|
||||
:class="{'icon-loading-small': loading.languages}">
|
||||
<Multiselect :value="userLanguage"
|
||||
:options="languages"
|
||||
:disabled="loading.languages||loading.all"
|
||||
:placeholder="t('settings', 'No language set')"
|
||||
label="name"
|
||||
track-by="code"
|
||||
class="multiselect-vue"
|
||||
:allow-empty="false"
|
||||
group-values="languages"
|
||||
group-label="label"
|
||||
@input="setUserLanguage" />
|
||||
</div>
|
||||
<div v-if="showConfig.showStoragePath" class="storageLocation">
|
||||
{{ user.storageLocation }}
|
||||
</div>
|
||||
<div v-if="showConfig.showUserBackend" class="userBackend">
|
||||
{{ user.backend }}
|
||||
</div>
|
||||
<div v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''" class="lastLogin">
|
||||
{{ user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never') }}
|
||||
</div>
|
||||
<div class="userActions">
|
||||
<div v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all" class="toggleUserActions">
|
||||
<div v-click-outside="hideMenu" class="icon-more" @click="toggleMenu" />
|
||||
<div class="popovermenu" :class="{ 'open': openedMenu }">
|
||||
<PopoverMenu :menu="userActions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
|
||||
<div class="icon-checkmark" />
|
||||
{{ feedbackMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClickOutside from 'vue-click-outside'
|
||||
import Vue from 'vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
import { PopoverMenu, Multiselect } from 'nextcloud-vue'
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
export default {
|
||||
name: 'UserRow',
|
||||
components: {
|
||||
PopoverMenu,
|
||||
Multiselect
|
||||
},
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
props: {
|
||||
user: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
settings: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
groups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
subAdminsGroups: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
quotaOptions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
showConfig: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
},
|
||||
languages: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
externalActions: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rand: parseInt(Math.random() * 1000),
|
||||
openedMenu: false,
|
||||
feedbackMessage: '',
|
||||
loading: {
|
||||
all: false,
|
||||
displayName: false,
|
||||
password: false,
|
||||
mailAddress: false,
|
||||
groups: false,
|
||||
subadmins: false,
|
||||
quota: false,
|
||||
delete: false,
|
||||
disable: false,
|
||||
languages: false,
|
||||
wipe: false
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/* USER POPOVERMENU ACTIONS */
|
||||
userActions() {
|
||||
let actions = [
|
||||
{
|
||||
icon: 'icon-delete',
|
||||
text: t('settings', 'Delete user'),
|
||||
action: this.deleteUser
|
||||
},
|
||||
{
|
||||
icon: 'icon-delete',
|
||||
text: t('settings', 'Wipe all devices'),
|
||||
action: this.wipeUserDevices
|
||||
},
|
||||
{
|
||||
icon: this.user.enabled ? 'icon-close' : 'icon-add',
|
||||
text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
|
||||
action: this.enableDisableUser
|
||||
}
|
||||
]
|
||||
if (this.user.email !== null && this.user.email !== '') {
|
||||
actions.push({
|
||||
icon: 'icon-mail',
|
||||
text: t('settings', 'Resend welcome email'),
|
||||
action: this.sendWelcomeMail
|
||||
})
|
||||
}
|
||||
return actions.concat(this.externalActions)
|
||||
},
|
||||
|
||||
/* GROUPS MANAGEMENT */
|
||||
userGroups() {
|
||||
let userGroups = this.groups.filter(group => this.user.groups.includes(group.id))
|
||||
return userGroups
|
||||
},
|
||||
userSubAdminsGroups() {
|
||||
let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id))
|
||||
return userSubAdminsGroups
|
||||
},
|
||||
availableGroups() {
|
||||
return this.groups.map((group) => {
|
||||
// clone object because we don't want
|
||||
// to edit the original groups
|
||||
let groupClone = Object.assign({}, group)
|
||||
|
||||
// two settings here:
|
||||
// 1. user NOT in group but no permission to add
|
||||
// 2. user is in group but no permission to remove
|
||||
groupClone.$isDisabled
|
||||
= (group.canAdd === false
|
||||
&& !this.user.groups.includes(group.id))
|
||||
|| (group.canRemove === false
|
||||
&& this.user.groups.includes(group.id))
|
||||
return groupClone
|
||||
})
|
||||
},
|
||||
|
||||
/* QUOTA MANAGEMENT */
|
||||
usedSpace() {
|
||||
if (this.user.quota.used) {
|
||||
return t('settings', '{size} used', { size: OC.Util.humanFileSize(this.user.quota.used) })
|
||||
}
|
||||
return t('settings', '{size} used', { size: OC.Util.humanFileSize(0) })
|
||||
},
|
||||
usedQuota() {
|
||||
let quota = this.user.quota.quota
|
||||
if (quota > 0) {
|
||||
quota = Math.min(100, Math.round(this.user.quota.used / quota * 100))
|
||||
} else {
|
||||
var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30))
|
||||
// asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
|
||||
quota = 95 * (1 - (1 / (usedInGB + 1)))
|
||||
}
|
||||
return isNaN(quota) ? 0 : quota
|
||||
},
|
||||
// Mapping saved values to objects
|
||||
userQuota() {
|
||||
if (this.user.quota.quota >= 0) {
|
||||
// if value is valid, let's map the quotaOptions or return custom quota
|
||||
let humanQuota = OC.Util.humanFileSize(this.user.quota.quota)
|
||||
let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota)
|
||||
return userQuota || { id: humanQuota, label: humanQuota }
|
||||
} else if (this.user.quota.quota === 'default') {
|
||||
// default quota is replaced by the proper value on load
|
||||
return this.quotaOptions[0]
|
||||
}
|
||||
return this.quotaOptions[1] // unlimited
|
||||
},
|
||||
|
||||
/* PASSWORD POLICY? */
|
||||
minPasswordLength() {
|
||||
return this.$store.getters.getPasswordPolicyMinLength
|
||||
},
|
||||
|
||||
/* LANGUAGE */
|
||||
userLanguage() {
|
||||
let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages)
|
||||
let userLang = availableLanguages.find(lang => lang.code === this.user.language)
|
||||
if (typeof userLang !== 'object' && this.user.language !== '') {
|
||||
return {
|
||||
code: this.user.language,
|
||||
name: this.user.language
|
||||
}
|
||||
} else if (this.user.language === '') {
|
||||
return false
|
||||
}
|
||||
return userLang
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// required if popup needs to stay opened after menu click
|
||||
// since we only have disable/delete actions, let's close it directly
|
||||
// this.popupItem = this.$el;
|
||||
},
|
||||
methods: {
|
||||
/* MENU HANDLING */
|
||||
toggleMenu() {
|
||||
this.openedMenu = !this.openedMenu
|
||||
},
|
||||
hideMenu() {
|
||||
this.openedMenu = false
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate avatar url
|
||||
*
|
||||
* @param {string} user The user name
|
||||
* @param {int} size Size integer, default 32
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAvatar(user, size = 32) {
|
||||
return OC.generateUrl(
|
||||
'/avatar/{user}/{size}?v={version}',
|
||||
{
|
||||
user: user,
|
||||
size: size,
|
||||
version: oc_userconfig.avatar.version
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
/**
|
||||
* Format array of groups objects to a string for the popup
|
||||
*
|
||||
* @param {array} groups The groups
|
||||
* @returns {string}
|
||||
*/
|
||||
formatGroupsTitle(groups) {
|
||||
let names = groups.map(group => group.name)
|
||||
return names.slice(2).join(', ')
|
||||
},
|
||||
|
||||
wipeUserDevices() {
|
||||
this.loading.wipe = true
|
||||
this.loading.all = true
|
||||
let userid = this.user.id
|
||||
return this.$store.dispatch('wipeUserDevices', userid)
|
||||
.then(() => {
|
||||
this.loading.wipe = false
|
||||
this.loading.all = false
|
||||
})
|
||||
},
|
||||
|
||||
deleteUser() {
|
||||
this.loading.delete = true
|
||||
this.loading.all = true
|
||||
let userid = this.user.id
|
||||
return this.$store.dispatch('deleteUser', userid)
|
||||
.then(() => {
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
})
|
||||
},
|
||||
|
||||
enableDisableUser() {
|
||||
this.loading.delete = true
|
||||
this.loading.all = true
|
||||
let userid = this.user.id
|
||||
let enabled = !this.user.enabled
|
||||
return this.$store.dispatch('enableDisableUser', { userid, enabled })
|
||||
.then(() => {
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user displayName
|
||||
*
|
||||
* @param {string} displayName The display name
|
||||
*/
|
||||
updateDisplayName() {
|
||||
let displayName = this.$refs.displayName.value
|
||||
this.loading.displayName = true
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'displayname',
|
||||
value: displayName
|
||||
}).then(() => {
|
||||
this.loading.displayName = false
|
||||
this.$refs.displayName.value = displayName
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user password
|
||||
*
|
||||
* @param {string} password The email adress
|
||||
*/
|
||||
updatePassword() {
|
||||
let password = this.$refs.password.value
|
||||
this.loading.password = true
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'password',
|
||||
value: password
|
||||
}).then(() => {
|
||||
this.loading.password = false
|
||||
this.$refs.password.value = '' // empty & show placeholder
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user mailAddress
|
||||
*
|
||||
* @param {string} mailAddress The email adress
|
||||
*/
|
||||
updateEmail() {
|
||||
let mailAddress = this.$refs.mailAddress.value
|
||||
this.loading.mailAddress = true
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'email',
|
||||
value: mailAddress
|
||||
}).then(() => {
|
||||
this.loading.mailAddress = false
|
||||
this.$refs.mailAddress.value = mailAddress
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new group and add user to it
|
||||
*
|
||||
* @param {string} gid Group id
|
||||
*/
|
||||
async createGroup(gid) {
|
||||
this.loading = { groups: true, subadmins: true }
|
||||
try {
|
||||
await this.$store.dispatch('addGroup', gid)
|
||||
let userid = this.user.id
|
||||
await this.$store.dispatch('addUserGroup', { userid, gid })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading = { groups: false, subadmins: false }
|
||||
}
|
||||
return this.$store.getters.getGroups[this.groups.length]
|
||||
},
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
*/
|
||||
async addUserGroup(group) {
|
||||
if (group.canAdd === false) {
|
||||
return false
|
||||
}
|
||||
this.loading.groups = true
|
||||
let userid = this.user.id
|
||||
let gid = group.id
|
||||
try {
|
||||
await this.$store.dispatch('addUserGroup', { userid, gid })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading.groups = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
*/
|
||||
async removeUserGroup(group) {
|
||||
if (group.canRemove === false) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.loading.groups = true
|
||||
let userid = this.user.id
|
||||
let gid = group.id
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('removeUserGroup', { userid, gid })
|
||||
this.loading.groups = false
|
||||
// remove user from current list if current list is the removed group
|
||||
if (this.$route.params.selectedGroup === gid) {
|
||||
this.$store.commit('deleteUser', userid)
|
||||
}
|
||||
} catch {
|
||||
this.loading.groups = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
*/
|
||||
async addUserSubAdmin(group) {
|
||||
this.loading.subadmins = true
|
||||
let userid = this.user.id
|
||||
let gid = group.id
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('addUserSubAdmin', { userid, gid })
|
||||
this.loading.subadmins = false
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
*/
|
||||
async removeUserSubAdmin(group) {
|
||||
this.loading.subadmins = true
|
||||
let userid = this.user.id
|
||||
let gid = group.id
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('removeUserSubAdmin', { userid, gid })
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading.subadmins = false
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch quota set request
|
||||
*
|
||||
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
|
||||
* @returns {string}
|
||||
*/
|
||||
async setUserQuota(quota = 'none') {
|
||||
this.loading.quota = true
|
||||
// ensure we only send the preset id
|
||||
quota = quota.id ? quota.id : quota
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'quota',
|
||||
value: quota
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading.quota = false
|
||||
}
|
||||
return quota
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate quota string to make sure it's a valid human file size
|
||||
*
|
||||
* @param {string} quota Quota in readable format '5 GB'
|
||||
* @returns {Promise|boolean}
|
||||
*/
|
||||
validateQuota(quota) {
|
||||
// only used for new presets sent through @Tag
|
||||
let validQuota = OC.Util.computerFileSize(quota)
|
||||
if (validQuota !== null && validQuota >= 0) {
|
||||
// unify format output
|
||||
return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)))
|
||||
}
|
||||
// if no valid do not change
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch language set request
|
||||
*
|
||||
* @param {Object} lang language object {code:'en', name:'English'}
|
||||
* @returns {Object}
|
||||
*/
|
||||
async setUserLanguage(lang) {
|
||||
this.loading.languages = true
|
||||
// ensure we only send the preset id
|
||||
try {
|
||||
await this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'language',
|
||||
value: lang.code
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
this.loading.languages = false
|
||||
}
|
||||
return lang
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch new welcome mail request
|
||||
*/
|
||||
sendWelcomeMail() {
|
||||
this.loading.all = true
|
||||
this.$store.dispatch('sendWelcomeMail', this.user.id)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
// Show feedback to indicate the success
|
||||
this.feedbackMessage = t('setting', 'Welcome mail sent!')
|
||||
setTimeout(() => {
|
||||
this.feedbackMessage = ''
|
||||
}, 2000)
|
||||
}
|
||||
this.loading.all = false
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -1,574 +0,0 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2018 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>
|
||||
<!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
|
||||
<div class="row" v-if="Object.keys(user).length ===1" :data-id="user.id">
|
||||
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
|
||||
<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
|
||||
v-if="!loading.delete && !loading.disable && !loading.wipe">
|
||||
</div>
|
||||
<div class="name">{{user.id}}</div>
|
||||
<div class="obfuscated">{{t('settings','You do not have permissions to see the details of this user')}}</div>
|
||||
</div>
|
||||
|
||||
<!-- User full data -->
|
||||
<div class="row" v-else :class="{'disabled': loading.delete || loading.disable}" :data-id="user.id">
|
||||
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}">
|
||||
<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
|
||||
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
|
||||
v-if="!loading.delete && !loading.disable && !loading.wipe">
|
||||
</div>
|
||||
<!-- dirty hack to ellipsis on two lines -->
|
||||
<div class="name">{{user.id}}</div>
|
||||
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" v-on:submit.prevent="updateDisplayName">
|
||||
<template v-if="user.backendCapabilities.setDisplayName">
|
||||
<input v-if="user.backendCapabilities.setDisplayName"
|
||||
:id="'displayName'+user.id+rand" type="text"
|
||||
:disabled="loading.displayName||loading.all"
|
||||
:value="user.displayname" ref="displayName"
|
||||
autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
||||
<input v-if="user.backendCapabilities.setDisplayName" type="submit" class="icon-confirm" value="" />
|
||||
</template>
|
||||
<div v-else class="name" v-tooltip.auto="t('settings', 'The backend does not support changing the display name')">{{user.displayname}}</div>
|
||||
</form>
|
||||
<form class="password" v-if="settings.canChangePassword && user.backendCapabilities.setPassword" :class="{'icon-loading-small': loading.password}"
|
||||
v-on:submit.prevent="updatePassword">
|
||||
<input :id="'password'+user.id+rand" type="password" required
|
||||
:disabled="loading.password||loading.all" :minlength="minPasswordLength"
|
||||
value="" :placeholder="t('settings', 'New password')" ref="password"
|
||||
autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
||||
<input type="submit" class="icon-confirm" value="" />
|
||||
</form>
|
||||
<div v-else></div>
|
||||
<form class="mailAddress" :class="{'icon-loading-small': loading.mailAddress}" v-on:submit.prevent="updateEmail">
|
||||
<input :id="'mailAddress'+user.id+rand" type="email"
|
||||
:disabled="loading.mailAddress||loading.all"
|
||||
:value="user.email" ref="mailAddress"
|
||||
autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false" />
|
||||
<input type="submit" class="icon-confirm" value="" />
|
||||
</form>
|
||||
<div class="groups" :class="{'icon-loading-small': loading.groups}">
|
||||
<multiselect :value="userGroups" :options="availableGroups" :disabled="loading.groups||loading.all"
|
||||
tag-placeholder="create" :placeholder="t('settings', 'Add user in group')"
|
||||
label="name" track-by="id" class="multiselect-vue" :limit="2"
|
||||
:multiple="true" :taggable="settings.isAdmin" :closeOnSelect="false"
|
||||
:tag-width="60"
|
||||
@tag="createGroup" @select="addUserGroup" @remove="removeUserGroup">
|
||||
<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userGroups)">+{{userGroups.length-2}}</span>
|
||||
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin" :class="{'icon-loading-small': loading.subadmins}">
|
||||
<multiselect :value="userSubAdminsGroups" :options="subAdminsGroups" :disabled="loading.subadmins||loading.all"
|
||||
:placeholder="t('settings', 'Set user as admin for')"
|
||||
label="name" track-by="id" class="multiselect-vue" :limit="2"
|
||||
:multiple="true" :closeOnSelect="false" :tag-width="60"
|
||||
@select="addUserSubAdmin" @remove="removeUserSubAdmin">
|
||||
<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)">+{{userSubAdminsGroups.length-2}}</span>
|
||||
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="quota" :class="{'icon-loading-small': loading.quota}" v-tooltip.auto="usedSpace">
|
||||
<multiselect :value="userQuota" :options="quotaOptions" :disabled="loading.quota||loading.all"
|
||||
tag-placeholder="create" :placeholder="t('settings', 'Select user quota')"
|
||||
label="label" track-by="id" class="multiselect-vue"
|
||||
:allowEmpty="false" :taggable="true"
|
||||
@tag="validateQuota" @input="setUserQuota">
|
||||
</multiselect>
|
||||
<progress class="quota-user-progress" :class="{'warn':usedQuota>80}" :value="usedQuota" max="100"></progress>
|
||||
</div>
|
||||
<div class="languages" :class="{'icon-loading-small': loading.languages}"
|
||||
v-if="showConfig.showLanguages">
|
||||
<multiselect :value="userLanguage" :options="languages" :disabled="loading.languages||loading.all"
|
||||
:placeholder="t('settings', 'No language set')"
|
||||
label="name" track-by="code" class="multiselect-vue"
|
||||
:allowEmpty="false" group-values="languages" group-label="label"
|
||||
@input="setUserLanguage">
|
||||
</multiselect>
|
||||
</div>
|
||||
<div class="storageLocation" v-if="showConfig.showStoragePath">{{user.storageLocation}}</div>
|
||||
<div class="userBackend" v-if="showConfig.showUserBackend">{{user.backend}}</div>
|
||||
<div class="lastLogin" v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''">
|
||||
{{user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never')}}
|
||||
</div>
|
||||
<div class="userActions">
|
||||
<div class="toggleUserActions" v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all">
|
||||
<div class="icon-more" v-click-outside="hideMenu" @click="toggleMenu"></div>
|
||||
<div class="popovermenu" :class="{ 'open': openedMenu }">
|
||||
<popover-menu :menu="userActions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="feedback" :style="{opacity: feedbackMessage !== '' ? 1 : 0}">
|
||||
<div class="icon-checkmark"></div>
|
||||
{{feedbackMessage}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClickOutside from 'vue-click-outside';
|
||||
import Vue from 'vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
import { PopoverMenu, Multiselect } from 'nextcloud-vue'
|
||||
|
||||
Vue.use(VTooltip)
|
||||
|
||||
export default {
|
||||
name: 'userRow',
|
||||
props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig', 'languages', 'externalActions'],
|
||||
components: {
|
||||
PopoverMenu,
|
||||
Multiselect
|
||||
},
|
||||
directives: {
|
||||
ClickOutside
|
||||
},
|
||||
mounted() {
|
||||
// required if popup needs to stay opened after menu click
|
||||
// since we only have disable/delete actions, let's close it directly
|
||||
// this.popupItem = this.$el;
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
rand: parseInt(Math.random() * 1000),
|
||||
openedMenu: false,
|
||||
feedbackMessage: '',
|
||||
loading: {
|
||||
all: false,
|
||||
displayName: false,
|
||||
password: false,
|
||||
mailAddress: false,
|
||||
groups: false,
|
||||
subadmins: false,
|
||||
quota: false,
|
||||
delete: false,
|
||||
disable: false,
|
||||
languages: false,
|
||||
wipe: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
/* USER POPOVERMENU ACTIONS */
|
||||
userActions() {
|
||||
let actions = [
|
||||
{
|
||||
icon: 'icon-delete',
|
||||
text: t('settings', 'Delete user'),
|
||||
action: this.deleteUser,
|
||||
},
|
||||
{
|
||||
icon: 'icon-delete',
|
||||
text: t('settings', 'Wipe all devices'),
|
||||
action: this.wipeUserDevices,
|
||||
},
|
||||
{
|
||||
icon: this.user.enabled ? 'icon-close' : 'icon-add',
|
||||
text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
|
||||
action: this.enableDisableUser,
|
||||
},
|
||||
];
|
||||
if (this.user.email !== null && this.user.email !== '') {
|
||||
actions.push({
|
||||
icon: 'icon-mail',
|
||||
text: t('settings','Resend welcome email'),
|
||||
action: this.sendWelcomeMail
|
||||
})
|
||||
}
|
||||
return actions.concat(this.externalActions);
|
||||
},
|
||||
|
||||
/* GROUPS MANAGEMENT */
|
||||
userGroups() {
|
||||
let userGroups = this.groups.filter(group => this.user.groups.includes(group.id));
|
||||
return userGroups;
|
||||
},
|
||||
userSubAdminsGroups() {
|
||||
let userSubAdminsGroups = this.subAdminsGroups.filter(group => this.user.subadmin.includes(group.id));
|
||||
return userSubAdminsGroups;
|
||||
},
|
||||
availableGroups() {
|
||||
return this.groups.map((group) => {
|
||||
// clone object because we don't want
|
||||
// to edit the original groups
|
||||
let groupClone = Object.assign({}, group);
|
||||
|
||||
// two settings here:
|
||||
// 1. user NOT in group but no permission to add
|
||||
// 2. user is in group but no permission to remove
|
||||
groupClone.$isDisabled =
|
||||
(group.canAdd === false &&
|
||||
!this.user.groups.includes(group.id)) ||
|
||||
(group.canRemove === false &&
|
||||
this.user.groups.includes(group.id));
|
||||
return groupClone;
|
||||
});
|
||||
},
|
||||
|
||||
/* QUOTA MANAGEMENT */
|
||||
usedSpace() {
|
||||
if (this.user.quota.used) {
|
||||
return t('settings', '{size} used', {size: OC.Util.humanFileSize(this.user.quota.used)});
|
||||
}
|
||||
return t('settings', '{size} used', {size: OC.Util.humanFileSize(0)});
|
||||
},
|
||||
usedQuota() {
|
||||
let quota = this.user.quota.quota;
|
||||
if (quota > 0) {
|
||||
quota = Math.min(100, Math.round(this.user.quota.used / quota * 100));
|
||||
} else {
|
||||
var usedInGB = this.user.quota.used / (10 * Math.pow(2, 30));
|
||||
//asymptotic curve approaching 50% at 10GB to visualize used stace with infinite quota
|
||||
quota = 95 * (1 - (1 / (usedInGB + 1)));
|
||||
}
|
||||
return isNaN(quota) ? 0 : quota;
|
||||
},
|
||||
// Mapping saved values to objects
|
||||
userQuota() {
|
||||
if (this.user.quota.quota >= 0) {
|
||||
// if value is valid, let's map the quotaOptions or return custom quota
|
||||
let humanQuota = OC.Util.humanFileSize(this.user.quota.quota);
|
||||
let userQuota = this.quotaOptions.find(quota => quota.id === humanQuota);
|
||||
return userQuota ? userQuota : {id:humanQuota, label:humanQuota};
|
||||
} else if (this.user.quota.quota === 'default') {
|
||||
// default quota is replaced by the proper value on load
|
||||
return this.quotaOptions[0];
|
||||
}
|
||||
return this.quotaOptions[1]; // unlimited
|
||||
},
|
||||
|
||||
/* PASSWORD POLICY? */
|
||||
minPasswordLength() {
|
||||
return this.$store.getters.getPasswordPolicyMinLength;
|
||||
},
|
||||
|
||||
/* LANGUAGE */
|
||||
userLanguage() {
|
||||
let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages);
|
||||
let userLang = availableLanguages.find(lang => lang.code === this.user.language);
|
||||
if (typeof userLang !== 'object' && this.user.language !== '') {
|
||||
return {
|
||||
code: this.user.language,
|
||||
name: this.user.language
|
||||
}
|
||||
} else if(this.user.language === '') {
|
||||
return false;
|
||||
}
|
||||
return userLang;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/* MENU HANDLING */
|
||||
toggleMenu() {
|
||||
this.openedMenu = !this.openedMenu;
|
||||
},
|
||||
hideMenu() {
|
||||
this.openedMenu = false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Generate avatar url
|
||||
*
|
||||
* @param {string} user The user name
|
||||
* @param {int} size Size integer, default 32
|
||||
* @returns {string}
|
||||
*/
|
||||
generateAvatar(user, size=32) {
|
||||
return OC.generateUrl(
|
||||
'/avatar/{user}/{size}?v={version}',
|
||||
{
|
||||
user: user,
|
||||
size: size,
|
||||
version: oc_userconfig.avatar.version
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Format array of groups objects to a string for the popup
|
||||
*
|
||||
* @param {array} groups The groups
|
||||
* @returns {string}
|
||||
*/
|
||||
formatGroupsTitle(groups) {
|
||||
let names = groups.map(group => group.name);
|
||||
return names.slice(2,).join(', ');
|
||||
},
|
||||
|
||||
wipeUserDevices() {
|
||||
this.loading.wipe = true;
|
||||
this.loading.all = true;
|
||||
let userid = this.user.id;
|
||||
return this.$store.dispatch('wipeUserDevices', userid)
|
||||
.then(() => {
|
||||
this.loading.wipe = false
|
||||
this.loading.all = false
|
||||
});
|
||||
},
|
||||
|
||||
deleteUser() {
|
||||
this.loading.delete = true;
|
||||
this.loading.all = true;
|
||||
let userid = this.user.id;
|
||||
return this.$store.dispatch('deleteUser', userid)
|
||||
.then(() => {
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
});
|
||||
},
|
||||
|
||||
enableDisableUser() {
|
||||
this.loading.delete = true;
|
||||
this.loading.all = true;
|
||||
let userid = this.user.id;
|
||||
let enabled = !this.user.enabled;
|
||||
return this.$store.dispatch('enableDisableUser', {userid, enabled})
|
||||
.then(() => {
|
||||
this.loading.delete = false
|
||||
this.loading.all = false
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user displayName
|
||||
*
|
||||
* @param {string} displayName The display name
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateDisplayName() {
|
||||
let displayName = this.$refs.displayName.value;
|
||||
this.loading.displayName = true;
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'displayname',
|
||||
value: displayName
|
||||
}).then(() => {
|
||||
this.loading.displayName = false;
|
||||
this.$refs.displayName.value = displayName;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user password
|
||||
*
|
||||
* @param {string} password The email adress
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updatePassword() {
|
||||
let password = this.$refs.password.value;
|
||||
this.loading.password = true;
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'password',
|
||||
value: password
|
||||
}).then(() => {
|
||||
this.loading.password = false;
|
||||
this.$refs.password.value = ''; // empty & show placeholder
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Set user mailAddress
|
||||
*
|
||||
* @param {string} mailAddress The email adress
|
||||
* @returns {Promise}
|
||||
*/
|
||||
updateEmail() {
|
||||
let mailAddress = this.$refs.mailAddress.value;
|
||||
this.loading.mailAddress = true;
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'email',
|
||||
value: mailAddress
|
||||
}).then(() => {
|
||||
this.loading.mailAddress = false;
|
||||
this.$refs.mailAddress.value = mailAddress;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Create a new group and add user to it
|
||||
*
|
||||
* @param {string} groups Group id
|
||||
* @returns {Promise}
|
||||
*/
|
||||
createGroup(gid) {
|
||||
this.loading = {groups:true, subadmins:true}
|
||||
this.$store.dispatch('addGroup', gid)
|
||||
.then(() => {
|
||||
this.loading = {groups:false, subadmins:false};
|
||||
let userid = this.user.id;
|
||||
this.$store.dispatch('addUserGroup', {userid, gid});
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading = {groups:false, subadmins:false};
|
||||
});
|
||||
return this.$store.getters.getGroups[this.groups.length];
|
||||
},
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
* @returns {Promise}
|
||||
*/
|
||||
addUserGroup(group) {
|
||||
if (group.canAdd === false) {
|
||||
return false;
|
||||
}
|
||||
this.loading.groups = true;
|
||||
let userid = this.user.id;
|
||||
let gid = group.id;
|
||||
return this.$store.dispatch('addUserGroup', {userid, gid})
|
||||
.then(() => this.loading.groups = false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removeUserGroup(group) {
|
||||
if (group.canRemove === false) {
|
||||
return false;
|
||||
}
|
||||
this.loading.groups = true;
|
||||
let userid = this.user.id;
|
||||
let gid = group.id;
|
||||
return this.$store.dispatch('removeUserGroup', {userid, gid})
|
||||
.then(() => {
|
||||
this.loading.groups = false
|
||||
// remove user from current list if current list is the removed group
|
||||
if (this.$route.params.selectedGroup === gid) {
|
||||
this.$store.commit('deleteUser', userid);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.loading.groups = false
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Add user to group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
* @returns {Promise}
|
||||
*/
|
||||
addUserSubAdmin(group) {
|
||||
this.loading.subadmins = true;
|
||||
let userid = this.user.id;
|
||||
let gid = group.id;
|
||||
return this.$store.dispatch('addUserSubAdmin', {userid, gid})
|
||||
.then(() => this.loading.subadmins = false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove user from group
|
||||
*
|
||||
* @param {object} group Group object
|
||||
* @returns {Promise}
|
||||
*/
|
||||
removeUserSubAdmin(group) {
|
||||
this.loading.subadmins = true;
|
||||
let userid = this.user.id;
|
||||
let gid = group.id;
|
||||
return this.$store.dispatch('removeUserSubAdmin', {userid, gid})
|
||||
.then(() => this.loading.subadmins = false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch quota set request
|
||||
*
|
||||
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
|
||||
* @returns {string}
|
||||
*/
|
||||
setUserQuota(quota = 'none') {
|
||||
this.loading.quota = true;
|
||||
// ensure we only send the preset id
|
||||
quota = quota.id ? quota.id : quota;
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'quota',
|
||||
value: quota
|
||||
}).then(() => this.loading.quota = false);
|
||||
return quota;
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate quota string to make sure it's a valid human file size
|
||||
*
|
||||
* @param {string} quota Quota in readable format '5 GB'
|
||||
* @returns {Promise|boolean}
|
||||
*/
|
||||
validateQuota(quota) {
|
||||
// only used for new presets sent through @Tag
|
||||
let validQuota = OC.Util.computerFileSize(quota);
|
||||
if (validQuota !== null && validQuota >= 0) {
|
||||
// unify format output
|
||||
return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
|
||||
}
|
||||
// if no valid do not change
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch language set request
|
||||
*
|
||||
* @param {Object} lang language object {code:'en', name:'English'}
|
||||
* @returns {Object}
|
||||
*/
|
||||
setUserLanguage(lang) {
|
||||
this.loading.languages = true;
|
||||
// ensure we only send the preset id
|
||||
this.$store.dispatch('setUserData', {
|
||||
userid: this.user.id,
|
||||
key: 'language',
|
||||
value: lang.code
|
||||
}).then(() => this.loading.languages = false);
|
||||
return lang;
|
||||
},
|
||||
|
||||
/**
|
||||
* Dispatch new welcome mail request
|
||||
*/
|
||||
sendWelcomeMail() {
|
||||
this.loading.all = true;
|
||||
this.$store.dispatch('sendWelcomeMail', this.user.id)
|
||||
.then(success => {
|
||||
if (success) {
|
||||
// Show feedback to indicate the success
|
||||
this.feedbackMessage = t('setting', 'Welcome mail sent!');
|
||||
setTimeout(() => {
|
||||
this.feedbackMessage = '';
|
||||
}, 2000);
|
||||
}
|
||||
this.loading.all = false;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -3,13 +3,14 @@ import Vue from 'vue'
|
|||
import AdminTwoFactor from './components/AdminTwoFactor.vue'
|
||||
import store from './store/admin-security'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
|
||||
Vue.prototype.t = t;
|
||||
Vue.prototype.t = t
|
||||
|
||||
// Not used here but required for legacy templates
|
||||
window.OC = window.OC || {};
|
||||
window.OC.Settings = window.OC.Settings || {};
|
||||
window.OC = window.OC || {}
|
||||
window.OC.Settings = window.OC.Settings || {}
|
||||
|
||||
store.replaceState(
|
||||
OCP.InitialState.loadState('settings', 'mandatory2FAState')
|
||||
|
|
|
@ -20,17 +20,17 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import { sync } from 'vuex-router-sync';
|
||||
import Vue from 'vue'
|
||||
import VTooltip from 'v-tooltip'
|
||||
import { sync } from 'vuex-router-sync'
|
||||
|
||||
import App from './App.vue';
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
Vue.use(VTooltip, { defaultHtml: false });
|
||||
Vue.use(VTooltip, { defaultHtml: false })
|
||||
|
||||
sync(store, router);
|
||||
sync(store, router)
|
||||
|
||||
// CSP config for webpack dynamic chunk loading
|
||||
// eslint-disable-next-line
|
||||
|
@ -43,15 +43,16 @@ __webpack_nonce__ = btoa(OC.requestToken)
|
|||
__webpack_public_path__ = OC.linkTo('settings', 'js/')
|
||||
|
||||
// bind to window
|
||||
Vue.prototype.t = t;
|
||||
Vue.prototype.OC = OC;
|
||||
Vue.prototype.OCA = OCA;
|
||||
Vue.prototype.oc_userconfig = oc_userconfig;
|
||||
Vue.prototype.t = t
|
||||
Vue.prototype.OC = OC
|
||||
Vue.prototype.OCA = OCA
|
||||
// eslint-disable-next-line camelcase
|
||||
Vue.prototype.oc_userconfig = oc_userconfig
|
||||
|
||||
const app = new Vue({
|
||||
router,
|
||||
store,
|
||||
render: h => h(App)
|
||||
}).$mount('#content');
|
||||
}).$mount('#content')
|
||||
|
||||
export { app, router, store };
|
||||
export { app, router, store }
|
||||
|
|
|
@ -19,22 +19,23 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import VueClipboard from 'vue-clipboard2';
|
||||
import VTooltip from 'v-tooltip';
|
||||
import Vue from 'vue'
|
||||
import VueClipboard from 'vue-clipboard2'
|
||||
import VTooltip from 'v-tooltip'
|
||||
|
||||
import AuthTokenSection from './components/AuthTokenSection';
|
||||
import AuthTokenSection from './components/AuthTokenSection'
|
||||
|
||||
__webpack_nonce__ = btoa(OC.requestToken);
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
|
||||
Vue.use(VueClipboard);
|
||||
Vue.use(VTooltip, { defaultHtml: false });
|
||||
Vue.prototype.t = t;
|
||||
Vue.use(VueClipboard)
|
||||
Vue.use(VTooltip, { defaultHtml: false })
|
||||
Vue.prototype.t = t
|
||||
|
||||
const View = Vue.extend(AuthTokenSection);
|
||||
const View = Vue.extend(AuthTokenSection)
|
||||
new View({
|
||||
propsData: {
|
||||
tokens: OCP.InitialState.loadState('settings', 'app_tokens'),
|
||||
canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token'),
|
||||
canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token')
|
||||
}
|
||||
}).$mount('#security-authtokens');
|
||||
}).$mount('#security-authtokens')
|
||||
|
|
|
@ -21,14 +21,14 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Vue from 'vue'
|
||||
import Router from 'vue-router'
|
||||
|
||||
// Dynamic loading
|
||||
const Users = () => import('./views/Users');
|
||||
const Apps = () => import('./views/Apps');
|
||||
const Users = () => import('./views/Users')
|
||||
const Apps = () => import('./views/Apps')
|
||||
|
||||
Vue.use(Router);
|
||||
Vue.use(Router)
|
||||
|
||||
/*
|
||||
* This is the list of routes where the vuejs app will
|
||||
|
@ -80,4 +80,4 @@ export default new Router({
|
|||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author 2019 Roeland Jago Douma <roeland@famdouma.nl>
|
||||
|
@ -24,7 +24,13 @@ import Vuex from 'vuex'
|
|||
|
||||
Vue.use(Vuex)
|
||||
|
||||
export const mutations = {
|
||||
const state = {
|
||||
enforced: false,
|
||||
enforcedGroups: [],
|
||||
excludedGroups: []
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
setEnforced(state, enabled) {
|
||||
Vue.set(state, 'enforced', enabled)
|
||||
},
|
||||
|
@ -36,28 +42,8 @@ export const mutations = {
|
|||
}
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
save ({commit}, ) {
|
||||
commit('setEnabled', false);
|
||||
|
||||
return generateCodes()
|
||||
.then(({codes, state}) => {
|
||||
commit('setEnabled', state.enabled);
|
||||
commit('setTotal', state.total);
|
||||
commit('setUsed', state.used);
|
||||
commit('setCodes', codes);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
state: {
|
||||
enforced: false,
|
||||
enforcedGroups: [],
|
||||
excludedGroups: [],
|
||||
},
|
||||
mutations,
|
||||
actions
|
||||
state,
|
||||
mutations
|
||||
})
|
||||
|
|
|
@ -21,11 +21,11 @@
|
|||
*/
|
||||
|
||||
import axios from 'nextcloud-axios'
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
import confirmPassword from 'nextcloud-password-confirmation'
|
||||
|
||||
const sanitize = function(url) {
|
||||
return url.replace(/\/$/, ''); // Remove last url slash
|
||||
};
|
||||
return url.replace(/\/$/, '') // Remove last url slash
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
|
@ -47,35 +47,35 @@ export default {
|
|||
*
|
||||
* Since Promise.then().catch().then() will always execute the last then
|
||||
* this.$store.dispatch('action').then will always be executed
|
||||
*
|
||||
*
|
||||
* If you want requireAdmin failure to also catch the API request failure
|
||||
* you will need to throw a new error in the api.get.catch()
|
||||
*
|
||||
*
|
||||
* e.g
|
||||
* api.requireAdmin().then((response) => {
|
||||
* api.get('url')
|
||||
* .then((response) => {API success})
|
||||
* .catch((error) => {throw error;});
|
||||
* }).catch((error) => {requireAdmin OR API failure});
|
||||
*
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
requireAdmin() {
|
||||
return confirmPassword();
|
||||
return confirmPassword()
|
||||
},
|
||||
get(url) {
|
||||
return axios.get(sanitize(url));
|
||||
return axios.get(sanitize(url))
|
||||
},
|
||||
post(url, data) {
|
||||
return axios.post(sanitize(url), data);
|
||||
return axios.post(sanitize(url), data)
|
||||
},
|
||||
patch(url, data) {
|
||||
return axios.patch(sanitize(url), data);
|
||||
return axios.patch(sanitize(url), data)
|
||||
},
|
||||
put(url, data) {
|
||||
return axios.put(sanitize(url), data);
|
||||
return axios.put(sanitize(url), data)
|
||||
},
|
||||
delete(url, data) {
|
||||
return axios.delete(sanitize(url), { data: data });
|
||||
return axios.delete(sanitize(url), { data: data })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -20,158 +20,158 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import api from './api';
|
||||
import Vue from 'vue';
|
||||
import api from './api'
|
||||
import Vue from 'vue'
|
||||
|
||||
const state = {
|
||||
apps: [],
|
||||
categories: [],
|
||||
updateCount: 0,
|
||||
loading: {},
|
||||
loadingList: false,
|
||||
};
|
||||
loadingList: false
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
|
||||
APPS_API_FAILURE(state, error) {
|
||||
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+error.error.response.data.data.message, {timeout: 7});
|
||||
console.log(state, error);
|
||||
OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + error.error.response.data.data.message, { timeout: 7 })
|
||||
console.error(state, error)
|
||||
},
|
||||
|
||||
initCategories(state, {categories, updateCount}) {
|
||||
state.categories = categories;
|
||||
state.updateCount = updateCount;
|
||||
initCategories(state, { categories, updateCount }) {
|
||||
state.categories = categories
|
||||
state.updateCount = updateCount
|
||||
},
|
||||
|
||||
setUpdateCount(state, updateCount) {
|
||||
state.updateCount = updateCount;
|
||||
state.updateCount = updateCount
|
||||
},
|
||||
|
||||
addCategory(state, category) {
|
||||
state.categories.push(category);
|
||||
state.categories.push(category)
|
||||
},
|
||||
|
||||
appendCategories(state, categoriesArray) {
|
||||
// convert obj to array
|
||||
state.categories = categoriesArray;
|
||||
state.categories = categoriesArray
|
||||
},
|
||||
|
||||
setAllApps(state, apps) {
|
||||
state.apps = apps;
|
||||
state.apps = apps
|
||||
},
|
||||
|
||||
setError(state, {appId, error}) {
|
||||
setError(state, { appId, error }) {
|
||||
if (!Array.isArray(appId)) {
|
||||
appId = [appId];
|
||||
appId = [appId]
|
||||
}
|
||||
appId.forEach((_id) => {
|
||||
let app = state.apps.find(app => app.id === _id);
|
||||
app.error = error;
|
||||
});
|
||||
let app = state.apps.find(app => app.id === _id)
|
||||
app.error = error
|
||||
})
|
||||
},
|
||||
|
||||
clearError(state, {appId, error}) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.error = null;
|
||||
clearError(state, { appId, error }) {
|
||||
let app = state.apps.find(app => app.id === appId)
|
||||
app.error = null
|
||||
},
|
||||
|
||||
enableApp(state, {appId, groups}) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.active = true;
|
||||
app.groups = groups;
|
||||
enableApp(state, { appId, groups }) {
|
||||
let app = state.apps.find(app => app.id === appId)
|
||||
app.active = true
|
||||
app.groups = groups
|
||||
},
|
||||
|
||||
disableApp(state, appId) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
app.active = false;
|
||||
app.groups = [];
|
||||
let app = state.apps.find(app => app.id === appId)
|
||||
app.active = false
|
||||
app.groups = []
|
||||
if (app.removable) {
|
||||
app.canUnInstall = true;
|
||||
app.canUnInstall = true
|
||||
}
|
||||
},
|
||||
|
||||
uninstallApp(state, appId) {
|
||||
state.apps.find(app => app.id === appId).active = false;
|
||||
state.apps.find(app => app.id === appId).groups = [];
|
||||
state.apps.find(app => app.id === appId).needsDownload = true;
|
||||
state.apps.find(app => app.id === appId).installed = false;
|
||||
state.apps.find(app => app.id === appId).canUnInstall = false;
|
||||
state.apps.find(app => app.id === appId).canInstall = true;
|
||||
state.apps.find(app => app.id === appId).active = false
|
||||
state.apps.find(app => app.id === appId).groups = []
|
||||
state.apps.find(app => app.id === appId).needsDownload = true
|
||||
state.apps.find(app => app.id === appId).installed = false
|
||||
state.apps.find(app => app.id === appId).canUnInstall = false
|
||||
state.apps.find(app => app.id === appId).canInstall = true
|
||||
},
|
||||
|
||||
updateApp(state, appId) {
|
||||
let app = state.apps.find(app => app.id === appId);
|
||||
let version = app.update;
|
||||
app.update = null;
|
||||
app.version = version;
|
||||
state.updateCount--;
|
||||
let app = state.apps.find(app => app.id === appId)
|
||||
let version = app.update
|
||||
app.update = null
|
||||
app.version = version
|
||||
state.updateCount--
|
||||
|
||||
},
|
||||
|
||||
resetApps(state) {
|
||||
state.apps = [];
|
||||
state.apps = []
|
||||
},
|
||||
reset(state) {
|
||||
state.apps = [];
|
||||
state.categories = [];
|
||||
state.updateCount = 0;
|
||||
state.apps = []
|
||||
state.categories = []
|
||||
state.updateCount = 0
|
||||
},
|
||||
startLoading(state, id) {
|
||||
if (Array.isArray(id)) {
|
||||
id.forEach((_id) => {
|
||||
Vue.set(state.loading, _id, true);
|
||||
Vue.set(state.loading, _id, true)
|
||||
})
|
||||
} else {
|
||||
Vue.set(state.loading, id, true);
|
||||
Vue.set(state.loading, id, true)
|
||||
}
|
||||
},
|
||||
stopLoading(state, id) {
|
||||
if (Array.isArray(id)) {
|
||||
id.forEach((_id) => {
|
||||
Vue.set(state.loading, _id, false);
|
||||
Vue.set(state.loading, _id, false)
|
||||
})
|
||||
} else {
|
||||
Vue.set(state.loading, id, false);
|
||||
Vue.set(state.loading, id, false)
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const getters = {
|
||||
loading(state) {
|
||||
return function(id) {
|
||||
return state.loading[id];
|
||||
return state.loading[id]
|
||||
}
|
||||
},
|
||||
getCategories(state) {
|
||||
return state.categories;
|
||||
return state.categories
|
||||
},
|
||||
getAllApps(state) {
|
||||
return state.apps;
|
||||
return state.apps
|
||||
},
|
||||
getUpdateCount(state) {
|
||||
return state.updateCount;
|
||||
return state.updateCount
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const actions = {
|
||||
|
||||
enableApp(context, { appId, groups }) {
|
||||
let apps;
|
||||
let apps
|
||||
if (Array.isArray(appId)) {
|
||||
apps = appId;
|
||||
apps = appId
|
||||
} else {
|
||||
apps = [appId];
|
||||
apps = [appId]
|
||||
}
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', apps);
|
||||
context.commit('startLoading', 'install');
|
||||
return api.post(OC.generateUrl(`settings/apps/enable`), {appIds: apps, groups: groups})
|
||||
context.commit('startLoading', apps)
|
||||
context.commit('startLoading', 'install')
|
||||
return api.post(OC.generateUrl(`settings/apps/enable`), { appIds: apps, groups: groups })
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps);
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
apps.forEach(_appId => {
|
||||
context.commit('enableApp', {appId: _appId, groups: groups});
|
||||
});
|
||||
context.commit('enableApp', { appId: _appId, groups: groups })
|
||||
})
|
||||
|
||||
// check for server health
|
||||
return api.get(OC.generateUrl('apps/files'))
|
||||
|
@ -182,146 +182,146 @@ const actions = {
|
|||
'settings',
|
||||
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.'
|
||||
),
|
||||
t('settings','App update'),
|
||||
function () {
|
||||
window.location.reload();
|
||||
t('settings', 'App update'),
|
||||
function() {
|
||||
window.location.reload()
|
||||
},
|
||||
true
|
||||
);
|
||||
)
|
||||
setTimeout(function() {
|
||||
location.reload();
|
||||
}, 5000);
|
||||
location.reload()
|
||||
}, 5000)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
.catch(() => {
|
||||
if (!Array.isArray(appId)) {
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: t('settings', 'Error: This app can not be enabled because it makes the server unstable')
|
||||
});
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps);
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: error.response.data.data.message
|
||||
});
|
||||
context.commit('APPS_API_FAILURE', { appId, error});
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
|
||||
},
|
||||
forceEnableApp(context, { appId, groups }) {
|
||||
let apps;
|
||||
if (Array.isArray(appId)) {
|
||||
apps = appId;
|
||||
} else {
|
||||
apps = [appId];
|
||||
}
|
||||
return api.requireAdmin().then(() => {
|
||||
context.commit('startLoading', apps);
|
||||
context.commit('startLoading', 'install');
|
||||
return api.post(OC.generateUrl(`settings/apps/force`), {appId})
|
||||
.then((response) => {
|
||||
// TODO: find a cleaner solution
|
||||
location.reload();
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps);
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: error.response.data.data.message
|
||||
});
|
||||
context.commit('APPS_API_FAILURE', { appId, error});
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
|
||||
},
|
||||
disableApp(context, { appId }) {
|
||||
let apps;
|
||||
if (Array.isArray(appId)) {
|
||||
apps = appId;
|
||||
} else {
|
||||
apps = [appId];
|
||||
}
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', apps);
|
||||
return api.post(OC.generateUrl(`settings/apps/disable`), {appIds: apps})
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps);
|
||||
apps.forEach(_appId => {
|
||||
context.commit('disableApp', _appId);
|
||||
});
|
||||
return true;
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps);
|
||||
})
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
},
|
||||
forceEnableApp(context, { appId, groups }) {
|
||||
let apps
|
||||
if (Array.isArray(appId)) {
|
||||
apps = appId
|
||||
} else {
|
||||
apps = [appId]
|
||||
}
|
||||
return api.requireAdmin().then(() => {
|
||||
context.commit('startLoading', apps)
|
||||
context.commit('startLoading', 'install')
|
||||
return api.post(OC.generateUrl(`settings/apps/force`), { appId })
|
||||
.then((response) => {
|
||||
// TODO: find a cleaner solution
|
||||
location.reload()
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('stopLoading', 'install')
|
||||
context.commit('setError', {
|
||||
appId: apps,
|
||||
error: error.response.data.data.message
|
||||
})
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
},
|
||||
disableApp(context, { appId }) {
|
||||
let apps
|
||||
if (Array.isArray(appId)) {
|
||||
apps = appId
|
||||
} else {
|
||||
apps = [appId]
|
||||
}
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', apps)
|
||||
return api.post(OC.generateUrl(`settings/apps/disable`), { appIds: apps })
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', apps)
|
||||
apps.forEach(_appId => {
|
||||
context.commit('disableApp', _appId)
|
||||
})
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', apps)
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
},
|
||||
uninstallApp(context, { appId }) {
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', appId);
|
||||
context.commit('startLoading', appId)
|
||||
return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`))
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', appId);
|
||||
context.commit('uninstallApp', appId);
|
||||
return true;
|
||||
context.commit('stopLoading', appId)
|
||||
context.commit('uninstallApp', appId)
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', appId);
|
||||
context.commit('stopLoading', appId)
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
},
|
||||
|
||||
updateApp(context, { appId }) {
|
||||
return api.requireAdmin().then((response) => {
|
||||
context.commit('startLoading', appId);
|
||||
context.commit('startLoading', 'install');
|
||||
context.commit('startLoading', appId)
|
||||
context.commit('startLoading', 'install')
|
||||
return api.get(OC.generateUrl(`settings/apps/update/${appId}`))
|
||||
.then((response) => {
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('stopLoading', appId);
|
||||
context.commit('updateApp', appId);
|
||||
return true;
|
||||
context.commit('stopLoading', 'install')
|
||||
context.commit('stopLoading', appId)
|
||||
context.commit('updateApp', appId)
|
||||
return true
|
||||
})
|
||||
.catch((error) => {
|
||||
context.commit('stopLoading', appId);
|
||||
context.commit('stopLoading', 'install');
|
||||
context.commit('stopLoading', appId)
|
||||
context.commit('stopLoading', 'install')
|
||||
context.commit('APPS_API_FAILURE', { appId, error })
|
||||
})
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }));
|
||||
}).catch((error) => context.commit('API_FAILURE', { appId, error }))
|
||||
},
|
||||
|
||||
getAllApps(context) {
|
||||
context.commit('startLoading', 'list');
|
||||
context.commit('startLoading', 'list')
|
||||
return api.get(OC.generateUrl(`settings/apps/list`))
|
||||
.then((response) => {
|
||||
context.commit('setAllApps', response.data.apps);
|
||||
context.commit('stopLoading', 'list');
|
||||
return true;
|
||||
context.commit('setAllApps', response.data.apps)
|
||||
context.commit('stopLoading', 'list')
|
||||
return true
|
||||
})
|
||||
.catch((error) => context.commit('API_FAILURE', error))
|
||||
},
|
||||
|
||||
getCategories(context) {
|
||||
context.commit('startLoading', 'categories');
|
||||
context.commit('startLoading', 'categories')
|
||||
return api.get(OC.generateUrl('settings/apps/categories'))
|
||||
.then((response) => {
|
||||
if (response.data.length > 0) {
|
||||
context.commit('appendCategories', response.data);
|
||||
context.commit('stopLoading', 'categories');
|
||||
return true;
|
||||
context.commit('appendCategories', response.data)
|
||||
context.commit('stopLoading', 'categories')
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
return false
|
||||
})
|
||||
.catch((error) => context.commit('API_FAILURE', error));
|
||||
},
|
||||
.catch((error) => context.commit('API_FAILURE', error))
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
export default { state, mutations, getters, actions };
|
||||
export default { state, mutations, getters, actions }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
|
@ -21,28 +21,28 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import users from './users';
|
||||
import apps from './apps';
|
||||
import settings from './settings';
|
||||
import oc from './oc';
|
||||
import Vue from 'vue'
|
||||
import Vuex from 'vuex'
|
||||
import users from './users'
|
||||
import apps from './apps'
|
||||
import settings from './settings'
|
||||
import oc from './oc'
|
||||
|
||||
Vue.use(Vuex)
|
||||
|
||||
const debug = process.env.NODE_ENV !== 'production';
|
||||
const debug = process.env.NODE_ENV !== 'production'
|
||||
|
||||
const mutations = {
|
||||
API_FAILURE(state, error) {
|
||||
try {
|
||||
let message = error.error.response.data.ocs.meta.message;
|
||||
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+message, {timeout: 7});
|
||||
} catch(e) {
|
||||
OC.Notification.showTemporary(t('settings','An error occured during the request. Unable to proceed.'));
|
||||
let message = error.error.response.data.ocs.meta.message
|
||||
OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + message, { timeout: 7 })
|
||||
} catch (e) {
|
||||
OC.Notification.showTemporary(t('settings', 'An error occured during the request. Unable to proceed.'))
|
||||
}
|
||||
console.log(state, error);
|
||||
console.error(state, error)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default new Vuex.Store({
|
||||
modules: {
|
||||
|
@ -54,4 +54,4 @@ export default new Vuex.Store({
|
|||
strict: debug,
|
||||
|
||||
mutations
|
||||
});
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*
|
||||
/**
|
||||
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
|
@ -20,28 +20,28 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import api from './api';
|
||||
import api from './api'
|
||||
|
||||
const state = {};
|
||||
const mutations = {};
|
||||
const getters = {};
|
||||
const state = {}
|
||||
const mutations = {}
|
||||
const getters = {}
|
||||
const actions = {
|
||||
/**
|
||||
* Set application config in database
|
||||
*
|
||||
* @param {Object} context
|
||||
* @param {Object} options
|
||||
*
|
||||
* @param {Object} context store context
|
||||
* @param {Object} options destructuring object
|
||||
* @param {string} options.app Application name
|
||||
* @param {boolean} options.key Config key
|
||||
* @param {boolean} options.value Value to set
|
||||
* @returns{Promise}
|
||||
*/
|
||||
setAppConfig(context, {app, key, value}) {
|
||||
setAppConfig(context, { app, key, value }) {
|
||||
return api.requireAdmin().then((response) => {
|
||||
return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), {value: value})
|
||||
.catch((error) => {throw error;});
|
||||
}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }));;
|
||||
}
|
||||
};
|
||||
return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), { value: value })
|
||||
.catch((error) => { throw error })
|
||||
}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }))
|
||||
}
|
||||
}
|
||||
|
||||
export default {state, mutations, getters, actions};
|
||||
export default { state, mutations, getters, actions }
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue