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

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 App from './App.vue';
import Vue from 'vue'
import App from './Accessibility.vue'
/* global t */
// bind to window
Vue.prototype.OC = OC;
Vue.prototype.t = t;
Vue.prototype.OC = OC
Vue.prototype.t = t
new Vue({
export default new Vue({
el: '#accessibility',
render: h => h(App)
});
})

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>
* Copyright (c) 2016
*
@ -18,18 +18,18 @@
* @param {jQuery} $el jQuery handle for this activity
* @param {string} view The view that displayes this activity
*/
prepareModelForDisplay: function (model, $el, view) {
prepareModelForDisplay: function(model, $el, view) {
if (model.get('app') !== 'comments' || model.get('type') !== 'comments') {
return;
return
}
if (view === 'ActivityTabView') {
$el.addClass('comment');
$el.addClass('comment')
if (model.get('message') && this._isLong(model.get('message'))) {
$el.addClass('collapsed');
var $overlay = $('<div>').addClass('message-overlay');
$el.find('.activitymessage').after($overlay);
$el.on('click', this._onClickCollapsedComment);
$el.addClass('collapsed')
var $overlay = $('<div>').addClass('message-overlay')
$el.find('.activitymessage').after($overlay)
$el.on('click', this._onClickCollapsedComment)
}
}
},
@ -38,22 +38,21 @@
* Copy of CommentsTabView._onClickComment()
*/
_onClickCollapsedComment: function(ev) {
var $row = $(ev.target);
var $row = $(ev.target)
if (!$row.is('.comment')) {
$row = $row.closest('.comment');
$row = $row.closest('.comment')
}
$row.removeClass('collapsed');
$row.removeClass('collapsed')
},
/*
* Copy of CommentsTabView._isLong()
*/
_isLong: function(message) {
return message.length > 250 || (message.match(/\n/g) || []).length > 1;
return message.length > 250 || (message.match(/\n/g) || []).length > 1
}
};
}
})()
})();
OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin);
OC.Plugins.register('OCA.Activity.RenderingPlugins', OCA.Comments.ActivityTabViewPlugin)

View File

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

View File

