Comply to eslint

Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
John Molakvoæ (skjnldsv) 2019-09-25 18:19:42 +02:00
parent 7fb6512351
commit b9bc2417e7
No known key found for this signature in database
GPG Key ID: 60C25B8C072916CF
239 changed files with 12309 additions and 10453 deletions

View File

@ -23,6 +23,9 @@ watch-js:
lint-fix: lint-fix:
npm run lint:fix npm run lint:fix
lint-fix-watch:
npm run lint:fix-watch
# Cleaning # Cleaning
clean: clean:
rm -rf apps/accessibility/js/ rm -rf apps/accessibility/js/

View File

@ -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']
}
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,60 +1,56 @@
<template> <template>
<div id="accessibility" class="section"> <div id="accessibility" class="section">
<h2>{{t('accessibility', 'Accessibility')}}</h2> <h2>{{ t('accessibility', 'Accessibility') }}</h2>
<p v-html="description" /> <p v-html="description" />
<p v-html="descriptionDetail" /> <p v-html="descriptionDetail" />
<div class="preview-list"> <div class="preview-list">
<preview :preview="highcontrast" <ItemPreview :key="highcontrast.id"
:key="highcontrast.id" :selected="selected.highcontrast" :preview="highcontrast"
v-on:select="selectHighContrast"></preview> :selected="selected.highcontrast"
<preview v-for="preview in themes" :preview="preview" @select="selectHighContrast" />
:key="preview.id" :selected="selected.theme" <ItemPreview v-for="preview in themes"
v-on:select="selectTheme"></preview> :key="preview.id"
<preview v-for="preview in fonts" :preview="preview" :preview="preview"
:key="preview.id" :selected="selected.font" :selected="selected.theme"
v-on:select="selectFont"></preview> @select="selectTheme" />
<ItemPreview v-for="preview in fonts"
:key="preview.id"
:preview="preview"
:selected="selected.font"
@select="selectFont" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import preview from './components/itemPreview'; import ItemPreview from './components/ItemPreview'
import axios from 'nextcloud-axios'; import axios from 'nextcloud-axios'
export default { export default {
name: 'Accessibility', name: 'Accessibility',
components: { preview }, components: { ItemPreview },
beforeMount() {
// importing server data into the app
const serverDataElmt = document.getElementById('serverData');
if (serverDataElmt !== null) {
this.serverData = JSON.parse(
document.getElementById('serverData').dataset.server
);
}
},
data() { data() {
return { return {
serverData: [] serverData: []
}; }
}, },
computed: { computed: {
themes() { themes() {
return this.serverData.themes; return this.serverData.themes
}, },
highcontrast() { highcontrast() {
return this.serverData.highcontrast; return this.serverData.highcontrast
}, },
fonts() { fonts() {
return this.serverData.fonts; return this.serverData.fonts
}, },
selected() { selected() {
return { return {
theme: this.serverData.selected.theme, theme: this.serverData.selected.theme,
highcontrast: this.serverData.selected.highcontrast, highcontrast: this.serverData.selected.highcontrast,
font: this.serverData.selected.font font: this.serverData.selected.font
}; }
}, },
description() { description() {
// using the `t` replace method escape html, we have to do it manually :/ // 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, We aim to be compliant with the {guidelines} 2.1 on AA level,
with the high contrast theme even on AAA level.` with the high contrast theme even on AAA level.`
) )
.replace('{guidelines}', this.guidelinesLink) .replace('{guidelines}', this.guidelinesLink)
}, },
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>` 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, dont hesitate to report them on {issuetracker}. `If you find any issues, dont hesitate to report them on {issuetracker}.
And if you want to get involved, come join {designteam}!` And if you want to get involved, come join {designteam}!`
) )
.replace('{issuetracker}', this.issuetrackerLink) .replace('{issuetracker}', this.issuetrackerLink)
.replace('{designteam}', this.designteamLink) .replace('{designteam}', this.designteamLink)
}, },
issuetrackerLink() { issuetrackerLink() {
return `<a target="_blank" href="https://github.com/nextcloud/server/issues/" rel="noreferrer nofollow">${t('accessibility', 'our issue tracker')}</a>` 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>` 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: { methods: {
selectHighContrast(id) { selectHighContrast(id) {
this.selectItem('highcontrast', id); this.selectItem('highcontrast', id)
}, },
selectTheme(id, idSelectedBefore) { selectTheme(id, idSelectedBefore) {
this.selectItem('theme', id); this.selectItem('theme', id)
document.body.classList.remove(idSelectedBefore); document.body.classList.remove(idSelectedBefore)
if (id) { if (id) {
document.body.classList.add(id); document.body.classList.add(id)
} }
}, },
selectFont(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 * @param {string} id the data of the change
*/ */
selectItem(type, id) { selectItem(type, id) {
axios.post( axios.post(OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type, { value: id })
OC.linkToOCS('apps/accessibility/api/v1/config', 2) + type,
{ value: id }
)
.then(response => { .then(response => {
this.serverData.selected[type] = id; this.serverData.selected[type] = id
// Remove old link // 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) { if (!link) {
// insert new css // insert new css
let link = document.createElement('link'); let link = document.createElement('link')
link.rel = 'stylesheet'; link.rel = 'stylesheet'
link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime(); link.href = OC.generateUrl('/apps/accessibility/css/user-style.css') + '?v=' + new Date().getTime()
document.head.appendChild(link); document.head.appendChild(link)
} else { } else {
// compare arrays // compare arrays
if ( if (
JSON.stringify(Object.values(this.selected)) === JSON.stringify(Object.values(this.selected))
JSON.stringify([false, false]) === JSON.stringify([false, false])
) { ) {
// if nothing is selected, blindly remove the css // if nothing is selected, blindly remove the css
link.remove(); link.remove()
} else { } else {
// force update // force update
link.href = link.href
link.href.split('?')[0] + = link.href.split('?')[0]
'?v=' + + '?v='
new Date().getTime(); + new Date().getTime()
} }
} }
}) })
.catch(err => { .catch(err => {
console.log(err, err.response); console.error(err, err.response)
OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.')); OC.Notification.showTemporary(t('accessibility', err.response.data.ocs.meta.message + '. Unable to apply the setting.'))
}); })
} }
} }
}; }
</script> </script>

View 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>

View File

@ -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>

View File

@ -1,12 +1,12 @@
import Vue from 'vue'; import Vue from 'vue'
import App from './App.vue'; import App from './Accessibility.vue'
/* global t */ /* global t */
// bind to window // bind to window
Vue.prototype.OC = OC; Vue.prototype.OC = OC
Vue.prototype.t = t; Vue.prototype.t = t
new Vue({ export default new Vue({
el: '#accessibility', el: '#accessibility',
render: h => h(App) render: h => h(App)
}); })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
/* /**
* @author Joas Schilling <coding@schilljs.com> * @author Joas Schilling <coding@schilljs.com>
* Copyright (c) 2016 * Copyright (c) 2016
* *
@ -18,18 +18,18 @@
* @param {jQuery} $el jQuery handle for this activity * @param {jQuery} $el jQuery handle for this activity
* @param {string} view The view that displayes 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') { if (model.get('app') !== 'comments' || model.get('type') !== 'comments') {
return; return
} }
if (view === 'ActivityTabView') { if (view === 'ActivityTabView') {
$el.addClass('comment'); $el.addClass('comment')
if (model.get('message') && this._isLong(model.get('message'))) { if (model.get('message') && this._isLong(model.get('message'))) {
$el.addClass('collapsed'); $el.addClass('collapsed')
var $overlay = $('<div>').addClass('message-overlay'); var $overlay = $('<div>').addClass('message-overlay')
$el.find('.activitymessage').after($overlay); $el.find('.activitymessage').after($overlay)
$el.on('click', this._onClickCollapsedComment); $el.on('click', this._onClickCollapsedComment)
} }
} }
}, },
@ -38,22 +38,21 @@
* Copy of CommentsTabView._onClickComment() * Copy of CommentsTabView._onClickComment()
*/ */
_onClickCollapsedComment: function(ev) { _onClickCollapsedComment: function(ev) {
var $row = $(ev.target); var $row = $(ev.target)
if (!$row.is('.comment')) { if (!$row.is('.comment')) {
$row = $row.closest('.comment'); $row = $row.closest('.comment')
} }
$row.removeClass('collapsed'); $row.removeClass('collapsed')
}, },
/* /*
* Copy of CommentsTabView._isLong() * Copy of CommentsTabView._isLong()
*/ */
_isLong: function(message) { _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);

View File

@ -13,8 +13,7 @@
/** /**
* @namespace * @namespace
*/ */
OCA.Comments = {}; OCA.Comments = {}
} }
})(); })()

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/* /*
* Copyright (c) 2016 * Copyright (c) 2016
* *
@ -20,148 +21,147 @@
var CommentCollection = OC.Backbone.Collection.extend( var CommentCollection = OC.Backbone.Collection.extend(
/** @lends OCA.Comments.CommentCollection.prototype */ { /** @lends OCA.Comments.CommentCollection.prototype */ {
sync: OC.Backbone.davSync, sync: OC.Backbone.davSync,
model: OCA.Comments.CommentModel, model: OCA.Comments.CommentModel,
/** /**
* Object type * Object type
* *
* @type string * @type string
*/ */
_objectType: 'files', _objectType: 'files',
/** /**
* Object id * Object id
* *
* @type string * @type string
*/ */
_objectId: null, _objectId: null,
/** /**
* True if there are no more page results left to fetch * True if there are no more page results left to fetch
* *
* @type bool * @type bool
*/ */
_endReached: false, _endReached: false,
/** /**
* Number of comments to fetch per page * Number of comments to fetch per page
* *
* @type int * @type int
*/ */
_limit : 20, _limit: 20,
/** /**
* Initializes the collection * Initializes the collection
* *
* @param {string} [options.objectType] object type * @param {string} [options.objectType] object type
* @param {string} [options.objectId] object id * @param {string} [options.objectId] object id
*/ */
initialize: function(models, options) { initialize: function(models, options) {
options = options || {}; options = options || {}
if (options.objectType) { if (options.objectType) {
this._objectType = options.objectType; this._objectType = options.objectType
} }
if (options.objectId) { if (options.objectId) {
this._objectId = options.objectId; this._objectId = options.objectId
} }
}, },
url: function() { url: function() {
return OC.linkToRemote('dav') + '/comments/' + return OC.linkToRemote('dav') + '/comments/'
encodeURIComponent(this._objectType) + '/' + + encodeURIComponent(this._objectType) + '/'
encodeURIComponent(this._objectId) + '/'; + encodeURIComponent(this._objectId) + '/'
}, },
setObjectId: function(objectId) { setObjectId: function(objectId) {
this._objectId = objectId; this._objectId = objectId
}, },
hasMoreResults: function() { hasMoreResults: function() {
return !this._endReached; return !this._endReached
}, },
reset: function() { reset: function() {
this._endReached = false; this._endReached = false
this._summaryModel = null; this._summaryModel = null
return OC.Backbone.Collection.prototype.reset.apply(this, arguments); return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
}, },
/** /**
* Fetch the next set of results * Fetch the next set of results
*/ */
fetchNext: function(options) { fetchNext: function(options) {
var self = this; var self = this
if (!this.hasMoreResults()) { if (!this.hasMoreResults()) {
return null; 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);
} }
}, 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 * Returns the matching summary model
* *
* @return {OCA.Comments.CommentSummaryModel} summary model * @returns {OCA.Comments.CommentSummaryModel} summary model
*/ */
getSummaryModel: function() { getSummaryModel: function() {
if (!this._summaryModel) { if (!this._summaryModel) {
this._summaryModel = new OCA.Comments.CommentSummaryModel({ this._summaryModel = new OCA.Comments.CommentSummaryModel({
id: this._objectId, id: this._objectId,
objectType: this._objectType objectType: this._objectType
}); })
} }
return this._summaryModel; return this._summaryModel
}, },
/** /**
* Updates the read marker for this comment thread * Updates the read marker for this comment thread
* *
* @param {Date} [date] optional date, defaults to now * @param {Date} [date] optional date, defaults to now
* @param {Object} [options] backbone options * @param {Object} [options] backbone options
*/ */
updateReadMarker: function(date, options) { updateReadMarker: function(date, options) {
options = options || {}; options = options || {}
return this.getSummaryModel().save({ return this.getSummaryModel().save({
readMarker: (date || new Date()).toUTCString() readMarker: (date || new Date()).toUTCString()
}, options); }, options)
} }
}); })
OCA.Comments.CommentCollection = CommentCollection;
})(OC, OCA);
OCA.Comments.CommentCollection = CommentCollection
})(OC, OCA)

View File

@ -12,7 +12,7 @@
_.extend(OC.Files.Client, { _.extend(OC.Files.Client, {
PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id', 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_ACTORTYPE: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorType',
PROPERTY_ACTORID: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorId', PROPERTY_ACTORID: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorId',
PROPERTY_ISUNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}isUnread', PROPERTY_ISUNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}isUnread',
@ -21,7 +21,7 @@
PROPERTY_ACTORDISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorDisplayName', PROPERTY_ACTORDISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}actorDisplayName',
PROPERTY_CREATIONDATETIME: '{' + OC.Files.Client.NS_OWNCLOUD + '}creationDateTime', PROPERTY_CREATIONDATETIME: '{' + OC.Files.Client.NS_OWNCLOUD + '}creationDateTime',
PROPERTY_MENTIONS: '{' + OC.Files.Client.NS_OWNCLOUD + '}mentions' PROPERTY_MENTIONS: '{' + OC.Files.Client.NS_OWNCLOUD + '}mentions'
}); })
/** /**
* @class OCA.Comments.CommentModel * @class OCA.Comments.CommentModel
@ -32,62 +32,62 @@
*/ */
var CommentModel = OC.Backbone.Model.extend( var CommentModel = OC.Backbone.Model.extend(
/** @lends OCA.Comments.CommentModel.prototype */ { /** @lends OCA.Comments.CommentModel.prototype */ {
sync: OC.Backbone.davSync, sync: OC.Backbone.davSync,
defaults: { defaults: {
actorType: 'users', actorType: 'users',
objectType: 'files' objectType: 'files'
}, },
davProperties: { davProperties: {
'id': OC.Files.Client.PROPERTY_FILEID, 'id': OC.Files.Client.PROPERTY_FILEID,
'message': OC.Files.Client.PROPERTY_MESSAGE, 'message': OC.Files.Client.PROPERTY_MESSAGE,
'actorType': OC.Files.Client.PROPERTY_ACTORTYPE, 'actorType': OC.Files.Client.PROPERTY_ACTORTYPE,
'actorId': OC.Files.Client.PROPERTY_ACTORID, 'actorId': OC.Files.Client.PROPERTY_ACTORID,
'actorDisplayName': OC.Files.Client.PROPERTY_ACTORDISPLAYNAME, 'actorDisplayName': OC.Files.Client.PROPERTY_ACTORDISPLAYNAME,
'creationDateTime': OC.Files.Client.PROPERTY_CREATIONDATETIME, 'creationDateTime': OC.Files.Client.PROPERTY_CREATIONDATETIME,
'objectType': OC.Files.Client.PROPERTY_OBJECTTYPE, 'objectType': OC.Files.Client.PROPERTY_OBJECTTYPE,
'objectId': OC.Files.Client.PROPERTY_OBJECTID, 'objectId': OC.Files.Client.PROPERTY_OBJECTID,
'isUnread': OC.Files.Client.PROPERTY_ISUNREAD, 'isUnread': OC.Files.Client.PROPERTY_ISUNREAD,
'mentions': OC.Files.Client.PROPERTY_MENTIONS 'mentions': OC.Files.Client.PROPERTY_MENTIONS
}, },
parse: function(data) { parse: function(data) {
return { return {
id: data.id, id: data.id,
message: data.message, message: data.message,
actorType: data.actorType, actorType: data.actorType,
actorId: data.actorId, actorId: data.actorId,
actorDisplayName: data.actorDisplayName, actorDisplayName: data.actorDisplayName,
creationDateTime: data.creationDateTime, creationDateTime: data.creationDateTime,
objectType: data.objectType, objectType: data.objectType,
objectId: data.objectId, objectId: data.objectId,
isUnread: (data.isUnread === 'true'), isUnread: (data.isUnread === 'true'),
mentions: this._parseMentions(data.mentions) 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;
} }
result[i] = {}; },
for (var child = mention.firstChild; child; child = child.nextSibling) {
if(_.isUndefined(child.localName) || !child.localName.startsWith('mention')) { _parseMentions: function(mentions) {
continue; 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; OCA.Comments.CommentModel = CommentModel
})(OC, OCA); })(OC, OCA)

View File

@ -15,4 +15,4 @@ import './vendor/At.js/dist/js/jquery.atwho.min'
import './style/autocomplete.scss' import './style/autocomplete.scss'
import './style/comments.scss' import './style/comments.scss'
window.OCA.Comments = OCA.Comments; window.OCA.Comments = OCA.Comments

View File

@ -8,7 +8,6 @@
* *
*/ */
/* global Handlebars */
(function() { (function() {
/** /**
@ -23,7 +22,7 @@
_scopes: [ _scopes: [
{ {
name: 'edit', name: 'edit',
displayName: t('comments', 'Edit comment'), displayName: t('comments', 'Edit comment'),
iconClass: 'icon-rename' iconClass: 'icon-rename'
}, },
{ {
@ -45,14 +44,14 @@
* @param {Object} event event object * @param {Object} event event object
*/ */
_onClickAction: function(event) { _onClickAction: function(event) {
var $target = $(event.currentTarget); var $target = $(event.currentTarget)
if (!$target.hasClass('menuitem')) { 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() { render: function() {
this.$el.html(OCA.Comments.Templates['commentsmodifymenu']({ this.$el.html(OCA.Comments.Templates['commentsmodifymenu']({
items: this._scopes items: this._scopes
})); }))
}, },
/** /**
* Displays the menu * Displays the menu
* @param {Event} context the click event
*/ */
show: function(context) { show: function(context) {
this._context = context; this._context = context
for(var i in this._scopes) { for (var i in this._scopes) {
this._scopes[i].active = false; this._scopes[i].active = false
} }
var $el = $(context.target)
var $el = $(context.target); var offsetIcon = $el.offset()
var offsetIcon = $el.offset(); var offsetContainer = $el.closest('.authorRow').offset()
var offsetContainer = $el.closest('.authorRow').offset();
// adding some extra top offset to push the menu below the button. // adding some extra top offset to push the menu below the button.
var position = { var position = {
top: offsetIcon.top - offsetContainer.top + 48, top: offsetIcon.top - offsetContainer.top + 48,
left: '', left: '',
right: '' right: ''
}; }
position.left = offsetIcon.left - offsetContainer.left; position.left = offsetIcon.left - offsetContainer.left
if (position.left > 200) { if (position.left > 200) {
// we need to position the menu to the right. // we need to position the menu to the right.
position.left = ''; position.left = ''
position.right = this.$el.closest('.comment').find('.date').width(); position.right = this.$el.closest('.comment').find('.date').width()
this.$el.removeClass('menu-left').addClass('menu-right'); this.$el.removeClass('menu-left').addClass('menu-right')
} else { } else {
this.$el.removeClass('menu-right').addClass('menu-left'); this.$el.removeClass('menu-right').addClass('menu-left')
} }
this.$el.css(position); this.$el.css(position)
this.render(); this.render()
this.$el.removeClass('hidden'); this.$el.removeClass('hidden')
OC.showMenu(null, this.$el); OC.showMenu(null, this.$el)
} }
}); })
OCA.Comments = OCA.Comments || {}; OCA.Comments = OCA.Comments || {}
OCA.Comments.CommentsModifyMenu = CommentsModifyMenu; OCA.Comments.CommentsModifyMenu = CommentsModifyMenu
})(OC, OCA); })(OC, OCA)

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,7 @@
_.extend(OC.Files.Client, { _.extend(OC.Files.Client, {
PROPERTY_READMARKER: '{' + OC.Files.Client.NS_OWNCLOUD + '}readMarker' PROPERTY_READMARKER: '{' + OC.Files.Client.NS_OWNCLOUD + '}readMarker'
}); })
/** /**
* @class OCA.Comments.CommentSummaryModel * @class OCA.Comments.CommentSummaryModel
@ -24,45 +24,47 @@
*/ */
var CommentSummaryModel = OC.Backbone.Model.extend( var CommentSummaryModel = OC.Backbone.Model.extend(
/** @lends OCA.Comments.CommentSummaryModel.prototype */ { /** @lends OCA.Comments.CommentSummaryModel.prototype */ {
sync: OC.Backbone.davSync, sync: OC.Backbone.davSync,
/** /**
* Object type * Object type
* *
* @type string * @type string
*/ */
_objectType: 'files', _objectType: 'files',
/** /**
* Object id * Object id
* *
* @type string * @type string
*/ */
_objectId: null, _objectId: null,
davProperties: { davProperties: {
'readMarker': OC.Files.Client.PROPERTY_READMARKER 'readMarker': OC.Files.Client.PROPERTY_READMARKER
}, },
/** /**
* Initializes the summary model * Initializes the summary model
* *
* @param {string} [options.objectType] object type * @param {any} [attrs] ignored
* @param {string} [options.objectId] object id * @param {Object} [options] destructuring object
*/ * @param {string} [options.objectType] object type
initialize: function(attrs, options) { * @param {string} [options.objectId] object id
options = options || {}; */
if (options.objectType) { initialize: function(attrs, options) {
this._objectType = options.objectType; options = options || {}
if (options.objectType) {
this._objectType = options.objectType
}
},
url: function() {
return OC.linkToRemote('dav') + '/comments/'
+ encodeURIComponent(this._objectType) + '/'
+ encodeURIComponent(this.id) + '/'
} }
}, })
url: function() { OCA.Comments.CommentSummaryModel = CommentSummaryModel
return OC.linkToRemote('dav') + '/comments/' + })(OC, OCA)
encodeURIComponent(this._objectType) + '/' +
encodeURIComponent(this.id) + '/';
}
});
OCA.Comments.CommentSummaryModel = CommentSummaryModel;
})(OC, OCA);

View File

@ -8,20 +8,18 @@
* *
*/ */
/* global Handlebars */
(function() { (function() {
_.extend(OC.Files.Client, { _.extend(OC.Files.Client, {
PROPERTY_COMMENTS_UNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}comments-unread' PROPERTY_COMMENTS_UNREAD: '{' + OC.Files.Client.NS_OWNCLOUD + '}comments-unread'
}); })
OCA.Comments = _.extend({}, OCA.Comments); OCA.Comments = _.extend({}, OCA.Comments)
if (!OCA.Comments) { if (!OCA.Comments) {
/** /**
* @namespace * @namespace
*/ */
OCA.Comments = {}; OCA.Comments = {}
} }
/** /**
@ -38,43 +36,43 @@
count: count, count: count,
countMessage: n('comments', '%n unread comment', '%n unread comments', count), countMessage: n('comments', '%n unread comment', '%n unread comments', count),
iconUrl: OC.imagePath('core', 'actions/comment') iconUrl: OC.imagePath('core', 'actions/comment')
}); })
}, },
attach: function(fileList) { attach: function(fileList) {
var self = this; var self = this
if (this.ignoreLists.indexOf(fileList.id) >= 0) { 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() { fileList._getWebdavProperties = function() {
var props = oldGetWebdavProperties.apply(this, arguments); var props = oldGetWebdavProperties.apply(this, arguments)
props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD); props.push(OC.Files.Client.PROPERTY_COMMENTS_UNREAD)
return props; return props
}; }
fileList.filesClient.addFileInfoParser(function(response) { fileList.filesClient.addFileInfoParser(function(response) {
var data = {}; var data = {}
var props = response.propStat[0].properties; var props = response.propStat[0].properties
var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD]; var commentsUnread = props[OC.Files.Client.PROPERTY_COMMENTS_UNREAD]
if (!_.isUndefined(commentsUnread) && commentsUnread !== '') { if (!_.isUndefined(commentsUnread) && commentsUnread !== '') {
data.commentsUnread = parseInt(commentsUnread, 10); data.commentsUnread = parseInt(commentsUnread, 10)
} }
return data; return data
}); })
fileList.$el.addClass('has-comments'); fileList.$el.addClass('has-comments')
var oldCreateRow = fileList._createRow; var oldCreateRow = fileList._createRow
fileList._createRow = function(fileData) { fileList._createRow = function(fileData) {
var $tr = oldCreateRow.apply(this, arguments); var $tr = oldCreateRow.apply(this, arguments)
if (fileData.commentsUnread) { 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 // register "comment" action for reading comments
fileList.fileActions.registerAction({ fileList.fileActions.registerAction({
@ -94,35 +92,35 @@
permissions: OC.PERMISSION_READ, permissions: OC.PERMISSION_READ,
type: OCA.Files.FileActions.TYPE_INLINE, type: OCA.Files.FileActions.TYPE_INLINE,
render: function(actionSpec, isDefault, context) { render: function(actionSpec, isDefault, context) {
var $file = context.$file; var $file = context.$file
var unreadComments = $file.data('comments-unread'); var unreadComments = $file.data('comments-unread')
if (unreadComments) { if (unreadComments) {
var $actionLink = $(self._formatCommentCount(unreadComments)); var $actionLink = $(self._formatCommentCount(unreadComments))
context.$file.find('a.name>span.fileactions').append($actionLink); context.$file.find('a.name>span.fileactions').append($actionLink)
return $actionLink; return $actionLink
} }
return ''; return ''
}, },
actionHandler: function(fileName, context) { actionHandler: function(fileName, context) {
context.$file.find('.action-comment').tooltip('hide'); context.$file.find('.action-comment').tooltip('hide')
// open sidebar in comments section // open sidebar in comments section
context.fileList.showDetailsView(fileName, 'commentsTabView'); context.fileList.showDetailsView(fileName, 'commentsTabView')
} }
}); })
// add attribute to "elementToFile" // add attribute to "elementToFile"
var oldElementToFile = fileList.elementToFile; var oldElementToFile = fileList.elementToFile
fileList.elementToFile = function($el) { fileList.elementToFile = function($el) {
var fileInfo = oldElementToFile.apply(this, arguments); var fileInfo = oldElementToFile.apply(this, arguments)
var commentsUnread = $el.data('comments-unread'); var commentsUnread = $el.data('comments-unread')
if (commentsUnread) { 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)

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/* /*
* Copyright (c) 2014 * Copyright (c) 2014
* *
@ -8,15 +9,15 @@
* *
*/ */
(function(OC, OCA, $) { (function(OC, OCA, $) {
"use strict"; 'use strict'
/** /**
* Construct a new FileActions instance * Construct a new FileActions instance
* @constructs Files * @constructs Files
*/ */
var Comment = function() { var Comment = function() {
this.initialize(); this.initialize()
}; }
Comment.prototype = { Comment.prototype = {
@ -27,25 +28,25 @@
*/ */
initialize: function() { initialize: function() {
var self = this; var self = this
this.fileAppLoaded = function() { this.fileAppLoaded = function() {
return !!OCA.Files && !!OCA.Files.App; return !!OCA.Files && !!OCA.Files.App
}; }
function inFileList($row, result) { function inFileList($row, result) {
return false; return false
if (! self.fileAppLoaded()) { if (!self.fileAppLoaded()) {
return false; return false
} }
var dir = self.fileList.getCurrentDirectory().replace(/\/+$/,''); var dir = self.fileList.getCurrentDirectory().replace(/\/+$/, '')
var resultDir = OC.dirname(result.path); var resultDir = OC.dirname(result.path)
return dir === resultDir && self.fileList.inList(result.name); return dir === resultDir && self.fileList.inList(result.name)
} }
function hideNoFilterResults() { function hideNoFilterResults() {
var $nofilterresults = $('.nofilterresults'); var $nofilterresults = $('.nofilterresults')
if ( ! $nofilterresults.hasClass('hidden') ) { if (!$nofilterresults.hasClass('hidden')) {
$nofilterresults.addClass('hidden'); $nofilterresults.addClass('hidden')
} }
} }
@ -64,73 +65,73 @@
*/ */
this.renderCommentResult = function($row, result) { this.renderCommentResult = function($row, result) {
if (inFileList($row, result)) { if (inFileList($row, result)) {
return null; return null
} }
hideNoFilterResults(); hideNoFilterResults()
/*render preview icon, show path beneath filename, /* render preview icon, show path beneath filename,
show size and last modified date on the right */ 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') $avatar.addClass('avatar')
.css('display', 'inline-block') .css('display', 'inline-block')
.css('vertical-align', 'middle') .css('vertical-align', 'middle')
.css('margin', '0 5px 2px 3px'); .css('margin', '0 5px 2px 3px')
if (result.authorName) { if (result.authorName) {
$avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName); $avatar.avatar(result.authorId, 21, undefined, false, undefined, result.authorName)
} else { } 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.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.result a').attr('href', result.link)
$row.find('td.icon') $row.find('td.icon')
.css('background-image', 'url(' + OC.imagePath('core', 'actions/comment') + ')') .css('background-image', 'url(' + OC.imagePath('core', 'actions/comment') + ')')
.css('opacity', '.4'); .css('opacity', '.4')
var dir = OC.dirname(result.path); var dir = OC.dirname(result.path)
// "result.path" does not include a leading "/", so "OC.dirname" // "result.path" does not include a leading "/", so "OC.dirname"
// returns the path itself for files or folders in the root. // returns the path itself for files or folders in the root.
if (dir === result.path) { if (dir === result.path) {
dir = '/'; dir = '/'
} }
$row.find('td.info a').attr('href', $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) { this.handleCommentClick = function($row, result, event) {
if (self.fileAppLoaded() && self.fileList.id === 'files') { if (self.fileAppLoaded() && self.fileList.id === 'files') {
self.fileList.changeDirectory(OC.dirname(result.path)); self.fileList.changeDirectory(OC.dirname(result.path))
self.fileList.scrollTo(result.name); self.fileList.scrollTo(result.name)
return false; return false
} else { } else {
return true; return true
} }
}; }
this.updateLegacyMimetype = function (result) { this.updateLegacyMimetype = function(result) {
// backward compatibility: // backward compatibility:
if (!result.mime && result.mime_type) { if (!result.mime && result.mime_type) {
result.mime = result.mime_type; result.mime = result.mime_type
} }
}; }
this.setFileList = function (fileList) { this.setFileList = function(fileList) {
this.fileList = fileList; this.fileList = fileList
}; }
OC.Plugins.register('OCA.Search.Core', this); OC.Plugins.register('OCA.Search.Core', this)
}, },
attach: function(search) { attach: function(search) {
search.setRenderer('comment', this.renderCommentResult.bind(this)); search.setRenderer('comment', this.renderCommentResult.bind(this))
search.setHandler('comment', this.handleCommentClick.bind(this)); search.setHandler('comment', this.handleCommentClick.bind(this))
} }
}; }
OCA.Search.comment = new Comment(); OCA.Search.comment = new Comment()
})(OC, OCA, $); })(OC, OCA, $)

View File

@ -727,11 +727,11 @@ OC.Uploader.prototype = _.extend({
* *
* @param {array} selection of files to upload * @param {array} selection of files to upload
* @param {object} callbacks - object with several callback methods * @param {object} callbacks - object with several callback methods
* @param {function} callbacks.onNoConflicts * @param {Function} callbacks.onNoConflicts
* @param {function} callbacks.onSkipConflicts * @param {Function} callbacks.onSkipConflicts
* @param {function} callbacks.onReplaceConflicts * @param {Function} callbacks.onReplaceConflicts
* @param {function} callbacks.onChooseConflicts * @param {Function} callbacks.onChooseConflicts
* @param {function} callbacks.onCancel * @param {Function} callbacks.onCancel
*/ */
checkExistingFiles: function (selection, callbacks) { checkExistingFiles: function (selection, callbacks) {
var fileList = this.fileList; var fileList = this.fileList;

View File

@ -336,7 +336,7 @@
* - JS periodically checks for this cookie and then knows when the download has started to call the callback * - JS periodically checks for this cookie and then knows when the download has started to call the callback
* *
* @param {string} url download URL * @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) { handleDownload: function(url, callback) {
var randomToken = Math.random().toString(36).substring(2), var randomToken = Math.random().toString(36).substring(2),

View File

@ -125,7 +125,7 @@ OCA.Files_External.StatusManager = {
/** /**
* Function to get external mount point list from the files_external API * 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) { getMountPointList: function (afterCallback) {

View File

@ -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']
}
};

View File

@ -12,7 +12,7 @@ if (!OCA.Sharing) {
/** /**
* @namespace OCA.Sharing * @namespace OCA.Sharing
*/ */
OCA.Sharing = {}; OCA.Sharing = {}
} }
/** /**
* @namespace * @namespace
@ -25,7 +25,7 @@ OCA.Sharing.App = {
initSharingIn: function($el) { initSharingIn: function($el) {
if (this._inFileList) { if (this._inFileList) {
return this._inFileList; return this._inFileList
} }
this._inFileList = new OCA.Sharing.FileList( this._inFileList = new OCA.Sharing.FileList(
@ -40,19 +40,19 @@ OCA.Sharing.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
this._extendFileList(this._inFileList); this._extendFileList(this._inFileList)
this._inFileList.appName = t('files_sharing', 'Shared with you'); this._inFileList.appName = t('files_sharing', 'Shared with you')
this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' + this._inFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
'<h2>' + t('files_sharing', 'Nothing shared with you yet') + '</h2>' + + '<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>'); + '<p>' + t('files_sharing', 'Files and folders others share with you will show up here') + '</p>')
return this._inFileList; return this._inFileList
}, },
initSharingOut: function($el) { initSharingOut: function($el) {
if (this._outFileList) { if (this._outFileList) {
return this._outFileList; return this._outFileList
} }
this._outFileList = new OCA.Sharing.FileList( this._outFileList = new OCA.Sharing.FileList(
$el, $el,
@ -66,19 +66,19 @@ OCA.Sharing.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
this._extendFileList(this._outFileList); this._extendFileList(this._outFileList)
this._outFileList.appName = t('files_sharing', 'Shared with others'); this._outFileList.appName = t('files_sharing', 'Shared with others')
this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>' + this._outFileList.$el.find('#emptycontent').html('<div class="icon-shared"></div>'
'<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>' + + '<h2>' + t('files_sharing', 'Nothing shared yet') + '</h2>'
'<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>'); + '<p>' + t('files_sharing', 'Files and folders you share will show up here') + '</p>')
return this._outFileList; return this._outFileList
}, },
initSharingLinks: function($el) { initSharingLinks: function($el) {
if (this._linkFileList) { if (this._linkFileList) {
return this._linkFileList; return this._linkFileList
} }
this._linkFileList = new OCA.Sharing.FileList( this._linkFileList = new OCA.Sharing.FileList(
$el, $el,
@ -92,19 +92,19 @@ OCA.Sharing.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
this._extendFileList(this._linkFileList); this._extendFileList(this._linkFileList)
this._linkFileList.appName = t('files_sharing', 'Shared by link'); this._linkFileList.appName = t('files_sharing', 'Shared by link')
this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>' + this._linkFileList.$el.find('#emptycontent').html('<div class="icon-public"></div>'
'<h2>' + t('files_sharing', 'No shared links') + '</h2>' + + '<h2>' + t('files_sharing', 'No shared links') + '</h2>'
'<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>'); + '<p>' + t('files_sharing', 'Files and folders you share by link will show up here') + '</p>')
return this._linkFileList; return this._linkFileList
}, },
initSharingDeleted: function($el) { initSharingDeleted: function($el) {
if (this._deletedFileList) { if (this._deletedFileList) {
return this._deletedFileList; return this._deletedFileList
} }
this._deletedFileList = new OCA.Sharing.FileList( this._deletedFileList = new OCA.Sharing.FileList(
$el, $el,
@ -119,19 +119,19 @@ OCA.Sharing.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
this._extendFileList(this._deletedFileList); this._extendFileList(this._deletedFileList)
this._deletedFileList.appName = t('files_sharing', 'Deleted shares'); this._deletedFileList.appName = t('files_sharing', 'Deleted shares')
this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' + this._deletedFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
'<h2>' + t('files_sharing', 'No deleted shares') + '</h2>' + + '<h2>' + t('files_sharing', 'No deleted shares') + '</h2>'
'<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>'); + '<p>' + t('files_sharing', 'Shares you deleted will show up here') + '</p>')
return this._deletedFileList; return this._deletedFileList
}, },
initShareingOverview: function($el) { initShareingOverview: function($el) {
if (this._overviewFileList) { if (this._overviewFileList) {
return this._overviewFileList; return this._overviewFileList
} }
this._overviewFileList = new OCA.Sharing.FileList( this._overviewFileList = new OCA.Sharing.FileList(
$el, $el,
@ -144,43 +144,43 @@ OCA.Sharing.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
this._extendFileList(this._overviewFileList); this._extendFileList(this._overviewFileList)
this._overviewFileList.appName = t('files_sharing', 'Shares'); this._overviewFileList.appName = t('files_sharing', 'Shares')
this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>' + this._overviewFileList.$el.find('#emptycontent').html('<div class="icon-share"></div>'
'<h2>' + t('files_sharing', 'No shares') + '</h2>' + + '<h2>' + t('files_sharing', 'No shares') + '</h2>'
'<p>' + t('files_sharing', 'Shares will show up here') + '</p>'); + '<p>' + t('files_sharing', 'Shares will show up here') + '</p>')
return this._overviewFileList; return this._overviewFileList
}, },
removeSharingIn: function() { removeSharingIn: function() {
if (this._inFileList) { if (this._inFileList) {
this._inFileList.$fileList.empty(); this._inFileList.$fileList.empty()
} }
}, },
removeSharingOut: function() { removeSharingOut: function() {
if (this._outFileList) { if (this._outFileList) {
this._outFileList.$fileList.empty(); this._outFileList.$fileList.empty()
} }
}, },
removeSharingLinks: function() { removeSharingLinks: function() {
if (this._linkFileList) { if (this._linkFileList) {
this._linkFileList.$fileList.empty(); this._linkFileList.$fileList.empty()
} }
}, },
removeSharingDeleted: function() { removeSharingDeleted: function() {
if (this._deletedFileList) { if (this._deletedFileList) {
this._deletedFileList.$fileList.empty(); this._deletedFileList.$fileList.empty()
} }
}, },
removeSharingOverview: function() { removeSharingOverview: function() {
if (this._overviewFileList) { if (this._overviewFileList) {
this._overviewFileList.$fileList.empty(); this._overviewFileList.$fileList.empty()
} }
}, },
@ -188,46 +188,46 @@ OCA.Sharing.App = {
* Destroy the app * Destroy the app
*/ */
destroy: function() { destroy: function() {
OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated); OCA.Files.fileActions.off('setDefault.app-sharing', this._onActionsUpdated)
OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated); OCA.Files.fileActions.off('registerAction.app-sharing', this._onActionsUpdated)
this.removeSharingIn(); this.removeSharingIn()
this.removeSharingOut(); this.removeSharingOut()
this.removeSharingLinks(); this.removeSharingLinks()
this._inFileList = null; this._inFileList = null
this._outFileList = null; this._outFileList = null
this._linkFileList = null; this._linkFileList = null
this._overviewFileList = null; this._overviewFileList = null
delete this._globalActionsInitialized; delete this._globalActionsInitialized
}, },
_createFileActions: function() { _createFileActions: function() {
// inherit file actions from the files app // 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 // note: not merging the legacy actions because legacy apps are not
// compatible with the sharing overview and need to be adapted first // compatible with the sharing overview and need to be adapted first
fileActions.registerDefaultActions(); fileActions.registerDefaultActions()
fileActions.merge(OCA.Files.fileActions); fileActions.merge(OCA.Files.fileActions)
if (!this._globalActionsInitialized) { if (!this._globalActionsInitialized) {
// in case actions are registered later // in case actions are registered later
this._onActionsUpdated = _.bind(this._onActionsUpdated, this); this._onActionsUpdated = _.bind(this._onActionsUpdated, this)
OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated); OCA.Files.fileActions.on('setDefault.app-sharing', this._onActionsUpdated)
OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated); OCA.Files.fileActions.on('registerAction.app-sharing', this._onActionsUpdated)
this._globalActionsInitialized = true; this._globalActionsInitialized = true
} }
// when the user clicks on a folder, redirect to the corresponding // when the user clicks on a folder, redirect to the corresponding
// folder in the files app instead of opening it directly // folder in the files app instead of opening it directly
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
OCA.Files.App.setActiveView('files', {silent: true}); OCA.Files.App.setActiveView('files', { silent: true })
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true)
}); })
fileActions.setDefault('dir', 'Open'); fileActions.setDefault('dir', 'Open')
return fileActions; return fileActions
}, },
_restoreShareAction: function() { _restoreShareAction: function() {
var fileActions = new OCA.Files.FileActions(); var fileActions = new OCA.Files.FileActions()
fileActions.registerAction({ fileActions.registerAction({
name: 'Restore', name: 'Restore',
displayName: '', displayName: '',
@ -237,70 +237,70 @@ OCA.Sharing.App = {
iconClass: 'icon-history', iconClass: 'icon-history',
type: OCA.Files.FileActions.TYPE_INLINE, type: OCA.Files.FileActions.TYPE_INLINE,
actionHandler: function(fileName, context) { 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) $.post(OC.linkToOCS('apps/files_sharing/api/v1/deletedshares', 2) + shareId)
.success(function(result) { .success(function(result) {
context.fileList.remove(context.fileInfoModel.attributes.name); context.fileList.remove(context.fileInfoModel.attributes.name)
}).fail(function() { }).fail(function() {
OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.')); OC.Notification.showTemporary(t('files_sharing', 'Something happened. Unable to restore the share.'))
}); })
} }
}); })
return fileActions; return fileActions
}, },
_onActionsUpdated: function(ev) { _onActionsUpdated: function(ev) {
_.each([this._inFileList, this._outFileList, this._linkFileList], function(list) { _.each([this._inFileList, this._outFileList, this._linkFileList], function(list) {
if (!list) { if (!list) {
return; return
} }
if (ev.action) { if (ev.action) {
list.fileActions.registerAction(ev.action); list.fileActions.registerAction(ev.action)
} else if (ev.defaultAction) { } else if (ev.defaultAction) {
list.fileActions.setDefault( list.fileActions.setDefault(
ev.defaultAction.mime, ev.defaultAction.mime,
ev.defaultAction.name ev.defaultAction.name
); )
} }
}); })
}, },
_extendFileList: function(fileList) { _extendFileList: function(fileList) {
// remove size column from summary // remove size column from summary
fileList.fileSummary.$el.find('.filesize').remove(); fileList.fileSummary.$el.find('.filesize').remove()
} }
}; }
$(document).ready(function() { $(document).ready(function() {
$('#app-content-sharingin').on('show', function(e) { $('#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() { $('#app-content-sharingin').on('hide', function() {
OCA.Sharing.App.removeSharingIn(); OCA.Sharing.App.removeSharingIn()
}); })
$('#app-content-sharingout').on('show', function(e) { $('#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() { $('#app-content-sharingout').on('hide', function() {
OCA.Sharing.App.removeSharingOut(); OCA.Sharing.App.removeSharingOut()
}); })
$('#app-content-sharinglinks').on('show', function(e) { $('#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() { $('#app-content-sharinglinks').on('hide', function() {
OCA.Sharing.App.removeSharingLinks(); OCA.Sharing.App.removeSharingLinks()
}); })
$('#app-content-deletedshares').on('show', function(e) { $('#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() { $('#app-content-deletedshares').on('hide', function() {
OCA.Sharing.App.removeSharingDeleted(); OCA.Sharing.App.removeSharingDeleted()
}); })
$('#app-content-shareoverview').on('show', function(e) { $('#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() { $('#app-content-shareoverview').on('hide', function() {
OCA.Sharing.App.removeSharingOverview(); OCA.Sharing.App.removeSharingOverview()
}); })
}); })

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,2 +1,2 @@
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=0)}([function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n()}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}]); !function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/js/",t(t.s=0)}([function(e,n,r){r.p=OC.linkTo("files_sharing","js/dist/"),r.nc=btoa(OC.requestToken),window.OCP.Collaboration.registerType("file",{action:function(){return new Promise((function(e,n){OC.dialogs.filepicker(t("files_sharing","Link to a file"),(function(t){OC.Files.getClient().getFileInfo(t).then((function(n,t){e(t.id)})).fail((function(){n(new Error("Cannot get fileinfo"))}))}),!1,null,!1,OC.dialogs.FILEPICKER_TYPE_CHOOSE,"",{allowDirectoryChooser:!0})}))},typeString:t("files_sharing","Link to a file"),typeIconClass:"icon-files-dark"})}]);
//# sourceMappingURL=collaboration.js.map //# sourceMappingURL=collaboration.js.map

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
(window.webpackJsonpFilesSharing=window.webpackJsonpFilesSharing||[]).push([[4],{14:function(e,o,i){"use strict";i.r(o);var n=i(16),l=i(18),r=i(28),u=i(31),s=i.n(u),a={name:"CollaborationView",computed:{fileId:function(){return this.$root.model&&this.$root.model.id?""+this.$root.model.id:null},filename:function(){return this.$root.model&&this.$root.model.name?""+this.$root.model.name:""}},components:{CollectionList:i(32).a}},d=i(56),c=Object(d.a)(a,(function(){var t=this.$createElement,e=this._self._c||t;return this.fileId?e("collection-list",{attrs:{type:"file",id:this.fileId,name:this.filename}}):this._e()}),[],!1,null,null,null).exports;i.d(o,"Vue",(function(){return n.default})),i.d(o,"View",(function(){return c})), (window.webpackJsonpFilesSharing=window.webpackJsonpFilesSharing||[]).push([[4],{14:function(e,o,i){"use strict";i.r(o);var n=i(16),l=i(18),r=i(28),u=i(31),s=i.n(u),a={name:"CollaborationView",components:{CollectionList:i(32).a},computed:{fileId:function(){return this.$root.model&&this.$root.model.id?""+this.$root.model.id:null},filename:function(){return this.$root.model&&this.$root.model.name?""+this.$root.model.name:""}}},d=i(56),c=Object(d.a)(a,(function(){var t=this.$createElement,e=this._self._c||t;return this.fileId?e("CollectionList",{attrs:{id:this.fileId,type:"file",name:this.filename}}):this._e()}),[],!1,null,null,null).exports;i.d(o,"Vue",(function(){return n.default})),i.d(o,"View",(function(){return c})),
/* /**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
* *
* @author Julius Härtl <jus@bitgrid.net> * @author Julius Härtl <jus@bitgrid.net>
@ -20,5 +20,5 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
n.default.prototype.t=t,n.default.component("PopoverMenu",r.PopoverMenu),n.default.directive("ClickOutside",s.a),r.Tooltip.options.defaultHtml=!1,n.default.directive("Tooltip",r.Tooltip),n.default.use(l.a)}}]); n.default.prototype.t=t,r.Tooltip.options.defaultHtml=!1,n.default.component("PopoverMenu",r.PopoverMenu),n.default.directive("ClickOutside",s.a),n.default.directive("Tooltip",r.Tooltip),n.default.use(l.a)}}]);
//# sourceMappingURL=files_sharing.4.js.map?v=07d5a7a4994f0ef93170 //# sourceMappingURL=files_sharing.4.js.map?v=4e4a795c94e467758967

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/* /*
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com> * Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
* *
@ -25,428 +26,423 @@
* @param {boolean} [options.linksOnly] true to return only link shares * @param {boolean} [options.linksOnly] true to return only link shares
*/ */
var FileList = function($el, options) { var FileList = function($el, options) {
this.initialize($el, options); this.initialize($el, options)
}; }
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Sharing.FileList.prototype */ { /** @lends OCA.Sharing.FileList.prototype */ {
appName: 'Shares', appName: 'Shares',
/** /**
* Whether the list shows the files shared with the user (true) or * Whether the list shows the files shared with the user (true) or
* the files that the user shared with others (false). * the files that the user shared with others (false).
*/ */
_sharedWithUser: false, _sharedWithUser: false,
_linksOnly: false, _linksOnly: false,
_showDeleted: false, _showDeleted: false,
_clientSideSort: true, _clientSideSort: true,
_allowSelection: false, _allowSelection: false,
_isOverview: false, _isOverview: false,
/** /**
* @private * @private
*/ */
initialize: function($el, options) { initialize: function($el, options) {
OCA.Files.FileList.prototype.initialize.apply(this, arguments); OCA.Files.FileList.prototype.initialize.apply(this, arguments)
if (this.initialized) { if (this.initialized) {
return; return
} }
// TODO: consolidate both options // TODO: consolidate both options
if (options && options.sharedWithUser) { if (options && options.sharedWithUser) {
this._sharedWithUser = true; this._sharedWithUser = true
} }
if (options && options.linksOnly) { if (options && options.linksOnly) {
this._linksOnly = true; this._linksOnly = true
} }
if (options && options.showDeleted) { if (options && options.showDeleted) {
this._showDeleted = true; this._showDeleted = true
} }
if (options && options.isOverview) { if (options && options.isOverview) {
this._isOverview = true; this._isOverview = true
} }
}, },
_renderRow: function() { _renderRow: function() {
// HACK: needed to call the overridden _renderRow // HACK: needed to call the overridden _renderRow
// this is because at the time this class is created // this is because at the time this class is created
// the overriding hasn't been done yet... // 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 // TODO: hook earlier and render the whole row here
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments); var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
$tr.find('.filesize').remove(); $tr.find('.filesize').remove()
$tr.find('td.date').before($tr.children('td:first')); $tr.find('td.date').before($tr.children('td:first'))
$tr.find('td.filename input:checkbox').remove(); $tr.find('td.filename input:checkbox').remove()
$tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(',')); $tr.attr('data-share-id', _.pluck(fileData.shares, 'id').join(','))
if (this._sharedWithUser) { if (this._sharedWithUser) {
$tr.attr('data-share-owner', fileData.shareOwner); $tr.attr('data-share-owner', fileData.shareOwner)
$tr.attr('data-mounttype', 'shared-root'); $tr.attr('data-mounttype', 'shared-root')
var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE; var permission = parseInt($tr.attr('data-permissions')) | OC.PERMISSION_DELETE
$tr.attr('data-permissions', permission); $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();
} }
$tr.attr('data-expiration', expirationTimestamp); if (this._showDeleted) {
var permission = fileData.permissions
// date column (1000 milliseconds to seconds, 60 seconds, 60 minutes, 24 hours) $tr.attr('data-share-permissions', permission)
// 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; // add row with expiration date for link only shares - influenced by _createRow of filelist
var text; if (this._linksOnly) {
if (expirationTimestamp > 0) { var expirationTimestamp = 0
formatted = OC.Util.formatDate(expirationTimestamp); if (fileData.shares && fileData.shares[0].expiration !== null) {
text = OC.Util.relativeModifiedDate(expirationTimestamp); expirationTimestamp = moment(fileData.shares[0].expiration).valueOf()
} else { }
formatted = t('files_sharing', 'No expiration date set'); $tr.attr('data-expiration', expirationTimestamp)
text = '';
modifiedColor = 160; // 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)
td = $('<td></td>').attr({"class": "date"}); var modifiedColor = Math.round((expirationTimestamp - (new Date()).getTime()) / 1000 / 60 / 60 / 24 * 5)
td.append($('<span></span>').attr({ // ensure that the brightest color is still readable
"class": "modified", if (modifiedColor >= 160) {
"title": formatted, modifiedColor = 160
"style": 'color:rgb(' + modifiedColor + ',' + modifiedColor + ',' + modifiedColor + ')' }
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) }).text(text)
.tooltip({placement: 'top'}) .tooltip({ placement: 'top' })
); )
$tr.append(td); $tr.append(td)
} }
return $tr; return $tr
}, },
/** /**
* Set whether the list should contain outgoing shares * Set whether the list should contain outgoing shares
* or incoming shares. * or incoming shares.
* *
* @param state true for incoming shares, false otherwise * @param state true for incoming shares, false otherwise
*/ */
setSharedWithUser: function(state) { setSharedWithUser: function(state) {
this._sharedWithUser = !!state; this._sharedWithUser = !!state
}, },
updateEmptyContent: function() { updateEmptyContent: function() {
var dir = this.getCurrentDirectory(); var dir = this.getCurrentDirectory()
if (dir === '/') { if (dir === '/') {
// root has special permissions // root has special permissions
this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty); this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty)
this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty)
// hide expiration date header for non link only shares // hide expiration date header for non link only shares
if (!this._linksOnly) { if (!this._linksOnly) {
this.$el.find('th.column-expiration').addClass('hidden'); 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() { getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE; return OC.PERMISSION_READ | OC.PERMISSION_DELETE
}, },
updateStorageStatistics: function() { updateStorageStatistics: function() {
// no op because it doesn't have // no op because it doesn't have
// storage info like free space / used space // storage info like free space / used space
}, },
updateRow: function($tr, fileInfo, options) { updateRow: function($tr, fileInfo, options) {
// no-op, suppress re-rendering // no-op, suppress re-rendering
return $tr; return $tr
}, },
reload: function() { reload: function() {
this.showMask(); this.showMask()
if (this._reloadCall) { if (this._reloadCall) {
this._reloadCall.abort(); 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));
} }
if (this._isOverview) {
shares.data.shared_with_me = !shares.data.shared_with_me; // there is only root
promises.push($.ajax(shares)); 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 shares = {
var callBack = this.reloadCallback.bind(this); url: OC.linkToOCS('apps/files_sharing/api/v1') + 'shares',
return this._reloadCall.then(callBack, callBack); /* 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) { var remoteShares = {
delete this._reloadCall; url: OC.linkToOCS('apps/files_sharing/api/v1') + 'remote_shares',
this.hideMask(); /* jshint camelcase: false */
data: {
format: 'json',
include_tags: true
},
type: 'GET',
beforeSend: function(xhr) {
xhr.setRequestHeader('OCS-APIREQUEST', 'true')
}
}
this.$el.find('#headerSharedWith').text( // Add the proper ajax requests to the list and run them
t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with') // 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 this._reloadCall = $.when.apply($, promises)
if (shares[0] && shares[0].ocs) { var callBack = this.reloadCallback.bind(this)
shares = shares[0]; return this._reloadCall.then(callBack, callBack)
} },
if (remoteShares && remoteShares[0] && remoteShares[0].ocs) {
remoteShares = remoteShares[0];
}
if (additionalShares && additionalShares[0] && additionalShares[0].ocs) {
additionalShares = additionalShares[0];
}
if (shares.ocs && shares.ocs.data) { reloadCallback: function(shares, remoteShares, additionalShares) {
files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser)); delete this._reloadCall
} this.hideMask()
if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) { this.$el.find('#headerSharedWith').text(
files = files.concat(this._makeFilesFromRemoteShares(remoteShares.ocs.data)); t('files_sharing', this._sharedWithUser ? 'Shared by' : 'Shared with')
} )
if (additionalShares && additionalShares.ocs && additionalShares.ocs.data) { var files = []
files = files.concat(this._makeFilesFromShares(additionalShares.ocs.data, !this._sharedWithUser));
}
// 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); if (shares.ocs && shares.ocs.data) {
return true; files = files.concat(this._makeFilesFromShares(shares.ocs.data, this._sharedWithUser))
}, }
_makeFilesFromRemoteShares: function(data) { if (remoteShares && remoteShares.ocs && remoteShares.ocs.data) {
var files = 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 // convert share data to file data
.map(function(share) { .map(function(share) {
var file = { var file = {
shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ""), shareOwner: share.owner + '@' + share.remote.replace(/.*?:\/\//g, ''),
name: OC.basename(share.mountpoint), name: OC.basename(share.mountpoint),
mtime: share.mtime * 1000, mtime: share.mtime * 1000,
mimetype: share.mimetype, mimetype: share.mimetype,
type: share.type, type: share.type,
id: share.file_id, id: share.file_id,
path: OC.dirname(share.mountpoint), path: OC.dirname(share.mountpoint),
permissions: share.permissions, permissions: share.permissions,
tags: share.tags || [] tags: share.tags || []
}; }
file.shares = [{ file.shares = [{
id: share.id, id: share.id,
type: OC.Share.SHARE_TYPE_REMOTE type: OC.Share.SHARE_TYPE_REMOTE
}]; }]
return file; return file
}) })
.value(); .value()
return files; return files
}, },
/** /**
* Converts the OCS API share response data to a file info * Converts the OCS API share response data to a file info
* list * list
* @param {Array} data OCS API share array * @param {Array} data OCS API share array
* @param {bool} sharedWithUser * @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 */ /* jshint camelcase: false */
var files = data; var files = data
if (this._linksOnly) { if (this._linksOnly) {
files = _.filter(data, function(share) { files = _.filter(data, function(share) {
return share.share_type === OC.Share.SHARE_TYPE_LINK; return share.share_type === OC.Share.SHARE_TYPE_LINK
}); })
} }
// OCS API uses non-camelcased names // OCS API uses non-camelcased names
files = _.chain(files) files = _.chain(files)
// convert share data to file data // convert share data to file data
.map(function(share) { .map(function(share) {
// TODO: use OC.Files.FileInfo // TODO: use OC.Files.FileInfo
var file = { var file = {
id: share.file_source, id: share.file_source,
icon: OC.MimeType.getIconUrl(share.mimetype), icon: OC.MimeType.getIconUrl(share.mimetype),
mimetype: share.mimetype, mimetype: share.mimetype,
tags: share.tags || [] 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;
} }
} if (share.item_type === 'folder') {
else { file.type = 'dir'
if (share.share_type !== OC.Share.SHARE_TYPE_LINK) { file.mimetype = 'httpd/unix-directory'
file.share.targetDisplayName = share.share_with_displayname; } else {
file.share.targetShareWithId = share.share_with; file.type = 'file'
} }
file.name = OC.basename(share.path); file.share = {
file.path = OC.dirname(share.path); id: share.id,
file.permissions = OC.PERMISSION_ALL; type: share.share_type,
if (file.path) { target: share.share_with,
file.extraData = share.path; stime: share.stime * 1000,
expiration: share.expiration
} }
} if (sharedWithUser) {
return file; 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 // Group all files and have a "shares" array with
// the share info for each file. // the share info for each file.
// //
// This uses a hash memo to cumulate share information // This uses a hash memo to cumulate share information
// inside the same file object (by file id). // inside the same file object (by file id).
.reduce(function(memo, file) { .reduce(function(memo, file) {
var data = memo[file.id]; var data = memo[file.id]
var recipient = file.share.targetDisplayName; var recipient = file.share.targetDisplayName
var recipientId = file.share.targetShareWithId; var recipientId = file.share.targetShareWithId
if (!data) { if (!data) {
data = memo[file.id] = file; data = memo[file.id] = file
data.shares = [file.share]; data.shares = [file.share]
// using a hash to make them unique, // using a hash to make them unique,
// this is only a list to be displayed // this is only a list to be displayed
data.recipients = {}; data.recipients = {}
data.recipientData = {}; data.recipientData = {}
// share types // share types
data.shareTypes = {}; data.shareTypes = {}
// counter is cheaper than calling _.keys().length // counter is cheaper than calling _.keys().length
data.recipientsCount = 0; data.recipientsCount = 0
data.mtime = file.share.stime; data.mtime = file.share.stime
} } else {
else {
// always take the most recent stime // always take the most recent stime
if (file.share.stime > data.mtime) { if (file.share.stime > data.mtime) {
data.mtime = file.share.stime; data.mtime = file.share.stime
}
data.shares.push(file.share)
} }
data.shares.push(file.share);
}
if (recipient) { if (recipient) {
// limit counterparts for output // limit counterparts for output
if (data.recipientsCount < 4) { if (data.recipientsCount < 4) {
// only store the first ones, they will be the only ones // only store the first ones, they will be the only ones
// displayed // displayed
data.recipients[recipient] = true; data.recipients[recipient] = true
data.recipientData[data.recipientsCount] = { data.recipientData[data.recipientsCount] = {
'shareWith': recipientId, 'shareWith': recipientId,
'shareWithDisplayName': recipient 'shareWithDisplayName': recipient
}; }
}
data.recipientsCount++
} }
data.recipientsCount++;
}
data.shareTypes[file.share.type] = true; data.shareTypes[file.share.type] = true
delete file.share; delete file.share
return memo; return memo
}, {}) }, {})
// Retrieve only the values of the returned hash // Retrieve only the values of the returned hash
.values() .values()
// Clean up // Clean up
.each(function(data) { .each(function(data) {
// convert the recipients map to a flat // convert the recipients map to a flat
// array of sorted names // array of sorted names
data.mountType = 'shared'; data.mountType = 'shared'
delete data.recipientsCount; delete data.recipientsCount
if (sharedWithUser) { if (sharedWithUser) {
// only for outgoing shares // only for outgoing shares
delete data.shareTypes; delete data.shareTypes
} else { } else {
data.shareTypes = _.keys(data.shareTypes); data.shareTypes = _.keys(data.shareTypes)
} }
}) })
// Finish the chain by getting the result // Finish the chain by getting the result
.value(); .value()
// Sort by expected sort comparator // Sort by expected sort comparator
return files.sort(this._sortComparator); return files.sort(this._sortComparator)
}, }
}); })
/** /**
* Share info attributes. * Share info attributes.
@ -486,5 +482,5 @@
* passing to HTML data attributes with jQuery) * passing to HTML data attributes with jQuery)
*/ */
OCA.Sharing.FileList = FileList; OCA.Sharing.FileList = FileList
})(); })()

View File

@ -1,6 +1,3 @@
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
__webpack_nonce__ = btoa(OC.requestToken);
import './share' import './share'
import './sharetabview' import './sharetabview'
import './sharebreadcrumbview' import './sharebreadcrumbview'
@ -10,4 +7,9 @@ import './style/sharebreadcrumb.scss'
import './collaborationresourceshandler.js' 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

View File

@ -1,4 +1,4 @@
/* /**
* @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net>
* *
* @author Julius Härtl <jus@bitgrid.net> * @author Julius Härtl <jus@bitgrid.net>
@ -20,21 +20,23 @@
* *
*/ */
import Vue from 'vue'; import Vue from 'vue'
import Vuex from 'vuex'; import Vuex from 'vuex'
import { Tooltip, PopoverMenu } from 'nextcloud-vue'; import { Tooltip, PopoverMenu } from 'nextcloud-vue'
import ClickOutside from 'vue-click-outside'; import ClickOutside from 'vue-click-outside'
Vue.prototype.t = t; import View from './views/CollaborationView'
Vue.component('PopoverMenu', PopoverMenu);
Vue.directive('ClickOutside', ClickOutside); Vue.prototype.t = t
Tooltip.options.defaultHtml = false 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 { export {
Vue, Vue,
View View
}; }

View File

@ -1,19 +1,21 @@
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/'); // eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken); __webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
window.OCP.Collaboration.registerType('file', { window.OCP.Collaboration.registerType('file', {
action: () => { action: () => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function (f) { OC.dialogs.filepicker(t('files_sharing', 'Link to a file'), function(f) {
const client = OC.Files.getClient(); const client = OC.Files.getClient()
client.getFileInfo(f).then((status, fileInfo) => { client.getFileInfo(f).then((status, fileInfo) => {
resolve(fileInfo.id); resolve(fileInfo.id)
}).fail(() => { }).fail(() => {
reject(); reject(new Error('Cannot get fileinfo'))
}); })
}, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true }); }, false, null, false, OC.dialogs.FILEPICKER_TYPE_CHOOSE, '', { allowDirectoryChooser: true })
}); })
}, },
typeString: t('files_sharing', 'Link to a file'), typeString: t('files_sharing', 'Link to a file'),
typeIconClass: 'icon-files-dark' typeIconClass: 'icon-files-dark'
}); })

View File

@ -1,5 +1,7 @@
__webpack_nonce__ = btoa(OC.requestToken); import '../js/app'
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/'); import '../js/sharedfilelist'
import '../js/app'; // eslint-disable-next-line camelcase
import '../js/sharedfilelist'; __webpack_nonce__ = btoa(OC.requestToken)
// eslint-disable-next-line camelcase
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/* /*
* Copyright (c) 2014 * Copyright (c) 2014
* *
@ -14,10 +15,10 @@
PROPERTY_SHARE_TYPES: '{' + OC.Files.Client.NS_OWNCLOUD + '}share-types', PROPERTY_SHARE_TYPES: '{' + OC.Files.Client.NS_OWNCLOUD + '}share-types',
PROPERTY_OWNER_ID: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-id', PROPERTY_OWNER_ID: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-id',
PROPERTY_OWNER_DISPLAY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-display-name' PROPERTY_OWNER_DISPLAY_NAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}owner-display-name'
}); })
if (!OCA.Sharing) { if (!OCA.Sharing) {
OCA.Sharing = {}; OCA.Sharing = {}
} }
/** /**
* @namespace * @namespace
@ -34,131 +35,130 @@
attach: function(fileList) { attach: function(fileList) {
// core sharing is disabled/not loaded // core sharing is disabled/not loaded
if (!OC.Share) { if (!OC.Share) {
return; return
} }
if (fileList.id === 'trashbin' || fileList.id === 'files.public') { if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
return; return
} }
var fileActions = fileList.fileActions; var fileActions = fileList.fileActions
var oldCreateRow = fileList._createRow; var oldCreateRow = fileList._createRow
fileList._createRow = function(fileData) { fileList._createRow = function(fileData) {
var tr = oldCreateRow.apply(this, arguments); var tr = oldCreateRow.apply(this, arguments)
var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData); var sharePermissions = OCA.Sharing.Util.getSharePermissions(fileData)
if (fileData.permissions === 0) { if (fileData.permissions === 0) {
// no permission, disabling sidebar // no permission, disabling sidebar
delete fileActions.actions.all.Comment; delete fileActions.actions.all.Comment
delete fileActions.actions.all.Details; delete fileActions.actions.all.Details
delete fileActions.actions.all.Goto; delete fileActions.actions.all.Goto
} }
tr.attr('data-share-permissions', sharePermissions); tr.attr('data-share-permissions', sharePermissions)
if (fileData.shareOwner) { if (fileData.shareOwner) {
tr.attr('data-share-owner', fileData.shareOwner); tr.attr('data-share-owner', fileData.shareOwner)
tr.attr('data-share-owner-id', fileData.shareOwnerId); tr.attr('data-share-owner-id', fileData.shareOwnerId)
// user should always be able to rename a mount point // user should always be able to rename a mount point
if (fileData.mountType === 'shared-root') { 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)) { 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) { 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) { fileList.elementToFile = function($el) {
var fileInfo = oldElementToFile.apply(this, arguments); var fileInfo = oldElementToFile.apply(this, arguments)
fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined; fileInfo.sharePermissions = $el.attr('data-share-permissions') || undefined
fileInfo.shareOwner = $el.attr('data-share-owner') || undefined; fileInfo.shareOwner = $el.attr('data-share-owner') || undefined
fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined; fileInfo.shareOwnerId = $el.attr('data-share-owner-id') || undefined
if( $el.attr('data-share-types')){ if ($el.attr('data-share-types')) {
fileInfo.shareTypes = $el.attr('data-share-types').split(','); fileInfo.shareTypes = $el.attr('data-share-types').split(',')
} }
if( $el.attr('data-expiration')){ if ($el.attr('data-expiration')) {
var expirationTimestamp = parseInt($el.attr('data-expiration')); var expirationTimestamp = parseInt($el.attr('data-expiration'))
fileInfo.shares = []; fileInfo.shares = []
fileInfo.shares.push({expiration: expirationTimestamp}); fileInfo.shares.push({ expiration: expirationTimestamp })
} }
return fileInfo; return fileInfo
}; }
var oldGetWebdavProperties = fileList._getWebdavProperties; var oldGetWebdavProperties = fileList._getWebdavProperties
fileList._getWebdavProperties = function() { fileList._getWebdavProperties = function() {
var props = oldGetWebdavProperties.apply(this, arguments); var props = oldGetWebdavProperties.apply(this, arguments)
props.push(OC.Files.Client.PROPERTY_OWNER_ID); props.push(OC.Files.Client.PROPERTY_OWNER_ID)
props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME); props.push(OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME)
props.push(OC.Files.Client.PROPERTY_SHARE_TYPES); props.push(OC.Files.Client.PROPERTY_SHARE_TYPES)
return props; return props
}; }
fileList.filesClient.addFileInfoParser(function(response) { fileList.filesClient.addFileInfoParser(function(response) {
var data = {}; var data = {}
var props = response.propStat[0].properties; var props = response.propStat[0].properties
var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS]; var permissionsProp = props[OC.Files.Client.PROPERTY_PERMISSIONS]
if (permissionsProp && permissionsProp.indexOf('S') >= 0) { if (permissionsProp && permissionsProp.indexOf('S') >= 0) {
data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME]; data.shareOwner = props[OC.Files.Client.PROPERTY_OWNER_DISPLAY_NAME]
data.shareOwnerId = props[OC.Files.Client.PROPERTY_OWNER_ID]; 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) { if (shareTypesProp) {
data.shareTypes = _.chain(shareTypesProp).filter(function(xmlvalue) { 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) { }).map(function(xmlvalue) {
return parseInt(xmlvalue.textContent || xmlvalue.text, 10); return parseInt(xmlvalue.textContent || xmlvalue.text, 10)
}).value(); }).value()
} }
return data; return data
}); })
// use delegate to catch the case with multiple file lists // use delegate to catch the case with multiple file lists
fileList.$el.on('fileActionsReady', function(ev){ fileList.$el.on('fileActionsReady', function(ev) {
var $files = ev.$files; var $files = ev.$files
_.each($files, function(file) { _.each($files, function(file) {
var $tr = $(file); var $tr = $(file)
var shareTypes = $tr.attr('data-share-types') || ''; var shareTypes = $tr.attr('data-share-types') || ''
var shareOwner = $tr.attr('data-share-owner'); var shareOwner = $tr.attr('data-share-owner')
if (shareTypes || shareOwner) { if (shareTypes || shareOwner) {
var hasLink = false; var hasLink = false
var hasShares = false; var hasShares = false
_.each(shareTypes.split(',') || [], function(shareType) { _.each(shareTypes.split(',') || [], function(shareType) {
shareType = parseInt(shareType, 10); shareType = parseInt(shareType, 10)
if (shareType === OC.Share.SHARE_TYPE_LINK) { if (shareType === OC.Share.SHARE_TYPE_LINK) {
hasLink = true; hasLink = true
} else if (shareType === OC.Share.SHARE_TYPE_EMAIL) { } else if (shareType === OC.Share.SHARE_TYPE_EMAIL) {
hasLink = true; hasLink = true
} else if (shareType === OC.Share.SHARE_TYPE_USER) { } else if (shareType === OC.Share.SHARE_TYPE_USER) {
hasShares = true; hasShares = true
} else if (shareType === OC.Share.SHARE_TYPE_GROUP) { } else if (shareType === OC.Share.SHARE_TYPE_GROUP) {
hasShares = true; hasShares = true
} else if (shareType === OC.Share.SHARE_TYPE_REMOTE) { } else if (shareType === OC.Share.SHARE_TYPE_REMOTE) {
hasShares = true; hasShares = true
} else if (shareType === OC.Share.SHARE_TYPE_CIRCLE) { } else if (shareType === OC.Share.SHARE_TYPE_CIRCLE) {
hasShares = true; hasShares = true
} else if (shareType === OC.Share.SHARE_TYPE_ROOM) { } 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() { fileList.$el.on('changeDirectory', function() {
OCA.Sharing.sharesLoaded = false; OCA.Sharing.sharesLoaded = false
}); })
fileActions.registerAction({ fileActions.registerAction({
name: 'Share', name: 'Share',
@ -193,40 +193,40 @@
type: OCA.Files.FileActions.TYPE_INLINE, type: OCA.Files.FileActions.TYPE_INLINE,
actionHandler: function(fileName, context) { actionHandler: function(fileName, context) {
// do not open sidebar if permission is set and equal to 0 // 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) { if (isNaN(permissions) || permissions > 0) {
fileList.showDetailsView(fileName, 'shareTabView'); fileList.showDetailsView(fileName, 'shareTabView')
} }
}, },
render: function(actionSpec, isDefault, context) { 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 no share permissions but share owner exists, still show the link
if ((permissions & OC.PERMISSION_SHARE) !== 0 || context.$file.attr('data-share-owner')) { 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 // 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 // detect changes and change the matching list entry
shareTab.on('sharesChanged', function(shareModel) { shareTab.on('sharesChanged', function(shareModel) {
var fileInfoModel = shareModel.fileInfoModel; var fileInfoModel = shareModel.fileInfoModel
var $tr = fileList.findFileEl(fileInfoModel.get('name')); var $tr = fileList.findFileEl(fileInfoModel.get('name'))
// We count email shares as link share // We count email shares as link share
var hasLinkShares = shareModel.hasLinkShares(); var hasLinkShares = shareModel.hasLinkShares()
shareModel.get('shares').forEach(function (share) { shareModel.get('shares').forEach(function(share) {
if (share.share_type === OC.Share.SHARE_TYPE_EMAIL) { 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)) { if (!OCA.Sharing.Util._updateFileActionIcon($tr, shareModel.hasUserShares(), hasLinkShares)) {
// remove icon, if applicable // 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 // FIXME: this is too convoluted. We need to get rid of the above updates
@ -237,12 +237,12 @@
// we need to modify the model // we need to modify the model
// (FIXME: yes, this is hacky) // (FIXME: yes, this is hacky)
icon: $tr.attr('data-icon') icon: $tr.attr('data-icon')
}); })
}); })
fileList.registerTabView(shareTab); fileList.registerTabView(shareTab)
var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({shareTab: shareTab}); var breadCrumbSharingDetailView = new OCA.Sharing.ShareBreadCrumbView({ shareTab: shareTab })
fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView); fileList.registerBreadCrumbDetailView(breadCrumbSharingDetailView)
}, },
/** /**
@ -252,18 +252,17 @@
// files app current cannot show recipients on load, so we don't update the // files app current cannot show recipients on load, so we don't update the
// icon when changed for consistency // icon when changed for consistency
if (fileList.id === 'files') { 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() // note: we only update the data attribute because updateIcon()
if (recipients.length) { if (recipients.length) {
var recipientData = _.mapObject(shareModel.get('shares'), function (share) { var recipientData = _.mapObject(shareModel.get('shares'), function(share) {
return {shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname}; return { shareWith: share.share_with, shareWithDisplayName: share.share_with_displayname }
}); })
$tr.attr('data-share-recipient-data', JSON.stringify(recipientData)); $tr.attr('data-share-recipient-data', JSON.stringify(recipientData))
} } else {
else { $tr.removeAttr('data-share-recipient-data')
$tr.removeAttr('data-share-recipient-data');
} }
}, },
@ -274,16 +273,16 @@
* @param {boolean} hasUserShares true if a user share exists * @param {boolean} hasUserShares true if a user share exists
* @param {boolean} hasLinkShares true if a link 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) { _updateFileActionIcon: function($tr, hasUserShares, hasLinkShares) {
// if the statuses are loaded already, use them for the icon // if the statuses are loaded already, use them for the icon
// (needed when scrolling to the next page) // (needed when scrolling to the next page)
if (hasUserShares || hasLinkShares || $tr.attr('data-share-recipient-data') || $tr.attr('data-share-owner')) { if (hasUserShares || hasLinkShares || $tr.attr('data-share-recipient-data') || $tr.attr('data-share-owner')) {
OC.Share.markFileAsShared($tr, true, hasLinkShares); OC.Share.markFileAsShared($tr, true, hasLinkShares)
return true; return true
} }
return false; return false
}, },
/** /**
@ -291,9 +290,9 @@
* @returns {String} * @returns {String}
*/ */
getSharePermissions: function(fileData) { 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)

View File

@ -1,5 +1,3 @@
/* global Handlebars, OC */
/** /**
* @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at> * @copyright 2016 Christoph Wurst <christoph@winzerhof-wurst.at>
* *
@ -23,7 +21,7 @@
*/ */
(function() { (function() {
'use strict'; 'use strict'
var BreadCrumbView = OC.Backbone.View.extend({ var BreadCrumbView = OC.Backbone.View.extend({
tagName: 'span', tagName: 'span',
@ -36,68 +34,68 @@
_shareTab: undefined, _shareTab: undefined,
initialize: function(options) { initialize: function(options) {
this._shareTab = options.shareTab; this._shareTab = options.shareTab
}, },
render: function(data) { render: function(data) {
this._dirInfo = data.dirInfo || null; this._dirInfo = data.dirInfo || null
if (this._dirInfo !== null && (this._dirInfo.path !== '/' || this._dirInfo.name !== '')) { if (this._dirInfo !== null && (this._dirInfo.path !== '/' || this._dirInfo.name !== '')) {
var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0; var isShared = data.dirInfo && data.dirInfo.shareTypes && data.dirInfo.shareTypes.length > 0
this.$el.removeClass('shared icon-public icon-shared'); this.$el.removeClass('shared icon-public icon-shared')
if (isShared) { if (isShared) {
this.$el.addClass('shared'); this.$el.addClass('shared')
if (data.dirInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) !== -1) { if (data.dirInfo.shareTypes.indexOf(OC.Share.SHARE_TYPE_LINK) !== -1) {
this.$el.addClass('icon-public'); this.$el.addClass('icon-public')
} else { } else {
this.$el.addClass('icon-shared'); this.$el.addClass('icon-shared')
} }
} else { } else {
this.$el.addClass('icon-shared'); this.$el.addClass('icon-shared')
} }
this.$el.show(); this.$el.show()
this.delegateEvents(); this.delegateEvents()
} else { } else {
this.$el.removeClass('shared icon-public icon-shared'); this.$el.removeClass('shared icon-public icon-shared')
this.$el.hide(); this.$el.hide()
} }
return this; return this
}, },
_onClick: function(e) { _onClick: function(e) {
e.preventDefault(); e.preventDefault()
var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo); var fileInfoModel = new OCA.Files.FileInfoModel(this._dirInfo)
var self = this; var self = this
fileInfoModel.on('change', function() { fileInfoModel.on('change', function() {
self.render({ self.render({
dirInfo: self._dirInfo dirInfo: self._dirInfo
}); })
}); })
this._shareTab.on('sharesChanged', function(shareModel) { this._shareTab.on('sharesChanged', function(shareModel) {
var shareTypes = []; var shareTypes = []
var shares = shareModel.getSharesWithCurrentItem(); 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) { if (shareTypes.indexOf(shares[i].share_type) === -1) {
shareTypes.push(shares[i].share_type); shareTypes.push(shares[i].share_type)
} }
} }
if (shareModel.hasLinkShares()) { 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 // Since the dirInfo isn't updated we need to do this dark hackery
self._dirInfo.shareTypes = shareTypes; self._dirInfo.shareTypes = shareTypes
self.render({ self.render({
dirInfo: self._dirInfo 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
})(); })()

View File

@ -11,77 +11,77 @@
/* @global Handlebars */ /* @global Handlebars */
(function() { (function() {
var TEMPLATE = var TEMPLATE
'<div>' + = '<div>'
'<div class="dialogContainer"></div>' + + '<div class="dialogContainer"></div>'
'<div id="collaborationResources"></div>' + + '<div id="collaborationResources"></div>'
'</div>'; + '</div>'
/** /**
* @memberof OCA.Sharing * @memberof OCA.Sharing
*/ */
var ShareTabView = OCA.Files.DetailTabView.extend( var ShareTabView = OCA.Files.DetailTabView.extend(
/** @lends OCA.Sharing.ShareTabView.prototype */ { /** @lends OCA.Sharing.ShareTabView.prototype */ {
id: 'shareTabView', id: 'shareTabView',
className: 'tab shareTabView', className: 'tab shareTabView',
initialize: function(name, options) { initialize: function(name, options) {
OCA.Files.DetailTabView.prototype.initialize.call(this, name, options); OCA.Files.DetailTabView.prototype.initialize.call(this, name, options)
OC.Plugins.attach('OCA.Sharing.ShareTabView', this); OC.Plugins.attach('OCA.Sharing.ShareTabView', this)
}, },
template: function(params) { template: function(params) {
return TEMPLATE; return TEMPLATE
}, },
getLabel: function() { getLabel: function() {
return t('files_sharing', 'Sharing'); return t('files_sharing', 'Sharing')
}, },
getIcon: function() { getIcon: function() {
return 'icon-shared'; return 'icon-shared'
}, },
/** /**
* Renders this details view * Renders this details view
*/ */
render: function() { render: function() {
var self = this; var self = this
if (this._dialog) { if (this._dialog) {
// remove/destroy older instance // remove/destroy older instance
this._dialog.model.off(); this._dialog.model.off()
this._dialog.remove(); this._dialog.remove()
this._dialog = null; 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));
} }
// TODO: the model should read these directly off the passed fileInfoModel if (this.model) {
var attributes = { this.$el.html(this.template())
itemType: this.model.isDirectory() ? 'folder' : 'file',
itemSource: this.model.get('id'), if (_.isUndefined(this.model.get('sharePermissions'))) {
possiblePermissions: this.model.get('sharePermissions') this.model.set('sharePermissions', OCA.Sharing.Util.getSharePermissions(this.model.attributes))
}; }
var configModel = new OC.Share.ShareConfigModel();
var shareModel = new OC.Share.ShareItemModel(attributes, { // TODO: the model should read these directly off the passed fileInfoModel
configModel: configModel, var attributes = {
fileInfoModel: this.model itemType: this.model.isDirectory() ? 'folder' : 'file',
}); itemSource: this.model.get('id'),
this._dialog = new OC.Share.ShareDialogView({ possiblePermissions: this.model.get('sharePermissions')
configModel: configModel, }
model: shareModel var configModel = new OC.Share.ShareConfigModel()
}); var shareModel = new OC.Share.ShareItemModel(attributes, {
this.$el.find('.dialogContainer').append(this._dialog.$el); configModel: configModel,
this._dialog.render(); fileInfoModel: this.model
this._dialog.model.fetch(); })
this._dialog.model.on('change', function() { this._dialog = new OC.Share.ShareDialogView({
self.trigger('sharesChanged', shareModel); 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) => { import('./collaborationresources').then((Resources) => {
var vm = new Resources.Vue({ var vm = new Resources.Vue({
@ -89,20 +89,19 @@
render: h => h(Resources.View), render: h => h(Resources.View),
data: { data: {
model: this.model.toJSON() model: this.model.toJSON()
}, }
}); })
this.model.on('change', () => { vm.data = this.model.toJSON() }) this.model.on('change', () => { vm.data = this.model.toJSON() })
}) })
} else { } else {
this.$el.empty(); this.$el.empty()
// TODO: render placeholder text? // TODO: render placeholder text?
}
this.trigger('rendered')
} }
this.trigger('rendered'); })
}
});
OCA.Sharing.ShareTabView = ShareTabView;
})();
OCA.Sharing.ShareTabView = ShareTabView
})()

View File

@ -21,7 +21,10 @@
--> -->
<template> <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> </template>
<script> <script>
@ -29,22 +32,22 @@ import { CollectionList } from 'nextcloud-vue-collections'
export default { export default {
name: 'CollaborationView', name: 'CollaborationView',
components: {
CollectionList
},
computed: { computed: {
fileId() { fileId() {
if (this.$root.model && this.$root.model.id) { if (this.$root.model && this.$root.model.id) {
return '' + this.$root.model.id; return '' + this.$root.model.id
} }
return null; return null
}, },
filename() { filename() {
if (this.$root.model && this.$root.model.name) { if (this.$root.model && this.$root.model.name) {
return '' + this.$root.model.name; return '' + this.$root.model.name
} }
return ''; return ''
} }
},
components: {
CollectionList
} }
} }
</script> </script>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,4 +1,4 @@
/* /**
* Copyright (c) 2014 * Copyright (c) 2014
* *
* This file is licensed under the Affero General Public License version 3 * This file is licensed under the Affero General Public License version 3
@ -11,7 +11,7 @@
/** /**
* @namespace OCA.Trashbin * @namespace OCA.Trashbin
*/ */
OCA.Trashbin = {}; OCA.Trashbin = {}
/** /**
* @namespace OCA.Trashbin.App * @namespace OCA.Trashbin.App
*/ */
@ -20,19 +20,19 @@ OCA.Trashbin.App = {
/** @type {OC.Files.Client} */ /** @type {OC.Files.Client} */
client: null, client: null,
initialize: function ($el) { initialize: function($el) {
if (this._initialized) { if (this._initialized) {
return; return
} }
this._initialized = true; this._initialized = true
this.client = new OC.Files.Client({ this.client = new OC.Files.Client({
host: OC.getHost(), host: OC.getHost(),
port: OC.getPort(), port: OC.getPort(),
root: OC.linkToRemoteBase('dav') + '/trashbin/' + OC.getCurrentUser().uid, root: OC.linkToRemoteBase('dav') + '/trashbin/' + OC.getCurrentUser().uid,
useHTTPS: OC.getProtocol() === 'https' useHTTPS: OC.getProtocol() === 'https'
}); })
var urlParams = OC.Util.History.parseUrlQuery(); var urlParams = OC.Util.History.parseUrlQuery()
this.fileList = new OCA.Trashbin.FileList( this.fileList = new OCA.Trashbin.FileList(
$('#app-content-trashbin'), { $('#app-content-trashbin'), {
fileActions: this._createFileActions(), fileActions: this._createFileActions(),
@ -43,12 +43,12 @@ OCA.Trashbin.App = {
{ {
name: 'restore', name: 'restore',
displayName: t('files_trashbin', 'Restore'), displayName: t('files_trashbin', 'Restore'),
iconClass: 'icon-history', iconClass: 'icon-history'
}, },
{ {
name: 'delete', name: 'delete',
displayName: t('files_trashbin', 'Delete permanently'), displayName: t('files_trashbin', 'Delete permanently'),
iconClass: 'icon-delete', iconClass: 'icon-delete'
} }
], ],
client: this.client, client: this.client,
@ -57,18 +57,18 @@ OCA.Trashbin.App = {
// if handling the event with the file list already created. // if handling the event with the file list already created.
shown: true shown: true
} }
); )
}, },
_createFileActions: function () { _createFileActions: function() {
var client = this.client; var client = this.client
var fileActions = new OCA.Files.FileActions(); var fileActions = new OCA.Files.FileActions()
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function(filename, context) {
var dir = context.fileList.getCurrentDirectory(); var dir = context.fileList.getCurrentDirectory()
context.fileList.changeDirectory(OC.joinPaths(dir, filename)); context.fileList.changeDirectory(OC.joinPaths(dir, filename))
}); })
fileActions.setDefault('dir', 'Open'); fileActions.setDefault('dir', 'Open')
fileActions.registerAction({ fileActions.registerAction({
name: 'Restore', name: 'Restore',
@ -77,21 +77,21 @@ OCA.Trashbin.App = {
mime: 'all', mime: 'all',
permissions: OC.PERMISSION_READ, permissions: OC.PERMISSION_READ,
iconClass: 'icon-history', iconClass: 'icon-history',
actionHandler: function (filename, context) { actionHandler: function(filename, context) {
var fileList = context.fileList; var fileList = context.fileList
var tr = fileList.findFileEl(filename); var tr = fileList.findFileEl(filename)
fileList.showFileBusyState(tr, true); fileList.showFileBusyState(tr, true)
var dir = context.fileList.getCurrentDirectory(); var dir = context.fileList.getCurrentDirectory()
client.move(OC.joinPaths('trash', dir, filename), OC.joinPaths('restore', filename), true) client.move(OC.joinPaths('trash', dir, filename), OC.joinPaths('restore', filename), true)
.then( .then(
fileList._removeCallback.bind(fileList, [filename]), fileList._removeCallback.bind(fileList, [filename]),
function () { function() {
fileList.showFileBusyState(tr, false); fileList.showFileBusyState(tr, false)
OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin')); OC.Notification.show(t('files_trashbin', 'Error while restoring file from trashbin'))
} }
); )
} }
}); })
fileActions.registerAction({ fileActions.registerAction({
name: 'Delete', name: 'Delete',
@ -99,39 +99,38 @@ OCA.Trashbin.App = {
mime: 'all', mime: 'all',
permissions: OC.PERMISSION_READ, permissions: OC.PERMISSION_READ,
iconClass: 'icon-delete', iconClass: 'icon-delete',
render: function (actionSpec, isDefault, context) { render: function(actionSpec, isDefault, context) {
var $actionLink = fileActions._makeActionLink(actionSpec, context); var $actionLink = fileActions._makeActionLink(actionSpec, context)
$actionLink.attr('original-title', t('files_trashbin', 'Delete permanently')); $actionLink.attr('original-title', t('files_trashbin', 'Delete permanently'))
$actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently')); $actionLink.children('img').attr('alt', t('files_trashbin', 'Delete permanently'))
context.$file.find('td:last').append($actionLink); context.$file.find('td:last').append($actionLink)
return $actionLink; return $actionLink
}, },
actionHandler: function (filename, context) { actionHandler: function(filename, context) {
var fileList = context.fileList; var fileList = context.fileList
$('.tipsy').remove(); $('.tipsy').remove()
var tr = fileList.findFileEl(filename); var tr = fileList.findFileEl(filename)
fileList.showFileBusyState(tr, true); fileList.showFileBusyState(tr, true)
var dir = context.fileList.getCurrentDirectory(); var dir = context.fileList.getCurrentDirectory()
client.remove(OC.joinPaths('trash', dir, filename)) client.remove(OC.joinPaths('trash', dir, filename))
.then( .then(
fileList._removeCallback.bind(fileList, [filename]), fileList._removeCallback.bind(fileList, [filename]),
function () { function() {
fileList.showFileBusyState(tr, false); fileList.showFileBusyState(tr, false)
OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin')); OC.Notification.show(t('files_trashbin', 'Error while removing file from trashbin'))
} }
); )
} }
}); })
return fileActions; return fileActions
} }
}; }
$(document).ready(function () { $(document).ready(function() {
$('#app-content-trashbin').one('show', function () { $('#app-content-trashbin').one('show', function() {
var App = OCA.Trashbin.App; var App = OCA.Trashbin.App
App.initialize($('#app-content-trashbin')); App.initialize($('#app-content-trashbin'))
// force breadcrumb init // force breadcrumb init
// App.fileList.changeDirectory(App.fileList.getCurrentDirectory(), false, true); // App.fileList.changeDirectory(App.fileList.getCurrentDirectory(), false, true);
}); })
}); })

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/* /*
* Copyright (c) 2014 * Copyright (c) 2014
* *
@ -8,25 +9,25 @@
* *
*/ */
(function() { (function() {
var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/); var DELETED_REGEXP = new RegExp(/^(.+)\.d[0-9]+$/)
var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename'; var FILENAME_PROP = '{http://nextcloud.org/ns}trashbin-filename'
var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time'; var DELETION_TIME_PROP = '{http://nextcloud.org/ns}trashbin-deletion-time'
var TRASHBIN_ORIGINAL_LOCATION = '{http://nextcloud.org/ns}trashbin-original-location'; 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. * Convert a file name in the format filename.d12345 to the real file name.
* This will use basename. * This will use basename.
* The name will not be changed if it has no ".d12345" suffix. * The name will not be changed if it has no ".d12345" suffix.
* @param {String} name file name * @param {String} name file name
* @return {String} converted file name * @returns {String} converted file name
*/ */
function getDeletedFileName(name) { function getDeletedFileName(name) {
name = OC.basename(name); name = OC.basename(name)
var match = DELETED_REGEXP.exec(name); var match = DELETED_REGEXP.exec(name)
if (match && match.length > 1) { if (match && match.length > 1) {
name = match[1]; name = match[1]
} }
return name; return name
} }
/** /**
@ -39,283 +40,282 @@
* @param [options] map of options * @param [options] map of options
*/ */
var FileList = function($el, options) { var FileList = function($el, options) {
this.client = options.client; this.client = options.client
this.initialize($el, options); this.initialize($el, options)
}; }
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
/** @lends OCA.Trashbin.FileList.prototype */ { /** @lends OCA.Trashbin.FileList.prototype */ {
id: 'trashbin', id: 'trashbin',
appName: t('files_trashbin', 'Deleted files'), appName: t('files_trashbin', 'Deleted files'),
/** @type {OC.Files.Client} */ /** @type {OC.Files.Client} */
client: null, client: null,
/** /**
* @private * @private
*/ */
initialize: function() { initialize: function() {
this.client.addFileInfoParser(function(response, data) { this.client.addFileInfoParser(function(response, data) {
var props = response.propStat[0].properties; var props = response.propStat[0].properties
var path = props[TRASHBIN_ORIGINAL_LOCATION]; var path = props[TRASHBIN_ORIGINAL_LOCATION]
return { return {
displayName: props[FILENAME_PROP], displayName: props[FILENAME_PROP],
mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000, mtime: parseInt(props[DELETION_TIME_PROP], 10) * 1000,
hasPreview: true, hasPreview: true,
path: path, path: path,
extraData: path extraData: path
} }
}); })
var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments); var result = OCA.Files.FileList.prototype.initialize.apply(this, arguments)
this.$el.find('.undelete').click('click', _.bind(this._onClickRestoreSelected, this)); 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 * Override crumb making to add "Deleted Files" entry
* and convert files with ".d" extensions to a more * and convert files with ".d" extensions to a more
* user friendly name. * user friendly name.
*/ */
this.breadcrumb._makeCrumbs = function() { this.breadcrumb._makeCrumbs = function() {
var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments); var parts = OCA.Files.BreadCrumb.prototype._makeCrumbs.apply(this, arguments)
for (var i = 1; i < parts.length; i++) { for (var i = 1; i < parts.length; i++) {
parts[i].name = getDeletedFileName(parts[i].name); parts[i].name = getDeletedFileName(parts[i].name)
}
return parts
} }
return parts;
};
OC.Plugins.attach('OCA.Trashbin.FileList', this); OC.Plugins.attach('OCA.Trashbin.FileList', this)
return result; return result
}, },
/** /**
* Override to only return read permissions * Override to only return read permissions
*/ */
getDirectoryPermissions: function() { getDirectoryPermissions: function() {
return OC.PERMISSION_READ | OC.PERMISSION_DELETE; return OC.PERMISSION_READ | OC.PERMISSION_DELETE
}, },
_setCurrentDir: function(targetDir) { _setCurrentDir: function(targetDir) {
OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments); OCA.Files.FileList.prototype._setCurrentDir.apply(this, arguments)
var baseDir = OC.basename(targetDir); var baseDir = OC.basename(targetDir)
if (baseDir !== '') { if (baseDir !== '') {
this.setPageTitle(getDeletedFileName(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'));
} }
); },
},
_onClickDeleteSelected: function(event) { _createRow: function() {
event.preventDefault(); // FIXME: MEGAHACK until we find a better solution
var self = this; var tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments)
var allFiles = this.$el.find('.select-all').is(':checked'); tr.find('td.filesize').remove()
var files = _.pluck(this.getSelectedFiles(), 'name'); return tr
for (var i = 0; i < files.length; i++) { },
var tr = this.findFileEl(files[i]);
this.showFileBusyState(tr, true);
}
if (allFiles) { getAjaxUrl: function(action, params) {
return this.client.remove(OC.joinPaths('trash', this.getCurrentDirectory())) var q = ''
.then( if (params) {
function() { q = '?' + OC.buildQueryString(params)
self.hideMask(); }
self.setFiles([]); return OC.filePath('files_trashbin', 'ajax', action + '.php') + q
}, },
function() {
OC.Notification.show(t('files_trashbin', 'Error while emptying trashbin')); setupUploadEvents: function() {
} // override and do nothing
); },
} else {
this.fileMultiSelectMenu.toggleLoading('delete', true); linkTo: function(dir) {
var deletePromises = files.map(function(file) { return OC.linkTo('files', 'index.php') + '?view=trashbin&dir=' + encodeURIComponent(dir).replace(/%2F/g, '/')
return self.client.remove(OC.joinPaths('trash', self.getCurrentDirectory(), file)) },
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( .then(
function() { function() {
self._removeCallback([file]); self._removeCallback([file])
} }
); )
}); })
return Promise.all(deletePromises).then( return Promise.all(restorePromises).then(
function() { function() {
self.fileMultiSelectMenu.toggleLoading('delete', false); self.fileMultiSelectMenu.toggleLoading('restore', false)
}, },
function() { 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) { _onClickDeleteSelected: function(event) {
var mime = $(this).parent().parent().data('mime'); event.preventDefault()
if (mime !== 'httpd/unix-directory') { var self = this
event.preventDefault(); var allFiles = this.$el.find('.select-all').is(':checked')
} var files = _.pluck(this.getSelectedFiles(), 'name')
return OCA.Files.FileList.prototype._onClickFile.apply(this, arguments); for (var i = 0; i < files.length; i++) {
}, var tr = this.findFileEl(files[i])
this.showFileBusyState(tr, true)
}
generatePreviewUrl: function(urlSpec) { if (allFiles) {
return OC.generateUrl('/apps/files_trashbin/preview?') + $.param(urlSpec); 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 // no downloads
return '#'; return '#'
}, },
updateStorageStatistics: function() { updateStorageStatistics: function() {
// no op because the trashbin doesn't have // no op because the trashbin doesn't have
// storage info like free space / used space // storage info like free space / used space
}, },
isSelectedDeletable: function() { isSelectedDeletable: function() {
return true; return true
}, },
/** /**
* Returns list of webdav properties to request * Returns list of webdav properties to request
*/ */
_getWebdavProperties: function() { _getWebdavProperties: function() {
return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties()); return [FILENAME_PROP, DELETION_TIME_PROP, TRASHBIN_ORIGINAL_LOCATION].concat(this.filesClient.getPropfindProperties())
}, },
/** /**
* Reloads the file list using ajax call * Reloads the file list using ajax call
* *
* @return ajax call object * @returns ajax call object
*/ */
reload: function() { reload: function() {
this._selectedFiles = {}; this._selectedFiles = {}
this._selectionSummary.clear(); this._selectionSummary.clear()
this.$el.find('.select-all').prop('checked', false); this.$el.find('.select-all').prop('checked', false)
this.showMask(); this.showMask()
if (this._reloadCall) { if (this._reloadCall) {
this._reloadCall.abort(); this._reloadCall.abort()
}
this._reloadCall = this.client.getFolderContents(
'trash/' + this.getCurrentDirectory(), {
includeParent: false,
properties: this._getWebdavProperties()
} }
); this._reloadCall = this.client.getFolderContents(
var callBack = this.reloadCallback.bind(this); 'trash/' + this.getCurrentDirectory(), {
return this._reloadCall.then(callBack, callBack); includeParent: false,
}, properties: this._getWebdavProperties()
reloadCallback: function(status, result) { }
delete this._reloadCall; )
this.hideMask(); var callBack = this.reloadCallback.bind(this)
return this._reloadCall.then(callBack, callBack)
},
reloadCallback: function(status, result) {
delete this._reloadCall
this.hideMask()
if (status === 401) { if (status === 401) {
return false; return false
} }
// Firewall Blocked request? // Firewall Blocked request?
if (status === 403) { if (status === 403) {
// Go home // Go home
this.changeDirectory('/'); this.changeDirectory('/')
OC.Notification.show(t('files', 'This operation is forbidden')); OC.Notification.show(t('files', 'This operation is forbidden'))
return false; return false
} }
// Did share service die or something else fail? // Did share service die or something else fail?
if (status === 500) { if (status === 500) {
// Go home // Go home
this.changeDirectory('/'); this.changeDirectory('/')
OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator')); OC.Notification.show(t('files', 'This directory is unavailable, please check the logs or contact the administrator'))
return false; return false
} }
if (status === 404) { if (status === 404) {
// go back home // go back home
this.changeDirectory('/'); this.changeDirectory('/')
return false; return false
} }
// aborted ? // aborted ?
if (status === 0){ if (status === 0) {
return true; return true
}
this.setFiles(result)
return true
} }
this.setFiles(result); })
return true;
},
});
OCA.Trashbin.FileList = FileList;
})();
OCA.Trashbin.FileList = FileList
})()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -9,7 +9,7 @@
*/ */
(function() { (function() {
OCA.Versions = OCA.Versions || {}; OCA.Versions = OCA.Versions || {}
/** /**
* @namespace * @namespace
@ -22,13 +22,12 @@
*/ */
attach: function(fileList) { attach: function(fileList) {
if (fileList.id === 'trashbin' || fileList.id === 'files.public') { 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)

View File

@ -8,7 +8,7 @@
* *
*/ */
(function () { (function() {
/** /**
* @memberof OCA.Versions * @memberof OCA.Versions
*/ */
@ -25,24 +25,24 @@
_client: null, _client: null,
setFileInfo: function (fileInfo) { setFileInfo: function(fileInfo) {
this._fileInfo = fileInfo; this._fileInfo = fileInfo
}, },
getFileInfo: function () { getFileInfo: function() {
return this._fileInfo; return this._fileInfo
}, },
setCurrentUser: function(user) { setCurrentUser: function(user) {
this._currentUser = user; this._currentUser = user
}, },
getCurrentUser: function() { getCurrentUser: function() {
return this._currentUser || OC.getCurrentUser().uid; return this._currentUser || OC.getCurrentUser().uid
}, },
setClient: function(client) { setClient: function(client) {
this._client = client; this._client = client
}, },
getClient: function() { getClient: function() {
@ -50,35 +50,34 @@
host: OC.getHost(), host: OC.getHost(),
root: OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser(), root: OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser(),
useHTTPS: OC.getProtocol() === 'https' useHTTPS: OC.getProtocol() === 'https'
}); })
}, },
url: function () { url: function() {
return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id'); return OC.linkToRemoteBase('dav') + '/versions/' + this.getCurrentUser() + '/versions/' + this._fileInfo.get('id')
}, },
parse: function(result) { parse: function(result) {
var fullPath = this._fileInfo.getFullPath(); var fullPath = this._fileInfo.getFullPath()
var fileId = this._fileInfo.get('id'); var fileId = this._fileInfo.get('id')
var name = this._fileInfo.get('name'); var name = this._fileInfo.get('name')
var user = this.getCurrentUser(); var user = this.getCurrentUser()
var client = this.getClient(); var client = this.getClient()
return _.map(result, function(version) { return _.map(result, function(version) {
version.fullPath = fullPath; version.fullPath = fullPath
version.fileId = fileId; version.fileId = fileId
version.name = name; version.name = name
version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10); version.timestamp = parseInt(moment(new Date(version.timestamp)).format('X'), 10)
version.id = OC.basename(version.href); version.id = OC.basename(version.href)
version.size = parseInt(version.size, 10); version.size = parseInt(version.size, 10)
version.user = user; version.user = user
version.client = client; version.client = client
return version; return version
}); })
} }
}); })
OCA.Versions = OCA.Versions || {}; OCA.Versions = OCA.Versions || {}
OCA.Versions.VersionCollection = VersionCollection;
})();
OCA.Versions.VersionCollection = VersionCollection
})()

View File

@ -8,9 +8,7 @@
* *
*/ */
/* global moment */ (function() {
(function () {
/** /**
* @memberof OCA.Versions * @memberof OCA.Versions
*/ */
@ -20,53 +18,55 @@
davProperties: { davProperties: {
'size': '{DAV:}getcontentlength', 'size': '{DAV:}getcontentlength',
'mimetype': '{DAV:}getcontenttype', 'mimetype': '{DAV:}getcontenttype',
'timestamp': '{DAV:}getlastmodified', 'timestamp': '{DAV:}getlastmodified'
}, },
/** /**
* Restores the original file to this revision * Restores the original file to this revision
*
* @param {Object} [options] options
* @returns {Promise}
*/ */
revert: function (options) { revert: function(options) {
options = options ? _.clone(options) : {}; options = options ? _.clone(options) : {}
var model = this; 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) return client.move('/versions/' + this.get('fileId') + '/' + this.get('id'), '/restore/target', true)
.done(function () { .done(function() {
if (options.success) { 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) { 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 () { getFullPath: function() {
return this.get('fullPath'); return this.get('fullPath')
}, },
getPreviewUrl: function () { getPreviewUrl: function() {
var url = OC.generateUrl('/apps/files_versions/preview'); var url = OC.generateUrl('/apps/files_versions/preview')
var params = { var params = {
file: this.get('fullPath'), file: this.get('fullPath'),
version: this.get('id') version: this.get('id')
}; }
return url + '?' + OC.buildQueryString(params); return url + '?' + OC.buildQueryString(params)
}, },
getDownloadUrl: function () { getDownloadUrl: function() {
return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id'); return OC.linkToRemoteBase('dav') + '/versions/' + this.get('user') + '/versions/' + this.get('fileId') + '/' + this.get('id')
} }
}); })
OCA.Versions = OCA.Versions || {}; OCA.Versions = OCA.Versions || {}
OCA.Versions.VersionModel = VersionModel;
})();
OCA.Versions.VersionModel = VersionModel
})()

View File

@ -8,7 +8,7 @@
* *
*/ */
import ItemTemplate from './templates/item.handlebars'; import ItemTemplate from './templates/item.handlebars'
import Template from './templates/template.handlebars'; import Template from './templates/template.handlebars';
(function() { (function() {
@ -28,137 +28,137 @@ import Template from './templates/template.handlebars';
}, },
initialize: function() { initialize: function() {
OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments); OCA.Files.DetailTabView.prototype.initialize.apply(this, arguments)
this.collection = new OCA.Versions.VersionCollection(); this.collection = new OCA.Versions.VersionCollection()
this.collection.on('request', this._onRequest, this); this.collection.on('request', this._onRequest, this)
this.collection.on('sync', this._onEndRequest, this); this.collection.on('sync', this._onEndRequest, this)
this.collection.on('update', this._onUpdate, this); this.collection.on('update', this._onUpdate, this)
this.collection.on('error', this._onError, this); this.collection.on('error', this._onError, this)
this.collection.on('add', this._onAddModel, this); this.collection.on('add', this._onAddModel, this)
}, },
getLabel: function() { getLabel: function() {
return t('files_versions', 'Versions'); return t('files_versions', 'Versions')
}, },
getIcon: function() { getIcon: function() {
return 'icon-history'; return 'icon-history'
}, },
nextPage: function() { nextPage: function() {
if (this._loading) { if (this._loading) {
return; return
} }
if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) { if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
return; return
} }
this.collection.fetch(); this.collection.fetch()
}, },
_onClickRevertVersion: function(ev) { _onClickRevertVersion: function(ev) {
var self = this; var self = this
var $target = $(ev.target); var $target = $(ev.target)
var fileInfoModel = this.collection.getFileInfo(); var fileInfoModel = this.collection.getFileInfo()
var revision; var revision
if (!$target.is('li')) { if (!$target.is('li')) {
$target = $target.closest('li'); $target = $target.closest('li')
} }
ev.preventDefault(); ev.preventDefault()
revision = $target.attr('data-revision'); revision = $target.attr('data-revision')
var versionModel = this.collection.get(revision); var versionModel = this.collection.get(revision)
versionModel.revert({ versionModel.revert({
success: function() { success: function() {
// reset and re-fetch the updated collection // reset and re-fetch the updated collection
self.$versionsContainer.empty(); self.$versionsContainer.empty()
self.collection.setFileInfo(fileInfoModel); self.collection.setFileInfo(fileInfoModel)
self.collection.reset([], {silent: true}); self.collection.reset([], { silent: true })
self.collection.fetch(); self.collection.fetch()
self.$el.find('.versions').removeClass('hidden'); self.$el.find('.versions').removeClass('hidden')
// update original model // update original model
fileInfoModel.trigger('busy', fileInfoModel, false); fileInfoModel.trigger('busy', fileInfoModel, false)
fileInfoModel.set({ fileInfoModel.set({
size: versionModel.get('size'), size: versionModel.get('size'),
mtime: versionModel.get('timestamp') * 1000, mtime: versionModel.get('timestamp') * 1000,
// temp dummy, until we can do a PROPFIND // temp dummy, until we can do a PROPFIND
etag: versionModel.get('id') + versionModel.get('timestamp') etag: versionModel.get('id') + versionModel.get('timestamp')
}); })
}, },
error: function() { error: function() {
fileInfoModel.trigger('busy', fileInfoModel, false); fileInfoModel.trigger('busy', fileInfoModel, false)
self.$el.find('.versions').removeClass('hidden'); self.$el.find('.versions').removeClass('hidden')
self._toggleLoading(false); self._toggleLoading(false)
OC.Notification.show(t('files_version', 'Failed to revert {file} to revision {timestamp}.', OC.Notification.show(t('files_version', 'Failed to revert {file} to revision {timestamp}.',
{ {
file: versionModel.getFullPath(), file: versionModel.getFullPath(),
timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000) timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000)
}), }),
{ {
type: 'error' type: 'error'
} }
); )
} }
}); })
// spinner // spinner
this._toggleLoading(true); this._toggleLoading(true)
fileInfoModel.trigger('busy', fileInfoModel, true); fileInfoModel.trigger('busy', fileInfoModel, true)
}, },
_toggleLoading: function(state) { _toggleLoading: function(state) {
this._loading = state; this._loading = state
this.$el.find('.loading').toggleClass('hidden', !state); this.$el.find('.loading').toggleClass('hidden', !state)
}, },
_onRequest: function() { _onRequest: function() {
this._toggleLoading(true); this._toggleLoading(true)
}, },
_onEndRequest: function() { _onEndRequest: function() {
this._toggleLoading(false); this._toggleLoading(false)
this.$el.find('.empty').toggleClass('hidden', !!this.collection.length); this.$el.find('.empty').toggleClass('hidden', !!this.collection.length)
}, },
_onAddModel: function(model) { _onAddModel: function(model) {
var $el = $(this.itemTemplate(this._formatItem(model))); var $el = $(this.itemTemplate(this._formatItem(model)))
this.$versionsContainer.append($el); this.$versionsContainer.append($el)
$el.find('.has-tooltip').tooltip(); $el.find('.has-tooltip').tooltip()
}, },
template: function(data) { template: function(data) {
return Template(data); return Template(data)
}, },
itemTemplate: function(data) { itemTemplate: function(data) {
return ItemTemplate(data); return ItemTemplate(data)
}, },
setFileInfo: function(fileInfo) { setFileInfo: function(fileInfo) {
if (fileInfo) { if (fileInfo) {
this.render(); this.render()
this.collection.setFileInfo(fileInfo); this.collection.setFileInfo(fileInfo)
this.collection.reset([], {silent: true}); this.collection.reset([], { silent: true })
this.nextPage(); this.nextPage()
} else { } else {
this.render(); this.render()
this.collection.reset(); this.collection.reset()
} }
}, },
_formatItem: function(version) { _formatItem: function(version) {
var timestamp = version.get('timestamp') * 1000; var timestamp = version.get('timestamp') * 1000
var size = version.has('size') ? version.get('size') : 0; var size = version.has('size') ? version.get('size') : 0
var preview = OC.MimeType.getIconUrl(version.get('mimetype')); var preview = OC.MimeType.getIconUrl(version.get('mimetype'))
var img = new Image(); var img = new Image()
img.onload = function () { img.onload = function() {
$('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl()); $('li[data-revision=' + version.get('id') + '] .preview').attr('src', version.getPreviewUrl())
}; }
img.src = version.getPreviewUrl(); img.src = version.getPreviewUrl()
return _.extend({ return _.extend({
versionId: version.get('id'), versionId: version.get('id'),
@ -175,7 +175,7 @@ import Template from './templates/template.handlebars';
previewUrl: preview, previewUrl: preview,
revertLabel: t('files_versions', 'Restore'), revertLabel: t('files_versions', 'Restore'),
canRevert: (this.collection.getFileInfo().get('permissions') & OC.PERMISSION_UPDATE) !== 0 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() { render: function() {
this.$el.html(this.template({ this.$el.html(this.template({
emptyResultLabel: t('files_versions', 'No other versions available'), emptyResultLabel: t('files_versions', 'No other versions available')
})); }))
this.$el.find('.has-tooltip').tooltip(); this.$el.find('.has-tooltip').tooltip()
this.$versionsContainer = this.$el.find('ul.versions'); this.$versionsContainer = this.$el.find('ul.versions')
this.delegateEvents(); this.delegateEvents()
}, },
/** /**
* Returns true for files, false for folders. * Returns true for files, false for folders.
* * @param {FileInfo} fileInfo fileInfo
* @return {bool} true for files, false for folders * @returns {bool} true for files, false for folders
*/ */
canDisplay: function(fileInfo) { canDisplay: function(fileInfo) {
if (!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
})(); })()

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -22,52 +22,71 @@
<template> <template>
<div id="oauth2" class="section"> <div id="oauth2" class="section">
<h2>{{ t('oauth2', 'OAuth 2.0 clients') }}</h2> <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> <p class="settings-hint">
<table class="grid" v-if="clients.length > 0"> {{ 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> <thead>
<tr> <tr>
<th id="headerName" scope="col">{{ t('oauth2', 'Name') }}</th> <th id="headerName" scope="col">
<th id="headerRedirectUri" scope="col">{{ t('oauth2', 'Redirection URI') }}</th> {{ t('oauth2', 'Name') }}
<th id="headerClientIdentifier" scope="col">{{ t('oauth2', 'Client Identifier') }}</th> </th>
<th id="headerSecret" scope="col">{{ t('oauth2', 'Secret') }}</th> <th id="headerRedirectUri" scope="col">
<th id="headerRemove">&nbsp;</th> {{ 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">
&nbsp;
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<OAuthItem v-for="client in clients" <OAuthItem v-for="client in clients"
:key="client.id" :key="client.id"
:client="client" :client="client"
@delete="deleteClient" @delete="deleteClient" />
/>
</tbody> </tbody>
</table> </table>
<br/> <br>
<h3>{{ t('oauth2', 'Add client') }}</h3> <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"> <form @submit.prevent="addClient">
<input type="text" id="name" name="name" :placeholder="t('oauth2', 'Name')" v-model="newClient.name"> <input id="name"
<input type="url" id="redirectUri" name="redirectUri" :placeholder="t('oauth2', 'Redirection URI')" v-model="newClient.redirectUri"> 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')"> <input type="submit" class="button" :value="t('oauth2', 'Add')">
</form> </form>
</div> </div>
</template> </template>
<script> <script>
import Axios from 'nextcloud-axios' import axios from 'nextcloud-axios'
import OAuthItem from './components/OAuthItem'; import OAuthItem from './components/OAuthItem'
export default { export default {
name: 'App', name: 'App',
components: {
OAuthItem
},
props: { props: {
clients: { clients: {
type: Array, type: Array,
required: true required: true
} }
}, },
components: {
OAuthItem
},
data: function() { data: function() {
return { return {
newClient: { newClient: {
@ -76,34 +95,34 @@ export default {
errorMsg: '', errorMsg: '',
error: false error: false
} }
}; }
}, },
methods: { methods: {
deleteClient(id) { deleteClient(id) {
Axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', {id: id})) axios.delete(OC.generateUrl('apps/oauth2/clients/{id}', { id: id }))
.then((response) => { .then((response) => {
this.clients = this.clients.filter(client => client.id !== id); this.clients = this.clients.filter(client => client.id !== id)
}); })
}, },
addClient() { addClient() {
this.newClient.error = false; this.newClient.error = false
Axios.post( axios.post(
OC.generateUrl('apps/oauth2/clients'), OC.generateUrl('apps/oauth2/clients'),
{ {
name: this.newClient.name, name: this.newClient.name,
redirectUri: this.newClient.redirectUri redirectUri: this.newClient.redirectUri
} }
).then(response => { ).then(response => {
this.clients.push(response.data); this.clients.push(response.data)
this.newClient.name = ''; this.newClient.name = ''
this.newClient.redirectUri = ''; this.newClient.redirectUri = ''
}).catch(reason => { }).catch(reason => {
this.newClient.error = true; this.newClient.error = true
this.newClient.errorMsg = reason.response.data.message; this.newClient.errorMsg = reason.response.data.message
}); })
} }
}, }
} }
</script> </script>

View File

@ -21,11 +21,13 @@
--> -->
<template> <template>
<tr> <tr>
<td>{{name}}</td> <td>{{ name }}</td>
<td>{{redirectUri}}</td> <td>{{ redirectUri }}</td>
<td><code>{{clientId}}</code></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><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)"></a></span></td> <td class="action-column">
<span><a class="icon-delete has-tooltip" :title="t('oauth2', 'Delete')" @click="$emit('delete', id)" /></span>
</td>
</tr> </tr>
</template> </template>
@ -39,27 +41,27 @@ export default {
} }
}, },
data: function() { data: function() {
return { return {
id: this.client.id, id: this.client.id,
name: this.client.name, name: this.client.name,
redirectUri: this.client.redirectUri, redirectUri: this.client.redirectUri,
clientId: this.client.clientId, clientId: this.client.clientId,
clientSecret: this.client.clientSecret, clientSecret: this.client.clientSecret,
renderSecret: false, renderSecret: false
}; }
}, },
computed: { computed: {
renderedSecret: function() { renderedSecret: function() {
if (this.renderSecret) { if (this.renderSecret) {
return this.clientSecret; return this.clientSecret
} else { } else {
return '****'; return '****'
} }
} }
}, },
methods: { methods: {
toggleSecret() { toggleSecret() {
this.renderSecret = !this.renderSecret; this.renderSecret = !this.renderSecret
} }
} }
} }

View File

@ -20,18 +20,19 @@
* *
*/ */
import Vue from 'vue'; import Vue from 'vue'
import App from './App.vue'; import App from './App.vue'
import { loadState } from 'nextcloud-initial-state' import { loadState } from 'nextcloud-initial-state'
Vue.prototype.t = t; Vue.prototype.t = t
Vue.prototype.OC = OC; Vue.prototype.OC = OC
const clients = loadState('oauth2', 'clients'); const clients = loadState('oauth2', 'clients')
const View = Vue.extend(App) const View = Vue.extend(App)
new View({ const oauth = new View({
propsData: { propsData: {
clients clients
} }
}).$mount('#oauth2'); })
oauth.$mount('#oauth2')

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,7 @@
--> -->
<template> <template>
<router-view></router-view> <router-view />
</template> </template>
<script> <script>
@ -29,9 +29,9 @@ export default {
name: 'App', name: 'App',
beforeMount: function() { beforeMount: function() {
// importing server data into the store // importing server data into the store
const serverDataElmt = document.getElementById('serverData'); const serverDataElmt = document.getElementById('serverData')
if (serverDataElmt !== null) { 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))
} }
} }
} }

View File

@ -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.') }} {{ 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>
<p v-if="loading"> <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> <span>{{ t('settings', 'Enforce two-factor authentication') }}</span>
</p> </p>
<p v-else> <p v-else>
<input type="checkbox" <input id="two-factor-enforced"
id="two-factor-enforced" v-model="enforced"
class="checkbox" type="checkbox"
v-model="enforced"> class="checkbox">
<label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label> <label for="two-factor-enforced">{{ t('settings', 'Enforce two-factor authentication') }}</label>
</p> </p>
<template v-if="enforced"> <template v-if="enforced">
@ -22,32 +22,30 @@
</p> </p>
<p> <p>
<Multiselect v-model="enforcedGroups" <Multiselect v-model="enforcedGroups"
:options="groups" :options="groups"
:placeholder="t('settings', 'Enforced groups')" :placeholder="t('settings', 'Enforced groups')"
:disabled="loading" :disabled="loading"
:multiple="true" :multiple="true"
:searchable="true" :searchable="true"
@search-change="searchGroup" :loading="loadingGroups"
:loading="loadingGroups" :show-no-options="false"
:show-no-options="false" :close-on-select="false"
:close-on-select="false"> @search-change="searchGroup" />
</Multiselect>
</p> </p>
<p> <p>
{{ t('settings', 'Two-factor authentication is not enforced for members of the following groups.') }} {{ t('settings', 'Two-factor authentication is not enforced for members of the following groups.') }}
</p> </p>
<p> <p>
<Multiselect v-model="excludedGroups" <Multiselect v-model="excludedGroups"
:options="groups" :options="groups"
:placeholder="t('settings', 'Excluded groups')" :placeholder="t('settings', 'Excluded groups')"
:disabled="loading" :disabled="loading"
:multiple="true" :multiple="true"
:searchable="true" :searchable="true"
@search-change="searchGroup" :loading="loadingGroups"
:loading="loadingGroups" :show-no-options="false"
:show-no-options="false" :close-on-select="false"
:close-on-select="false"> @search-change="searchGroup" />
</Multiselect>
</p> </p>
<p> <p>
<em> <em>
@ -57,10 +55,10 @@
</p> </p>
</template> </template>
<p> <p>
<button class="button primary" <button v-if="dirty"
v-if="dirty" class="button primary"
v-on:click="saveChanges" :disabled="loading"
:disabled="loading"> @click="saveChanges">
{{ t('settings', 'Save changes') }} {{ t('settings', 'Save changes') }}
</button> </button>
</p> </p>
@ -68,94 +66,93 @@
</template> </template>
<script> <script>
import Axios from 'nextcloud-axios' import axios from 'nextcloud-axios'
import { mapState } from 'vuex' import { Multiselect } from 'nextcloud-vue'
import {Multiselect} from 'nextcloud-vue' import _ from 'lodash'
import _ from 'lodash'
export default { export default {
name: "AdminTwoFactor", name: 'AdminTwoFactor',
components: { components: {
Multiselect Multiselect
}, },
data () { data() {
return { return {
loading: false, loading: false,
dirty: false, dirty: false,
groups: [], groups: [],
loadingGroups: false, loadingGroups: false
}
},
computed: {
enforced: {
get: function() {
return this.$store.state.enforced
},
set: function(val) {
this.dirty = true
this.$store.commit('setEnforced', val)
} }
}, },
computed: { enforcedGroups: {
enforced: { get: function() {
get: function () { return this.$store.state.enforcedGroups
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)
}
}, },
set: function(val) {
this.dirty = true
this.$store.commit('setEnforcedGroups', val)
}
}, },
mounted () { excludedGroups: {
// Groups are loaded dynamically, but the assigned ones *should* get: function() {
// be valid groups, so let's add them as initial state return this.$store.state.excludedGroups
this.groups = _.sortedUniq(_.uniq(this.enforcedGroups.concat(this.excludedGroups))) },
set: function(val) {
// Populate the groups with a first set so the dropdown is not empty this.dirty = true
// when opening the page the first time this.$store.commit('setExcludedGroups', val)
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)
} }
} }
},
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> </script>
<style> <style>

View 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>

View 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">
&nbsp;
</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>

View 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>

View 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>

View 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>

View File

@ -23,31 +23,29 @@
<tr :data-id="token.id" <tr :data-id="token.id"
:class="wiping"> :class="wiping">
<td class="client"> <td class="client">
<div :class="iconName.icon"></div> <div :class="iconName.icon" />
</td> </td>
<td class="token-name"> <td class="token-name">
<input v-if="token.canRename && renaming" <input v-if="token.canRename && renaming"
type="text" ref="input"
ref="input" v-model="newName"
v-model="newName" type="text"
@keyup.enter="rename" @keyup.enter="rename"
@blur="cancelRename" @blur="cancelRename"
@keyup.esc="cancelRename"> @keyup.esc="cancelRename">
<span v-else>{{iconName.name}}</span> <span v-else>{{ iconName.name }}</span>
<span v-if="wiping" <span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
</td> </td>
<td> <td>
<span class="last-activity" v-tooltip="lastActivity">{{lastActivityRelative}}</span> <span v-tooltip="lastActivity" class="last-activity">{{ lastActivityRelative }}</span>
</td> </td>
<td class="more"> <td class="more">
<Actions v-if="!token.current" <Actions v-if="!token.current"
:actions="actions"
:open.sync="actionOpen"
v-tooltip.auto="{ v-tooltip.auto="{
content: t('settings', 'Device settings'), content: t('settings', 'Device settings'),
container: 'body' container: 'body'
}"> }"
:open.sync="actionOpen">
<ActionCheckbox v-if="token.type === 1" <ActionCheckbox v-if="token.type === 1"
:checked="token.scope.filesystem" :checked="token.scope.filesystem"
@change.stop.prevent="$emit('toggleScope', token, 'filesystem', !token.scope.filesystem)"> @change.stop.prevent="$emit('toggleScope', token, 'filesystem', !token.scope.filesystem)">
@ -91,7 +89,7 @@ import {
Actions, Actions,
ActionButton, ActionButton,
ActionCheckbox ActionCheckbox
} from 'nextcloud-vue'; } from 'nextcloud-vue'
const userAgentMap = { const userAgentMap = {
ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/, 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 // Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/, androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */, iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
ipad: /\(iPad\; *CPU +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.*$/, iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)-iOS.*$/,
androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/, androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud-android.*$/,
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud\-Talk.*$/, iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud\-Talk.*$/, androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
// DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1 // DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
davDroid: /DAV(droid|x5)\/([0-9.]+)/, 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) // 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+)/, 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 // 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+)/ sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
}; }
const nameMap = { const nameMap = {
ie: t('setting', 'Internet Explorer'), ie: t('setting', 'Internet Explorer'),
edge: t('setting', 'Edge'), edge: t('setting', 'Edge'),
@ -134,7 +132,7 @@ const nameMap = {
davDroid: 'DAVdroid', davDroid: 'DAVdroid',
webPirate: 'WebPirate', webPirate: 'WebPirate',
sailfishBrowser: 'SailfishBrowser' sailfishBrowser: 'SailfishBrowser'
}; }
const iconMap = { const iconMap = {
ie: 'icon-desktop', ie: 'icon-desktop',
edge: 'icon-desktop', edge: 'icon-desktop',
@ -151,10 +149,10 @@ const iconMap = {
davDroid: 'icon-phone', davDroid: 'icon-phone',
webPirate: 'icon-link', webPirate: 'icon-link',
sailfishBrowser: 'icon-link' sailfishBrowser: 'icon-link'
}; }
export default { export default {
name: "AuthToken", name: 'AuthToken',
components: { components: {
Actions, Actions,
ActionButton, ActionButton,
@ -163,91 +161,93 @@ export default {
props: { props: {
token: { token: {
type: Object, type: Object,
required: true, required: true
} }
}, },
computed: { data() {
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 () {
return { return {
showMore: this.token.canScope || this.token.canDelete, showMore: this.token.canScope || this.token.canDelete,
renaming: false, renaming: false,
newName: '', 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: { methods: {
startRename() { startRename() {
// Close action (popover menu) // Close action (popover menu)
this.actionOpen = false; this.actionOpen = false
this.newName = this.token.name; this.newName = this.token.name
this.renaming = true; this.renaming = true
this.$nextTick(() => { this.$nextTick(() => {
this.$refs.input.select(); this.$refs.input.select()
}); })
}, },
cancelRename() { cancelRename() {
this.renaming = false; this.renaming = false
}, },
revoke() { revoke() {
this.actionOpen = false; this.actionOpen = false
this.$emit('delete', this.token) this.$emit('delete', this.token)
}, },
rename() { rename() {
this.renaming = false; this.renaming = false
this.$emit('rename', this.token, this.newName); this.$emit('rename', this.token, this.newName)
}, },
wipe() { wipe() {
this.actionOpen = false; this.actionOpen = false
this.$emit('wipe', this.token); this.$emit('wipe', this.token)
} }
} }
} }

View File

@ -22,67 +22,67 @@
<template> <template>
<table id="app-tokens-table"> <table id="app-tokens-table">
<thead v-if="tokens.length"> <thead v-if="tokens.length">
<tr> <tr>
<th></th> <th />
<th>{{ t('settings', 'Device') }}</th> <th>{{ t('settings', 'Device') }}</th>
<th>{{ t('settings', 'Last activity') }}</th> <th>{{ t('settings', 'Last activity') }}</th>
<th></th> <th />
</tr> </tr>
</thead> </thead>
<tbody class="token-list"> <tbody class="token-list">
<AuthToken v-for="token in sortedTokens" <AuthToken v-for="token in sortedTokens"
:key="token.id" :key="token.id"
:token="token" :token="token"
@toggleScope="toggleScope" @toggleScope="toggleScope"
@rename="rename" @rename="rename"
@delete="onDelete" @delete="onDelete"
@wipe="onWipe" /> @wipe="onWipe" />
</tbody> </tbody>
</table> </table>
</template> </template>
<script> <script>
import AuthToken from './AuthToken'; import AuthToken from './AuthToken'
export default { export default {
name: 'AuthTokenList', name: 'AuthTokenList',
components: { components: {
AuthToken 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: { rename(token, newName) {
tokens: { // Just pass it on
type: Array, this.$emit('rename', token, newName)
required: true,
}
}, },
computed: { onDelete(token) {
sortedTokens () { // Just pass it on
return this.tokens.sort((t1, t2) => { this.$emit('delete', token)
var ts1 = parseInt(t1.lastActivity, 10);
var ts2 = parseInt(t2.lastActivity, 10);
return ts2 - ts1;
})
}
}, },
methods: { onWipe(token) {
toggleScope (token, scope, value) { // Just pass it on
// Just pass it on this.$emit('wipe', token)
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);
}
} }
} }
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -22,155 +22,159 @@
<template> <template>
<div id="security" class="section"> <div id="security" class="section">
<h2>{{ t('settings', 'Devices & sessions') }}</h2> <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" <AuthTokenList :tokens="tokens"
@toggleScope="toggleTokenScope" @toggleScope="toggleTokenScope"
@rename="rename" @rename="rename"
@delete="deleteToken" @delete="deleteToken"
@wipe="wipeToken" /> @wipe="wipeToken" />
<AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" /> <AuthTokenSetupDialogue v-if="canCreateToken" :add="addNewToken" />
</div> </div>
</template> </template>
<script> <script>
import Axios from 'nextcloud-axios'; import axios from 'nextcloud-axios'
import confirmPassword from 'nextcloud-password-confirmation'; import confirmPassword from 'nextcloud-password-confirmation'
import AuthTokenList from './AuthTokenList'; import AuthTokenList from './AuthTokenList'
import AuthTokenSetupDialogue from './AuthTokenSetupDialogue'; import AuthTokenSetupDialogue from './AuthTokenSetupDialogue'
const confirm = () => { const confirm = () => {
return new Promise(res => { return new Promise(resolve => {
OC.dialogs.confirm( OC.dialogs.confirm(
t('core', 'Do you really want to wipe your data from this device?'), t('core', 'Do you really want to wipe your data from this device?'),
t('core', 'Confirm wipe'), t('core', 'Confirm wipe'),
res, resolve,
true true
) )
}) })
} }
/** /**
* Tap into a promise without losing the value * Tap into a promise without losing the value
*/ * @param {Function} cb the callback
const tap = cb => val => { * @returns {any} val the value
cb(val); */
return val; const tap = cb => val => {
}; cb(val)
return val
}
export default { export default {
name: "AuthTokenSection", name: 'AuthTokenSection',
props: { components: {
tokens: { AuthTokenSetupDialogue,
type: Array, AuthTokenList
required: true, },
}, props: {
canCreateToken: { tokens: {
type: Boolean, type: Array,
required: true 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: { toggleTokenScope(token, scope, value) {
AuthTokenSetupDialogue, console.debug('updating app token scope', token.id, scope, value)
AuthTokenList
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() { rename(token, newName) {
return { console.debug('renaming app token', token.id, token.name, newName)
baseUrl: OC.generateUrl('/settings/personal/authtokens'),
} 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: { updateToken(token) {
addNewToken (name) { return axios.put(this.baseUrl + '/' + token.id, token)
console.debug('creating a new app token', name); .then(resp => resp.data)
},
deleteToken(token) {
console.debug('deleting app token', token)
const data = { this.tokens = this.tokens.filter(t => t !== token)
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);
const oldVal = token.scope[scope]; return axios.delete(this.baseUrl + '/' + token.id)
token.scope[scope] = value; .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) // Restore
.then(tap(() => console.debug('app token scope updated'))) this.tokens.push(token)
.catch(err => { })
console.error.bind('could not update app token scope', err); },
OC.Notification.showTemporary(t('core', 'Error while updating device token scope')); async wipeToken(token) {
console.debug('wiping app token', token)
// Restore try {
token.scope[scope] = oldVal; await confirmPassword()
throw err; if (!(await confirm())) {
}) console.debug('wipe aborted by user')
}, return
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'));
} }
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> </script>
<style scoped> <style scoped>

View File

@ -22,13 +22,14 @@
<template> <template>
<div v-if="!adding"> <div v-if="!adding">
<input v-model="deviceName" <input v-model="deviceName"
type="text" type="text"
@keydown.enter="submit" :disabled="loading"
:disabled="loading" :placeholder="t('settings', 'App name')"
:placeholder="t('settings', 'App name')"> @keydown.enter="submit">
<button class="button" <button class="button"
:disabled="loading" :disabled="loading"
@click="submit">{{ t('settings', 'Create new app password') }} @click="submit">
{{ t('settings', 'Create new app password') }}
</button> </button>
</div> </div>
<div v-else> <div v-else>
@ -37,142 +38,142 @@
<div class="app-password-row"> <div class="app-password-row">
<span class="app-password-label">{{ t('settings', 'Username') }}</span> <span class="app-password-label">{{ t('settings', 'Username') }}</span>
<input :value="loginName" <input :value="loginName"
type="text" type="text"
class="monospaced" class="monospaced"
readonly="readonly" readonly="readonly"
@focus="selectInput"/> @focus="selectInput">
</div> </div>
<div class="app-password-row"> <div class="app-password-row">
<span class="app-password-label">{{ t('settings', 'Password') }}</span> <span class="app-password-label">{{ t('settings', 'Password') }}</span>
<input :value="appPassword" <input ref="appPassword"
type="text" :value="appPassword"
class="monospaced" type="text"
ref="appPassword" class="monospaced"
readonly="readonly" readonly="readonly"
@focus="selectInput"/> @focus="selectInput">
<a class="icon icon-clippy" <a ref="clipboardButton"
ref="clipboardButton" v-tooltip="copyTooltipOptions"
v-tooltip="copyTooltipOptions" v-clipboard:copy="appPassword"
@mouseover="hoveringCopyButton = true" v-clipboard:success="onCopyPassword"
@mouseleave="hoveringCopyButton = false" v-clipboard:error="onCopyPasswordFailed"
v-clipboard:copy="appPassword" class="icon icon-clippy"
v-clipboard:success="onCopyPassword" @mouseover="hoveringCopyButton = true"
v-clipboard:error="onCopyPasswordFailed"></a> @mouseleave="hoveringCopyButton = false" />
<button class="button" <button class="button"
@click="reset"> @click="reset">
{{ t('settings', 'Done') }} {{ t('settings', 'Done') }}
</button> </button>
</div> </div>
<div class="app-password-row"> <div class="app-password-row">
<span class="app-password-label"></span> <span class="app-password-label" />
<a v-if="!showQR" <a v-if="!showQR"
@click="showQR = true"> @click="showQR = true">
{{ t('settings', 'Show QR code for mobile apps') }} {{ t('settings', 'Show QR code for mobile apps') }}
</a> </a>
<QR v-else <QR v-else
:value="qrUrl"></QR> :value="qrUrl" />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import QR from '@chenfengyuan/vue-qrcode'; import QR from '@chenfengyuan/vue-qrcode'
import confirmPassword from 'nextcloud-password-confirmation'; import confirmPassword from 'nextcloud-password-confirmation'
export default { export default {
name: 'AuthTokenSetupDialogue', name: 'AuthTokenSetupDialogue',
components: { components: {
QR, QR
}, },
props: { props: {
add: { add: {
type: Function, type: Function,
required: true, 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) { if (this.passwordCopied) {
return { return {
...base, ...base,
content:t('core', 'Copied!'), content: t('core', 'Copied!'),
show: true, show: true
} }
} else { } else {
return { return {
...base, ...base,
content: t('core', 'Copy'), content: t('core', 'Copy'),
show: this.hoveringCopyButton, show: this.hoveringCopyButton
}
} }
} }
}
},
methods: {
selectInput(e) {
e.currentTarget.select()
}, },
methods: { submit: function() {
selectInput (e) { confirmPassword()
e.currentTarget.select(); .then(() => {
}, this.loading = true
submit: function () { return this.add(this.deviceName)
confirmPassword() })
.then(() => { .then(token => {
this.loading = true; this.adding = true
return this.add(this.deviceName) 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; .catch(err => {
this.loginName = token.loginName; console.error('could not create a new app password', err)
this.appPassword = token.token; OC.Notification.showTemporary(t('core', 'Error while creating device token'))
const server = window.location.protocol + '//' + window.location.host + OC.getRootPath(); this.reset()
this.qrUrl = `nc://login/user:${token.loginName}&password:${token.token}&server:${server}`; })
},
this.$nextTick(() => { onCopyPassword() {
this.$refs.appPassword.select(); this.passwordCopied = true
}); this.$refs.clipboardButton.blur()
}) setTimeout(() => { this.passwordCopied = false }, 3000)
.catch(err => { },
console.error('could not create a new app password', err); onCopyPasswordFailed() {
OC.Notification.showTemporary(t('core', 'Error while creating device token')); OC.Notification.showTemporary(t('core', 'Could not copy app password. Please copy it manually.'))
},
this.reset(); reset() {
}); this.adding = false
}, this.loading = false
onCopyPassword() { this.showQR = false
this.passwordCopied = true; this.qrUrl = ''
this.$refs.clipboardButton.blur(); this.deviceName = ''
setTimeout(() => this.passwordCopied = false, 3000); this.appPassword = ''
}, this.loginName = ''
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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View 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>

