Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>tags/v14.0.0beta1
@@ -67,6 +67,13 @@ div[contenteditable=true], | |||
cursor: default; | |||
opacity: 0.5; | |||
} | |||
&:required { | |||
box-shadow: none; | |||
} | |||
&:invalid { | |||
box-shadow: none !important; | |||
border-color: $color-error; | |||
} | |||
/* Primary action button, use sparingly */ | |||
&.primary { | |||
background-color: $color-primary-element; | |||
@@ -216,7 +223,8 @@ input { | |||
margin-left: -8px !important; | |||
border-left-color: transparent !important; | |||
border-radius: 0 $border-radius $border-radius 0 !important; | |||
background-clip: padding-box; /* Avoid background under border */ | |||
background-clip: padding-box; | |||
/* Avoid background under border */ | |||
background-color: $color-main-background !important; | |||
opacity: 1; | |||
width: 34px; | |||
@@ -227,6 +235,7 @@ input { | |||
background-image: url('../img/actions/confirm-fade.svg?v=2') !important; | |||
} | |||
} | |||
/* only show confirm borders if input is not focused */ | |||
&:not(:active):not(:hover):not(:focus){ | |||
+ .icon-confirm { | |||
@@ -244,14 +253,19 @@ input { | |||
&:active, | |||
&:hover, | |||
&:focus { | |||
&:invalid { | |||
+ .icon-confirm { | |||
border-color: $color-error; | |||
} | |||
} | |||
+ .icon-confirm { | |||
border-color: $color-primary-element !important; | |||
border-left-color: transparent !important; | |||
z-index: 2; /* above previous input */ | |||
/* above previous input */ | |||
z-index: 2; | |||
} | |||
} | |||
} | |||
} | |||
@@ -606,6 +620,169 @@ input { | |||
} | |||
} | |||
/* Vue multiselect */ | |||
.multiselect.multiselect-vue { | |||
margin: 1px 2px; | |||
padding: 0 !important; | |||
display: inline-block; | |||
min-width: 160px; | |||
width: 160px; | |||
position: relative; | |||
background-color: $color-main-background; | |||
&.multiselect--active { | |||
/* Opened: force display the input */ | |||
input.multiselect__input { | |||
opacity: 1 !important; | |||
} | |||
} | |||
&.multiselect--disabled { | |||
background-color: nc-darken($color-main-background, 8%); | |||
} | |||
.multiselect__tags { | |||
display: flex; | |||
flex-wrap: nowrap; | |||
overflow: hidden; | |||
border: 1px solid nc-darken($color-main-background, 14%); | |||
cursor: pointer; | |||
position: relative; | |||
border-radius: 3px; | |||
height: 38px; | |||
/* tag wrapper */ | |||
.multiselect__tags-wrap { | |||
align-items: center; | |||
display: inline-flex; | |||
overflow: hidden; | |||
max-width: 100%; | |||
position: relative; | |||
padding: 3px 5px; | |||
/* no tags or simple select? Show input directly | |||
input is used to display single value */ | |||
&:empty ~ input.multiselect__input { | |||
opacity: 1 !important; | |||
/* hide default empty text, show input instead */ | |||
+ span:not(.multiselect__single) { | |||
display: none; | |||
} | |||
} | |||
/* selected tag */ | |||
.multiselect__tag { | |||
flex: 0 0 auto; | |||
line-height: 20px; | |||
padding: 1px 5px; | |||
background-image: none; | |||
color: nc-lighten($color-main-text, 33%); | |||
border: 1px solid nc-darken($color-main-background, 14%); | |||
display: inline-flex; | |||
align-items: center; | |||
border-radius: 3px; | |||
margin-right: 5px; | |||
} | |||
} | |||
/* Single select default value */ | |||
.multiselect__single { | |||
padding: 8px 10px; | |||
flex: 0 0 100%; | |||
z-index: 5; | |||
background-color: $color-main-background; | |||
cursor: pointer; | |||
} | |||
/* displayed text if tag limit reached */ | |||
.multiselect__strong { | |||
flex: 0 0 auto; | |||
line-height: 20px; | |||
color: nc-lighten($color-main-text, 33%); | |||
display: inline-flex; | |||
align-items: center; | |||
opacity: .7; | |||
} | |||
/* default multiselect input for search and placeholder */ | |||
input.multiselect__input { | |||
width: 100% !important; | |||
position: absolute !important; | |||
margin: 0; | |||
opacity: 0; | |||
/* let's leave it on top of tags but hide it */ | |||
height: 100%; | |||
border: none; | |||
/* override hide to force show the placeholder */ | |||
display: block !important; | |||
} | |||
} | |||
/* results wrapper */ | |||
.multiselect__content-wrapper { | |||
position: absolute; | |||
width: 100%; | |||
margin-top: -1px; | |||
border: 1px solid nc-darken($color-main-background, 14%); | |||
background: $color-main-background; | |||
z-index: 50; | |||
.multiselect__content { | |||
width: 100%; | |||
padding: 5px 0; | |||
} | |||
li { | |||
padding: 5px; | |||
position: relative; | |||
display: flex; | |||
align-items: center; | |||
background-color: transparent; | |||
&, | |||
span { | |||
cursor: pointer; | |||
} | |||
> span { | |||
white-space: nowrap; | |||
overflow: hidden; | |||
text-overflow: ellipsis; | |||
height: 20px; | |||
margin: 0; | |||
min-height: 1em; | |||
-webkit-touch-callout: none; | |||
-webkit-user-select: none; | |||
-moz-user-select: none; | |||
-ms-user-select: none; | |||
user-select: none; | |||
display: inline-flex; | |||
align-items: center; | |||
background-color: transparent !important; | |||
color: nc-lighten($color-main-text, 33%); | |||
width: 100%; | |||
/* selected checkmark icon */ | |||
&::before { | |||
content: ' '; | |||
background-image: url('../img/actions/checkmark.svg?v=1'); | |||
background-repeat: no-repeat; | |||
background-position: center; | |||
min-width: 16px; | |||
min-height: 16px; | |||
display: block; | |||
opacity: 0.5; | |||
margin-right: 5px; | |||
visibility: hidden; | |||
} | |||
/* add the prop tag-placeholder="create" to add the + | |||
* icon on top of an unknown-and-ready-to-be-created entry | |||
*/ | |||
&[data-select='create'] { | |||
&::before { | |||
background-image: url('../img/actions/add.svg?v=1'); | |||
visibility: visible; | |||
} | |||
} | |||
&.multiselect__option--highlight { | |||
color: $color-main-text; | |||
} | |||
&.multiselect__option--selected { | |||
&::before { | |||
visibility: visible; | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
/* Progressbar */ | |||
progress { | |||
display: block; |
@@ -0,0 +1,6 @@ | |||
{ | |||
"presets": [ | |||
["env", { "modules": false }], | |||
"stage-3" | |||
] | |||
} |
@@ -0,0 +1,9 @@ | |||
root = true | |||
[*] | |||
charset = utf-8 | |||
indent_style = space | |||
indent_size = 2 | |||
end_of_line = lf | |||
insert_final_newline = true | |||
trim_trailing_whitespace = true |
@@ -0,0 +1,12 @@ | |||
.DS_Store | |||
node_modules/ | |||
dist/ | |||
npm-debug.log | |||
yarn-error.log | |||
# Editor directories and files | |||
.idea | |||
*.suo | |||
*.ntvs* | |||
*.njsproj | |||
*.sln |
@@ -0,0 +1,18 @@ | |||
# settings | |||
> A Vue.js project | |||
## Build Setup | |||
``` bash | |||
# install dependencies | |||
npm install | |||
# serve with hot reload at localhost:8080 | |||
npm run dev | |||
# build for production with minification | |||
npm run build | |||
``` | |||
For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader). |
@@ -675,101 +675,6 @@ span.usersLastLoginTooltip { | |||
} | |||
} | |||
tr:hover > td { | |||
&.password > span, &.displayName > span, &.mailAddress > span { | |||
margin: 0; | |||
cursor: pointer; | |||
} | |||
&.password > img, &.displayName > img, &.mailAddress > img { | |||
visibility: visible; | |||
cursor: pointer; | |||
} | |||
} | |||
td.userActions { | |||
.toggleUserActions { | |||
width: 44px; | |||
height: 44px; | |||
position: relative; | |||
.action { | |||
display: block; | |||
padding: 14px; | |||
opacity: 0.5; | |||
.icon-more { | |||
display: inline-block; | |||
} | |||
&:hover, | |||
&:focus { | |||
opacity: 1; | |||
} | |||
} | |||
} | |||
} | |||
div.recoveryPassword { | |||
left: 50em; | |||
display: block; | |||
position: absolute; | |||
top: -1px; | |||
} | |||
input#recoveryPassword { | |||
width: 15em; | |||
} | |||
#controls select.quota { | |||
margin: 3px; | |||
margin-right: 10px; | |||
height: 37px; | |||
} | |||
#userlist td.quota { | |||
position: relative; | |||
width: 10em; | |||
progress.quota-user-progress { | |||
position: absolute; | |||
width: calc(10em + 0px); | |||
margin-top: -7px; | |||
z-index: 0; | |||
margin-left: 1px; | |||
height: 3px; | |||
} | |||
} | |||
select { | |||
&.quota-user { | |||
width: 10em; | |||
height: 34px; | |||
z-index: 50; | |||
position: relative; | |||
} | |||
+ progress.quota-user-progress { | |||
position: absolute; | |||
width: calc(10em + 0px); | |||
margin-top: -7px; | |||
z-index: 0; | |||
margin-left: 1px; | |||
height: 3px; | |||
} | |||
} | |||
input.userFilter { | |||
width: 200px; | |||
} | |||
#newusergroups + input[type='submit'] { | |||
position: relative; | |||
top: -1px; | |||
} | |||
#headerGroups, #headerSubAdmins, #headerQuota { | |||
padding-left: 18px; | |||
} | |||
#headerAvatar { | |||
width: 32px; | |||
} | |||
/* used to highlight a user row in red */ | |||
#userlist tr.row-warning { | |||
@@ -1350,3 +1255,174 @@ doesnotexist:-o-prefocus, .strengthify-wrapper { | |||
margin-top: 22px; | |||
} | |||
} | |||
/* USERS LIST -------------------------------------------------------------- */ | |||
#body-settings { | |||
#app-navigation { | |||
/* Hack to override the javascript orderBy */ | |||
#usergrouplist > li { | |||
order: 4; | |||
&#_everyone { | |||
order:1; | |||
} | |||
&#admin { | |||
order:2; | |||
} | |||
&#_disabled { | |||
order:3; | |||
} | |||
} | |||
} | |||
$grid-row-height: 46px; | |||
#app-content.user-list-grid { | |||
display: grid; | |||
grid-auto-columns: 1fr; | |||
grid-auto-rows: $grid-row-height; | |||
grid-column-gap: 20px; | |||
.row { | |||
display: grid; | |||
grid-row-start: span 1; | |||
align-items: center; | |||
/* let's define the column until storage path, | |||
what follows will be manually defined */ | |||
grid-template-columns: 44px; | |||
grid-auto-columns: min-content; | |||
border-top: $color-border 1px solid; | |||
.name, | |||
.displayName, | |||
.password { | |||
width: 150px; | |||
} | |||
.mailAddress{ | |||
width: 200px; | |||
} | |||
.groups, | |||
.subadmins, | |||
.quota { | |||
width: 170px; | |||
} | |||
.storageLocation { | |||
width: 250px; | |||
} | |||
.userBackend, | |||
.lastLogin, | |||
.userActions { | |||
width: 100px; | |||
} | |||
&#grid-header, | |||
&#new-user { | |||
position: sticky; | |||
align-self: normal; | |||
background-color: $color-main-background; | |||
z-index: 55; /* above multiselect */ | |||
top: 0; | |||
&.sticky { | |||
box-shadow: 0 -2px 10px 1px $color-box-shadow; | |||
} | |||
} | |||
&#grid-header { | |||
color: nc-lighten($color-main-text, 60%); | |||
z-index: 60; /* above new-user */ | |||
} | |||
&#new-user { | |||
top: $grid-row-height; | |||
} | |||
&:hover { | |||
input:not([type='submit']):not(:focus):not(:active) { | |||
border-color: nc-darken($color-main-background, 14%) !important; | |||
} | |||
} | |||
> div, | |||
> form { | |||
grid-row: 1; | |||
display: inline-flex; | |||
align-items: center; | |||
color: nc-lighten($color-main-text, 33%); | |||
position: relative; | |||
> input:not(:focus):not(:active) { | |||
border-color: transparent; | |||
cursor: pointer; | |||
} | |||
> input:focus, >input:active { | |||
+ .icon-confirm { | |||
display: block !important; | |||
} | |||
} | |||
&:not(.userActions) > input:not([type='submit']) { | |||
width: 100%; | |||
min-width: 0; | |||
} | |||
&.quota { | |||
.multiselect--active + progress { | |||
display: none; | |||
} | |||
progress { | |||
position: absolute; | |||
width: 160px; | |||
left: 2px; | |||
bottom: 2px; | |||
height: 3px; | |||
} | |||
} | |||
.icon-confirm { | |||
width: 32px; | |||
height: 32px; | |||
flex: 0 0 32px; | |||
cursor: pointer; | |||
&:not(:active) { | |||
display: none; | |||
} | |||
} | |||
&.avatar { | |||
height: 32px; | |||
width: 32px; | |||
margin: 6px; | |||
img { | |||
display: block; | |||
} | |||
} | |||
.toggleUserActions { | |||
position: relative; | |||
.icon-more { | |||
width: 44px; | |||
height: 44px; | |||
opacity: .5; | |||
cursor: pointer; | |||
:hover { | |||
opacity: .7; | |||
} | |||
} | |||
} | |||
.v-select { | |||
&.open .selected-tag-wrap { | |||
display: none; | |||
} | |||
.dropdown-toggle .selected-tag { | |||
padding-right: 5px; | |||
.close { | |||
/* no delete on tags*/ | |||
display: none; | |||
} | |||
} | |||
.dropdown-menu li a .icon-add { | |||
position: absolute; | |||
width: 16px; | |||
height: 16px; | |||
opacity: .5; | |||
left: 7px; | |||
} | |||
} | |||
} | |||
} | |||
.infinite-loading-container { | |||
display: flex; | |||
align-items: center; | |||
justify-content: center; | |||
} | |||
.users-list-end { | |||
opacity: .5; | |||
user-select: none; | |||
} | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
<!DOCTYPE html> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="utf-8"> | |||
<title>settings</title> | |||
</head> | |||
<body> | |||
<div id="app"></div> | |||
<script src="/dist/build.js"></script> | |||
</body> | |||
</html> |
@@ -1,213 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/** | |||
* takes care of deleting things represented by an ID | |||
* | |||
* @class | |||
* @param {string} endpoint the corresponding ajax PHP script. Currently limited | |||
* to settings - ajax path. | |||
* @param {string} paramID the by the script expected parameter name holding the | |||
* ID of the object to delete | |||
* @param {markCallback} markCallback function to be called after successfully | |||
* marking the object for deletion. | |||
* @param {removeCallback} removeCallback the function to be called after | |||
* successful delete. | |||
*/ | |||
/* globals escapeHTML */ | |||
function DeleteHandler(endpoint, paramID, markCallback, removeCallback) { | |||
this.oidToDelete = false; | |||
this.canceled = false; | |||
this.ajaxEndpoint = endpoint; | |||
this.ajaxParamID = paramID; | |||
this.markCallback = markCallback; | |||
this.removeCallback = removeCallback; | |||
this.undoCallback = false; | |||
this.notifier = false; | |||
this.notificationDataID = false; | |||
this.notificationMessage = false; | |||
this.notificationPlaceholder = '%oid'; | |||
} | |||
/** | |||
* Number of milliseconds after which the operation is performed. | |||
*/ | |||
DeleteHandler.TIMEOUT_MS = 7000; | |||
/** | |||
* Timer after which the action will be performed anyway. | |||
*/ | |||
DeleteHandler.prototype._timeout = null; | |||
/** | |||
* The function to be called after successfully marking the object for deletion | |||
* @callback markCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* The function to be called after successful delete. The id of the object will | |||
* be passed as argument. Unsuccessful operations will display an error using | |||
* OC.dialogs, no callback is fired. | |||
* @callback removeCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* This callback is fired after "undo" was clicked so the consumer can update | |||
* the web interface | |||
* @callback undoCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* enabled the notification system. Required for undo UI. | |||
* | |||
* @param {object} notifier Usually OC.Notification | |||
* @param {string} dataID an identifier for the notifier, e.g. 'deleteuser' | |||
* @param {string} message the message that should be shown upon delete. %oid | |||
* will be replaced with the affected id of the item to be deleted | |||
* @param {undoCallback} undoCallback called after "undo" was clicked | |||
*/ | |||
DeleteHandler.prototype.setNotification = function(notifier, dataID, message, undoCallback) { | |||
this.notifier = notifier; | |||
this.notificationDataID = dataID; | |||
this.notificationMessage = message; | |||
this.undoCallback = undoCallback; | |||
var dh = this; | |||
$('#notification') | |||
.off('click.deleteHandler_' + dataID) | |||
.on('click.deleteHandler_' + dataID, '.undo', function () { | |||
if ($('#notification').data(dh.notificationDataID)) { | |||
var oid = dh.oidToDelete; | |||
dh.cancel(); | |||
if(typeof dh.undoCallback !== 'undefined') { | |||
dh.undoCallback(oid); | |||
} | |||
} | |||
dh.notifier.hide(); | |||
}); | |||
}; | |||
/** | |||
* shows the Undo Notification (if configured) | |||
*/ | |||
DeleteHandler.prototype.showNotification = function() { | |||
if(this.notifier !== false) { | |||
if(!this.notifier.isHidden()) { | |||
this.hideNotification(); | |||
} | |||
$('#notification').data(this.notificationDataID, true); | |||
var msg = this.notificationMessage.replace( | |||
this.notificationPlaceholder, escapeHTML(this.oidToDelete)); | |||
this.notifier.showHtml(msg); | |||
} | |||
}; | |||
/** | |||
* hides the Undo Notification | |||
*/ | |||
DeleteHandler.prototype.hideNotification = function() { | |||
if(this.notifier !== false) { | |||
$('#notification').removeData(this.notificationDataID); | |||
this.notifier.hide(); | |||
} | |||
}; | |||
/** | |||
* initializes the delete operation for a given object id | |||
* | |||
* @param {string} oid the object id | |||
*/ | |||
DeleteHandler.prototype.mark = function(oid) { | |||
if(this.oidToDelete !== false) { | |||
// passing true to avoid hiding the notification | |||
// twice and causing the second notification | |||
// to disappear immediately | |||
this.deleteEntry(true); | |||
} | |||
this.oidToDelete = oid; | |||
this.canceled = false; | |||
this.markCallback(oid); | |||
this.showNotification(); | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
if (DeleteHandler.TIMEOUT_MS > 0) { | |||
this._timeout = window.setTimeout( | |||
_.bind(this.deleteEntry, this), | |||
DeleteHandler.TIMEOUT_MS | |||
); | |||
} | |||
}; | |||
/** | |||
* cancels a delete operation | |||
*/ | |||
DeleteHandler.prototype.cancel = function() { | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
this.canceled = true; | |||
this.oidToDelete = false; | |||
}; | |||
/** | |||
* executes a delete operation. Requires that the operation has been | |||
* initialized by mark(). On error, it will show a message via | |||
* OC.dialogs.alert. On success, a callback is fired so that the client can | |||
* update the web interface accordingly. | |||
* | |||
* @param {boolean} [keepNotification] true to keep the notification, false to hide | |||
* it, defaults to false | |||
*/ | |||
DeleteHandler.prototype.deleteEntry = function(keepNotification) { | |||
var deferred = $.Deferred(); | |||
if(this.canceled || this.oidToDelete === false) { | |||
return deferred.resolve().promise(); | |||
} | |||
var dh = this; | |||
if(!keepNotification && $('#notification').data(this.notificationDataID) === true) { | |||
dh.hideNotification(); | |||
} | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
var payload = {}; | |||
payload[dh.ajaxParamID] = dh.oidToDelete; | |||
return $.ajax({ | |||
type: 'DELETE', | |||
url: OC.generateUrl(dh.ajaxEndpoint+'/{oid}',{oid: this.oidToDelete}), | |||
// FIXME: do not use synchronous ajax calls as they block the browser ! | |||
async: false, | |||
success: function (result) { | |||
// Remove undo option, & remove user from table | |||
//TODO: following line | |||
dh.removeCallback(dh.oidToDelete); | |||
dh.canceled = true; | |||
}, | |||
error: function (jqXHR) { | |||
OC.dialogs.alert(jqXHR.responseJSON.data.message, t('settings', 'Unable to delete {objName}', {objName: dh.oidToDelete})); | |||
dh.undoCallback(dh.oidToDelete); | |||
} | |||
}); | |||
}; |
@@ -1,78 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/** | |||
* @brief this object takes care of the filter functionality on the user | |||
* management page | |||
* @param {UserList} userList the UserList object | |||
* @param {GroupList} groupList the GroupList object | |||
*/ | |||
function UserManagementFilter (userList, groupList) { | |||
this.userList = userList; | |||
this.groupList = groupList; | |||
this.oldFilter = ''; | |||
this.init(); | |||
} | |||
/** | |||
* @brief sets up when the filter action shall be triggered | |||
*/ | |||
UserManagementFilter.prototype.init = function () { | |||
OC.Plugins.register('OCA.Search', this); | |||
}; | |||
/** | |||
* @brief the filter action needs to be done, here the accurate steps are being | |||
* taken care of | |||
*/ | |||
UserManagementFilter.prototype.run = _.debounce(function (filter) { | |||
if (filter === this.oldFilter) { | |||
return; | |||
} | |||
this.oldFilter = filter; | |||
this.userList.filter = filter; | |||
this.userList.empty(); | |||
this.userList.update(GroupList.getCurrentGID()); | |||
if (this.groupList.filterGroups) { | |||
// user counts are being updated nevertheless | |||
this.groupList.empty(); | |||
} | |||
this.groupList.update(); | |||
}, | |||
300 | |||
); | |||
/** | |||
* @brief returns the filter String | |||
* @returns string | |||
*/ | |||
UserManagementFilter.prototype.getPattern = function () { | |||
var input = this.filterInput.val(), | |||
html = $('html'), | |||
isIE8or9 = html.hasClass('lte9'); | |||
// FIXME - TODO - once support for IE8 and IE9 is dropped | |||
if (isIE8or9 && input == this.filterInput.attr('placeholder')) { | |||
input = ''; | |||
} | |||
return input; | |||
}; | |||
/** | |||
* @brief adds reset functionality to an HTML element | |||
* @param jQuery the jQuery representation of that element | |||
*/ | |||
UserManagementFilter.prototype.addResetButton = function (button) { | |||
var umf = this; | |||
button.click(function () { | |||
umf.filterInput.val(''); | |||
umf.run(); | |||
}); | |||
}; | |||
UserManagementFilter.prototype.attach = function (search) { | |||
search.setFilter('settings', this.run.bind(this)); | |||
}; |
@@ -1,385 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Raghu Nayyar <beingminimal@gmail.com> | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/* globals escapeHTML, UserList, DeleteHandler */ | |||
var $userGroupList, | |||
$sortGroupBy; | |||
var GroupList; | |||
GroupList = { | |||
activeGID: '', | |||
everyoneGID: '_everyone', | |||
filter: '', | |||
filterGroups: false, | |||
addGroup: function (gid, displayName, usercount) { | |||
if (_.isUndefined(displayName)) { | |||
displayName = gid; | |||
} | |||
var $li = $userGroupList.find('.isgroup:last-child').clone(); | |||
$li | |||
.data('gid', gid) | |||
.find('.groupname').text(displayName); | |||
GroupList.setUserCount($li, usercount); | |||
$li.appendTo($userGroupList); | |||
GroupList.sortGroups(); | |||
return $li; | |||
}, | |||
setUserCount: function (groupLiElement, usercount) { | |||
if ($sortGroupBy !== 1) { | |||
// If we don't sort by group count we don't display them either | |||
return; | |||
} | |||
var $groupLiElement = $(groupLiElement); | |||
if (usercount === undefined || usercount === 0 || usercount < 0) { | |||
usercount = ''; | |||
$groupLiElement.data('usercount', 0); | |||
} else { | |||
$groupLiElement.data('usercount', usercount); | |||
} | |||
$groupLiElement.find('.usercount').text(usercount); | |||
}, | |||
getUserCount: function ($groupLiElement) { | |||
var count = parseInt($groupLiElement.data('usercount'), 10); | |||
return isNaN(count) ? 0 : count; | |||
}, | |||
modGroupCount: function(gid, diff) { | |||
var $li = GroupList.getGroupLI(gid); | |||
var count = GroupList.getUserCount($li) + diff; | |||
GroupList.setUserCount($li, count); | |||
}, | |||
incEveryoneCount: function() { | |||
GroupList.modGroupCount(GroupList.everyoneGID, 1); | |||
}, | |||
decEveryoneCount: function() { | |||
GroupList.modGroupCount(GroupList.everyoneGID, -1); | |||
}, | |||
incGroupCount: function(gid) { | |||
GroupList.modGroupCount(gid, 1); | |||
}, | |||
decGroupCount: function(gid) { | |||
GroupList.modGroupCount(gid, -1); | |||
}, | |||
getCurrentGID: function () { | |||
return GroupList.activeGID; | |||
}, | |||
sortGroups: function () { | |||
var lis = $userGroupList.find('.isgroup').get(); | |||
lis.sort(function (a, b) { | |||
// "Everyone" always at the top | |||
if ($(a).data('gid') === '_everyone') { | |||
return -1; | |||
} else if ($(b).data('gid') === '_everyone') { | |||
return 1; | |||
} | |||
// "admin" always as second | |||
if ($(a).data('gid') === 'admin') { | |||
return -1; | |||
} else if ($(b).data('gid') === 'admin') { | |||
return 1; | |||
} | |||
if ($sortGroupBy === 1) { | |||
// Sort by user count first | |||
var $usersGroupA = $(a).data('usercount'), | |||
$usersGroupB = $(b).data('usercount'); | |||
if ($usersGroupA > 0 && $usersGroupA > $usersGroupB) { | |||
return -1; | |||
} | |||
if ($usersGroupB > 0 && $usersGroupB > $usersGroupA) { | |||
return 1; | |||
} | |||
} | |||
// Fallback or sort by group name | |||
return UserList.alphanum( | |||
$(a).find('a span').text(), | |||
$(b).find('a span').text() | |||
); | |||
}); | |||
var items = []; | |||
$.each(lis, function (index, li) { | |||
items.push(li); | |||
if (items.length === 100) { | |||
$userGroupList.append(items); | |||
items = []; | |||
} | |||
}); | |||
if (items.length > 0) { | |||
$userGroupList.append(items); | |||
} | |||
}, | |||
createGroup: function (groupid) { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.createGroup, this, groupid)); | |||
return; | |||
} | |||
$.post( | |||
OC.generateUrl('/settings/users/groups'), | |||
{ | |||
id: groupid | |||
}, | |||
function (result) { | |||
if (result.groupname) { | |||
var addedGroup = result.groupname; | |||
UserList.availableGroups[groupid] = {displayName: result.groupname}; | |||
GroupList.addGroup(groupid, result.groupname); | |||
} | |||
GroupList.toggleAddGroup(); | |||
}).fail(function(result) { | |||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', {message: result.responseJSON.message})); | |||
}); | |||
}, | |||
update: function () { | |||
if (GroupList.updating) { | |||
return; | |||
} | |||
GroupList.updating = true; | |||
$.get( | |||
OC.generateUrl('/settings/users/groups'), | |||
{ | |||
pattern: this.filter, | |||
filterGroups: this.filterGroups ? 1 : 0, | |||
sortGroups: $sortGroupBy | |||
}, | |||
function (result) { | |||
var lis = []; | |||
if (result.status === 'success') { | |||
$.each(result.data, function (i, subset) { | |||
$.each(subset, function (index, group) { | |||
if (GroupList.getGroupLI(group.name).length > 0) { | |||
GroupList.setUserCount(GroupList.getGroupLI(group.name).first(), group.usercount); | |||
} | |||
else { | |||
var $li = GroupList.addGroup(group.id, group.name, group.usercount); | |||
$li.addClass('appear transparent'); | |||
lis.push($li); | |||
} | |||
}); | |||
}); | |||
if (result.data.length > 0) { | |||
GroupList.doSort(); | |||
} | |||
else { | |||
GroupList.noMoreEntries = true; | |||
} | |||
_.defer(function () { | |||
$(lis).each(function () { | |||
this.removeClass('transparent'); | |||
}); | |||
}); | |||
} | |||
GroupList.updating = false; | |||
} | |||
); | |||
}, | |||
elementBelongsToAddGroup: function (el) { | |||
return !(el !== $('#newgroup-form').get(0) && | |||
$('#newgroup-form').find($(el)).length === 0); | |||
}, | |||
hasAddGroupNameText: function () { | |||
var name = $('#newgroupname').val(); | |||
return $.trim(name) !== ''; | |||
}, | |||
showDisabledUsers: function () { | |||
UserList.empty(); | |||
UserList.update('_disabledUsers'); | |||
$userGroupList.find('li').removeClass('active'); | |||
GroupList.getGroupLI('_disabledUsers').addClass('active'); | |||
}, | |||
showGroup: function (gid) { | |||
GroupList.activeGID = gid; | |||
UserList.empty(); | |||
UserList.update(gid === '_everyone' ? '' : gid); | |||
$userGroupList.find('li').removeClass('active'); | |||
if (gid !== undefined) { | |||
GroupList.getGroupLI(gid).addClass('active'); | |||
} | |||
}, | |||
isAddGroupButtonVisible: function () { | |||
return !$('#newgroup-entry').hasClass('editing'); | |||
}, | |||
toggleAddGroup: function (event) { | |||
if (GroupList.isAddGroupButtonVisible()) { | |||
if (event) { | |||
event.stopPropagation(); | |||
} | |||
$('#newgroup-entry').addClass('editing'); | |||
$('#newgroupname').select(); | |||
GroupList.handleAddGroupInput(''); | |||
} | |||
else { | |||
$('#newgroup-entry').removeClass('editing'); | |||
$('#newgroupname').val(''); | |||
} | |||
}, | |||
handleAddGroupInput: function (input) { | |||
if(input.length) { | |||
$('#newgroup-form input[type="submit"]').attr('disabled', null); | |||
} else { | |||
$('#newgroup-form input[type="submit"]').attr('disabled', 'disabled'); | |||
} | |||
}, | |||
isGroupNameValid: function (groupname) { | |||
if ($.trim(groupname) === '') { | |||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', { | |||
message: t('settings', 'A valid group name must be provided') | |||
})); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
hide: function (gid) { | |||
GroupList.getGroupLI(gid).hide(); | |||
}, | |||
show: function (gid) { | |||
GroupList.getGroupLI(gid).show(); | |||
}, | |||
remove: function (gid) { | |||
GroupList.getGroupLI(gid).remove(); | |||
}, | |||
empty: function () { | |||
$userGroupList.find('.isgroup').filter(function(index, item){ | |||
return $(item).data('gid') !== ''; | |||
}).remove(); | |||
}, | |||
initDeleteHandling: function () { | |||
//set up handler | |||
var GroupDeleteHandler = new DeleteHandler('/settings/users/groups', 'groupname', | |||
GroupList.hide, GroupList.remove); | |||
//configure undo | |||
OC.Notification.hide(); | |||
var msg = escapeHTML(t('settings', 'deleted {groupName}', {groupName: '%oid'})) + '<span class="undo">' + | |||
escapeHTML(t('settings', 'undo')) + '</span>'; | |||
GroupDeleteHandler.setNotification(OC.Notification, 'deletegroup', msg, | |||
GroupList.show); | |||
//when to mark user for delete | |||
var deleteAction = function () { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(deleteAction, this)); | |||
return; | |||
} | |||
// Call function for handling delete/undo | |||
GroupDeleteHandler.mark(GroupList.getElementGID($(this).parent())); | |||
}; | |||
$userGroupList.on('click', '.delete', deleteAction); | |||
//delete a marked user when leaving the page | |||
$(window).on('beforeunload', function () { | |||
GroupDeleteHandler.deleteEntry(); | |||
}); | |||
}, | |||
getGroupLI: function (gid) { | |||
return $userGroupList.find('li.isgroup').filter(function () { | |||
return GroupList.getElementGID(this) === gid; | |||
}); | |||
}, | |||
getElementGID: function (element) { | |||
return ($(element).closest('li').data('gid') || '').toString(); | |||
}, | |||
getEveryoneCount: function () { | |||
$.ajax({ | |||
type: "GET", | |||
dataType: "json", | |||
url: OC.generateUrl('/settings/users/stats') | |||
}).success(function (data) { | |||
$('#everyonegroup').data('usercount', data.totalUsers); | |||
$('#everyonecount').text(data.totalUsers); | |||
}); | |||
} | |||
}; | |||
$(document).ready( function () { | |||
$userGroupList = $('#usergrouplist'); | |||
GroupList.initDeleteHandling(); | |||
$sortGroupBy = $userGroupList.data('sort-groups'); | |||
if ($sortGroupBy === 1) { | |||
// Disabled due to performance issues, when we don't need it for sorting | |||
GroupList.getEveryoneCount(); | |||
} | |||
// Display or hide of Create Group List Element | |||
$('#newgroup-init').on('click', function (e) { | |||
GroupList.toggleAddGroup(e); | |||
}); | |||
$(document).on('click keydown keyup', function(event) { | |||
if(!GroupList.isAddGroupButtonVisible() && | |||
!GroupList.elementBelongsToAddGroup(event.target) && | |||
!GroupList.hasAddGroupNameText()) { | |||
GroupList.toggleAddGroup(); | |||
} | |||
// Escape | |||
if(!GroupList.isAddGroupButtonVisible() && event.keyCode && event.keyCode === 27) { | |||
GroupList.toggleAddGroup(); | |||
} | |||
}); | |||
// Responsible for Creating Groups. | |||
$('#newgroup-form form').submit(function (event) { | |||
event.preventDefault(); | |||
if(GroupList.isGroupNameValid($('#newgroupname').val())) { | |||
GroupList.createGroup($('#newgroupname').val()); | |||
} | |||
}); | |||
// click on group name | |||
$userGroupList.on('click', '.isgroup', function () { | |||
GroupList.showGroup(GroupList.getElementGID(this)); | |||
}); | |||
// show disabled users | |||
$userGroupList.on('click', '.disabledusers', function () { | |||
GroupList.showDisabledUsers(); | |||
}); | |||
$('#newgroupname').on('input', function(){ | |||
GroupList.handleAddGroupInput(this.value); | |||
}); | |||
// highlight `everyone` group at DOMReady by default | |||
GroupList.showGroup('_everyone'); | |||
}); |
@@ -0,0 +1,24 @@ | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2018 John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* | |||
* | |||
* @license AGPL-3.0 | |||
* | |||
* This code is free software: you can redistribute it and/or modify | |||
* it under the terms of the GNU Affero General Public License, version 3, | |||
* as published by the Free Software Foundation. | |||
* | |||
* 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, version 3, | |||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
$tmpl = new OC_Template("settings", "settings", "user"); | |||
$tmpl->printPage(); |
@@ -0,0 +1,42 @@ | |||
{ | |||
"name": "settings", | |||
"description": "Nextcloud settings", | |||
"version": "1.0.0", | |||
"author": "John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>", | |||
"license": "AGPL3", | |||
"private": true, | |||
"scripts": { | |||
"dev": "cross-env NODE_ENV=development webpack", | |||
"watch": "cross-env NODE_ENV=development webpack --progress --watch", | |||
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules" | |||
}, | |||
"dependencies": { | |||
"axios": "^0.18.0", | |||
"vue": "^2.5.11", | |||
"vue-click-outside": "^1.0.7", | |||
"vue-infinite-loading": "^2.2.3", | |||
"vue-localstorage": "^0.6.2", | |||
"vue-multiselect": "^2.1", | |||
"vue-router": "^3.0.1", | |||
"vuex": "^3.0.1", | |||
"vuex-router-sync": "^5.0.0" | |||
}, | |||
"browserslist": [ | |||
"last 2 versions", | |||
"ie >= 11" | |||
], | |||
"devDependencies": { | |||
"babel-core": "^6.26.0", | |||
"babel-loader": "^7.1.2", | |||
"babel-preset-env": "^1.6.0", | |||
"babel-preset-stage-3": "^6.24.1", | |||
"cross-env": "^5.0.5", | |||
"css-loader": "^0.28.7", | |||
"file-loader": "^1.1.4", | |||
"node-sass": "^4.5.3", | |||
"sass-loader": "^6.0.6", | |||
"vue-loader": "^13.0.5", | |||
"vue-template-compiler": "^2.4.4", | |||
"webpack": "^3.6.0" | |||
} | |||
} |
@@ -50,7 +50,7 @@ $application->registerRoutes($this, [ | |||
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'], | |||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'], | |||
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], | |||
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], | |||
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{id}/displayName', 'verb' => 'PUT'], | |||
['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], | |||
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'], | |||
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'], |
@@ -0,0 +1,3 @@ | |||
{ | |||
"esversion": 6 | |||
} |
@@ -0,0 +1,16 @@ | |||
<template> | |||
<router-view></router-view> | |||
</template> | |||
<script> | |||
export default { | |||
name: 'App', | |||
beforeMount: function () { | |||
// importing server data into the store | |||
const serverDataElmt = document.getElementById('serverData'); | |||
if (serverDataElmt !== null) { | |||
this.$store.commit('setServerData', JSON.parse(document.getElementById('serverData').dataset.server)); | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,32 @@ | |||
<template> | |||
<div id="app-navigation"> | |||
<div class="app-navigation-new" v-if="menu.new"> | |||
<button type="button" :id="menu.new.id" :class="menu.new.icon" @click="menu.new.action">{{menu.new.text}}</button> | |||
</div> | |||
<ul :id="menu.id"> | |||
<navigation-item v-for="(item, key) in menu.items" :item="item" :key="key" /> | |||
</ul> | |||
<div id="app-settings"> | |||
<div id="app-settings-header"> | |||
<button class="settings-button" | |||
data-apps-slide-toggle="#app-settings-content" | |||
>{{t('settings', 'Settings')}}</button> | |||
</div> | |||
<div id="app-settings-content"> | |||
<slot name="settings-content"></slot> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import navigationItem from './appNavigation/navigationItem'; | |||
export default { | |||
name: 'appNavigation', | |||
props: ['menu'], | |||
components: { | |||
navigationItem | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,108 @@ | |||
<template> | |||
<li :id="item.id" :class="[{'icon-loading-small': item.loading, 'open': item.opened, 'collapsible': item.collapsible&&item.children&&item.children.length>0 }, item.classes]"> | |||
<!-- Bullet --> | |||
<div v-if="item.bullet" class="app-navigation-entry-bullet" :style="{ backgroundColor: item.bullet }"></div> | |||
<!-- Main link --> | |||
<a :href="(item.href) ? item.href : '#' " @click="toggleCollapse" :class="item.icon" >{{item.text}}</a> | |||
<!-- Popover, counter and button(s) --> | |||
<div v-if="item.utils" class="app-navigation-entry-utils"> | |||
<ul> | |||
<!-- counter --> | |||
<li v-if="Number.isInteger(item.utils.counter)" | |||
class="app-navigation-entry-utils-counter">{{item.utils.counter}}</li> | |||
<!-- first action if only one action and counter --> | |||
<li v-if="item.utils.actions && item.utils.actions.length === 1 && Number.isInteger(item.utils.counter)" | |||
class="app-navigation-entry-utils-menu-button"> | |||
<button :class="item.utils.actions[0].icon"></button> | |||
</li> | |||
<!-- second action only two actions and no counter --> | |||
<li v-else-if="item.utils.actions && item.utils.actions.length === 2 && !Number.isInteger(item.utils.counter)" | |||
v-for="action in item.utils.actions" :key="action.action" | |||
class="app-navigation-entry-utils-menu-button"> | |||
<button :class="action.icon"></button> | |||
</li> | |||
<!-- menu if only at least one action and counter OR two actions and no counter--> | |||
<li v-else-if="item.utils.actions && item.utils.actions.length > 1 && (Number.isInteger(item.utils.counter) || item.utils.actions.length > 2)" | |||
class="app-navigation-entry-utils-menu-button"> | |||
<button v-click-outside="hideMenu" @click="showMenu" ></button> | |||
</li> | |||
</ul> | |||
</div> | |||
<!-- if more than 2 actions or more than 1 actions with counter --> | |||
<div v-if="item.utils && item.utils.actions && item.utils.actions.length > 1 && (Number.isInteger(item.utils.counter) || item.utils.actions.length > 2)" | |||
class="app-navigation-entry-menu" :class="{ 'open': openedMenu }"> | |||
<popover-menu :menu="item.utils.actions"/> | |||
</div> | |||
<!-- undo entry --> | |||
<div class="app-navigation-entry-deleted" v-if="item.undo"> | |||
<div class="app-navigation-entry-deleted-description">{{item.undo.text}}</div> | |||
<button class="app-navigation-entry-deleted-button icon-history" :title="t('settings', 'Undo')"></button> | |||
</div> | |||
<!-- edit entry --> | |||
<div class="app-navigation-entry-edit" v-if="item.edit"> | |||
<form> | |||
<input type="text" v-model="item.text"> | |||
<input type="submit" value="" class="icon-confirm"> | |||
<input type="submit" value="" class="icon-close" @click.stop.prevent="cancelEdit"> | |||
</form> | |||
</div> | |||
<!-- if the item has children, inject the component with proper data --> | |||
<ul v-if="item.children"> | |||
<navigation-item v-for="(item, key) in item.children" :item="item" :key="key" /> | |||
</ul> | |||
</li> | |||
</template> | |||
<script> | |||
import popoverMenu from '../popoverMenu'; | |||
import ClickOutside from 'vue-click-outside'; | |||
import Vue from 'vue'; | |||
export default { | |||
name: 'navigationItem', | |||
props: ['item'], | |||
components: { | |||
popoverMenu | |||
}, | |||
directives: { | |||
ClickOutside | |||
}, | |||
data () { | |||
return { | |||
openedMenu: false | |||
} | |||
}, | |||
methods: { | |||
showMenu () { | |||
this.openedMenu = true; | |||
}, | |||
hideMenu () { | |||
this.openedMenu = false; | |||
}, | |||
toggleCollapse () { | |||
// if item.opened isn't set, Vue won't trigger view updates https://vuejs.org/v2/api/#Vue-set | |||
// ternary is here to detect the undefined state of item.opened | |||
Vue.set(this.item, 'opened', this.item.opened ? !this.item.opened : true); | |||
}, | |||
cancelEdit () { | |||
// remove the editing class | |||
if (Array.isArray(this.item.classes)) | |||
this.item.classes = this.item.classes.filter(item => item !== 'editing'); | |||
} | |||
}, | |||
mounted () { | |||
// prevent click outside event with popupItem. | |||
this.popupItem = this.$el; | |||
}, | |||
} | |||
</script> |
@@ -0,0 +1,18 @@ | |||
<template> | |||
<ul> | |||
<popover-item v-for="(item, key) in menu" :item="item" :key="key" /> | |||
</ul> | |||
</template> | |||
<script> | |||
import popoverItem from './popoverMenu/popoverItem'; | |||
export default { | |||
name: 'popoverMenu', | |||
props: ['menu'], | |||
components: { | |||
popoverItem | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,23 @@ | |||
<template> | |||
<li> | |||
<a @click="dispatchToStore" v-if="item.href" :href="(item.href) ? item.href : '#' "> | |||
<span :class="item.icon"></span> | |||
<span>{{item.text}}</span> | |||
</a> | |||
<button @click="dispatchToStore(item.action)" v-else> | |||
<span :class="item.icon"></span> | |||
<span>{{item.text}}</span> | |||
</button> | |||
</li> | |||
</template> | |||
<script> | |||
export default { | |||
props: ['item'], | |||
methods: { | |||
dispatchToStore () { | |||
this.$store.dispatch(this.item.action, this.item.data); | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,205 @@ | |||
<template> | |||
<div id="app-content" class="user-list-grid" v-on:scroll.passive="onScroll"> | |||
<div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}"> | |||
<div id="headerAvatar" class="avatar"></div> | |||
<div id="headerName" class="name">{{ t('settings', 'Username') }}</div> | |||
<div id="headerDisplayName" class="displayName">{{ t('settings', 'Full 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 id="headerSubAdmins" class="subadmins" | |||
v-if="subAdminsGroups.length>0">{{ t('settings', 'Group admin for') }}</div> | |||
<div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div> | |||
<div class="headerStorageLocation storageLocation" | |||
v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div> | |||
<div class="headerUserBackend userBackend" | |||
v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div> | |||
<div class="headerLastLogin lastLogin" | |||
v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div> | |||
<div class="userActions"></div> | |||
</div> | |||
<form class="row" id="new-user" v-show="showConfig.showNewUserForm" | |||
v-on:submit.prevent="createUser" :disabled="loading" | |||
:class="{'sticky': scrolled && showConfig.showNewUserForm}"> | |||
<div :class="loading?'icon-loading-small':'icon-add'"></div> | |||
<div class="name"> | |||
<input id="newusername" type="text" required v-model="newUser.id" | |||
:placeholder="t('settings', 'User name')" name="username" | |||
autocomplete="off" autocapitalize="none" autocorrect="off" | |||
pattern="[a-zA-Z0-9 _\.@\-']+"> | |||
</div> | |||
<div class="displayName"> | |||
<input id="newdisplayname" type="text" v-model="newUser.displayName" | |||
:placeholder="t('settings', 'Display name')" name="displayname" | |||
autocomplete="off" autocapitalize="none" autocorrect="off"> | |||
</div> | |||
<div class="password"> | |||
<input id="newuserpassword" type="password" v-model="newUser.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" type="email" v-model="newUser.mailAddress" | |||
:required="newUser.password===''" | |||
:placeholder="t('settings', 'Mail address')" name="email" | |||
autocomplete="off" autocapitalize="none" autocorrect="off"> | |||
</div> | |||
<div class="groups"> | |||
<multiselect :options="groups" v-model="newUser.groups" | |||
:placeholder="t('settings', 'Add user in group')" | |||
label="name" track-by="id" class="multiselect-vue" | |||
:multiple="true" :close-on-select="false"> | |||
<span slot="noResult">{{t('settings','No result')}}</span> | |||
</multiselect> | |||
</div> | |||
<div class="subadmins" v-if="subAdminsGroups.length>0"> | |||
<multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups" | |||
:placeholder="t('settings', 'Set user as admin for')" | |||
label="name" track-by="id" class="multiselect-vue" | |||
:multiple="true" :close-on-select="false"> | |||
<span slot="noResult">{{t('settings','No result')}}</span> | |||
</multiselect> | |||
</div> | |||
<div class="quota"> | |||
<multiselect :options="quotaOptions" v-model="newUser.quota" | |||
:placeholder="t('settings', 'Select user quota')" | |||
label="label" track-by="id" class="multiselect-vue" | |||
:allowEmpty="false" :taggable="true" | |||
@tag="validateQuota" > | |||
</multiselect> | |||
</div> | |||
<div class="storageLocation" v-if="showConfig.showStoragePath"></div> | |||
<div class="userBackend" v-if="showConfig.showUserBackend"></div> | |||
<div class="lastLogin" v-if="showConfig.showLastLogin"></div> | |||
<div class="userActions"> | |||
<input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip" | |||
value="" :title="t('settings', 'Add a new user')"> | |||
<input type="reset" id="newreset" class="button icon-close has-tooltip" @click="resetForm" | |||
value="" :title="t('settings', 'Cancel and reset the form')"> | |||
</div> | |||
</form> | |||
<user-row v-for="(user, key) in users" :user="user" :key="key" :settings="settings" :showConfig="showConfig" | |||
:groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" /> | |||
<infinite-loading @infinite="infiniteHandler"> | |||
<span slot="spinner"><div class="users-icon-loading"></div></span> | |||
<span slot="no-more"><div class="users-list-end">— {{t('settings', 'no more results')}} —</div></span> | |||
</infinite-loading> | |||
</div> | |||
</template> | |||
<script> | |||
import userRow from './userList/userRow'; | |||
import Multiselect from 'vue-multiselect'; | |||
import InfiniteLoading from 'vue-infinite-loading'; | |||
export default { | |||
name: 'userList', | |||
props: ['users', 'showConfig'], | |||
components: { | |||
userRow, | |||
Multiselect, | |||
InfiniteLoading | |||
}, | |||
data() { | |||
let unlimitedQuota = {id:'none', label:t('settings', 'Unlimited')}, | |||
defaultQuota = {id:'default', label:t('settings', 'Default quota')}; | |||
return { | |||
unlimitedQuota: unlimitedQuota, | |||
defaultQuota: defaultQuota, | |||
loading: false, | |||
scrolled: false, | |||
newUser: { | |||
id:'', | |||
displayName:'', | |||
password:'', | |||
mailAddress:'', | |||
groups: [], | |||
subAdminsGroups: [], | |||
quota: defaultQuota | |||
} | |||
}; | |||
}, | |||
mounted() { | |||
if (!this.settings.canChangePassword) { | |||
OC.Notification.showTemporary(t('settings','Password change is disabled because the master key is disabled')); | |||
} | |||
}, | |||
computed: { | |||
settings() { | |||
return this.$store.getters.getServerData; | |||
}, | |||
groups() { | |||
// data provided php side + remove the disabled group | |||
return this.$store.getters.getGroups.filter(group => group.id !== '_disabled'); | |||
}, | |||
subAdminsGroups() { | |||
// data provided php side | |||
return this.$store.getters.getServerData.subadmingroups; | |||
}, | |||
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; | |||
}, | |||
}, | |||
methods: { | |||
onScroll(event) { | |||
this.scrolled = event.target.scrollTop>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)); | |||
return this.newUser.quota = {id: quota, label: quota}; | |||
} | |||
// Default is unlimited | |||
return this.newUser.quota = this.quotaOptions[0]; | |||
}, | |||
infiniteHandler($state) { | |||
this.$store.dispatch('getUsers', {offset:this.usersOffset, limit:this.usersLimit}) | |||
.then((response) => {response?$state.loaded():$state.complete()}); | |||
}, | |||
resetForm () { | |||
// revert form to original state | |||
Object.assign(this.newUser, this.$options.data.call(this).newUser); | |||
this.loading = false; | |||
}, | |||
createUser() { | |||
this.loading = true; | |||
this.$store.dispatch('addUser', { | |||
userid: this.newUser.id, | |||
password: this.newUser.password, | |||
email: this.newUser.mailAddress, | |||
groups: this.newUser.groups.map(group => group.id) | |||
}).then(() =>this.resetForm()); | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,370 @@ | |||
<template> | |||
<div class="row"> | |||
<div class="avatar"><img 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> | |||
<form class="displayName" :class="{'icon-loading-small': loading.displayName}" v-on:submit.prevent="updateDisplayName"> | |||
<input :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 type="submit" class="icon-confirm" value="" /> | |||
</form> | |||
<form class="password" v-if="settings.canChangePassword" :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="groups" :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" :limitText="limitGroups" | |||
:multiple="true" :taggable="true" :closeOnSelect="false" | |||
@tag="createGroup" @select="addUserGroup" @remove="removeUserGroup"> | |||
</multiselect> | |||
</div> | |||
<div class="subadmins" v-if="subAdminsGroups.length>0" :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" :limitText="limitGroups" | |||
:multiple="true" :closeOnSelect="false" | |||
@select="addUserSubAdmin" @remove="removeUserSubAdmin"> | |||
<span slot="noResult">{{t('settings','No result')}}</span> | |||
</multiselect> | |||
</div> | |||
<div 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" | |||
: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="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" :title="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'"> | |||
<div class="icon-more" v-click-outside="hideMenu" @click="showMenu"></div> | |||
<div class="popovermenu" :class="{ 'open': openedMenu }"> | |||
<popover-menu :menu="userActions" /> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<script> | |||
import popoverMenu from '../popoverMenu'; | |||
import ClickOutside from 'vue-click-outside'; | |||
import Multiselect from 'vue-multiselect'; | |||
//import Multiselect from '../../../node_modules/vue-multiselect/src/index'; | |||
export default { | |||
name: 'userRow', | |||
props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig'], | |||
components: { | |||
popoverMenu, | |||
Multiselect | |||
}, | |||
directives: { | |||
ClickOutside | |||
}, | |||
mounted() { | |||
// prevent click outside event with popupItem. | |||
this.popupItem = this.$el; | |||
}, | |||
data() { | |||
return { | |||
rand: parseInt(Math.random() * 1000), | |||
openedMenu: false, | |||
loading: { | |||
all: false, | |||
displayName: false, | |||
password: false, | |||
mailAddress: false, | |||
groups: false, | |||
subadmins: false, | |||
quota: false | |||
} | |||
} | |||
}, | |||
computed: { | |||
/* USER POPOVERMENU ACTIONS */ | |||
userActions() { | |||
return [{ | |||
icon: 'icon-delete', | |||
text: t('settings','Delete user'), | |||
action: 'deleteUser', | |||
data: this.user.id | |||
},{ | |||
'icon': this.user.enabled ? 'icon-close' : 'icon-add', | |||
'text': this.user.enabled ? t('settings','Disable user') : t('settings','Enable user'), | |||
'action': 'enableDisableUser', | |||
data: {userid: this.user.id, enabled: !this.user.enabled} | |||
}] | |||
}, | |||
/* 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; | |||
}, | |||
/* QUOTA MANAGEMENT */ | |||
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 === 0 || 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; | |||
} | |||
}, | |||
methods: { | |||
/* MENU HANDLING */ | |||
showMenu () { | |||
this.openedMenu = true; | |||
}, | |||
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 the limit text in the selected options | |||
* | |||
* @param {int} count elements left | |||
* @returns {string} | |||
*/ | |||
limitGroups(count) { | |||
return '+'+count; | |||
}, | |||
/** | |||
* 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 | |||
* | |||
* @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}); | |||
}); | |||
return this.$store.getters.getGroups[this.groups.length]; | |||
}, | |||
/** | |||
* Add user to group | |||
* | |||
* @param {object} group Group object | |||
* @returns {Promise} | |||
*/ | |||
addUserGroup(group) { | |||
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) { | |||
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); | |||
}, | |||
/** | |||
* 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); | |||
}, | |||
/** | |||
* Validate quota string to make sure it's a valid human file size | |||
* | |||
* @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 === 0) { | |||
return this.setUserQuota('none'); | |||
} else if (validQuota !== null) { | |||
// unify format output | |||
return this.setUserQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota))); | |||
} | |||
// if no valid doo not change | |||
return false; | |||
} | |||
} | |||
} | |||
</script> |
@@ -0,0 +1,20 @@ | |||
import Vue from 'vue'; | |||
import { sync } from 'vuex-router-sync'; | |||
import App from './App.vue'; | |||
import router from './router'; | |||
import store from './store'; | |||
sync(store, router); | |||
// bind to window | |||
Vue.prototype.t = t; | |||
Vue.prototype.OC = OC; | |||
Vue.prototype.oc_userconfig = oc_userconfig; | |||
const app = new Vue({ | |||
router, | |||
store, | |||
render: h => h(App) | |||
}).$mount('#content'); | |||
export { app, router, store }; |
@@ -0,0 +1,23 @@ | |||
import Vue from 'vue'; | |||
import Router from 'vue-router'; | |||
import Users from './views/Users'; | |||
Vue.use(Router); | |||
/* | |||
* This is the list of routes where the vuejs app will | |||
* take over php to provide data | |||
* You need to forward the php routing (routes.php) to | |||
* /settings/main.php, where the vue-router will ensure | |||
* the proper route. | |||
* ⚠️ Routes needs to match the php routes. | |||
*/ | |||
export default new Router({ | |||
mode: 'history', | |||
base: window.location.pathName, | |||
routes: [{ | |||
path: '/settings/users', | |||
component: Users | |||
}] | |||
}); |
@@ -0,0 +1,50 @@ | |||
import axios from 'axios'; | |||
const requestToken = document.getElementsByTagName('head')[0].getAttribute('data-requesttoken'); | |||
const tokenHeaders = { headers: { requesttoken: requestToken } }; | |||
const sanitize = function(url) { | |||
return url.replace(/\/$/, ''); // Remove last slash of url | |||
} | |||
export default { | |||
requireAdmin() { | |||
return new Promise(function(resolve, reject) { | |||
setTimeout(reject, 5000); // automatically reject 5s if not ok | |||
function waitForpassword() { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
setTimeout(waitForpassword, 500); | |||
return; | |||
} | |||
resolve(); | |||
} | |||
waitForpassword(); | |||
OC.PasswordConfirmation.requirePasswordConfirmation(); | |||
}).catch((error) => console.log('Required password not entered')); | |||
}, | |||
get(url) { | |||
return axios.get(sanitize(url), tokenHeaders) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); | |||
}, | |||
post(url, data) { | |||
return axios.post(sanitize(url), data, tokenHeaders) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); | |||
}, | |||
patch(url, data) { | |||
return axios.patch(sanitize(url), { data: data, headers: tokenHeaders.headers }) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); | |||
}, | |||
put(url, data) { | |||
return axios.put(sanitize(url), data, tokenHeaders) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); | |||
}, | |||
delete(url, data) { | |||
return axios.delete(sanitize(url), { data: data, headers: tokenHeaders.headers }) | |||
.then((response) => Promise.resolve(response)) | |||
.catch((error) => Promise.reject(error)); | |||
} | |||
}; |
@@ -0,0 +1,24 @@ | |||
import Vue from 'vue' | |||
import Vuex from 'vuex' | |||
import users from './users' | |||
import settings from './settings' | |||
Vue.use(Vuex) | |||
const debug = process.env.NODE_ENV !== 'production' | |||
const mutations = { | |||
API_FAILURE(state, error) { | |||
console.log(state, error); | |||
} | |||
} | |||
export default new Vuex.Store({ | |||
modules: { | |||
users, | |||
settings | |||
}, | |||
strict: debug, | |||
mutations | |||
}) |
@@ -0,0 +1,18 @@ | |||
import api from './api'; | |||
const state = { | |||
serverData: {} | |||
}; | |||
const mutations = { | |||
setServerData(state, data) { | |||
state.serverData = data; | |||
} | |||
}; | |||
const getters = { | |||
getServerData(state) { | |||
return state.serverData; | |||
} | |||
} | |||
const actions = {} | |||
export default {state, mutations, getters, actions}; |
@@ -0,0 +1,380 @@ | |||
import api from './api'; | |||
const orderGroups = function(groups, orderBy) { | |||
/* const SORT_USERCOUNT = 1; | |||
* const SORT_GROUPNAME = 2; | |||
* https://github.com/nextcloud/server/blob/208e38e84e1a07a49699aa90dc5b7272d24489f0/lib/private/Group/MetaData.php#L34 | |||
*/ | |||
if (orderBy === 1) { | |||
return groups.sort((a, b) => a.usercount < b.usercount); | |||
} else { | |||
return groups.sort((a, b) => a.name.localeCompare(b.name)); | |||
} | |||
} | |||
const state = { | |||
users: [], | |||
groups: [], | |||
orderBy: 1, | |||
minPasswordLength: 0, | |||
usersOffset: 0, | |||
usersLimit: 25, | |||
}; | |||
const mutations = { | |||
appendUsers(state, usersObj) { | |||
// convert obj to array | |||
let users = state.users.concat(Object.keys(usersObj).map(userid => usersObj[userid])); | |||
state.usersOffset += state.usersLimit; | |||
state.users = users; | |||
}, | |||
setPasswordPolicyMinLength(state, length) { | |||
state.minPasswordLength = length!=='' ? length : 0; | |||
}, | |||
initGroups(state, {groups, orderBy}) { | |||
state.groups = groups; | |||
state.orderBy = orderBy; | |||
state.groups = orderGroups(state.groups, state.orderBy); | |||
}, | |||
addGroup(state, groupid) { | |||
try { | |||
state.groups.push({ | |||
id: groupid, | |||
name: groupid, | |||
usercount: 0 // user will be added after the creation | |||
}); | |||
state.groups = orderGroups(state.groups, state.orderBy); | |||
} catch (e) { | |||
console.log('Can\'t create group', e); | |||
} | |||
}, | |||
addUserGroup(state, { userid, gid }) { | |||
// this should not be needed as it would means the user contains a group | |||
// the server database doesn't have. | |||
let group = state.groups.find(groupSearch => groupSearch.id == gid); | |||
if (group) { | |||
group.usercount++; // increase count | |||
} | |||
let groups = state.users.find(user => user.id == userid).groups; | |||
groups.push(gid); | |||
state.groups = orderGroups(state.groups, state.orderBy); | |||
}, | |||
removeUserGroup(state, { userid, gid }) { | |||
// this should not be needed as it would means the user contains a group | |||
// the server database doesn't have. | |||
let group = state.groups.find(groupSearch => groupSearch.id == gid); | |||
if (group) { | |||
group.usercount--; // lower count | |||
} | |||
let groups = state.users.find(user => user.id == userid).groups; | |||
groups.splice(groups.indexOf(gid),1); | |||
state.groups = orderGroups(state.groups, state.orderBy); | |||
}, | |||
addUserSubAdmin(state, { userid, gid }) { | |||
let groups = state.users.find(user => user.id == userid).subadmin; | |||
groups.push(gid); | |||
}, | |||
removeUserSubAdmin(state, { userid, gid }) { | |||
let groups = state.users.find(user => user.id == userid).subadmin; | |||
groups.splice(groups.indexOf(gid),1); | |||
}, | |||
deleteUser(state, userid) { | |||
let userIndex = state.users.findIndex(user => user.id == userid); | |||
state.users.splice(userIndex, 1); | |||
}, | |||
addUserData(state, response) { | |||
state.users.push(response.data.ocs.data); | |||
}, | |||
enableDisableUser(state, { userid, enabled }) { | |||
state.users.find(user => user.id == userid).enabled = enabled; | |||
state.groups.find(group => group.id == '_disabled').usercount += enabled ? -1 : 1; | |||
}, | |||
setUserData(state, { userid, key, value }) { | |||
if (key === 'quota') { | |||
let humanValue = OC.Util.computerFileSize(value); | |||
state.users.find(user => user.id == userid)[key][key] = humanValue?humanValue:value; | |||
} else { | |||
state.users.find(user => user.id == userid)[key] = value; | |||
} | |||
}, | |||
}; | |||
const getters = { | |||
getUsers(state) { | |||
return state.users; | |||
}, | |||
getGroups(state) { | |||
return state.groups; | |||
}, | |||
getPasswordPolicyMinLength(state) { | |||
return state.minPasswordLength; | |||
}, | |||
getUsersOffset(state) { | |||
return state.usersOffset; | |||
}, | |||
getUsersLimit(state) { | |||
return state.usersLimit; | |||
} | |||
}; | |||
const actions = { | |||
/** | |||
* Get all users with full details | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {int} options.offset List offset to request | |||
* @param {int} options.limit List number to return from offset | |||
* @returns {Promise} | |||
*/ | |||
getUsers(context, { offset, limit, search }) { | |||
search = typeof search === 'string' ? search : ''; | |||
return api.get(OC.linkToOCS(`cloud/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2)) | |||
.then((response) => { | |||
if (Object.keys(response.data.ocs.data.users).length > 0) { | |||
context.commit('appendUsers', response.data.ocs.data.users); | |||
return true; | |||
} | |||
return false; | |||
}) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}, | |||
/** | |||
* Get all users with full details | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {int} options.offset List offset to request | |||
* @param {int} options.limit List number to return from offset | |||
* @returns {Promise} | |||
*/ | |||
getUsersFromList(context, { offset, limit, search }) { | |||
search = typeof search === 'string' ? search : ''; | |||
return api.get(OC.linkToOCS(`cloud/users/details?offset=${offset}&limit=${limit}&search=${search}`, 2)) | |||
.then((response) => { | |||
if (Object.keys(response.data.ocs.data.users).length > 0) { | |||
context.commit('appendUsers', response.data.ocs.data.users); | |||
return true; | |||
} | |||
return false; | |||
}) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}, | |||
/** | |||
* Get all users with full details from a groupid | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {int} options.offset List offset to request | |||
* @param {int} options.limit List number to return from offset | |||
* @returns {Promise} | |||
*/ | |||
getUsersFromGroup(context, { groupid, offset, limit }) { | |||
return api.get(OC.linkToOCS(`cloud/users/${groupid}/details?offset=${offset}&limit=${limit}`, 2)) | |||
.then((response) => context.commit('getUsersFromList', response.data.ocs.data.users)) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}, | |||
getPasswordPolicyMinLength(context) { | |||
return api.get(OC.linkToOCS('apps/provisioning_api/api/v1/config/apps/password_policy/minLength', 2)) | |||
.then((response) => context.commit('setPasswordPolicyMinLength', response.data.ocs.data.data)) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}, | |||
/** | |||
* Add group | |||
* | |||
* @param {Object} context | |||
* @param {string} gid Group id | |||
* @returns {Promise} | |||
*/ | |||
addGroup(context, gid) { | |||
return api.requireAdmin().then((response) => { | |||
return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid}) | |||
.then((response) => context.commit('addGroup', gid)) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}); | |||
}, | |||
/** | |||
* Add group | |||
* | |||
* @param {Object} context | |||
* @param {string} gid Group id | |||
* @returns {Promise} | |||
*/ | |||
removeGroup(context, gid) { | |||
return api.requireAdmin().then((response) => { | |||
return api.post(OC.linkToOCS(`cloud/groups`, 2), {groupid: gid}) | |||
.then((response) => context.commit('removeGroup', gid)) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}); | |||
}, | |||
/** | |||
* Add user to group | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.gid Group id | |||
* @returns {Promise} | |||
*/ | |||
addUserGroup(context, { userid, gid }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.post(OC.linkToOCS(`cloud/users/${userid}/groups`, 2), { groupid: gid }) | |||
.then((response) => context.commit('addUserGroup', { userid, gid })) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}); | |||
}, | |||
/** | |||
* Remove user from group | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.gid Group id | |||
* @returns {Promise} | |||
*/ | |||
removeUserGroup(context, { userid, gid }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.delete(OC.linkToOCS(`cloud/users/${userid}/groups`, 2), { groupid: gid }) | |||
.then((response) => context.commit('removeUserGroup', { userid, gid })) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** | |||
* Add user to group admin | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.gid Group id | |||
* @returns {Promise} | |||
*/ | |||
addUserSubAdmin(context, { userid, gid }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.post(OC.linkToOCS(`cloud/users/${userid}/subadmins`, 2), { groupid: gid }) | |||
.then((response) => context.commit('addUserSubAdmin', { userid, gid })) | |||
.catch((error) => context.commit('API_FAILURE', error)); | |||
}); | |||
}, | |||
/** | |||
* Remove user from group admin | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.gid Group id | |||
* @returns {Promise} | |||
*/ | |||
removeUserSubAdmin(context, { userid, gid }) { | |||
return api.requireAdmin().then((response) => { | |||
return api.delete(OC.linkToOCS(`cloud/users/${userid}/subadmins`, 2), { groupid: gid }) | |||
.then((response) => context.commit('removeUserSubAdmin', { userid, gid })) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** | |||
* Delete a user | |||
* | |||
* @param {Object} context | |||
* @param {string} userid User id | |||
* @returns {Promise} | |||
*/ | |||
deleteUser(context, userid) { | |||
return api.requireAdmin().then((response) => { | |||
return api.delete(OC.linkToOCS(`cloud/users/${userid}`, 2)) | |||
.then((response) => context.commit('deleteUser', userid)) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** | |||
* Add a user | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.password User password | |||
* @param {string} options.email User email | |||
* @returns {Promise} | |||
*/ | |||
addUser({context, dispatch}, {userid, password, email, groups}) { | |||
return api.requireAdmin().then((response) => { | |||
return api.post(OC.linkToOCS(`cloud/users`, 2), {userid, password, email, groups}) | |||
.then((response) => dispatch('addUserData', userid)) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** | |||
* Get user data and commit addition | |||
* | |||
* @param {Object} context | |||
* @param {string} userid User id | |||
* @returns {Promise} | |||
*/ | |||
addUserData(context, userid) { | |||
return api.requireAdmin().then((response) => { | |||
return api.get(OC.linkToOCS(`cloud/users/${userid}`, 2)) | |||
.then((response) => context.commit('addUserData', response)) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** Enable or disable user | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {boolean} options.enabled User enablement status | |||
* @returns {Promise} | |||
*/ | |||
enableDisableUser(context, { userid, enabled = true }) { | |||
let userStatus = enabled ? 'enable' : 'disable'; | |||
return api.requireAdmin().then((response) => { | |||
return api.put(OC.linkToOCS(`cloud/users/${userid}/${userStatus}`, 2)) | |||
.then((response) => context.commit('enableDisableUser', { userid, enabled })) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
}, | |||
/** | |||
* Edit user data | |||
* | |||
* @param {Object} context | |||
* @param {Object} options | |||
* @param {string} options.userid User id | |||
* @param {string} options.key User field to edit | |||
* @param {string} options.value Value of the change | |||
* @returns {Promise} | |||
*/ | |||
setUserData(context, { userid, key, value }) { | |||
if (['email', 'quota', 'displayname', 'password'].indexOf(key) !== -1) { | |||
// We allow empty email or displayname | |||
if (typeof value === 'string' && | |||
( | |||
(['quota', 'password'].indexOf(key) !== -1 && value.length > 0) || | |||
['email', 'displayname'].indexOf(key) !== -1 | |||
) | |||
) { | |||
return api.requireAdmin().then((response) => { | |||
return api.put(OC.linkToOCS(`cloud/users/${userid}`, 2), { key: key, value: value }) | |||
.then((response) => context.commit('setUserData', { userid, key, value })) | |||
.catch((error) => context.commit('API_FAILURE', { userid, error })); | |||
}); | |||
} | |||
} | |||
return Promise.reject(new Error('Invalid request data')); | |||
} | |||
}; | |||
export default { state, mutations, getters, actions }; |
@@ -0,0 +1,152 @@ | |||
<template> | |||
<div id="app"> | |||
<app-navigation :menu="menu"> | |||
<template slot="settings-content"> | |||
<div> | |||
<input type="checkbox" id="showLastLogin" class="checkbox" | |||
:checked="showLastLogin" v-model="showLastLogin"> | |||
<label for="showLastLogin">{{t('settings', 'Show last login')}}</label> | |||
</div> | |||
<div> | |||
<input type="checkbox" id="showUserBackend" class="checkbox" | |||
:checked="showUserBackend" v-model="showUserBackend"> | |||
<label for="showUserBackend">{{t('settings', 'Show user backend')}}</label> | |||
</div> | |||
<div> | |||
<input type="checkbox" id="showStoragePath" class="checkbox" | |||
:checked="showStoragePath" v-model="showStoragePath"> | |||
<label for="showStoragePath">{{t('settings', 'Show storage path')}}</label> | |||
</div> | |||
</template> | |||
</app-navigation> | |||
<user-list :users="users" :showConfig="showConfig" /> | |||
</div> | |||
</template> | |||
<script> | |||
import appNavigation from '../components/appNavigation'; | |||
import userList from '../components/userList'; | |||
import Vue from 'vue'; | |||
import VueLocalStorage from 'vue-localstorage' | |||
Vue.use(VueLocalStorage) | |||
export default { | |||
name: 'Users', | |||
components: { | |||
appNavigation, | |||
userList | |||
}, | |||
beforeMount() { | |||
this.$store.commit('initGroups', { | |||
groups: this.$store.getters.getServerData.groups, | |||
orderBy: this.$store.getters.getServerData.sortGroups | |||
}); | |||
this.$store.dispatch('getPasswordPolicyMinLength'); | |||
}, | |||
data() { | |||
return { | |||
showConfig: { | |||
showStoragePath: false, | |||
showUserBackend: false, | |||
showLastLogin: false, | |||
showNewUserForm: false | |||
} | |||
} | |||
}, | |||
methods: { | |||
getLocalstorage(key) { | |||
// force initialization | |||
this.showConfig[key] = this.$localStorage.get(key) === 'true'; | |||
return this.showConfig[key]; | |||
}, | |||
setLocalStorage(key, status) { | |||
this.showConfig[key] = status; | |||
this.$localStorage.set(key, status); | |||
return status; | |||
} | |||
}, | |||
computed: { | |||
users() { | |||
return this.$store.getters.getUsers; | |||
}, | |||
loading() { | |||
return Object.keys(this.users).length === 0; | |||
}, | |||
usersOffset() { | |||
return this.$store.getters.getUsersOffset; | |||
}, | |||
usersLimit() { | |||
return this.$store.getters.getUsersLimit; | |||
}, | |||
showLastLogin: { | |||
get: function() {return this.getLocalstorage('showLastLogin')}, | |||
set: function(status) { | |||
this.setLocalStorage('showLastLogin', status); | |||
} | |||
}, | |||
showUserBackend: { | |||
get: function() {return this.getLocalstorage('showUserBackend')}, | |||
set: function(status) { | |||
this.setLocalStorage('showUserBackend', status); | |||
} | |||
}, | |||
showStoragePath: { | |||
get: function() {return this.getLocalstorage('showStoragePath')}, | |||
set: function(status) { | |||
this.setLocalStorage('showStoragePath', status); | |||
} | |||
}, | |||
menu() { | |||
let self = this; | |||
// Data provided php side | |||
let groups = this.$store.getters.getGroups; | |||
groups = Array.isArray(groups) ? groups : []; | |||
// Map groups | |||
groups = groups.map(group => { | |||
let item = {}; | |||
item.id = group.id.replace(' ', '_'); | |||
item.classes = []; | |||
item.href = '#group'+group.id.replace(' ', '_'); | |||
item.text = group.name; | |||
item.utils = {counter: group.usercount}; | |||
return item; | |||
}); | |||
// Adjust data | |||
if (groups[0].id === 'admin') { | |||
groups[0].text = t('settings', 'Admins');} // rename admin group | |||
if (groups[1].id === '_disabled') { | |||
groups[1].text = t('settings', 'Disabled users'); // rename disabled group | |||
if (groups[1].utils.counter === 0) { | |||
groups.splice(1, 1); // remove disabled if empty | |||
} | |||
} | |||
// Add everyone group | |||
groups.unshift({ | |||
id: '_everyone', | |||
classes: ['active'], | |||
href:'#group_everyone', | |||
text: t('settings', 'Everyone'), | |||
utils: {counter: this.users.length} | |||
}); | |||
// Return | |||
return { | |||
id: 'usergrouplist', | |||
new: { | |||
id:'new-user-button', | |||
text: t('settings','New user'), | |||
icon: 'icon-add', | |||
action: function(){self.showConfig.showNewUserForm=!self.showConfig.showNewUserForm} | |||
}, | |||
items: groups | |||
} | |||
} | |||
} | |||
} | |||
</script> | |||
<style lang="scss"> | |||
</style> |
@@ -1,9 +1,27 @@ | |||
<?php /** | |||
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/?> | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This is the default empty template to load Vue! | |||
* Do your cbackend computations into a php files | |||
* then serve this file as template and include your data into | |||
* the $serverData template variable | |||
* | |||
* $tmpl = new OC_Template('settings', 'settings', 'user'); | |||
* $tmpl->assign('serverData', $serverData); | |||
* $tmpl->printPage(); | |||
<?php foreach($_['forms'] as $form) { | |||
print_unescaped($form); | |||
} | |||
*/ | |||
script('settings', 'main'); | |||
style('settings', 'settings'); | |||
// Did we have some data to inject ? | |||
if(is_array($_['serverData'])) { | |||
$serverData = json_encode($_['serverData']); | |||
?> | |||
<span id="serverData" data-server="<?php p($serverData);?>"></span> | |||
<?php } ?> |
@@ -1,80 +0,0 @@ | |||
<?php | |||
/** | |||
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com> | |||
* Copyright (c) 2017, John Molakvoæ <skjnldsv@protonmail.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
script('settings', [ | |||
'users/deleteHandler', | |||
'users/filter', | |||
'users/users', | |||
'users/groups' | |||
]); | |||
script('core', [ | |||
'multiselect', | |||
'singleselect' | |||
]); | |||
style('settings', 'settings'); | |||
$userlistParams = array(); | |||
$allGroups=array(); | |||
foreach($_["adminGroup"] as $group) { | |||
$allGroups[$group['id']] = array('displayName' => $group['name']); | |||
} | |||
foreach($_["groups"] as $group) { | |||
$allGroups[$group['id']] = array('displayName' => $group['name']); | |||
} | |||
$userlistParams['subadmingroups'] = $allGroups; | |||
$userlistParams['allGroups'] = json_encode($allGroups); | |||
$items = array_flip($userlistParams['subadmingroups']); | |||
unset($items['admin']); | |||
$userlistParams['subadmingroups'] = array_flip($items); | |||
translation('settings'); | |||
?> | |||
<div id="app-navigation"> | |||
<?php print_unescaped($this->inc('users/part.createuser')); ?> | |||
<?php print_unescaped($this->inc('users/part.grouplist')); ?> | |||
<div id="app-settings"> | |||
<div id="app-settings-header"> | |||
<button class="settings-button" tabindex="0" data-apps-slide-toggle="#app-settings-content"><?php p($l->t('Settings'));?></button> | |||
</div> | |||
<div id="app-settings-content"> | |||
<?php print_unescaped($this->inc('users/part.setquota')); ?> | |||
<div id="userlistoptions"> | |||
<p> | |||
<input type="checkbox" name="StorageLocation" value="StorageLocation" id="CheckboxStorageLocation" | |||
class="checkbox" <?php if ($_['show_storage_location'] === 'true') print_unescaped('checked="checked"'); ?> /> | |||
<label for="CheckboxStorageLocation"> | |||
<?php p($l->t('Show storage location')) ?> | |||
</label> | |||
</p> | |||
<p> | |||
<input type="checkbox" name="UserBackend" value="UserBackend" id="CheckboxUserBackend" | |||
class="checkbox" <?php if ($_['show_backend'] === 'true') print_unescaped('checked="checked"'); ?> /> | |||
<label for="CheckboxUserBackend"> | |||
<?php p($l->t('Show user backend')) ?> | |||
</label> | |||
</p> | |||
<p> | |||
<input type="checkbox" name="LastLogin" value="LastLogin" id="CheckboxLastLogin" | |||
class="checkbox" <?php if ($_['show_last_login'] === 'true') print_unescaped('checked="checked"'); ?> /> | |||
<label for="CheckboxLastLogin"> | |||
<?php p($l->t('Show last login')) ?> | |||
</label> | |||
</p> | |||
<p class="info-text"> | |||
<?php p($l->t('When the password of a new user is left empty, an activation email with a link to set the password is sent.')) ?> | |||
</p> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<div id="app-content"> | |||
<?php print_unescaped($this->inc('users/part.userlist', $userlistParams)); ?> | |||
</div> |
@@ -1,3 +0,0 @@ | |||
<div class="app-navigation-new"> | |||
<button type="button" id="new-user-button" class="icon-add"><?php p($l->t('Add user'))?></button> | |||
</div> |
@@ -1,69 +0,0 @@ | |||
<ul id="usergrouplist" data-sort-groups="<?php p($_['sortGroups']); ?>"> | |||
<!-- Add new group --> | |||
<?php if ($_['isAdmin']) { ?> | |||
<li id="newgroup-entry"> | |||
<a href="#" class="icon-add" id="newgroup-init"><?php p($l->t('Add group'))?></a> | |||
<div class="app-navigation-entry-edit" id="newgroup-form"> | |||
<form> | |||
<input type="text" id="newgroupname" placeholder="<?php p($l->t('Add group'))?>"> | |||
<input type="submit" value="" class="icon-checkmark"> | |||
</form> | |||
</div> | |||
</li> | |||
<?php } ?> | |||
<!-- Everyone --> | |||
<li id="everyonegroup" data-gid="_everyone" data-usercount="" class="isgroup"> | |||
<a href="#"> | |||
<span class="groupname"> | |||
<?php p($l->t('Everyone')); ?> | |||
</span> | |||
</a> | |||
<div class="app-navigation-entry-utils"> | |||
<ul> | |||
<li class="usercount app-navigation-entry-utils-counter" id="everyonecount"></li> | |||
</ul> | |||
</div> | |||
</li> | |||
<!-- The Admin Group --> | |||
<?php foreach($_["adminGroup"] as $adminGroup): ?> | |||
<li data-gid="admin" data-usercount="<?php if($adminGroup['usercount'] > 0) { p($adminGroup['usercount']); } ?>" class="isgroup"> | |||
<a href="#"><span class="groupname"><?php p($l->t('Admins')); ?></span></a> | |||
<div class="app-navigation-entry-utils"> | |||
<ul> | |||
<li class="app-navigation-entry-utils-counter"><?php if($adminGroup['usercount'] > 0) { p($adminGroup['usercount']); } ?></li> | |||
</ul> | |||
</div> | |||
</li> | |||
<?php endforeach; ?> | |||
<!-- Disabled Users --> | |||
<?php $disabledUsersGroup = $_["disabledUsersGroup"] ?> | |||
<li data-gid="_disabledUsers" data-usercount="<?php if($disabledUsersGroup['usercount'] > 0) { p($disabledUsersGroup['usercount']); } ?>" class="isgroup"> | |||
<a href="#"><span class="groupname"><?php p($l->t('Disabled')); ?></span></a> | |||
<div class="app-navigation-entry-utils"> | |||
<ul> | |||
<li class="app-navigation-entry-utils-counter"><?php if($disabledUsersGroup['usercount'] > 0) { p($disabledUsersGroup['usercount']); } ?></li> | |||
</ul> | |||
</div> | |||
</li> | |||
<!--List of Groups--> | |||
<?php foreach($_["groups"] as $group): ?> | |||
<li data-gid="<?php p($group['id']) ?>" data-usercount="<?php p($group['usercount']) ?>" class="isgroup"> | |||
<a href="#" class="dorename"> | |||
<span class="groupname"><?php p($group['name']); ?></span> | |||
</a> | |||
<div class="app-navigation-entry-utils"> | |||
<ul> | |||
<li class="app-navigation-entry-utils-counter"><?php if($group['usercount'] > 0) { p($group['usercount']); } ?></li> | |||
<?php if($_['isAdmin']): ?> | |||
<li class="app-navigation-entry-utils-menu-button delete"> | |||
<button class="icon-delete"></button> | |||
</li> | |||
<?php endif; ?> | |||
</ul> | |||
</div> | |||
</li> | |||
<?php endforeach; ?> | |||
</ul> |
@@ -1,35 +0,0 @@ | |||
<div class="quota"> | |||
<!-- Default storage --> | |||
<span><?php p($l->t('Default quota'));?></span> | |||
<?php if((bool) $_['isAdmin']): ?> | |||
<select id='default_quota' data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>" data-tipsy-gravity="s"> | |||
<option <?php if($_['default_quota'] === 'none') print_unescaped('selected="selected"');?> value='none'> | |||
<?php p($l->t('Unlimited'));?> | |||
</option> | |||
<?php foreach($_['quota_preset'] as $preset):?> | |||
<?php if($preset !== 'default'):?> | |||
<option <?php if($_['default_quota']==$preset) print_unescaped('selected="selected"');?> value='<?php p($preset);?>'> | |||
<?php p($preset);?> | |||
</option> | |||
<?php endif;?> | |||
<?php endforeach;?> | |||
<?php if($_['defaultQuotaIsUserDefined']):?> | |||
<option selected="selected" value='<?php p($_['default_quota']);?>'> | |||
<?php p($_['default_quota']);?> | |||
</option> | |||
<?php endif;?> | |||
<option data-new value='other'> | |||
<?php p($l->t('Other'));?> | |||
... | |||
</option> | |||
</select> | |||
<?php endif; ?> | |||
<?php if((bool) !$_['isAdmin']): ?> | |||
: | |||
<?php if( $_['default_quota'] === 'none'): ?> | |||
<?php p($l->t('Unlimited'));?> | |||
<?php else: ?> | |||
<?php p($_['default_quota']);?> | |||
<?php endif; ?> | |||
<?php endif; ?> | |||
</div> |
@@ -1,149 +0,0 @@ | |||
<form class="newUserMenu" id="newuser" autocomplete="off"> | |||
<table id="userlist" class="grid" data-groups="<?php p($_['allGroups']);?>"> | |||
<thead> | |||
<tr> | |||
<th id="headerAvatar" scope="col"></th> | |||
<th id="headerName" scope="col"><?php p($l->t('Username'))?></th> | |||
<th id="headerDisplayName" scope="col"><?php p($l->t( 'Full name' )); ?></th> | |||
<th id="headerPassword" scope="col"><?php p($l->t( 'Password' )); ?></th> | |||
<th class="mailAddress" scope="col"><?php p($l->t( 'Email' )); ?></th> | |||
<th id="headerGroups" scope="col"><?php p($l->t( 'Groups' )); ?></th> | |||
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?> | |||
<th id="headerSubAdmins" scope="col"><?php p($l->t('Group admin for')); ?></th> | |||
<?php endif;?> | |||
<?php if((bool)$_['recoveryAdminEnabled']): ?> | |||
<th id="recoveryPassword" scope="col"><?php p($l->t('Recovery password')); ?></th> | |||
<?php endif; ?> | |||
<th id="headerQuota" scope="col"><?php p($l->t('Quota')); ?></th> | |||
<th class="storageLocation" scope="col"><?php p($l->t('Storage location')); ?></th> | |||
<th class="userBackend" scope="col"><?php p($l->t('User backend')); ?></th> | |||
<th class="lastLogin" scope="col"><?php p($l->t('Last login')); ?></th> | |||
<th class="userActions"></th> | |||
</tr> | |||
<tr id="newuserHeader" style="display:none"> | |||
<td class="icon-add"></td> | |||
<td class="name"> | |||
<input id="newusername" type="text" required | |||
placeholder="<?php p($l->t('Username'))?>" name="username" | |||
autocomplete="off" autocapitalize="none" autocorrect="off" /> | |||
</td> | |||
<td class="displayName"> | |||
<input id="newdisplayname" type="text" | |||
placeholder="<?php p($l->t('Full name'))?>" name="displayname" | |||
autocomplete="off" autocapitalize="none" autocorrect="off" /> | |||
</td> | |||
<td class="password"> | |||
<input id="newuserpassword" type="password" | |||
placeholder="<?php p($l->t('Password'))?>" name="password" | |||
autocomplete="new-password" autocapitalize="none" autocorrect="off" /> | |||
</td> | |||
<td class="mailAddress"> | |||
<input id="newemail" type="email" | |||
placeholder="<?php p($l->t('E-Mail'))?>" name="email" | |||
autocomplete="off" autocapitalize="none" autocorrect="off" /> | |||
</td> | |||
<td class="groups"> | |||
<div class="groupsListContainer multiselect button" data-placeholder="<?php p($l->t('Groups'))?>"> | |||
<span class="title groupsList"></span> | |||
<span class="icon-triangle-s"></span> | |||
</div> | |||
</td> | |||
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?> | |||
<td></td> | |||
<?php endif;?> | |||
<?php if((bool)$_['recoveryAdminEnabled']): ?> | |||
<td class="recoveryPassword"> | |||
<input id="recoveryPassword" | |||
type="password" | |||
placeholder="<?php p($l->t('Admin Recovery Password'))?>" | |||
title="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>" | |||
alt="<?php p($l->t('Enter the recovery password in order to recover the users files during password change'))?>"/> | |||
</td> | |||
<?php endif; ?> | |||
<td class="quota"></td> | |||
<td class="storageLocation" scope="col"></td> | |||
<td class="userBackend" scope="col"></td> | |||
<td class="lastLogin" scope="col"></td> | |||
<td class="userActions"> | |||
<input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip" value="" title="<?php p($l->t('Add user'))?>" /> | |||
<input type="reset" id="newreset" class="button icon-close has-tooltip" value="" title="<?php p($l->t('Cancel'))?>" /> | |||
</td> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
<!-- the following <tr> is used as a template for the JS part --> | |||
<tr style="display:none"> | |||
<td class="avatar"><div class="avatardiv"></div></td> | |||
<td class="name" scope="row"></td> | |||
<td class="displayName"><span></span> <img class="action" | |||
src="<?php p(image_path('core', 'actions/rename.svg'))?>" | |||
alt="<?php p($l->t('change full name'))?>" title="<?php p($l->t('change full name'))?>"/> | |||
</td> | |||
<td class="password"><span>●●●●●●●</span> <img class="action" | |||
src="<?php print_unescaped(image_path('core', 'actions/rename.svg'))?>" | |||
alt="<?php p($l->t('set new password'))?>" title="<?php p($l->t('set new password'))?>"/> | |||
</td> | |||
<td class="mailAddress"><span></span><div class="loading-small hidden"></div> <img class="action" | |||
src="<?php p(image_path('core', 'actions/rename.svg'))?>" | |||
alt="<?php p($l->t('change email address'))?>" title="<?php p($l->t('change email address'))?>"/> | |||
</td> | |||
<td class="groups"><div class="groupsListContainer multiselect button" | |||
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div> | |||
</td> | |||
<?php if(is_array($_['subadmins']) || $_['subadmins']): ?> | |||
<td class="subadmins"><div class="groupsListContainer multiselect button" | |||
><span class="title groupsList"></span><span class="icon-triangle-s"></span></div> | |||
</td> | |||
<?php endif;?> | |||
<?php if((bool)$_['recoveryAdminEnabled']): ?> | |||
<td></td> | |||
<?php endif; ?> | |||
<td class="quota"> | |||
<select class="quota-user" data-inputtitle="<?php p($l->t('Please enter storage quota (ex: "512 MB" or "12 GB")')) ?>"> | |||
<option value='default'> | |||
<?php p($l->t('Default'));?> | |||
</option> | |||
<option value='none'> | |||
<?php p($l->t('Unlimited'));?> | |||
</option> | |||
<?php foreach($_['quota_preset'] as $preset):?> | |||
<option value='<?php p($preset);?>'> | |||
<?php p($preset);?> | |||
</option> | |||
<?php endforeach;?> | |||
<option value='other' data-new> | |||
<?php p($l->t('Other'));?> ... | |||
</option> | |||
</select> | |||
<progress class="quota-user-progress" value="" max="100"></progress> | |||
</td> | |||
<td class="storageLocation"></td> | |||
<td class="userBackend"></td> | |||
<td class="lastLogin"></td> | |||
<td class="userActions"> | |||
<div class="toggleUserActions"> | |||
<a class="action"><span class="icon-more"></span></a> | |||
<div class="popovermenu"> | |||
<ul class="userActionsMenu"> | |||
<li> | |||
<a href="#" class="menuitem action-togglestate permanent" data-action="togglestate"></a> | |||
</li> | |||
<li> | |||
<a href="#" class="menuitem action-remove permanent" data-action="remove"> | |||
<span class="icon icon-delete"></span> | |||
<span><?php p($l->t('Delete')); ?></span> | |||
</a> | |||
</li> | |||
</ul> | |||
</div> | |||
</div> | |||
</td> | |||
</tr> | |||
</tbody> | |||
</table> | |||
</form> | |||
<div class="emptycontent" style="display:none"> | |||
<div class="icon-search"></div> | |||
<h2></h2> | |||
</div> |
@@ -1,220 +0,0 @@ | |||
/** | |||
* ownCloud | |||
* | |||
* @author Vincent Petry | |||
* @copyright 2014 Vincent Petry <pvince81@owncloud.com> | |||
* | |||
* This library is free software; you can redistribute it and/or | |||
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE | |||
* License as published by the Free Software Foundation; either | |||
* version 3 of the License, or any later version. | |||
* | |||
* This library 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 library. If not, see <http://www.gnu.org/licenses/>. | |||
* | |||
*/ | |||
describe('DeleteHandler tests', function() { | |||
var showNotificationSpy; | |||
var hideNotificationSpy; | |||
var clock; | |||
var removeCallback; | |||
var markCallback; | |||
var undoCallback; | |||
function init(markCallback, removeCallback, undoCallback) { | |||
var handler = new DeleteHandler('dummyendpoint.php', 'paramid', markCallback, removeCallback); | |||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry', undoCallback); | |||
return handler; | |||
} | |||
beforeEach(function() { | |||
showNotificationSpy = sinon.spy(OC.Notification, 'showHtml'); | |||
hideNotificationSpy = sinon.spy(OC.Notification, 'hide'); | |||
clock = sinon.useFakeTimers(); | |||
removeCallback = sinon.stub(); | |||
markCallback = sinon.stub(); | |||
undoCallback = sinon.stub(); | |||
$('#testArea').append('<div id="notification"></div>'); | |||
}); | |||
afterEach(function() { | |||
showNotificationSpy.restore(); | |||
hideNotificationSpy.restore(); | |||
clock.restore(); | |||
}); | |||
it('shows a notification when marking for delete', function() { | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
expect(showNotificationSpy.calledOnce).toEqual(true); | |||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry'); | |||
expect(markCallback.calledOnce).toEqual(true); | |||
expect(markCallback.getCall(0).args[0]).toEqual('some_uid'); | |||
expect(removeCallback.notCalled).toEqual(true); | |||
expect(undoCallback.notCalled).toEqual(true); | |||
expect(fakeServer.requests.length).toEqual(0); | |||
}); | |||
it('deletes first entry and reshows notification on second delete', function() { | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
204, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_other_uid/, [ | |||
204, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
expect(showNotificationSpy.calledOnce).toEqual(true); | |||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_uid entry'); | |||
showNotificationSpy.resetHistory(); | |||
handler.mark('some_other_uid'); | |||
expect(hideNotificationSpy.calledOnce).toEqual(true); | |||
expect(showNotificationSpy.calledOnce).toEqual(true); | |||
expect(showNotificationSpy.getCall(0).args[0]).toEqual('removed some_other_uid entry'); | |||
expect(markCallback.calledTwice).toEqual(true); | |||
expect(markCallback.getCall(0).args[0]).toEqual('some_uid'); | |||
expect(markCallback.getCall(1).args[0]).toEqual('some_other_uid'); | |||
// called only once, because it is called once the second user is deleted | |||
expect(removeCallback.calledOnce).toEqual(true); | |||
expect(undoCallback.notCalled).toEqual(true); | |||
// previous one was delete | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
expect(request.url).toEqual(OC.webroot + '/index.php/dummyendpoint.php/some_uid'); | |||
}); | |||
it('automatically deletes after timeout', function() { | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
204, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
clock.tick(5000); | |||
// nothing happens yet | |||
expect(fakeServer.requests.length).toEqual(0); | |||
clock.tick(3000); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
expect(request.url).toEqual(OC.webroot + '/index.php/dummyendpoint.php/some_uid'); | |||
}); | |||
it('deletes when deleteEntry is called', function() { | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
200, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
handler.deleteEntry(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
expect(request.url).toEqual(OC.webroot + '/index.php/dummyendpoint.php/some_uid'); | |||
}); | |||
it('deletes when deleteEntry is called and escapes', function() { | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
200, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid<>/"..\\'); | |||
handler.deleteEntry(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
expect(request.url).toEqual(OC.webroot + '/index.php/dummyendpoint.php/some_uid%3C%3E%2F%22..%5C'); | |||
}); | |||
it('cancels deletion when undo is clicked', function() { | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback); | |||
handler.mark('some_uid'); | |||
$('#notification .undo').click(); | |||
expect(undoCallback.calledOnce).toEqual(true); | |||
// timer was cancelled | |||
clock.tick(10000); | |||
expect(fakeServer.requests.length).toEqual(0); | |||
}); | |||
it('cancels deletion when cancel method is called', function() { | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.setNotification(OC.Notification, 'dataid', 'removed %oid entry <span class="undo">Undo</span>', undoCallback); | |||
handler.mark('some_uid'); | |||
handler.cancel(); | |||
// not sure why, seems to be by design | |||
expect(undoCallback.notCalled).toEqual(true); | |||
// timer was cancelled | |||
clock.tick(10000); | |||
expect(fakeServer.requests.length).toEqual(0); | |||
}); | |||
it('calls removeCallback after successful server side deletion', function() { | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
200, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'success'}) | |||
]); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
handler.deleteEntry(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
var query = OC.parseQueryString(request.requestBody); | |||
expect(removeCallback.calledOnce).toEqual(true); | |||
expect(undoCallback.notCalled).toEqual(true); | |||
expect(removeCallback.getCall(0).args[0]).toEqual('some_uid'); | |||
}); | |||
it('calls undoCallback and shows alert after failed server side deletion', function() { | |||
// stub t to avoid extra calls | |||
var tStub = sinon.stub(window, 't').returns('text'); | |||
fakeServer.respondWith(/\/index\.php\/dummyendpoint.php\/some_uid/, [ | |||
403, | |||
{ 'Content-Type': 'application/json' }, | |||
JSON.stringify({status: 'error', data: {message: 'test error'}}) | |||
]); | |||
var alertDialogStub = sinon.stub(OC.dialogs, 'alert'); | |||
var handler = init(markCallback, removeCallback, undoCallback); | |||
handler.mark('some_uid'); | |||
handler.deleteEntry(); | |||
expect(fakeServer.requests.length).toEqual(1); | |||
var request = fakeServer.requests[0]; | |||
var query = OC.parseQueryString(request.requestBody); | |||
expect(removeCallback.notCalled).toEqual(true); | |||
expect(undoCallback.calledOnce).toEqual(true); | |||
expect(undoCallback.getCall(0).args[0]).toEqual('some_uid'); | |||
expect(alertDialogStub.calledOnce); | |||
alertDialogStub.restore(); | |||
tStub.restore(); | |||
}); | |||
}); |
@@ -17,6 +17,7 @@ | |||
* @author Stephan Peijnik <speijnik@anexia-it.com> | |||
* @author Thomas Müller <thomas.mueller@tmit.eu> | |||
* @author Thomas Pulzer <t.pulzer@kniel.de> | |||
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* | |||
* @license AGPL-3.0 | |||
* | |||
@@ -30,7 +31,7 @@ | |||
* GNU Affero General Public License for more details. | |||
* | |||
* You should have received a copy of the GNU Affero General Public License, version 3, | |||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||
* along with this program. If not, see <http://www.gnu.org/licenses/> | |||
* | |||
*/ | |||
@@ -41,12 +42,10 @@ OC_Util::checkSubAdminUser(); | |||
$userManager = \OC::$server->getUserManager(); | |||
$groupManager = \OC::$server->getGroupManager(); | |||
$appManager = \OC::$server->getAppManager(); | |||
// Set the sort option: SORT_USERCOUNT or SORT_GROUPNAME | |||
$sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT; | |||
$config = \OC::$server->getConfig(); | |||
/* SORT OPTION: SORT_USERCOUNT or SORT_GROUPNAME */ | |||
$sortGroupsBy = \OC\Group\MetaData::SORT_USERCOUNT; | |||
if ($config->getSystemValue('sort_groups_by_name', false)) { | |||
$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME; | |||
} else { | |||
@@ -62,14 +61,16 @@ if ($config->getSystemValue('sort_groups_by_name', false)) { | |||
} | |||
} | |||
$uid = \OC_User::getUser(); | |||
$isAdmin = OC_User::isAdminUser($uid); | |||
/* ENCRYPTION CONFIG */ | |||
$isEncryptionEnabled = \OC::$server->getEncryptionManager()->isEnabled(); | |||
$useMasterKey = $config->getAppValue('encryption', 'useMasterKey', true); | |||
// If masterKey enabled, then you can change password. This is to avoid data loss! | |||
$canChangePassword = ($isEncryptionEnabled && $useMasterKey) || $useMasterKey; | |||
$isDisabled = true; | |||
$user = $userManager->get($uid); | |||
if ($user) { | |||
$isDisabled = !$user->isEnabled(); | |||
} | |||
/* GROUPS */ | |||
$uid = \OC_User::getUser(); | |||
$isAdmin = \OC_User::isAdminUser($uid); | |||
$groupsInfo = new \OC\Group\MetaData( | |||
$uid, | |||
@@ -81,10 +82,7 @@ $groupsInfo = new \OC\Group\MetaData( | |||
$groupsInfo->setSorting($sortGroupsBy); | |||
list($adminGroup, $groups) = $groupsInfo->get(); | |||
$recoveryAdminEnabled = $appManager->isEnabledForUser('encryption') && | |||
$config->getAppValue( 'encryption', 'recoveryAdminEnabled', '0'); | |||
if($isAdmin) { | |||
if ($isAdmin) { | |||
$subAdmins = \OC::$server->getGroupManager()->getSubAdmin()->getAllSubAdmins(); | |||
// New class returns IUser[] so convert back | |||
$result = []; | |||
@@ -95,7 +93,7 @@ if($isAdmin) { | |||
]; | |||
} | |||
$subAdmins = $result; | |||
}else{ | |||
} else { | |||
/* Retrieve group IDs from $groups array, so we can pass that information into OC_Group::displayNamesInGroups() */ | |||
$gids = array(); | |||
foreach($groups as $group) { | |||
@@ -108,42 +106,38 @@ if($isAdmin) { | |||
$disabledUsers = $isLDAPUsed ? 0 : $userManager->countDisabledUsers(); | |||
$disabledUsersGroup = [ | |||
'id' => '_disabledUsers', | |||
'name' => '_disabledUsers', | |||
'id' => '_disabled', | |||
'name' => 'Disabled users', | |||
'usercount' => $disabledUsers | |||
]; | |||
$allGroups = array_merge_recursive($adminGroup, $groups); | |||
// load preset quotas | |||
/* QUOTAS PRESETS */ | |||
$quotaPreset=$config->getAppValue('files', 'quota_preset', '1 GB, 5 GB, 10 GB'); | |||
$quotaPreset=explode(',', $quotaPreset); | |||
foreach($quotaPreset as &$preset) { | |||
$preset=trim($preset); | |||
} | |||
$quotaPreset=array_diff($quotaPreset, array('default', 'none')); | |||
$defaultQuota=$config->getAppValue('files', 'default_quota', 'none'); | |||
$defaultQuotaIsUserDefined=array_search($defaultQuota, $quotaPreset)===false | |||
&& array_search($defaultQuota, array('none', 'default'))===false; | |||
\OC::$server->getEventDispatcher()->dispatch('OC\Settings\Users::loadAdditionalScripts'); | |||
$tmpl = new OC_Template("settings", "users/main", "user"); | |||
$tmpl->assign('groups', $groups); | |||
$tmpl->assign('sortGroups', $sortGroupsBy); | |||
$tmpl->assign('adminGroup', $adminGroup); | |||
$tmpl->assign('disabledUsersGroup', $disabledUsersGroup); | |||
$tmpl->assign('isAdmin', (int)$isAdmin); | |||
$tmpl->assign('subadmins', $subAdmins); | |||
$tmpl->assign('numofgroups', count($groups) + count($adminGroup)); | |||
$tmpl->assign('quota_preset', $quotaPreset); | |||
$tmpl->assign('default_quota', $defaultQuota); | |||
$tmpl->assign('defaultQuotaIsUserDefined', $defaultQuotaIsUserDefined); | |||
$tmpl->assign('recoveryAdminEnabled', $recoveryAdminEnabled); | |||
$tmpl->assign('show_storage_location', $config->getAppValue('core', 'umgmt_show_storage_location', 'false')); | |||
$tmpl->assign('show_last_login', $config->getAppValue('core', 'umgmt_show_last_login', 'false')); | |||
$tmpl->assign('show_email', $config->getAppValue('core', 'umgmt_show_email', 'false')); | |||
$tmpl->assign('show_backend', $config->getAppValue('core', 'umgmt_show_backend', 'false')); | |||
$tmpl->assign('send_email', $config->getAppValue('core', 'umgmt_send_email', 'false')); | |||
/* FINAL DATA */ | |||
$serverData = array(); | |||
// groups | |||
$serverData['groups'] = array_merge_recursive($adminGroup, [$disabledUsersGroup], $groups); | |||
$serverData['subadmingroups'] = $groups; | |||
// Various data | |||
$serverData['subadmins'] = $subAdmins; | |||
$serverData['sortGroups'] = $sortGroupsBy; | |||
$serverData['quotaPreset'] = $quotaPreset; | |||
$serverData['userCount'] = $userManager->countUsers(); | |||
// Settings | |||
$serverData['defaultQuota'] = $defaultQuota; | |||
$serverData['canChangePassword'] = $canChangePassword; | |||
// print template + vue + serve data | |||
$tmpl = new OC_Template('settings', 'settings', 'user'); | |||
$tmpl->assign('serverData', $serverData); | |||
$tmpl->printPage(); |
@@ -0,0 +1,108 @@ | |||
var path = require('path') | |||
var webpack = require('webpack') | |||
module.exports = { | |||
entry: './src/main.js', | |||
output: { | |||
path: path.resolve(__dirname, './js'), | |||
publicPath: '/dist/', | |||
filename: 'main.js' | |||
}, | |||
module: { | |||
rules: [ | |||
{ | |||
test: /\.css$/, | |||
use: [ | |||
'vue-style-loader', | |||
'css-loader' | |||
], | |||
}, | |||
{ | |||
test: /\.scss$/, | |||
use: [ | |||
'vue-style-loader', | |||
'css-loader', | |||
'sass-loader' | |||
], | |||
}, | |||
{ | |||
test: /\.sass$/, | |||
use: [ | |||
'vue-style-loader', | |||
'css-loader', | |||
'sass-loader?indentedSyntax' | |||
], | |||
}, | |||
{ | |||
test: /\.vue$/, | |||
loader: 'vue-loader', | |||
options: { | |||
loaders: { | |||
// Since sass-loader (weirdly) has SCSS as its default parse mode, we map | |||
// the "scss" and "sass" values for the lang attribute to the right configs here. | |||
// other preprocessors should work out of the box, no loader config like this necessary. | |||
'scss': [ | |||
'vue-style-loader', | |||
'css-loader', | |||
'sass-loader' | |||
], | |||
'sass': [ | |||
'vue-style-loader', | |||
'css-loader', | |||
'sass-loader?indentedSyntax' | |||
] | |||
} | |||
// other vue-loader options go here | |||
} | |||
}, | |||
{ | |||
test: /\.js$/, | |||
loader: 'babel-loader', | |||
exclude: /node_modules/ | |||
}, | |||
{ | |||
test: /\.(png|jpg|gif|svg)$/, | |||
loader: 'file-loader', | |||
options: { | |||
name: '[name].[ext]?[hash]' | |||
} | |||
} | |||
] | |||
}, | |||
resolve: { | |||
alias: { | |||
'vue$': 'vue/dist/vue.esm.js' | |||
}, | |||
extensions: ['*', '.js', '.vue', '.json'] | |||
}, | |||
devServer: { | |||
historyApiFallback: true, | |||
noInfo: true, | |||
overlay: true | |||
}, | |||
performance: { | |||
hints: false | |||
}, | |||
devtool: '#eval-source-map' | |||
} | |||
if (process.env.NODE_ENV === 'production') { | |||
module.exports.devtool = '#source-map' | |||
// http://vue-loader.vuejs.org/en/workflow/production.html | |||
module.exports.plugins = (module.exports.plugins || []).concat([ | |||
new webpack.DefinePlugin({ | |||
'process.env': { | |||
NODE_ENV: '"production"' | |||
} | |||
}), | |||
new webpack.optimize.UglifyJsPlugin({ | |||
sourceMap: true, | |||
compress: { | |||
warnings: false | |||
} | |||
}), | |||
new webpack.LoaderOptionsPlugin({ | |||
minimize: true | |||
}) | |||
]) | |||
} |