@ -1,3 +1,4 @@
/* eslint-disable */
/*
* Copyright (c) 2016
*
@ -20,148 +21,147 @@
var CommentCollection = OC.Backbone.Collection.extend(
/** @lends OCA.Comments.CommentCollection.prototype */ {
sync: OC.Backbone.davSync,
sync: OC.Backbone.davSync,
model: OCA.Comments.CommentModel,
model: OCA.Comments.CommentModel,
/**
/**
* Object type
*
* @type string
*/
_objectType: 'files',
_objectType: 'files',
/**
/**
* Object id
*
* @type string
*/
_objectId: null,
_objectId: null,
/**
/**
* True if there are no more page results left to fetch
*
* @type bool
*/
_endReached: false,
_endReached: false,
/**
/**
* Number of comments to fetch per page
*
* @type int
*/
_limit : 20,
_limit: 20,
/**
/**
* Initializes the collection
*
* @param {string} [options.objectType] object type
* @param {string} [options.objectId] object id
*/
initialize: function(models, options) {
options = options || {};
if (options.objectType) {
this._objectType = options.objectType;
}
if (options.objectId) {
this._objectId = options.objectId;
}
},
initialize: function(models, options) {
options = options || {}
if (options.objectType) {
this._objectType = options.objectType
}
if (options.objectId) {
this._objectId = options.objectId
}
},
url: function() {
return OC.linkToRemote('dav') + '/comments/' +
encodeURIComponent(this._objectType) + '/' +
encodeURIComponent(this._objectId) + '/';
},
url: function() {
return OC.linkToRemote('dav') + '/comments/'
+ encodeURIComponent(this._objectType) + '/'
+ encodeURIComponent(this._objectId) + '/'
},
setObjectId: function(objectId) {
this._objectId = objectId;
},
setObjectId: function(objectId) {
this._objectId = objectId
},
hasMoreResults: function() {
return !this._endReached;
},
hasMoreResults: function() {
return !this._endReached
},
reset: function() {
this._endReached = false;
this._summaryModel = null;
return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
},
reset: function() {
this._endReached = false
this._summaryModel = null
return OC.Backbone.Collection.prototype.reset.apply(this, arguments)
},
/**
/**
* Fetch the next set of results
*/
fetchNext: function(options) {
var self = this;
if (!this.hasMoreResults()) {
return null;
}
var body = '<?xml version="1.0" encoding="utf-8" ?>\n' +
'<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n' +
// load one more so we know there is more
' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n' +
' <oc:offset>' + this.length + '</oc:offset>\n' +
'</oc:filter-comments>\n';
options = options || {};
var success = options.success;
options = _.extend({
remove: false,
parse: true,
data: body,
davProperties: CommentCollection.prototype.model.prototype.davProperties,
success: function(resp) {
if (resp.length <= self._limit) {
// no new entries, end reached
self._endReached = true;
} else {
// remove last entry, for next page load
resp = _.initial(resp);
}
if (!self.set(resp, options)) {
return false;
}
if (success) {
success.apply(null, arguments);
}
self.trigger('sync', 'REPORT', self, options);
fetchNext: function(options) {
var self = this
if (!this.hasMoreResults()) {
return null
}
}, options);
return this.sync('REPORT', this, options);
},
var body = '<?xml version="1.0" encoding="utf-8" ?>\n'
+ '<oc:filter-comments xmlns:D="DAV:" xmlns:oc="http://owncloud.org/ns">\n'
// load one more so we know there is more
+ ' <oc:limit>' + (this._limit + 1) + '</oc:limit>\n'
+ ' <oc:offset>' + this.length + '</oc:offset>\n'
+ '</oc:filter-comments>\n'
/**
options = options || {}
var success = options.success
options = _.extend({
remove: false,
parse: true,
data: body,
davProperties: CommentCollection.prototype.model.prototype.davProperties,
success: function(resp) {
if (resp.length <= self._limit) {
// no new entries, end reached
self._endReached = true
} else {
// remove last entry, for next page load
resp = _.initial(resp)
}
if (!self.set(resp, options)) {
return false
}
if (success) {
success.apply(null, arguments)
}
self.trigger('sync', 'REPORT', self, options)
}
}, options)
return this.sync('REPORT', this, options)
},
/**
* Returns the matching summary model
*
* @return {OCA.Comments.CommentSummaryModel} summary model
* @returns {OCA.Comments.CommentSummaryModel} summary model
*/
getSummaryModel: function() {
if (!this._summaryModel) {
this._summaryModel = new OCA.Comments.CommentSummaryModel({
id: this._objectId,
objectType: this._objectType
});
}
return this._summaryModel;
},
getSummaryModel: function() {
if (!this._summaryModel) {
this._summaryModel = new OCA.Comments.CommentSummaryModel({
id: this._objectId,
objectType: this._objectType
})
}
return this._summaryModel
},
/**
/**
* Updates the read marker for this comment thread
*
* @param {Date} [date] optional date, defaults to now
* @param {Object} [options] backbone options
*/
updateReadMarker: function(date, options) {
options = options || {};
updateReadMarker: function(date, options) {
options = options || {}
return this.getSummaryModel().save({
readMarker: (date || new Date()).toUTCString()
}, options);
}
});
OCA.Comments.CommentCollection = CommentCollection;
})(OC, OCA);
return this.getSummaryModel().save({
readMarker: (date || new Date()).toUTCString()
}, options)
}
})
OCA.Comments.CommentCollection = CommentCollection
})(OC, OCA)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ OCA.Files_External.StatusManager = {
/**
* Function to get external mount point list from the files_external API
* @param {function} afterCallback function to be executed
* @param {Function} afterCallback function to be executed
*/
getMountPointList: function (afterCallback) {

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

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

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

View File

@ -1,6 +1,3 @@
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/');
__webpack_nonce__ = btoa(OC.requestToken);
import './share'
import './sharetabview'
import './sharebreadcrumbview'
@ -10,4 +7,9 @@ import './style/sharebreadcrumb.scss'
import './collaborationresourceshandler.js'
window.OCA.Sharing = OCA.Sharing;
// eslint-disable-next-line camelcase
__webpack_public_path__ = OC.linkTo('files_sharing', 'js/dist/')
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
window.OCA.Sharing = OCA.Sharing

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

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

View File

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

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() {
OCA.Versions = OCA.Versions || {};
OCA.Versions = OCA.Versions || {}
/**
* @namespace
@ -22,13 +22,12 @@
*/
attach: function(fileList) {
if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
return;
return
}
fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', {order: -10}));
fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView', { order: -10 }))
}
};
})();
OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util);
}
})()
OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util)

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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"
:class="wiping">
<td class="client">
<div :class="iconName.icon"></div>
<div :class="iconName.icon" />
</td>
<td class="token-name">
<input v-if="token.canRename && renaming"
type="text"
ref="input"
v-model="newName"
@keyup.enter="rename"
@blur="cancelRename"
@keyup.esc="cancelRename">
<span v-else>{{iconName.name}}</span>
<span v-if="wiping"
class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
ref="input"
v-model="newName"
type="text"
@keyup.enter="rename"
@blur="cancelRename"
@keyup.esc="cancelRename">
<span v-else>{{ iconName.name }}</span>
<span v-if="wiping" class="wiping-warning">({{ t('settings', 'Marked for remote wipe') }})</span>
</td>
<td>
<span class="last-activity" v-tooltip="lastActivity">{{lastActivityRelative}}</span>
<span v-tooltip="lastActivity" class="last-activity">{{ lastActivityRelative }}</span>
</td>
<td class="more">
<Actions v-if="!token.current"
:actions="actions"
:open.sync="actionOpen"
v-tooltip.auto="{
content: t('settings', 'Device settings'),
container: 'body'
}">
}"
:open.sync="actionOpen">
<ActionCheckbox v-if="token.type === 1"
:checked="token.scope.filesystem"
@change.stop.prevent="$emit('toggleScope', token, 'filesystem', !token.scope.filesystem)">
@ -91,7 +89,7 @@ import {
Actions,
ActionButton,
ActionCheckbox
} from 'nextcloud-vue';
} from 'nextcloud-vue'
const userAgentMap = {
ie: /(?:MSIE|Trident|Trident\/7.0; rv)[ :](\d+)/,
@ -106,18 +104,18 @@ const userAgentMap = {
// Android Chrome user agent: https://developers.google.com/chrome/mobile/docs/user-agent
androidChrome: /Android.*(?:; (.*) Build\/).*Chrome\/(\d+)[0-9.]+/,
iphone: / *CPU +iPhone +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
ipad: /\(iPad\; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)\-iOS.*$/,
androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud\-android.*$/,
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud\-Talk.*$/,
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud\-Talk.*$/,
ipad: /\(iPad; *CPU +OS +([0-9]+)_(?:[0-9_])+ +like +Mac +OS +X */,
iosClient: /^Mozilla\/5\.0 \(iOS\) (ownCloud|Nextcloud)-iOS.*$/,
androidClient: /^Mozilla\/5\.0 \(Android\) ownCloud-android.*$/,
iosTalkClient: /^Mozilla\/5\.0 \(iOS\) Nextcloud-Talk.*$/,
androidTalkClient: /^Mozilla\/5\.0 \(Android\) Nextcloud-Talk.*$/,
// DAVdroid/1.2 (2016/07/03; dav4android; okhttp3) Android/6.0.1
davDroid: /DAV(droid|x5)\/([0-9.]+)/,
// Mozilla/5.0 (U; Linux; Maemo; Jolla; Sailfish; like Android 4.3) AppleWebKit/538.1 (KHTML, like Gecko) WebPirate/2.0 like Mobile Safari/538.1 (compatible)
webPirate: /(Sailfish).*WebPirate\/(\d+)/,
// Mozilla/5.0 (Maemo; Linux; U; Jolla; Sailfish; Mobile; rv:31.0) Gecko/31.0 Firefox/31.0 SailfishBrowser/1.0
sailfishBrowser: /(Sailfish).*SailfishBrowser\/(\d+)/
};
}
const nameMap = {
ie: t('setting', 'Internet Explorer'),
edge: t('setting', 'Edge'),
@ -134,7 +132,7 @@ const nameMap = {
davDroid: 'DAVdroid',
webPirate: 'WebPirate',
sailfishBrowser: 'SailfishBrowser'
};
}
const iconMap = {
ie: 'icon-desktop',
edge: 'icon-desktop',
@ -151,10 +149,10 @@ const iconMap = {
davDroid: 'icon-phone',
webPirate: 'icon-link',
sailfishBrowser: 'icon-link'
};
}
export default {
name: "AuthToken",
name: 'AuthToken',
components: {
Actions,
ActionButton,
@ -163,91 +161,93 @@ export default {
props: {
token: {
type: Object,
required: true,
required: true
}
},
computed: {
lastActivityRelative () {
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000);
},
lastActivity () {
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL');
},
iconName () {
// pretty format sync client user agent
let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/);
let icon = '';
if (matches) {
this.token.name = t('settings', 'Sync client - {os}', {
os: matches[1],
version: matches[2]
});
icon = 'icon-desktop';
}
// preserve title for cases where we format it further
const title = this.token.name;
let name = this.token.name;
for (let client in userAgentMap) {
if (matches = title.match(userAgentMap[client])) {
if (matches[2] && matches[1]) { // version number and os
name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1];
} else if (matches[1]) { // only version number
name = nameMap[client] + ' ' + matches[1];
} else {
name = nameMap[client];
}
icon = iconMap[client];
}
}
if (this.token.current) {
name = t('settings', 'This session');
}
return {
icon,
name,
};
},
wiping() {
return this.token.type === 2;
}
},
data () {
data() {
return {
showMore: this.token.canScope || this.token.canDelete,
renaming: false,
newName: '',
actionOpen: false,
};
actionOpen: false
}
},
computed: {
lastActivityRelative() {
return OC.Util.relativeModifiedDate(this.token.lastActivity * 1000)
},
lastActivity() {
return OC.Util.formatDate(this.token.lastActivity * 1000, 'LLL')
},
iconName() {
// pretty format sync client user agent
let matches = this.token.name.match(/Mozilla\/5\.0 \((\w+)\) (?:mirall|csyncoC)\/(\d+\.\d+\.\d+)/)
let icon = ''
if (matches) {
/* eslint-disable-next-line */
this.token.name = t('settings', 'Sync client - {os}', {
os: matches[1],
version: matches[2]
})
icon = 'icon-desktop'
}
// preserve title for cases where we format it further
const title = this.token.name
let name = this.token.name
for (let client in userAgentMap) {
const matches = title.match(userAgentMap[client])
if (matches) {
if (matches[2] && matches[1]) { // version number and os
name = nameMap[client] + ' ' + matches[2] + ' - ' + matches[1]
} else if (matches[1]) { // only version number
name = nameMap[client] + ' ' + matches[1]
} else {
name = nameMap[client]
}
icon = iconMap[client]
}
}
if (this.token.current) {
name = t('settings', 'This session')
}
return {
icon,
name
}
},
wiping() {
return this.token.type === 2
}
},
methods: {
startRename() {
// Close action (popover menu)
this.actionOpen = false;
this.actionOpen = false
this.newName = this.token.name;
this.renaming = true;
this.newName = this.token.name
this.renaming = true
this.$nextTick(() => {
this.$refs.input.select();
});
this.$refs.input.select()
})
},
cancelRename() {
this.renaming = false;
this.renaming = false
},
revoke() {
this.actionOpen = false;
this.actionOpen = false
this.$emit('delete', this.token)
},
rename() {
this.renaming = false;
this.$emit('rename', this.token, this.newName);
this.renaming = false
this.$emit('rename', this.token, this.newName)
},
wipe() {
this.actionOpen = false;
this.$emit('wipe', this.token);
this.actionOpen = false
this.$emit('wipe', this.token)
}
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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 store from './store/admin-security'
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
Vue.prototype.t = t;
Vue.prototype.t = t
// Not used here but required for legacy templates
window.OC = window.OC || {};
window.OC.Settings = window.OC.Settings || {};
window.OC = window.OC || {}
window.OC.Settings = window.OC.Settings || {}
store.replaceState(
OCP.InitialState.loadState('settings', 'mandatory2FAState')

View File

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

View File

@ -19,22 +19,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import Vue from 'vue';
import VueClipboard from 'vue-clipboard2';
import VTooltip from 'v-tooltip';
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
import VTooltip from 'v-tooltip'
import AuthTokenSection from './components/AuthTokenSection';
import AuthTokenSection from './components/AuthTokenSection'
__webpack_nonce__ = btoa(OC.requestToken);
// eslint-disable-next-line camelcase
__webpack_nonce__ = btoa(OC.requestToken)
Vue.use(VueClipboard);
Vue.use(VTooltip, { defaultHtml: false });
Vue.prototype.t = t;
Vue.use(VueClipboard)
Vue.use(VTooltip, { defaultHtml: false })
Vue.prototype.t = t
const View = Vue.extend(AuthTokenSection);
const View = Vue.extend(AuthTokenSection)
new View({
propsData: {
tokens: OCP.InitialState.loadState('settings', 'app_tokens'),
canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token'),
canCreateToken: OCP.InitialState.loadState('settings', 'can_create_app_token')
}
}).$mount('#security-authtokens');
}).$mount('#security-authtokens')

View File

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

View File

@ -1,4 +1,4 @@
/*
/**
* @copyright 2019 Roeland Jago Douma <roeland@famdouma.nl>
*
* @author 2019 Roeland Jago Douma <roeland@famdouma.nl>
@ -24,7 +24,13 @@ import Vuex from 'vuex'
Vue.use(Vuex)
export const mutations = {
const state = {
enforced: false,
enforcedGroups: [],
excludedGroups: []
}
const mutations = {
setEnforced(state, enabled) {
Vue.set(state, 'enforced', enabled)
},
@ -36,28 +42,8 @@ export const mutations = {
}
}
export const actions = {
save ({commit}, ) {
commit('setEnabled', false);
return generateCodes()
.then(({codes, state}) => {
commit('setEnabled', state.enabled);
commit('setTotal', state.total);
commit('setUsed', state.used);
commit('setCodes', codes);
return true;
});
}
}
export default new Vuex.Store({
strict: process.env.NODE_ENV !== 'production',
state: {
enforced: false,
enforcedGroups: [],
excludedGroups: [],
},
mutations,
actions
state,
mutations
})

View File

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

View File

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

View File

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

View File

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

View File

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