View 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>

View 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>

View File

@ -25,160 +25,177 @@
<div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}"> <div id="apps-list" class="apps-list" :class="{installed: (useBundleView || useListView), store: useAppStoreView}">
<template v-if="useListView"> <template v-if="useListView">
<transition-group name="app-list" tag="div" class="apps-list-container"> <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> </transition-group>
</template> </template>
<template v-for="bundle in bundles" v-if="useBundleView && bundleApps(bundle.id).length > 0"> <transition-group v-if="useBundleView"
<transition-group name="app-list" tag="div" class="apps-list-container"> name="app-list"
tag="div"
<div class="apps-header" :key="bundle.id"> class="apps-list-container">
<div class="app-image"></div> <template v-for="bundle in bundles">
<h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" v-on:click="toggleBundle(bundle.id)"></h2> <div :key="bundle.id" class="apps-header">
<div class="app-version"></div> <div class="app-image" />
<div class="app-level"></div> <h2>{{ bundle.name }} <input type="button" :value="bundleToggleText(bundle.id)" @click="toggleBundle(bundle.id)"></h2>
<div class="app-groups"></div> <div class="app-version" />
<div class="actions">&nbsp;</div> <div class="app-level" />
<div class="app-groups" />
<div class="actions">
&nbsp;
</div>
</div> </div>
<app-item v-for="app in bundleApps(bundle.id)" :key="bundle.id + app.id" :app="app" :category="category"/> <AppItem v-for="app in bundleApps(bundle.id)"
</transition-group> :key="bundle.id + app.id"
</template> :app="app"
:category="category" />
</template>
</transition-group>
<template v-if="useAppStoreView"> <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> </template>
</div> </div>
<div id="apps-list-search" class="apps-list installed"> <div id="apps-list-search" class="apps-list installed">
<div class="apps-list-container"> <div class="apps-list-container">
<template v-if="search !== '' && searchApps.length > 0"> <template v-if="search !== '' && searchApps.length > 0">
<div class="section"> <div class="section">
<div></div> <div />
<td colspan="5"> <td colspan="5">
<h2>{{ t('settings', 'Results from other categories') }}</h2> <h2>{{ t('settings', 'Results from other categories') }}</h2>
</td> </td>
</div> </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> </template>
</div> </div>
</div> </div>
<div id="apps-list-empty" class="emptycontent emptycontent-search" v-if="search !== '' && !loading && searchApps.length === 0 && apps.length === 0"> <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"></div> <div id="app-list-empty-icon" class="icon-settings-dark" />
<h2>{{ t('settings', 'No apps found for your version')}}</h2> <h2>{{ t('settings', 'No apps found for your version') }}</h2>
</div> </div>
<div id="searchresults"></div> <div id="searchresults" />
</div> </div>
</template> </template>
<script> <script>
import appItem from './appList/appItem'; import AppItem from './AppList/AppItem'
import prefix from './prefixMixin'; import PrefixMixin from './PrefixMixin'
export default { export default {
name: 'appList', name: 'AppList',
mixins: [prefix],
props: ['category', 'app', 'search'],
components: { components: {
appItem AppItem
}, },
mixins: [PrefixMixin],
props: ['category', 'app', 'search'],
computed: { computed: {
loading() { loading() {
return this.$store.getters.loading('list'); return this.$store.getters.loading('list')
}, },
apps() { apps() {
let apps = this.$store.getters.getAllApps let apps = this.$store.getters.getAllApps
.filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) .filter(app => app.name.toLowerCase().search(this.search.toLowerCase()) !== -1)
.sort(function (a, b) { .sort(function(a, b) {
const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name; const sortStringA = '' + (a.active ? 0 : 1) + (a.update ? 0 : 1) + a.name
const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name; const sortStringB = '' + (b.active ? 0 : 1) + (b.update ? 0 : 1) + b.name
return OC.Util.naturalSortCompare(sortStringA, sortStringB); return OC.Util.naturalSortCompare(sortStringA, sortStringB)
}); })
if (this.category === 'installed') { if (this.category === 'installed') {
return apps.filter(app => app.installed); return apps.filter(app => app.installed)
} }
if (this.category === 'enabled') { if (this.category === 'enabled') {
return apps.filter(app => app.active && app.installed); return apps.filter(app => app.active && app.installed)
} }
if (this.category === 'disabled') { 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') { if (this.category === 'app-bundles') {
return apps.filter(app => app.bundles); return apps.filter(app => app.bundles)
} }
if (this.category === 'updates') { if (this.category === 'updates') {
return apps.filter(app => app.update); return apps.filter(app => app.update)
} }
// filter app store categories // filter app store categories
return apps.filter(app => { return apps.filter(app => {
return app.appstore && app.category !== undefined && return app.appstore && app.category !== undefined
(app.category === this.category || app.category.indexOf(this.category) > -1); && (app.category === this.category || app.category.indexOf(this.category) > -1)
}); })
}, },
bundles() { bundles() {
return this.$store.getters.getServerData.bundles; return this.$store.getters.getServerData.bundles.filter(bundle => this.bundleApps(bundle.id).length > 0)
}, },
bundleApps() { bundleApps() {
return function(bundle) { return function(bundle) {
return this.$store.getters.getAllApps return this.$store.getters.getAllApps
.filter(app => app.bundleId === bundle); .filter(app => app.bundleId === bundle)
} }
}, },
searchApps() { searchApps() {
if (this.search === '') { if (this.search === '') {
return []; return []
} }
return this.$store.getters.getAllApps return this.$store.getters.getAllApps
.filter(app => { .filter(app => {
if (app.name.toLowerCase().search(this.search.toLowerCase()) !== -1) { 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() { useAppStoreView() {
return !this.useListView && !this.useBundleView; return !this.useListView && !this.useBundleView
}, },
useListView() { 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() { useBundleView() {
return (this.category === 'app-bundles'); return (this.category === 'app-bundles')
}, },
allBundlesEnabled() { allBundlesEnabled() {
let self = this; let self = this
return function(id) { return function(id) {
return self.bundleApps(id).filter(app => !app.active).length === 0; return self.bundleApps(id).filter(app => !app.active).length === 0
} }
}, },
bundleToggleText() { bundleToggleText() {
let self = this; let self = this
return function(id) { return function(id) {
if (self.allBundlesEnabled(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: { methods: {
toggleBundle(id) { toggleBundle(id) {
if (this.allBundlesEnabled(id)) { if (this.allBundlesEnabled(id)) {
return this.disableBundle(id); return this.disableBundle(id)
} }
return this.enableBundle(id); return this.enableBundle(id)
}, },
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: [] }) 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) { 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: [] }) this.$store.dispatch('disableApp', { appId: apps, groups: [] })
.catch((error) => { OC.Notification.show(error)}); .catch((error) => { OC.Notification.show(error) })
} }
}, }
} }
</script> </script>

View File

@ -21,18 +21,18 @@
--> -->
<template> <template>
<img :src="scoreImage" class="app-score-image" /> <img :src="scoreImage" class="app-score-image">
</template> </template>
<script> <script>
export default { export default {
name: 'appScore', name: 'AppScore',
props: ['score'], props: ['score'],
computed: { computed: {
scoreImage() { scoreImage() {
let score = Math.round( this.score * 10 ); let score = Math.round(this.score * 10)
let imageName = 'rating/s' + score + '.svg'; let imageName = 'rating/s' + score + '.svg'
return OC.imagePath('core', imageName); return OC.imagePath('core', imageName)
}
} }
}; }
</script> }
</script>

View File

@ -21,12 +21,12 @@
--> -->
<script> <script>
export default { export default {
name: 'prefixMixin', name: 'PrefixMixin',
methods: { methods: {
prefix (prefix, content) { prefix(prefix, content) {
return prefix + '_' + content; return prefix + '_' + content
},
} }
} }
</script> }
</script>

View File

@ -21,20 +21,20 @@
--> -->
<script> <script>
export default { export default {
name: 'svgFilterMixin', name: 'SvgFilterMixin',
mounted() { data() {
this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100 )) + new Date().getSeconds() + new Date().getMilliseconds(); return {
}, filterId: ''
computed: { }
filterUrl () { },
return `url(#${this.filterId})`; computed: {
}, filterUrl() {
}, return `url(#${this.filterId})`
data() { }
return { },
filterId: '', mounted() {
}; this.filterId = 'invertIconApps' + Math.floor((Math.random() * 100)) + new Date().getSeconds() + new Date().getMilliseconds()
},
} }
</script> }
</script>

View 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>

View File

@ -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>

View File

@ -3,13 +3,14 @@ import Vue from 'vue'
import AdminTwoFactor from './components/AdminTwoFactor.vue' import AdminTwoFactor from './components/AdminTwoFactor.vue'
import store from './store/admin-security' import store from './store/admin-security'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken) __webpack_nonce__ = btoa(OC.requestToken)
Vue.prototype.t = t; Vue.prototype.t = t
// Not used here but required for legacy templates // Not used here but required for legacy templates
window.OC = window.OC || {}; window.OC = window.OC || {}
window.OC.Settings = window.OC.Settings || {}; window.OC.Settings = window.OC.Settings || {}
store.replaceState( store.replaceState(
OCP.InitialState.loadState('settings', 'mandatory2FAState') OCP.InitialState.loadState('settings', 'mandatory2FAState')

View File

@ -20,17 +20,17 @@
* *
*/ */
import Vue from 'vue'; import Vue from 'vue'
import VTooltip from 'v-tooltip'; import VTooltip from 'v-tooltip'
import { sync } from 'vuex-router-sync'; import { sync } from 'vuex-router-sync'
import App from './App.vue'; import App from './App.vue'
import router from './router'; import router from './router'
import store from './store'; 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 // CSP config for webpack dynamic chunk loading
// eslint-disable-next-line // eslint-disable-next-line
@ -43,15 +43,16 @@ __webpack_nonce__ = btoa(OC.requestToken)
__webpack_public_path__ = OC.linkTo('settings', 'js/') __webpack_public_path__ = OC.linkTo('settings', 'js/')
// bind to window // bind to window
Vue.prototype.t = t; Vue.prototype.t = t
Vue.prototype.OC = OC; Vue.prototype.OC = OC
Vue.prototype.OCA = OCA; Vue.prototype.OCA = OCA
Vue.prototype.oc_userconfig = oc_userconfig; // eslint-disable-next-line camelcase
Vue.prototype.oc_userconfig = oc_userconfig
const app = new Vue({ const app = new Vue({
router, router,
store, store,
render: h => h(App) render: h => h(App)
}).$mount('#content'); }).$mount('#content')
export { app, router, store }; export { app, router, store }

View File

@ -19,22 +19,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
import Vue from 'vue'; import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'; import VueClipboard from 'vue-clipboard2'
import VTooltip from 'v-tooltip'; 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(VueClipboard)
Vue.use(VTooltip, { defaultHtml: false }); Vue.use(VTooltip, { defaultHtml: false })
Vue.prototype.t = t; Vue.prototype.t = t
const View = Vue.extend(AuthTokenSection); const View = Vue.extend(AuthTokenSection)
new View({ new View({
propsData: { propsData: {
tokens: OCP.InitialState.loadState('settings', 'app_tokens'), 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')

View File

@ -21,14 +21,14 @@
* *
*/ */
import Vue from 'vue'; import Vue from 'vue'
import Router from 'vue-router'; import Router from 'vue-router'
// Dynamic loading // Dynamic loading
const Users = () => import('./views/Users'); const Users = () => import('./views/Users')
const Apps = () => import('./views/Apps'); const Apps = () => import('./views/Apps')
Vue.use(Router); Vue.use(Router)
/* /*
* This is the list of routes where the vuejs app will * This is the list of routes where the vuejs app will
@ -80,4 +80,4 @@ export default new Router({
] ]
} }
] ]
}); })

View File

@ -1,4 +1,4 @@
/* /**
* @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl> * @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl>
* *
* @author 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) Vue.use(Vuex)
export const mutations = { const state = {
enforced: false,
enforcedGroups: [],
excludedGroups: []
}
const mutations = {
setEnforced(state, enabled) { setEnforced(state, enabled) {
Vue.set(state, 'enforced', 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({ export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production', strict: process.env.NODE_ENV !== 'production',
state: { state,
enforced: false, mutations
enforcedGroups: [],
excludedGroups: [],
},
mutations,
actions
}) })

View File

@ -21,11 +21,11 @@
*/ */
import axios from 'nextcloud-axios' import axios from 'nextcloud-axios'
import confirmPassword from 'nextcloud-password-confirmation' import confirmPassword from 'nextcloud-password-confirmation'
const sanitize = function(url) { const sanitize = function(url) {
return url.replace(/\/$/, ''); // Remove last url slash return url.replace(/\/$/, '') // Remove last url slash
}; }
export default { export default {
@ -47,35 +47,35 @@ export default {
* *
* Since Promise.then().catch().then() will always execute the last then * Since Promise.then().catch().then() will always execute the last then
* this.$store.dispatch('action').then will always be executed * this.$store.dispatch('action').then will always be executed
* *
* If you want requireAdmin failure to also catch the API request failure * 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() * you will need to throw a new error in the api.get.catch()
* *
* e.g * e.g
* api.requireAdmin().then((response) => { * api.requireAdmin().then((response) => {
* api.get('url') * api.get('url')
* .then((response) => {API success}) * .then((response) => {API success})
* .catch((error) => {throw error;}); * .catch((error) => {throw error;});
* }).catch((error) => {requireAdmin OR API failure}); * }).catch((error) => {requireAdmin OR API failure});
* *
* @returns {Promise} * @returns {Promise}
*/ */
requireAdmin() { requireAdmin() {
return confirmPassword(); return confirmPassword()
}, },
get(url) { get(url) {
return axios.get(sanitize(url)); return axios.get(sanitize(url))
}, },
post(url, data) { post(url, data) {
return axios.post(sanitize(url), data); return axios.post(sanitize(url), data)
}, },
patch(url, data) { patch(url, data) {
return axios.patch(sanitize(url), data); return axios.patch(sanitize(url), data)
}, },
put(url, data) { put(url, data) {
return axios.put(sanitize(url), data); return axios.put(sanitize(url), data)
}, },
delete(url, data) { delete(url, data) {
return axios.delete(sanitize(url), { data: data }); return axios.delete(sanitize(url), { data: data })
} }
}; }

View File

@ -20,158 +20,158 @@
* *
*/ */
import api from './api'; import api from './api'
import Vue from 'vue'; import Vue from 'vue'
const state = { const state = {
apps: [], apps: [],
categories: [], categories: [],
updateCount: 0, updateCount: 0,
loading: {}, loading: {},
loadingList: false, loadingList: false
}; }
const mutations = { const mutations = {
APPS_API_FAILURE(state, error) { 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}); 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); console.error(state, error)
}, },
initCategories(state, {categories, updateCount}) { initCategories(state, { categories, updateCount }) {
state.categories = categories; state.categories = categories
state.updateCount = updateCount; state.updateCount = updateCount
}, },
setUpdateCount(state, updateCount) { setUpdateCount(state, updateCount) {
state.updateCount = updateCount; state.updateCount = updateCount
}, },
addCategory(state, category) { addCategory(state, category) {
state.categories.push(category); state.categories.push(category)
}, },
appendCategories(state, categoriesArray) { appendCategories(state, categoriesArray) {
// convert obj to array // convert obj to array
state.categories = categoriesArray; state.categories = categoriesArray
}, },
setAllApps(state, apps) { setAllApps(state, apps) {
state.apps = apps; state.apps = apps
}, },
setError(state, {appId, error}) { setError(state, { appId, error }) {
if (!Array.isArray(appId)) { if (!Array.isArray(appId)) {
appId = [appId]; appId = [appId]
} }
appId.forEach((_id) => { appId.forEach((_id) => {
let app = state.apps.find(app => app.id === _id); let app = state.apps.find(app => app.id === _id)
app.error = error; app.error = error
}); })
}, },
clearError(state, {appId, error}) { clearError(state, { appId, error }) {
let app = state.apps.find(app => app.id === appId); let app = state.apps.find(app => app.id === appId)
app.error = null; app.error = null
}, },
enableApp(state, {appId, groups}) { enableApp(state, { appId, groups }) {
let app = state.apps.find(app => app.id === appId); let app = state.apps.find(app => app.id === appId)
app.active = true; app.active = true
app.groups = groups; app.groups = groups
}, },
disableApp(state, appId) { disableApp(state, appId) {
let app = state.apps.find(app => app.id === appId); let app = state.apps.find(app => app.id === appId)
app.active = false; app.active = false
app.groups = []; app.groups = []
if (app.removable) { if (app.removable) {
app.canUnInstall = true; app.canUnInstall = true
} }
}, },
uninstallApp(state, appId) { uninstallApp(state, appId) {
state.apps.find(app => app.id === appId).active = false; state.apps.find(app => app.id === appId).active = false
state.apps.find(app => app.id === appId).groups = []; state.apps.find(app => app.id === appId).groups = []
state.apps.find(app => app.id === appId).needsDownload = true; 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).installed = false
state.apps.find(app => app.id === appId).canUnInstall = 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).canInstall = true
}, },
updateApp(state, appId) { updateApp(state, appId) {
let app = state.apps.find(app => app.id === appId); let app = state.apps.find(app => app.id === appId)
let version = app.update; let version = app.update
app.update = null; app.update = null
app.version = version; app.version = version
state.updateCount--; state.updateCount--
}, },
resetApps(state) { resetApps(state) {
state.apps = []; state.apps = []
}, },
reset(state) { reset(state) {
state.apps = []; state.apps = []
state.categories = []; state.categories = []
state.updateCount = 0; state.updateCount = 0
}, },
startLoading(state, id) { startLoading(state, id) {
if (Array.isArray(id)) { if (Array.isArray(id)) {
id.forEach((_id) => { id.forEach((_id) => {
Vue.set(state.loading, _id, true); Vue.set(state.loading, _id, true)
}) })
} else { } else {
Vue.set(state.loading, id, true); Vue.set(state.loading, id, true)
} }
}, },
stopLoading(state, id) { stopLoading(state, id) {
if (Array.isArray(id)) { if (Array.isArray(id)) {
id.forEach((_id) => { id.forEach((_id) => {
Vue.set(state.loading, _id, false); Vue.set(state.loading, _id, false)
}) })
} else { } else {
Vue.set(state.loading, id, false); Vue.set(state.loading, id, false)
} }
}, }
}; }
const getters = { const getters = {
loading(state) { loading(state) {
return function(id) { return function(id) {
return state.loading[id]; return state.loading[id]
} }
}, },
getCategories(state) { getCategories(state) {
return state.categories; return state.categories
}, },
getAllApps(state) { getAllApps(state) {
return state.apps; return state.apps
}, },
getUpdateCount(state) { getUpdateCount(state) {
return state.updateCount; return state.updateCount
} }
}; }
const actions = { const actions = {
enableApp(context, { appId, groups }) { enableApp(context, { appId, groups }) {
let apps; let apps
if (Array.isArray(appId)) { if (Array.isArray(appId)) {
apps = appId; apps = appId
} else { } else {
apps = [appId]; apps = [appId]
} }
return api.requireAdmin().then((response) => { return api.requireAdmin().then((response) => {
context.commit('startLoading', apps); context.commit('startLoading', apps)
context.commit('startLoading', 'install'); context.commit('startLoading', 'install')
return api.post(OC.generateUrl(`settings/apps/enable`), {appIds: apps, groups: groups}) return api.post(OC.generateUrl(`settings/apps/enable`), { appIds: apps, groups: groups })
.then((response) => { .then((response) => {
context.commit('stopLoading', apps); context.commit('stopLoading', apps)
context.commit('stopLoading', 'install'); context.commit('stopLoading', 'install')
apps.forEach(_appId => { apps.forEach(_appId => {
context.commit('enableApp', {appId: _appId, groups: groups}); context.commit('enableApp', { appId: _appId, groups: groups })
}); })
// check for server health // check for server health
return api.get(OC.generateUrl('apps/files')) return api.get(OC.generateUrl('apps/files'))
@ -182,146 +182,146 @@ const actions = {
'settings', 'settings',
'The app has been enabled but needs to be updated. You will be redirected to the update page in 5 seconds.' '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'), t('settings', 'App update'),
function () { function() {
window.location.reload(); window.location.reload()
}, },
true true
); )
setTimeout(function() { setTimeout(function() {
location.reload(); location.reload()
}, 5000); }, 5000)
} }
}) })
.catch((error) => { .catch(() => {
if (!Array.isArray(appId)) { if (!Array.isArray(appId)) {
context.commit('setError', { context.commit('setError', {
appId: apps, appId: apps,
error: t('settings', 'Error: This app can not be enabled because it makes the server unstable') error: t('settings', 'Error: This app can not be enabled because it makes the server unstable')
}); })
} }
}); })
}) })
.catch((error) => { .catch((error) => {
context.commit('stopLoading', apps); context.commit('stopLoading', apps)
context.commit('stopLoading', 'install'); context.commit('stopLoading', 'install')
context.commit('setError', { context.commit('setError', {
appId: apps, appId: apps,
error: error.response.data.data.message 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 }) 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 }) { uninstallApp(context, { appId }) {
return api.requireAdmin().then((response) => { return api.requireAdmin().then((response) => {
context.commit('startLoading', appId); context.commit('startLoading', appId)
return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`)) return api.get(OC.generateUrl(`settings/apps/uninstall/${appId}`))
.then((response) => { .then((response) => {
context.commit('stopLoading', appId); context.commit('stopLoading', appId)
context.commit('uninstallApp', appId); context.commit('uninstallApp', appId)
return true; return true
}) })
.catch((error) => { .catch((error) => {
context.commit('stopLoading', appId); context.commit('stopLoading', appId)
context.commit('APPS_API_FAILURE', { appId, error }) 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 }) { updateApp(context, { appId }) {
return api.requireAdmin().then((response) => { return api.requireAdmin().then((response) => {
context.commit('startLoading', appId); context.commit('startLoading', appId)
context.commit('startLoading', 'install'); context.commit('startLoading', 'install')
return api.get(OC.generateUrl(`settings/apps/update/${appId}`)) return api.get(OC.generateUrl(`settings/apps/update/${appId}`))
.then((response) => { .then((response) => {
context.commit('stopLoading', 'install'); context.commit('stopLoading', 'install')
context.commit('stopLoading', appId); context.commit('stopLoading', appId)
context.commit('updateApp', appId); context.commit('updateApp', appId)
return true; return true
}) })
.catch((error) => { .catch((error) => {
context.commit('stopLoading', appId); context.commit('stopLoading', appId)
context.commit('stopLoading', 'install'); context.commit('stopLoading', 'install')
context.commit('APPS_API_FAILURE', { appId, error }) 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) { getAllApps(context) {
context.commit('startLoading', 'list'); context.commit('startLoading', 'list')
return api.get(OC.generateUrl(`settings/apps/list`)) return api.get(OC.generateUrl(`settings/apps/list`))
.then((response) => { .then((response) => {
context.commit('setAllApps', response.data.apps); context.commit('setAllApps', response.data.apps)
context.commit('stopLoading', 'list'); context.commit('stopLoading', 'list')
return true; return true
}) })
.catch((error) => context.commit('API_FAILURE', error)) .catch((error) => context.commit('API_FAILURE', error))
}, },
getCategories(context) { getCategories(context) {
context.commit('startLoading', 'categories'); context.commit('startLoading', 'categories')
return api.get(OC.generateUrl('settings/apps/categories')) return api.get(OC.generateUrl('settings/apps/categories'))
.then((response) => { .then((response) => {
if (response.data.length > 0) { if (response.data.length > 0) {
context.commit('appendCategories', response.data); context.commit('appendCategories', response.data)
context.commit('stopLoading', 'categories'); context.commit('stopLoading', 'categories')
return true; 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 }

View File

@ -1,4 +1,4 @@
/* /**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
* *
* @author John Molakvoæ <skjnldsv@protonmail.com> * @author John Molakvoæ <skjnldsv@protonmail.com>
@ -21,28 +21,28 @@
* *
*/ */
import Vue from 'vue'; import Vue from 'vue'
import Vuex from 'vuex'; import Vuex from 'vuex'
import users from './users'; import users from './users'
import apps from './apps'; import apps from './apps'
import settings from './settings'; import settings from './settings'
import oc from './oc'; import oc from './oc'
Vue.use(Vuex) Vue.use(Vuex)
const debug = process.env.NODE_ENV !== 'production'; const debug = process.env.NODE_ENV !== 'production'
const mutations = { const mutations = {
API_FAILURE(state, error) { API_FAILURE(state, error) {
try { try {
let message = error.error.response.data.ocs.meta.message; 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}); OC.Notification.showHtml(t('settings', 'An error occured during the request. Unable to proceed.') + '<br>' + message, { timeout: 7 })
} catch(e) { } catch (e) {
OC.Notification.showTemporary(t('settings','An error occured during the request. Unable to proceed.')); 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({ export default new Vuex.Store({
modules: { modules: {
@ -54,4 +54,4 @@ export default new Vuex.Store({
strict: debug, strict: debug,
mutations mutations
}); })

View File

@ -1,4 +1,4 @@
/* /**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
* *
* @author 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 state = {}
const mutations = {}; const mutations = {}
const getters = {}; const getters = {}
const actions = { const actions = {
/** /**
* Set application config in database * Set application config in database
* *
* @param {Object} context * @param {Object} context store context
* @param {Object} options * @param {Object} options destructuring object
* @param {string} options.app Application name * @param {string} options.app Application name
* @param {boolean} options.key Config key * @param {boolean} options.key Config key
* @param {boolean} options.value Value to set * @param {boolean} options.value Value to set
* @returns{Promise} * @returns{Promise}
*/ */
setAppConfig(context, {app, key, value}) { setAppConfig(context, { app, key, value }) {
return api.requireAdmin().then((response) => { return api.requireAdmin().then((response) => {
return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), {value: value}) return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), { value: value })
.catch((error) => {throw error;}); .catch((error) => { throw error })
}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }));; }).catch((error) => context.commit('API_FAILURE', { app, key, value, error }))
} }
}; }
export default {state, mutations, getters, actions}; export default { state, mutations, getters, actions }

View File

@ -1,4 +1,4 @@
/* /**
* @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com> * @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
* *
* @author John Molakvoæ <skjnldsv@protonmail.com> * @author John Molakvoæ <skjnldsv@protonmail.com>
@ -20,21 +20,19 @@
* *
*/ */
import api from './api';
const state = { const state = {
serverData: {} serverData: {}
}; }
const mutations = { const mutations = {
setServerData(state, data) { setServerData(state, data) {
state.serverData = data; state.serverData = data
} }
}; }
const getters = { const getters = {
getServerData(state) { getServerData(state) {
return state.serverData; return state.serverData
} }
}; }
const actions = {}; const actions = {}
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