Merge pull request #8824 from nextcloud/settings-vue
Vue migration: settings
This commit is contained in:
commit
a2c518ee5a
|
@ -594,6 +594,13 @@ pipeline:
|
||||||
when:
|
when:
|
||||||
matrix:
|
matrix:
|
||||||
TESTS-ACCEPTANCE: login
|
TESTS-ACCEPTANCE: login
|
||||||
|
acceptance-users:
|
||||||
|
image: nextcloudci/acceptance-php7.1:acceptance-php7.1-2
|
||||||
|
commands:
|
||||||
|
- tests/acceptance/run-local.sh --timeout-multiplier 10 --nextcloud-server-domain acceptance-users --selenium-server selenium:4444 allow-git-repository-modifications features/users.feature
|
||||||
|
when:
|
||||||
|
matrix:
|
||||||
|
TESTS-ACCEPTANCE: users
|
||||||
nodb-codecov:
|
nodb-codecov:
|
||||||
image: nextcloudci/php7.0:php7.0-19
|
image: nextcloudci/php7.0:php7.0-19
|
||||||
commands:
|
commands:
|
||||||
|
@ -761,6 +768,8 @@ matrix:
|
||||||
TESTS-ACCEPTANCE: header
|
TESTS-ACCEPTANCE: header
|
||||||
- TESTS: acceptance
|
- TESTS: acceptance
|
||||||
TESTS-ACCEPTANCE: login
|
TESTS-ACCEPTANCE: login
|
||||||
|
- TESTS: acceptance
|
||||||
|
TESTS-ACCEPTANCE: users
|
||||||
- TESTS: jsunit
|
- TESTS: jsunit
|
||||||
- TESTS: syntax-php7.0
|
- TESTS: syntax-php7.0
|
||||||
- TESTS: syntax-php7.1
|
- TESTS: syntax-php7.1
|
||||||
|
|
|
@ -67,6 +67,13 @@ div[contenteditable=true],
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
&:required {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
&:invalid {
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-color: $color-error;
|
||||||
|
}
|
||||||
/* Primary action button, use sparingly */
|
/* Primary action button, use sparingly */
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: $color-primary-element;
|
background-color: $color-primary-element;
|
||||||
|
@ -216,7 +223,8 @@ input {
|
||||||
margin-left: -8px !important;
|
margin-left: -8px !important;
|
||||||
border-left-color: transparent !important;
|
border-left-color: transparent !important;
|
||||||
border-radius: 0 $border-radius $border-radius 0 !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;
|
background-color: $color-main-background !important;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
|
@ -227,6 +235,7 @@ input {
|
||||||
background-image: url('../img/actions/confirm-fade.svg?v=2') !important;
|
background-image: url('../img/actions/confirm-fade.svg?v=2') !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* only show confirm borders if input is not focused */
|
/* only show confirm borders if input is not focused */
|
||||||
&:not(:active):not(:hover):not(:focus){
|
&:not(:active):not(:hover):not(:focus){
|
||||||
+ .icon-confirm {
|
+ .icon-confirm {
|
||||||
|
@ -244,14 +253,19 @@ input {
|
||||||
&:active,
|
&:active,
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus {
|
&:focus {
|
||||||
|
&:invalid {
|
||||||
|
+ .icon-confirm {
|
||||||
|
border-color: $color-error;
|
||||||
|
}
|
||||||
|
}
|
||||||
+ .icon-confirm {
|
+ .icon-confirm {
|
||||||
border-color: $color-primary-element !important;
|
border-color: $color-primary-element !important;
|
||||||
border-left-color: transparent !important;
|
border-left-color: transparent !important;
|
||||||
z-index: 2; /* above previous input */
|
/* above previous input */
|
||||||
|
z-index: 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -606,6 +620,206 @@ 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;
|
||||||
|
cursor: text !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.multiselect--disabled,
|
||||||
|
&.multiselect--disabled .multiselect__single {
|
||||||
|
background-color: nc-darken($color-main-background, 8%) !important;
|
||||||
|
}
|
||||||
|
.multiselect__tags {
|
||||||
|
/* space between tags and limit tag */
|
||||||
|
$space-between: 5px;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid nc-darken($color-main-background, 14%);
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 3px;
|
||||||
|
height: 34px;
|
||||||
|
/* tag wrapper */
|
||||||
|
.multiselect__tags-wrap {
|
||||||
|
align-items: center;
|
||||||
|
display: inline-flex;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 100%;
|
||||||
|
position: relative;
|
||||||
|
padding: 3px $space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
/* 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: 1 0 0;
|
||||||
|
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;
|
||||||
|
/* require to override the default width
|
||||||
|
and force the tag to shring properly */
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 50%;
|
||||||
|
max-width: fit-content;
|
||||||
|
max-width: -moz-fit-content;
|
||||||
|
/* css hack, detect if more than two tags
|
||||||
|
if so, flex-basis is set to half */
|
||||||
|
&:only-child {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
&:not(:last-child) {
|
||||||
|
margin-right: $space-between;
|
||||||
|
}
|
||||||
|
/* ellipsis the groups to be sure
|
||||||
|
we display at least two of them */
|
||||||
|
> span {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Single select default value */
|
||||||
|
.multiselect__single {
|
||||||
|
padding: 8px 10px;
|
||||||
|
flex: 0 0 100%;
|
||||||
|
z-index: 1; /* above input */
|
||||||
|
background-color: $color-main-background;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/* displayed text if tag limit reached */
|
||||||
|
.multiselect__strong,
|
||||||
|
.multiselect__limit {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
line-height: 20px;
|
||||||
|
color: nc-lighten($color-main-text, 33%);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
opacity: .7;
|
||||||
|
margin-right: $space-between;
|
||||||
|
/* above the input */
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
/* 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;
|
||||||
|
/* only when not active */
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* 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;
|
||||||
|
max-height: 250px;
|
||||||
|
overflow-y: auto;
|
||||||
|
.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 */
|
||||||
|
&:not(.multiselect__option--disabled)::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;
|
||||||
|
}
|
||||||
|
&.multiselect__option--disabled {
|
||||||
|
background-color: nc-darken($color-main-background, 8%);
|
||||||
|
}
|
||||||
|
/* 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 */
|
/* Progressbar */
|
||||||
progress {
|
progress {
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -75,8 +75,9 @@ ul.multiselectoptions {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div.multiselect,
|
/* TODO drop old legacy multiselect! */
|
||||||
select.multiselect {
|
div.multiselect:not(.multiselect-vue),
|
||||||
|
select.multiselect:not(.multiselect-vue) {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
max-width: 200px;
|
max-width: 200px;
|
||||||
min-width: 150px !important;
|
min-width: 150px !important;
|
||||||
|
|
|
@ -11,119 +11,128 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: block;
|
display: block;
|
||||||
font-family: 'Open Sans', Frutiger, Calibri, 'Myriad Pro', Myriad, sans-serif;
|
font-family: 'Open Sans', Frutiger, Calibri, 'Myriad Pro', Myriad, sans-serif;
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
line-break: auto;
|
line-break: auto;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
text-shadow: none;
|
text-shadow: none;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
z-index: 100000;
|
z-index: 100000;
|
||||||
filter: drop-shadow(0 1px 10px $color-box-shadow);
|
/* default to top */
|
||||||
&.in {
|
margin-top: -3px;
|
||||||
opacity: 1;
|
padding: 10px 0;
|
||||||
}
|
filter: drop-shadow(0 1px 10px $color-box-shadow);
|
||||||
|
&.in,
|
||||||
&.top {
|
&.tooltip[aria-hidden='false'] {
|
||||||
margin-top: -3px;
|
visibility: visible;
|
||||||
padding: 10px 0;
|
opacity: 1;
|
||||||
}
|
transition: opacity .15s;
|
||||||
&.bottom {
|
}
|
||||||
margin-top: 3px;
|
&.top .tooltip-arrow,
|
||||||
padding: 10px 0;
|
&[x-placement^='top'] {
|
||||||
}
|
left: 50%;
|
||||||
|
margin-left: -10px;
|
||||||
&.right {
|
}
|
||||||
margin-left: 3px;
|
&.bottom,
|
||||||
padding: 0 10px;
|
&[x-placement^='bottom'] {
|
||||||
.tooltip-arrow {
|
margin-top: 3px;
|
||||||
top: 50%;
|
padding: 10px 0;
|
||||||
left: 0;
|
}
|
||||||
margin-top: -10px;
|
&.right,
|
||||||
border-width: 10px 10px 10px 0;
|
&[x-placement^='right'] {
|
||||||
border-right-color: $color-main-background;
|
margin-left: 3px;
|
||||||
}
|
padding: 0 10px;
|
||||||
}
|
.tooltip-arrow {
|
||||||
&.left {
|
top: 50%;
|
||||||
margin-left: -3px;
|
left: 0;
|
||||||
padding: 0 5px;
|
margin-top: -10px;
|
||||||
.tooltip-arrow {
|
border-width: 10px 10px 10px 0;
|
||||||
top: 50%;
|
border-right-color: $color-main-background;
|
||||||
right: 0;
|
}
|
||||||
margin-top: -10px;
|
}
|
||||||
border-width: 10px 0 10px 10px;
|
&.left,
|
||||||
border-left-color: $color-main-background;
|
&[x-placement^='left'] {
|
||||||
}
|
margin-left: -3px;
|
||||||
}
|
padding: 0 5px;
|
||||||
|
.tooltip-arrow {
|
||||||
/* TOP */
|
top: 50%;
|
||||||
&.top .tooltip-arrow,
|
right: 0;
|
||||||
&.top-left .tooltip-arrow,
|
margin-top: -10px;
|
||||||
&.top-right .tooltip-arrow {
|
border-width: 10px 0 10px 10px;
|
||||||
bottom: 0;
|
border-left-color: $color-main-background;
|
||||||
border-width: 10px 10px 0;
|
}
|
||||||
border-top-color: $color-main-background;
|
}
|
||||||
}
|
/* TOP */
|
||||||
&.top .tooltip-arrow {
|
&.top,
|
||||||
left: 50%;
|
&.top-left,
|
||||||
margin-left: -10px;
|
&[x-placement^='top'],
|
||||||
}
|
&.top-right {
|
||||||
&.top-left .tooltip-arrow {
|
.tooltip-arrow {
|
||||||
right: 10px;
|
bottom: 0;
|
||||||
margin-bottom: -10px;
|
border-width: 10px 10px 0;
|
||||||
}
|
border-top-color: $color-main-background;
|
||||||
&.top-right .tooltip-arrow {
|
}
|
||||||
left: 10px;
|
}
|
||||||
margin-bottom: -10px;
|
&.top-left .tooltip-arrow {
|
||||||
}
|
right: 10px;
|
||||||
|
margin-bottom: -10px;
|
||||||
/* BOTTOM */
|
}
|
||||||
&.bottom .tooltip-arrow,
|
&.top-right .tooltip-arrow {
|
||||||
&.bottom-left .tooltip-arrow,
|
left: 10px;
|
||||||
&.bottom-right .tooltip-arrow {
|
margin-bottom: -10px;
|
||||||
top: 0;
|
}
|
||||||
border-width: 0 10px 10px;
|
/* BOTTOM */
|
||||||
border-bottom-color: $color-main-background;
|
&.bottom,
|
||||||
}
|
&[x-placement^='bottom'],
|
||||||
&.bottom .tooltip-arrow {
|
&.bottom-left,
|
||||||
left: 50%;
|
&.bottom-right {
|
||||||
margin-left: -10px;
|
.tooltip-arrow {
|
||||||
}
|
top: 0;
|
||||||
&.bottom-left .tooltip-arrow {
|
border-width: 0 10px 10px;
|
||||||
right: 10px;
|
border-bottom-color: $color-main-background;
|
||||||
margin-top: -10px;
|
}
|
||||||
}
|
}
|
||||||
&.bottom-right .tooltip-arrow {
|
&[x-placement^='bottom'] .tooltip-arrow,
|
||||||
left: 10px;
|
&.bottom .tooltip-arrow {
|
||||||
margin-top: -10px;
|
left: 50%;
|
||||||
}
|
margin-left: -10px;
|
||||||
|
}
|
||||||
|
&.bottom-left .tooltip-arrow {
|
||||||
|
right: 10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
|
&.bottom-right .tooltip-arrow {
|
||||||
|
left: 10px;
|
||||||
|
margin-top: -10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-inner {
|
.tooltip-inner {
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
padding: 5px 8px;
|
padding: 5px 8px;
|
||||||
background-color: $color-main-background;
|
background-color: $color-main-background;
|
||||||
color: $color-main-text;
|
color: $color-main-text;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: $border-radius;
|
border-radius: $border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-arrow {
|
.tooltip-arrow {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
|
@ -913,7 +913,6 @@ return array(
|
||||||
'OC\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/settings/Controller/ChangePasswordController.php',
|
'OC\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/settings/Controller/ChangePasswordController.php',
|
||||||
'OC\\Settings\\Controller\\CheckSetupController' => $baseDir . '/settings/Controller/CheckSetupController.php',
|
'OC\\Settings\\Controller\\CheckSetupController' => $baseDir . '/settings/Controller/CheckSetupController.php',
|
||||||
'OC\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/settings/Controller/CommonSettingsTrait.php',
|
'OC\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/settings/Controller/CommonSettingsTrait.php',
|
||||||
'OC\\Settings\\Controller\\GroupsController' => $baseDir . '/settings/Controller/GroupsController.php',
|
|
||||||
'OC\\Settings\\Controller\\LogSettingsController' => $baseDir . '/settings/Controller/LogSettingsController.php',
|
'OC\\Settings\\Controller\\LogSettingsController' => $baseDir . '/settings/Controller/LogSettingsController.php',
|
||||||
'OC\\Settings\\Controller\\MailSettingsController' => $baseDir . '/settings/Controller/MailSettingsController.php',
|
'OC\\Settings\\Controller\\MailSettingsController' => $baseDir . '/settings/Controller/MailSettingsController.php',
|
||||||
'OC\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/settings/Controller/PersonalSettingsController.php',
|
'OC\\Settings\\Controller\\PersonalSettingsController' => $baseDir . '/settings/Controller/PersonalSettingsController.php',
|
||||||
|
|
|
@ -943,7 +943,6 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
|
||||||
'OC\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/../../..' . '/settings/Controller/ChangePasswordController.php',
|
'OC\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/../../..' . '/settings/Controller/ChangePasswordController.php',
|
||||||
'OC\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/../../..' . '/settings/Controller/CheckSetupController.php',
|
'OC\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/../../..' . '/settings/Controller/CheckSetupController.php',
|
||||||
'OC\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/../../..' . '/settings/Controller/CommonSettingsTrait.php',
|
'OC\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/../../..' . '/settings/Controller/CommonSettingsTrait.php',
|
||||||
'OC\\Settings\\Controller\\GroupsController' => __DIR__ . '/../../..' . '/settings/Controller/GroupsController.php',
|
|
||||||
'OC\\Settings\\Controller\\LogSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/LogSettingsController.php',
|
'OC\\Settings\\Controller\\LogSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/LogSettingsController.php',
|
||||||
'OC\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/MailSettingsController.php',
|
'OC\\Settings\\Controller\\MailSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/MailSettingsController.php',
|
||||||
'OC\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/PersonalSettingsController.php',
|
'OC\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/PersonalSettingsController.php',
|
||||||
|
|
|
@ -59,6 +59,11 @@ class Factory implements IFactory {
|
||||||
*/
|
*/
|
||||||
protected $pluralFunctions = [];
|
protected $pluralFunctions = [];
|
||||||
|
|
||||||
|
const COMMON_LANGUAGE_CODES = [
|
||||||
|
'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
|
||||||
|
'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
|
||||||
|
];
|
||||||
|
|
||||||
/** @var IConfig */
|
/** @var IConfig */
|
||||||
protected $config;
|
protected $config;
|
||||||
|
|
||||||
|
@ -137,9 +142,9 @@ class Factory implements IFactory {
|
||||||
*
|
*
|
||||||
* @link https://github.com/owncloud/core/issues/21955
|
* @link https://github.com/owncloud/core/issues/21955
|
||||||
*/
|
*/
|
||||||
if($this->config->getSystemValue('installed', false)) {
|
if ($this->config->getSystemValue('installed', false)) {
|
||||||
$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() : null;
|
$userId = !is_null($this->userSession->getUser()) ? $this->userSession->getUser()->getUID() : null;
|
||||||
if(!is_null($userId)) {
|
if (!is_null($userId)) {
|
||||||
$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
|
$userLang = $this->config->getUserValue($userId, 'core', 'lang', null);
|
||||||
} else {
|
} else {
|
||||||
$userLang = null;
|
$userLang = null;
|
||||||
|
@ -310,7 +315,7 @@ class Factory implements IFactory {
|
||||||
*/
|
*/
|
||||||
private function isSubDirectory($sub, $parent) {
|
private function isSubDirectory($sub, $parent) {
|
||||||
// Check whether $sub contains no ".."
|
// Check whether $sub contains no ".."
|
||||||
if(strpos($sub, '..') !== false) {
|
if (strpos($sub, '..') !== false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -441,4 +446,74 @@ class Factory implements IFactory {
|
||||||
return $function;
|
return $function;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns the common language and other languages in an
|
||||||
|
* associative array
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getLanguages() {
|
||||||
|
$forceLanguage = $this->config->getSystemValue('force_language', false);
|
||||||
|
if ($forceLanguage !== false) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$languageCodes = $this->findAvailableLanguages();
|
||||||
|
|
||||||
|
$commonLanguages = [];
|
||||||
|
$languages = [];
|
||||||
|
|
||||||
|
foreach($languageCodes as $lang) {
|
||||||
|
$l = $this->get('lib', $lang);
|
||||||
|
// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
|
||||||
|
$potentialName = (string) $l->t('__language_name__');
|
||||||
|
if ($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
|
||||||
|
$ln = array(
|
||||||
|
'code' => $lang,
|
||||||
|
'name' => $potentialName
|
||||||
|
);
|
||||||
|
} else if ($lang === 'en') {
|
||||||
|
$ln = array(
|
||||||
|
'code' => $lang,
|
||||||
|
'name' => 'English (US)'
|
||||||
|
);
|
||||||
|
} else {//fallback to language code
|
||||||
|
$ln = array(
|
||||||
|
'code' => $lang,
|
||||||
|
'name' => $lang
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// put appropriate languages into appropriate arrays, to print them sorted
|
||||||
|
// common languages -> divider -> other languages
|
||||||
|
if (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
|
||||||
|
$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)] = $ln;
|
||||||
|
} else {
|
||||||
|
$languages[] = $ln;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($commonLanguages);
|
||||||
|
|
||||||
|
// sort now by displayed language not the iso-code
|
||||||
|
usort( $languages, function ($a, $b) {
|
||||||
|
if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
|
||||||
|
// If a doesn't have a name, but b does, list b before a
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
|
||||||
|
// If a does have a name, but b doesn't, list a before b
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Otherwise compare the names
|
||||||
|
return strcmp($a['name'], $b['name']);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
// reset indexes
|
||||||
|
'commonlanguages' => array_values($commonLanguages),
|
||||||
|
'languages' => $languages
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,7 +247,7 @@ class NavigationManager implements INavigationManager {
|
||||||
'type' => 'settings',
|
'type' => 'settings',
|
||||||
'id' => 'core_users',
|
'id' => 'core_users',
|
||||||
'order' => 4,
|
'order' => 4,
|
||||||
'href' => $this->urlGenerator->linkToRoute('settings_users'),
|
'href' => $this->urlGenerator->linkToRoute('settings.Users.usersList'),
|
||||||
'name' => $l->t('Users'),
|
'name' => $l->t('Users'),
|
||||||
'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'),
|
'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'),
|
||||||
]);
|
]);
|
||||||
|
|
|
@ -39,6 +39,7 @@ use OCP\L10N\IFactory;
|
||||||
use OCP\Settings\ISettings;
|
use OCP\Settings\ISettings;
|
||||||
|
|
||||||
class PersonalInfo implements ISettings {
|
class PersonalInfo implements ISettings {
|
||||||
|
|
||||||
/** @var IConfig */
|
/** @var IConfig */
|
||||||
private $config;
|
private $config;
|
||||||
/** @var IUserManager */
|
/** @var IUserManager */
|
||||||
|
@ -51,12 +52,6 @@ class PersonalInfo implements ISettings {
|
||||||
private $appManager;
|
private $appManager;
|
||||||
/** @var IFactory */
|
/** @var IFactory */
|
||||||
private $l10nFactory;
|
private $l10nFactory;
|
||||||
|
|
||||||
const COMMON_LANGUAGE_CODES = [
|
|
||||||
'en', 'es', 'fr', 'de', 'de_DE', 'ja', 'ar', 'ru', 'nl', 'it',
|
|
||||||
'pt_BR', 'pt_PT', 'da', 'fi_FI', 'nb_NO', 'sv', 'tr', 'zh_CN', 'ko'
|
|
||||||
];
|
|
||||||
|
|
||||||
/** @var IL10N */
|
/** @var IL10N */
|
||||||
private $l;
|
private $l;
|
||||||
|
|
||||||
|
@ -198,64 +193,29 @@ class PersonalInfo implements ISettings {
|
||||||
|
|
||||||
$uid = $user->getUID();
|
$uid = $user->getUID();
|
||||||
|
|
||||||
$userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
|
$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
|
||||||
$languageCodes = $this->l10nFactory->findAvailableLanguages();
|
$languages = $this->l10nFactory->getLanguages();
|
||||||
|
|
||||||
$commonLanguages = [];
|
// associate the user language with the proper array
|
||||||
$languages = [];
|
$userLangIndex = array_search($userConfLang, array_column($languages['commonlanguages'], 'code'));
|
||||||
|
$userLang = $languages['commonlanguages'][$userLangIndex];
|
||||||
foreach($languageCodes as $lang) {
|
// search in the other languages
|
||||||
$l = \OC::$server->getL10N('lib', $lang);
|
if ($userLangIndex === false) {
|
||||||
// TRANSLATORS this is the language name for the language switcher in the personal settings and should be the localized version
|
$userLangIndex = array_search($userConfLang, array_column($languages['languages'], 'code'));
|
||||||
$potentialName = (string) $l->t('__language_name__');
|
$userLang = $languages['languages'][$userLangIndex];
|
||||||
if($l->getLanguageCode() === $lang && $potentialName[0] !== '_') {//first check if the language name is in the translation file
|
|
||||||
$ln = array('code' => $lang, 'name' => $potentialName);
|
|
||||||
} elseif ($lang === 'en') {
|
|
||||||
$ln = ['code' => $lang, 'name' => 'English (US)'];
|
|
||||||
}else{//fallback to language code
|
|
||||||
$ln=array('code'=>$lang, 'name'=>$lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
// put appropriate languages into appropriate arrays, to print them sorted
|
|
||||||
// used language -> common languages -> divider -> other languages
|
|
||||||
if ($lang === $userLang) {
|
|
||||||
$userLang = $ln;
|
|
||||||
} elseif (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
|
|
||||||
$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)]=$ln;
|
|
||||||
} else {
|
|
||||||
$languages[]=$ln;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if user language is not available but set somehow: show the actual code as name
|
// if user language is not available but set somehow: show the actual code as name
|
||||||
if (!is_array($userLang)) {
|
if (!is_array($userLang)) {
|
||||||
$userLang = [
|
$userLang = [
|
||||||
'code' => $userLang,
|
'code' => $userConfLang,
|
||||||
'name' => $userLang,
|
'name' => $userConfLang,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
ksort($commonLanguages);
|
return array_merge(
|
||||||
|
array('activelanguage' => $userLang),
|
||||||
// sort now by displayed language not the iso-code
|
$languages
|
||||||
usort( $languages, function ($a, $b) {
|
);
|
||||||
if ($a['code'] === $a['name'] && $b['code'] !== $b['name']) {
|
|
||||||
// If a doesn't have a name, but b does, list b before a
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if ($a['code'] !== $a['name'] && $b['code'] === $b['name']) {
|
|
||||||
// If a does have a name, but b doesn't, list a before b
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
// Otherwise compare the names
|
|
||||||
return strcmp($a['name'], $b['name']);
|
|
||||||
});
|
|
||||||
|
|
||||||
return [
|
|
||||||
'activelanguage' => $userLang,
|
|
||||||
'commonlanguages' => $commonLanguages,
|
|
||||||
'languages' => $languages
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"presets": [
|
||||||
|
[
|
||||||
|
"env",
|
||||||
|
{
|
||||||
|
"targets": {
|
||||||
|
"browsers": ["last 2 versions", "ie >= 11"]
|
||||||
|
},
|
||||||
|
"modules": false,
|
||||||
|
"blacklist": ["useStrict"],
|
||||||
|
"useBuiltIns": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = tab
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
|
[{package.json,.travis.yml,webpack.config.js}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
|
@ -0,0 +1,12 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
npm-debug.log
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
|
@ -1,157 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
*
|
|
||||||
* @author Joas Schilling <coding@schilljs.com>
|
|
||||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
||||||
* @author Morris Jobke <hey@morrisjobke.de>
|
|
||||||
*
|
|
||||||
* @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/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace OC\Settings\Controller;
|
|
||||||
|
|
||||||
use OC\AppFramework\Http;
|
|
||||||
use OC\Group\MetaData;
|
|
||||||
use OCP\AppFramework\Controller;
|
|
||||||
use OCP\AppFramework\Http\DataResponse;
|
|
||||||
use OCP\IGroup;
|
|
||||||
use OCP\IGroupManager;
|
|
||||||
use OCP\IL10N;
|
|
||||||
use OCP\IRequest;
|
|
||||||
use OCP\IUserSession;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package OC\Settings\Controller
|
|
||||||
*/
|
|
||||||
class GroupsController extends Controller {
|
|
||||||
/** @var IGroupManager */
|
|
||||||
private $groupManager;
|
|
||||||
/** @var IL10N */
|
|
||||||
private $l10n;
|
|
||||||
/** @var IUserSession */
|
|
||||||
private $userSession;
|
|
||||||
/** @var bool */
|
|
||||||
private $isAdmin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $appName
|
|
||||||
* @param IRequest $request
|
|
||||||
* @param IGroupManager $groupManager
|
|
||||||
* @param IUserSession $userSession
|
|
||||||
* @param bool $isAdmin
|
|
||||||
* @param IL10N $l10n
|
|
||||||
*/
|
|
||||||
public function __construct($appName,
|
|
||||||
IRequest $request,
|
|
||||||
IGroupManager $groupManager,
|
|
||||||
IUserSession $userSession,
|
|
||||||
$isAdmin,
|
|
||||||
IL10N $l10n) {
|
|
||||||
parent::__construct($appName, $request);
|
|
||||||
$this->groupManager = $groupManager;
|
|
||||||
$this->userSession = $userSession;
|
|
||||||
$this->isAdmin = $isAdmin;
|
|
||||||
$this->l10n = $l10n;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @NoAdminRequired
|
|
||||||
*
|
|
||||||
* @param string $pattern
|
|
||||||
* @param bool $filterGroups
|
|
||||||
* @param int $sortGroups
|
|
||||||
* @return DataResponse
|
|
||||||
*/
|
|
||||||
public function index($pattern = '', $filterGroups = false, $sortGroups = MetaData::SORT_USERCOUNT) {
|
|
||||||
$groupPattern = $filterGroups ? $pattern : '';
|
|
||||||
|
|
||||||
$groupsInfo = new MetaData(
|
|
||||||
$this->userSession->getUser()->getUID(),
|
|
||||||
$this->isAdmin,
|
|
||||||
$this->groupManager,
|
|
||||||
$this->userSession
|
|
||||||
);
|
|
||||||
$groupsInfo->setSorting($sortGroups);
|
|
||||||
list($adminGroups, $groups) = $groupsInfo->get($groupPattern, $pattern);
|
|
||||||
|
|
||||||
return new DataResponse(
|
|
||||||
array(
|
|
||||||
'data' => array('adminGroups' => $adminGroups, 'groups' => $groups)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @PasswordConfirmationRequired
|
|
||||||
* @param string $id
|
|
||||||
* @return DataResponse
|
|
||||||
*/
|
|
||||||
public function create($id) {
|
|
||||||
if($this->groupManager->groupExists($id)) {
|
|
||||||
return new DataResponse(
|
|
||||||
array(
|
|
||||||
'message' => (string)$this->l10n->t('Group already exists.')
|
|
||||||
),
|
|
||||||
Http::STATUS_CONFLICT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$group = $this->groupManager->createGroup($id);
|
|
||||||
if($group instanceof IGroup) {
|
|
||||||
return new DataResponse(['groupname' => $group->getDisplayName()], Http::STATUS_CREATED);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'error',
|
|
||||||
'data' => array(
|
|
||||||
'message' => (string)$this->l10n->t('Unable to add group.')
|
|
||||||
)
|
|
||||||
),
|
|
||||||
Http::STATUS_FORBIDDEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @PasswordConfirmationRequired
|
|
||||||
* @param string $id
|
|
||||||
* @return DataResponse
|
|
||||||
*/
|
|
||||||
public function destroy($id) {
|
|
||||||
$group = $this->groupManager->get($id);
|
|
||||||
if ($group) {
|
|
||||||
if ($group->delete()) {
|
|
||||||
return new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'success',
|
|
||||||
'data' => ['groupname' => $group->getDisplayName()]
|
|
||||||
),
|
|
||||||
Http::STATUS_NO_CONTENT
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'error',
|
|
||||||
'data' => array(
|
|
||||||
'message' => (string)$this->l10n->t('Unable to delete group.')
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Http::STATUS_FORBIDDEN
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
||||||
|
all: dev-setup build-js-production
|
||||||
|
|
||||||
|
dev-setup: clean clean-dev npm-init
|
||||||
|
|
||||||
|
npm-init:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm-update:
|
||||||
|
npm update
|
||||||
|
|
||||||
|
build-js:
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
build-js-production:
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
watch-js:
|
||||||
|
npm run watch
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -f js/main.js
|
||||||
|
rm -f js/main.js.map
|
||||||
|
|
||||||
|
clean-dev:
|
||||||
|
rm -rf node_modules
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
# Settings section
|
||||||
|
|
||||||
|
> Nextcloud settings with Vue
|
||||||
|
|
||||||
|
## Build Setup
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
# install dependencies
|
||||||
|
make dev-setup
|
||||||
|
|
||||||
|
# build for development
|
||||||
|
make build-js
|
||||||
|
|
||||||
|
# build for development and watch edits
|
||||||
|
make watch-js
|
||||||
|
|
||||||
|
# build for production with minification
|
||||||
|
make build-js-production
|
||||||
|
```
|
|
@ -1,77 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
*
|
|
||||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
|
||||||
* @author Bart Visscher <bartv@thisnet.nl>
|
|
||||||
* @author Björn Schießle <bjoern@schiessle.org>
|
|
||||||
* @author Christopher Schäpers <kondou@ts.unde.re>
|
|
||||||
* @author Felix Moeller <mail@felixmoeller.de>
|
|
||||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
|
||||||
* @author Joas Schilling <coding@schilljs.com>
|
|
||||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
||||||
* @author Morris Jobke <hey@morrisjobke.de>
|
|
||||||
* @author Robin Appelman <robin@icewind.nl>
|
|
||||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
||||||
*
|
|
||||||
* @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/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
OC_JSON::checkSubAdminUser();
|
|
||||||
\OC_JSON::callCheck();
|
|
||||||
|
|
||||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
|
||||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
|
||||||
$l = \OC::$server->getL10N('core');
|
|
||||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = isset($_POST["username"]) ? (string)$_POST["username"] : '';
|
|
||||||
|
|
||||||
$isUserAccessible = false;
|
|
||||||
$currentUserObject = \OC::$server->getUserSession()->getUser();
|
|
||||||
$targetUserObject = \OC::$server->getUserManager()->get($username);
|
|
||||||
if($targetUserObject !== null && $currentUserObject !== null) {
|
|
||||||
$isUserAccessible = \OC::$server->getGroupManager()->getSubAdmin()->isUserAccessible($currentUserObject, $targetUserObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(($username === '' && !OC_User::isAdminUser(OC_User::getUser()))
|
|
||||||
|| (!OC_User::isAdminUser(OC_User::getUser())
|
|
||||||
&& !$isUserAccessible)) {
|
|
||||||
$l = \OC::$server->getL10N('core');
|
|
||||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Authentication error') )));
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
//make sure the quota is in the expected format
|
|
||||||
$quota= (string)$_POST["quota"];
|
|
||||||
if($quota !== 'none' and $quota !== 'default') {
|
|
||||||
$quota= OC_Helper::computerFileSize($quota);
|
|
||||||
$quota=OC_Helper::humanFileSize($quota);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return Success story
|
|
||||||
if($username) {
|
|
||||||
$targetUserObject->setQuota($quota);
|
|
||||||
}else{//set the default quota when no username is specified
|
|
||||||
if($quota === 'default') {//'default' as default quota makes no sense
|
|
||||||
$quota='none';
|
|
||||||
}
|
|
||||||
\OC::$server->getConfig()->setAppValue('files', 'default_quota', $quota);
|
|
||||||
}
|
|
||||||
OC_JSON::success(array("data" => array( "username" => $username , 'quota' => $quota)));
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
*
|
|
||||||
* @author Bart Visscher <bartv@thisnet.nl>
|
|
||||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
|
||||||
* @author Joas Schilling <coding@schilljs.com>
|
|
||||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
||||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
|
||||||
*
|
|
||||||
* @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/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
OC_JSON::checkAdminUser();
|
|
||||||
\OC_JSON::callCheck();
|
|
||||||
|
|
||||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
|
||||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
|
||||||
$l = \OC::$server->getL10N('core');
|
|
||||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = (string)$_POST['username'];
|
|
||||||
$group = (string)$_POST['group'];
|
|
||||||
|
|
||||||
$subAdminManager = \OC::$server->getGroupManager()->getSubAdmin();
|
|
||||||
$targetUserObject = \OC::$server->getUserManager()->get($username);
|
|
||||||
$targetGroupObject = \OC::$server->getGroupManager()->get($group);
|
|
||||||
|
|
||||||
$isSubAdminOfGroup = false;
|
|
||||||
if($targetUserObject !== null && $targetGroupObject !== null) {
|
|
||||||
$isSubAdminOfGroup = $subAdminManager->isSubAdminOfGroup($targetUserObject, $targetGroupObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toggle group
|
|
||||||
if($isSubAdminOfGroup) {
|
|
||||||
$subAdminManager->deleteSubAdmin($targetUserObject, $targetGroupObject);
|
|
||||||
} else {
|
|
||||||
$subAdminManager->createSubAdmin($targetUserObject, $targetGroupObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
OC_JSON::success();
|
|
|
@ -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 */
|
/* used to highlight a user row in red */
|
||||||
|
|
||||||
#userlist tr.row-warning {
|
#userlist tr.row-warning {
|
||||||
|
@ -1350,3 +1255,178 @@ doesnotexist:-o-prefocus, .strengthify-wrapper {
|
||||||
margin-top: 22px;
|
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 {
|
||||||
|
// TODO replace with css4 subgrid when available
|
||||||
|
display: grid;
|
||||||
|
grid-row-start: span 1;
|
||||||
|
grid-gap: 3px;
|
||||||
|
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;
|
||||||
|
&.disabled {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
.name,
|
||||||
|
.displayName,
|
||||||
|
.password {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
.mailAddress,
|
||||||
|
.groups,
|
||||||
|
.subadmins {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.quota {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
.languages {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
/* fake input for groups validation */
|
||||||
|
input#newgroups {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 80% !important;
|
||||||
|
margin: 0 10%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// separate prop to set initial value to top:0
|
||||||
|
&#new-user {
|
||||||
|
top: $grid-row-height;
|
||||||
|
}
|
||||||
|
&#grid-header {
|
||||||
|
color: nc-lighten($color-main-text, 60%);
|
||||||
|
z-index: 60; /* above new-user */
|
||||||
|
}
|
||||||
|
&: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: calc(100% - 4px); /* minus left and right */
|
||||||
|
left: 2px;
|
||||||
|
bottom: 2px;
|
||||||
|
height: 3px;
|
||||||
|
z-index: 5; /* above multiselect */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* Fill the grid cell */
|
||||||
|
.multiselect.multiselect-vue {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.infinite-loading-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
grid-row-start: span 4;
|
||||||
|
}
|
||||||
|
.users-list-end {
|
||||||
|
opacity: .5;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -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');
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"name": "settings",
|
||||||
|
"description": "Nextcloud settings",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>",
|
||||||
|
"license": "AGPL3",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "webpack --config webpack.dev.js",
|
||||||
|
"watch": "webpack --progress --watch --config webpack.dev.js",
|
||||||
|
"build": "webpack --progress --hide-modules --config webpack.prod.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"babel-polyfill": "^6.26.0",
|
||||||
|
"v-tooltip": "^2.0.0-rc.32",
|
||||||
|
"vue": "^2.5.16",
|
||||||
|
"vue-click-outside": "^1.0.7",
|
||||||
|
"vue-infinite-loading": "^2.3.1",
|
||||||
|
"vue-localstorage": "^0.6.2",
|
||||||
|
"vue-multiselect": "^2.1.0",
|
||||||
|
"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.3",
|
||||||
|
"babel-loader": "^7.1.4",
|
||||||
|
"babel-preset-env": "^1.7.0",
|
||||||
|
"babel-preset-stage-3": "^6.24.1",
|
||||||
|
"css-loader": "^0.28.11",
|
||||||
|
"file-loader": "^1.1.11",
|
||||||
|
"node-sass": "^4.9.0",
|
||||||
|
"sass-loader": "^7.0.1",
|
||||||
|
"vue-loader": "^14.2.2",
|
||||||
|
"vue-template-compiler": "^2.5.16",
|
||||||
|
"webpack": "^4.8.3",
|
||||||
|
"webpack-cli": "^2.1.3",
|
||||||
|
"webpack-merge": "^4.1.2"
|
||||||
|
}
|
||||||
|
}
|
|
@ -39,7 +39,6 @@ namespace OC\Settings;
|
||||||
$application = new Application();
|
$application = new Application();
|
||||||
$application->registerRoutes($this, [
|
$application->registerRoutes($this, [
|
||||||
'resources' => [
|
'resources' => [
|
||||||
'users' => ['url' => '/settings/users/users'],
|
|
||||||
'AuthSettings' => ['url' => '/settings/personal/authtokens'],
|
'AuthSettings' => ['url' => '/settings/personal/authtokens'],
|
||||||
],
|
],
|
||||||
'routes' => [
|
'routes' => [
|
||||||
|
@ -50,12 +49,10 @@ $application->registerRoutes($this, [
|
||||||
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'],
|
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'],
|
||||||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'],
|
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'],
|
||||||
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'],
|
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'],
|
||||||
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'],
|
|
||||||
['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'],
|
|
||||||
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'],
|
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'],
|
||||||
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'],
|
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'],
|
||||||
['name' => 'Users#setEnabled', 'url' => '/settings/users/{id}/setEnabled', 'verb' => 'POST'],
|
['name' => 'Users#usersList', 'url' => '/settings/users', 'verb' => 'GET'],
|
||||||
['name' => 'Users#stats', 'url' => '/settings/users/stats', 'verb' => 'GET'],
|
['name' => 'Users#usersListByGroup', 'url' => '/settings/users/{group}', 'verb' => 'GET'],
|
||||||
['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'],
|
['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'],
|
||||||
['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'],
|
['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', 'verb' => 'GET'],
|
||||||
['name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'],
|
['name' => 'LogSettings#download', 'url' => '/settings/admin/log/download', 'verb' => 'GET'],
|
||||||
|
@ -70,12 +67,7 @@ $application->registerRoutes($this, [
|
||||||
['name' => 'AdminSettings#index', 'url' => '/settings/admin/{section}', 'verb' => 'GET', 'defaults' => ['section' => 'server']],
|
['name' => 'AdminSettings#index', 'url' => '/settings/admin/{section}', 'verb' => 'GET', 'defaults' => ['section' => 'server']],
|
||||||
['name' => 'AdminSettings#form', 'url' => '/settings/admin/{section}', 'verb' => 'GET'],
|
['name' => 'AdminSettings#form', 'url' => '/settings/admin/{section}', 'verb' => 'GET'],
|
||||||
['name' => 'ChangePassword#changePersonalPassword', 'url' => '/settings/personal/changepassword', 'verb' => 'POST'],
|
['name' => 'ChangePassword#changePersonalPassword', 'url' => '/settings/personal/changepassword', 'verb' => 'POST'],
|
||||||
['name' => 'ChangePassword#changeUserPassword', 'url' => '/settings/users/changepassword', 'verb' => 'POST'],
|
['name' => 'ChangePassword#changeUserPassword', 'url' => '/settings/users/changepassword', 'verb' => 'POST']
|
||||||
['name' => 'Groups#index', 'url' => '/settings/users/groups', 'verb' => 'GET'],
|
|
||||||
['name' => 'Groups#show', 'url' => '/settings/users/groups/{id}', 'requirements' => ['id' => '[^?]*'], 'verb' => 'GET'],
|
|
||||||
['name' => 'Groups#create', 'url' => '/settings/users/groups', 'verb' => 'POST'],
|
|
||||||
['name' => 'Groups#update', 'url' => '/settings/users/groups/{id}', 'requirements' => ['id' => '[^?]*'], 'verb' => 'PUT'],
|
|
||||||
['name' => 'Groups#destroy', 'url' => '/settings/users/groups/{id}', 'requirements' => ['id' => '[^?]*'], 'verb' => 'DELETE'],
|
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -84,18 +76,7 @@ $application->registerRoutes($this, [
|
||||||
// Settings pages
|
// Settings pages
|
||||||
$this->create('settings_help', '/settings/help')
|
$this->create('settings_help', '/settings/help')
|
||||||
->actionInclude('settings/help.php');
|
->actionInclude('settings/help.php');
|
||||||
$this->create('settings_users', '/settings/users')
|
|
||||||
->actionInclude('settings/users.php');
|
|
||||||
// Settings ajax actions
|
// Settings ajax actions
|
||||||
// users
|
|
||||||
$this->create('settings_ajax_setquota', '/settings/ajax/setquota.php')
|
|
||||||
->actionInclude('settings/ajax/setquota.php');
|
|
||||||
$this->create('settings_ajax_togglegroups', '/settings/ajax/togglegroups.php')
|
|
||||||
->actionInclude('settings/ajax/togglegroups.php');
|
|
||||||
$this->create('settings_ajax_togglesubadmins', '/settings/ajax/togglesubadmins.php')
|
|
||||||
->actionInclude('settings/ajax/togglesubadmins.php');
|
|
||||||
$this->create('settings_ajax_changegorupname', '/settings/ajax/changegroupname.php')
|
|
||||||
->actionInclude('settings/ajax/changegroupname.php');
|
|
||||||
// apps
|
// apps
|
||||||
$this->create('settings_ajax_enableapp', '/settings/ajax/enableapp.php')
|
$this->create('settings_ajax_enableapp', '/settings/ajax/enableapp.php')
|
||||||
->actionInclude('settings/ajax/enableapp.php');
|
->actionInclude('settings/ajax/enableapp.php');
|
||||||
|
@ -105,6 +86,3 @@ $this->create('settings_ajax_updateapp', '/settings/ajax/updateapp.php')
|
||||||
->actionInclude('settings/ajax/updateapp.php');
|
->actionInclude('settings/ajax/updateapp.php');
|
||||||
$this->create('settings_ajax_uninstallapp', '/settings/ajax/uninstallapp.php')
|
$this->create('settings_ajax_uninstallapp', '/settings/ajax/uninstallapp.php')
|
||||||
->actionInclude('settings/ajax/uninstallapp.php');
|
->actionInclude('settings/ajax/uninstallapp.php');
|
||||||
// admin
|
|
||||||
$this->create('settings_ajax_excludegroups', '/settings/ajax/excludegroups.php')
|
|
||||||
->actionInclude('settings/ajax/excludegroups.php');
|
|
||||||
|
|
|
@ -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" :class="{'icon-loading': menu.loading}">
|
||||||
|
<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,117 @@
|
||||||
|
<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 v-if="item.href" :href="(item.href) ? item.href : '#' " @click="toggleCollapse" :class="item.icon" >
|
||||||
|
<img v-if="item.iconUrl" :alt="item.text" :src="item.iconUrl">
|
||||||
|
{{item.text}}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Router link if specified. href OR router -->
|
||||||
|
<router-link :to="item.router" v-else-if="item.router" :class="item.icon" >
|
||||||
|
<img v-if="item.iconUrl" :alt="item.text" :src="item.iconUrl">
|
||||||
|
{{item.text}}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
|
<!-- 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 @click="item.utils.actions[0].action" :class="item.utils.actions[0].icon" :title="item.utils.actions[0].text"></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 @click="action.action" :class="action.icon" :title="action.text"></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,28 @@
|
||||||
|
<template>
|
||||||
|
<li>
|
||||||
|
<!-- If item.href is set, a link will be directly used -->
|
||||||
|
<a @click="item.action" v-if="item.href" :href="(item.href) ? item.href : '#' ">
|
||||||
|
<span :class="item.icon"></span>
|
||||||
|
<span v-if="item.text">{{item.text}}</span>
|
||||||
|
<p v-else-if="item.longtext">{{item.longtext}}</p>
|
||||||
|
</a>
|
||||||
|
<!-- If item.action is set instead, a button will be used -->
|
||||||
|
<button @click="item.action" v-else-if="item.action">
|
||||||
|
<span :class="item.icon"></span>
|
||||||
|
<span v-if="item.text">{{item.text}}</span>
|
||||||
|
<p v-else-if="item.longtext">{{item.longtext}}</p>
|
||||||
|
</button>
|
||||||
|
<!-- If item.longtext is set AND the item does not have an action -->
|
||||||
|
<span v-else>
|
||||||
|
<span :class="item.icon"></span>
|
||||||
|
<span v-if="item.text">{{item.text}}</span>
|
||||||
|
<p v-else-if="item.longtext">{{item.longtext}}</p>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ['item']
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,298 @@
|
||||||
|
<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 && settings.isAdmin">{{ t('settings', 'Group admin for') }}</div>
|
||||||
|
<div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
|
||||||
|
<div id="headerLanguages" class="languages"
|
||||||
|
v-if="showConfig.showLanguages">{{ t('settings', 'Languages') }}</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">
|
||||||
|
<!-- hidden input trick for vanilla html5 form validation -->
|
||||||
|
<input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
|
||||||
|
tabindex="-1" id="newgroups" :required="!settings.isAdmin" />
|
||||||
|
<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"
|
||||||
|
:allowEmpty="settings.isAdmin">
|
||||||
|
<!-- If user is not admin, he is a subadmin.
|
||||||
|
Subadmins can't create users outside their groups
|
||||||
|
Therefore, empty select is forbidden -->
|
||||||
|
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin">
|
||||||
|
<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 results')}}</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="languages" v-if="showConfig.showLanguages">
|
||||||
|
<multiselect :options="languages" v-model="newUser.language"
|
||||||
|
:placeholder="t('settings', 'Default language')"
|
||||||
|
label="name" track-by="code" class="multiselect-vue"
|
||||||
|
:allowEmpty="false" group-values="languages" group-label="label">
|
||||||
|
</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 filteredUsers" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
|
||||||
|
:groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" :languages="languages" />
|
||||||
|
<infinite-loading @infinite="infiniteHandler" ref="infiniteLoading">
|
||||||
|
<div slot="spinner"><div class="users-icon-loading icon-loading"></div></div>
|
||||||
|
<div slot="no-more"><div class="users-list-end">— {{t('settings', 'no more results')}} —</div></div>
|
||||||
|
<div slot="no-results">
|
||||||
|
<div id="emptycontent">
|
||||||
|
<div class="icon-contacts-dark"></div>
|
||||||
|
<h2>{{t('settings', 'No users in here')}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</infinite-loading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import userRow from './userList/userRow';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import InfiniteLoading from 'vue-infinite-loading';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'userList',
|
||||||
|
props: ['users', 'showConfig', 'selectedGroup'],
|
||||||
|
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,
|
||||||
|
language: {code: 'en', name: t('settings', 'Default language')}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (!this.settings.canChangePassword) {
|
||||||
|
OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Init default language from server data. The use of this.settings
|
||||||
|
* requires a computed variable, which break the v-model binding of the form,
|
||||||
|
* this is a much easier solution than getter and setter on a computed var
|
||||||
|
*/
|
||||||
|
Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In case the user directly loaded the user list within a group
|
||||||
|
* the watch won't be triggered. We need to initialize it.
|
||||||
|
*/
|
||||||
|
this.setNewUserDefaultGroup(this.$route.params.selectedGroup);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
settings() {
|
||||||
|
return this.$store.getters.getServerData;
|
||||||
|
},
|
||||||
|
filteredUsers() {
|
||||||
|
if (this.selectedGroup === 'disabled') {
|
||||||
|
let disabledUsers = this.users.filter(user => user.enabled !== true);
|
||||||
|
if (disabledUsers.length===0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) {
|
||||||
|
// disabled group is empty, redirection to all users
|
||||||
|
this.$router.push({name: 'users'});
|
||||||
|
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
|
||||||
|
}
|
||||||
|
return disabledUsers;
|
||||||
|
}
|
||||||
|
return this.users.filter(user => user.enabled === true);
|
||||||
|
},
|
||||||
|
groups() {
|
||||||
|
// data provided php side + remove the disabled group
|
||||||
|
return this.$store.getters.getGroups
|
||||||
|
.filter(group => group.id !== 'disabled')
|
||||||
|
.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* LANGUAGES */
|
||||||
|
languages() {
|
||||||
|
return Array(
|
||||||
|
{
|
||||||
|
label: t('settings', 'Common languages'),
|
||||||
|
languages: this.settings.languages.commonlanguages
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t('settings', 'All languages'),
|
||||||
|
languages: this.settings.languages.languages
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// watch url change and group select
|
||||||
|
selectedGroup: function (val, old) {
|
||||||
|
this.$store.commit('resetUsers');
|
||||||
|
this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
|
||||||
|
this.setNewUserDefaultGroup(val);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
group: this.selectedGroup !== 'disabled' ? this.selectedGroup : ''
|
||||||
|
})
|
||||||
|
.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),
|
||||||
|
subadmin: this.newUser.subAdminsGroups.map(group => group.id),
|
||||||
|
quota: this.newUser.quota.id,
|
||||||
|
language: this.newUser.language.code,
|
||||||
|
}).then(() => this.resetForm())
|
||||||
|
.catch(() => this.loading = false);
|
||||||
|
},
|
||||||
|
setNewUserDefaultGroup(value) {
|
||||||
|
if (value && value.length > 0) {
|
||||||
|
// setting new user default group to the current selected one
|
||||||
|
let currentGroup = this.groups.find(group => group.id === value);
|
||||||
|
if (currentGroup) {
|
||||||
|
this.newUser.groups = [currentGroup];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback, empty selected group
|
||||||
|
this.newUser.groups = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,449 @@
|
||||||
|
<template>
|
||||||
|
<div class="row" :class="{'disabled': loading.delete || loading.disable}">
|
||||||
|
<div class="avatar" :class="{'icon-loading-small': loading.delete || loading.disable}">
|
||||||
|
<img alt="" width="32" height="32" :src="generateAvatar(user.id, 32)"
|
||||||
|
:srcset="generateAvatar(user.id, 64)+' 2x, '+generateAvatar(user.id, 128)+' 4x'"
|
||||||
|
v-if="!loading.delete && !loading.disable">
|
||||||
|
</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"
|
||||||
|
:multiple="true" :taggable="settings.isAdmin" :closeOnSelect="false"
|
||||||
|
@tag="createGroup" @select="addUserGroup" @remove="removeUserGroup">
|
||||||
|
<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userGroups)">+{{userGroups.length-2}}</span>
|
||||||
|
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin" :class="{'icon-loading-small': loading.subadmins}">
|
||||||
|
<multiselect :value="userSubAdminsGroups" :options="subAdminsGroups" :disabled="loading.subadmins||loading.all"
|
||||||
|
:placeholder="t('settings', 'Set user as admin for')"
|
||||||
|
label="name" track-by="id" class="multiselect-vue" :limit="2"
|
||||||
|
:multiple="true" :closeOnSelect="false"
|
||||||
|
@select="addUserSubAdmin" @remove="removeUserSubAdmin">
|
||||||
|
<span slot="limit" class="multiselect__limit" v-tooltip.auto="formatGroupsTitle(userSubAdminsGroups)">+{{userSubAdminsGroups.length-2}}</span>
|
||||||
|
<span slot="noResult">{{t('settings', 'No results')}}</span>
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div class="quota" :class="{'icon-loading-small': loading.quota}">
|
||||||
|
<multiselect :value="userQuota" :options="quotaOptions" :disabled="loading.quota||loading.all"
|
||||||
|
tag-placeholder="create" :placeholder="t('settings', 'Select user quota')"
|
||||||
|
label="label" track-by="id" class="multiselect-vue"
|
||||||
|
:allowEmpty="false" :taggable="true"
|
||||||
|
@tag="validateQuota" @input="setUserQuota">
|
||||||
|
</multiselect>
|
||||||
|
<progress class="quota-user-progress" :class="{'warn':usedQuota>80}" :value="usedQuota" max="100"></progress>
|
||||||
|
</div>
|
||||||
|
<div class="languages" :class="{'icon-loading-small': loading.languages}"
|
||||||
|
v-if="showConfig.showLanguages">
|
||||||
|
<multiselect :value="userLanguage" :options="languages" :disabled="loading.languages||loading.all"
|
||||||
|
:placeholder="t('settings', 'No language set')"
|
||||||
|
label="name" track-by="code" class="multiselect-vue"
|
||||||
|
:allowEmpty="false" group-values="languages" group-label="label"
|
||||||
|
@input="setUserLanguage">
|
||||||
|
</multiselect>
|
||||||
|
</div>
|
||||||
|
<div class="storageLocation" v-if="showConfig.showStoragePath">{{user.storageLocation}}</div>
|
||||||
|
<div class="userBackend" v-if="showConfig.showUserBackend">{{user.backend}}</div>
|
||||||
|
<div class="lastLogin" v-if="showConfig.showLastLogin" v-tooltip.auto="user.lastLogin>0 ? OC.Util.formatDate(user.lastLogin) : ''">
|
||||||
|
{{user.lastLogin>0 ? OC.Util.relativeModifiedDate(user.lastLogin) : t('settings','Never')}}
|
||||||
|
</div>
|
||||||
|
<div class="userActions">
|
||||||
|
<div class="toggleUserActions" v-if="OC.currentUser !== user.id && user.id !== 'admin' && !loading.all">
|
||||||
|
<div class="icon-more" v-click-outside="hideMenu" @click="toggleMenu"></div>
|
||||||
|
<div class="popovermenu" :class="{ 'open': openedMenu }">
|
||||||
|
<popover-menu :menu="userActions" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import popoverMenu from '../popoverMenu';
|
||||||
|
import ClickOutside from 'vue-click-outside';
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import Vue from 'vue'
|
||||||
|
import VTooltip from 'v-tooltip'
|
||||||
|
|
||||||
|
Vue.use(VTooltip)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'userRow',
|
||||||
|
props: ['user', 'settings', 'groups', 'subAdminsGroups', 'quotaOptions', 'showConfig', 'languages'],
|
||||||
|
components: {
|
||||||
|
popoverMenu,
|
||||||
|
Multiselect
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
ClickOutside
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// required if popup needs to stay opened after menu click
|
||||||
|
// since we only have disable/delete actions, let's close it directly
|
||||||
|
// this.popupItem = this.$el;
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
rand: parseInt(Math.random() * 1000),
|
||||||
|
openedMenu: false,
|
||||||
|
loading: {
|
||||||
|
all: false,
|
||||||
|
displayName: false,
|
||||||
|
password: false,
|
||||||
|
mailAddress: false,
|
||||||
|
groups: false,
|
||||||
|
subadmins: false,
|
||||||
|
quota: false,
|
||||||
|
delete: false,
|
||||||
|
disable: false,
|
||||||
|
languages: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/* USER POPOVERMENU ACTIONS */
|
||||||
|
userActions() {
|
||||||
|
return [{
|
||||||
|
icon: 'icon-delete',
|
||||||
|
text: t('settings','Delete user'),
|
||||||
|
action: this.deleteUser
|
||||||
|
},{
|
||||||
|
icon: this.user.enabled ? 'icon-close' : 'icon-add',
|
||||||
|
text: this.user.enabled ? t('settings','Disable user') : t('settings','Enable user'),
|
||||||
|
action: this.enableDisableUser
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* LANGUAGE */
|
||||||
|
userLanguage() {
|
||||||
|
let availableLanguages = this.languages[0].languages.concat(this.languages[1].languages);
|
||||||
|
let userLang = availableLanguages.find(lang => lang.code === this.user.language);
|
||||||
|
if (typeof userLang !== 'object' && this.user.language !== '') {
|
||||||
|
return {
|
||||||
|
code: this.user.language,
|
||||||
|
name: this.user.language
|
||||||
|
}
|
||||||
|
} else if(this.user.language === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return userLang;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* MENU HANDLING */
|
||||||
|
toggleMenu() {
|
||||||
|
this.openedMenu = !this.openedMenu;
|
||||||
|
},
|
||||||
|
hideMenu() {
|
||||||
|
this.openedMenu = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate avatar url
|
||||||
|
*
|
||||||
|
* @param {string} user The user name
|
||||||
|
* @param {int} size Size integer, default 32
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
generateAvatar(user, size=32) {
|
||||||
|
return OC.generateUrl(
|
||||||
|
'/avatar/{user}/{size}?v={version}',
|
||||||
|
{
|
||||||
|
user: user,
|
||||||
|
size: size,
|
||||||
|
version: oc_userconfig.avatar.version
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format array of groups objects to a string for the popup
|
||||||
|
*
|
||||||
|
* @param {array} groups The groups
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatGroupsTitle(groups) {
|
||||||
|
let names = groups.map(group => group.name);
|
||||||
|
return names.slice(2,).join(', ');
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteUser() {
|
||||||
|
this.loading.delete = true;
|
||||||
|
this.loading.all = true;
|
||||||
|
let userid = this.user.id;
|
||||||
|
return this.$store.dispatch('deleteUser', {userid})
|
||||||
|
.then(() => {
|
||||||
|
this.loading.delete = false
|
||||||
|
this.loading.all = false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
enableDisableUser() {
|
||||||
|
this.loading.delete = true;
|
||||||
|
this.loading.all = true;
|
||||||
|
let userid = this.user.id;
|
||||||
|
let enabled = !this.user.enabled;
|
||||||
|
return this.$store.dispatch('enableDisableUser', {userid, enabled})
|
||||||
|
.then(() => {
|
||||||
|
this.loading.delete = false
|
||||||
|
this.loading.all = false
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user displayName
|
||||||
|
*
|
||||||
|
* @param {string} displayName The display name
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
updateDisplayName() {
|
||||||
|
let displayName = this.$refs.displayName.value;
|
||||||
|
this.loading.displayName = true;
|
||||||
|
this.$store.dispatch('setUserData', {
|
||||||
|
userid: this.user.id,
|
||||||
|
key: 'displayname',
|
||||||
|
value: displayName
|
||||||
|
}).then(() => {
|
||||||
|
this.loading.displayName = false;
|
||||||
|
this.$refs.displayName.value = displayName;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user password
|
||||||
|
*
|
||||||
|
* @param {string} password The email adress
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
updatePassword() {
|
||||||
|
let password = this.$refs.password.value;
|
||||||
|
this.loading.password = true;
|
||||||
|
this.$store.dispatch('setUserData', {
|
||||||
|
userid: this.user.id,
|
||||||
|
key: 'password',
|
||||||
|
value: password
|
||||||
|
}).then(() => {
|
||||||
|
this.loading.password = false;
|
||||||
|
this.$refs.password.value = ''; // empty & show placeholder
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user mailAddress
|
||||||
|
*
|
||||||
|
* @param {string} mailAddress The email adress
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
updateEmail() {
|
||||||
|
let mailAddress = this.$refs.mailAddress.value;
|
||||||
|
this.loading.mailAddress = true;
|
||||||
|
this.$store.dispatch('setUserData', {
|
||||||
|
userid: this.user.id,
|
||||||
|
key: 'email',
|
||||||
|
value: mailAddress
|
||||||
|
}).then(() => {
|
||||||
|
this.loading.mailAddress = false;
|
||||||
|
this.$refs.mailAddress.value = mailAddress;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new group
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
// remove user from current list if current list is the removed group
|
||||||
|
if (this.$route.params.selectedGroup === gid) {
|
||||||
|
this.$store.commit('deleteUser', userid);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add user to group
|
||||||
|
*
|
||||||
|
* @param {object} group Group object
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
addUserSubAdmin(group) {
|
||||||
|
this.loading.subadmins = true;
|
||||||
|
let userid = this.user.id;
|
||||||
|
let gid = group.id;
|
||||||
|
return this.$store.dispatch('addUserSubAdmin', {userid, gid})
|
||||||
|
.then(() => this.loading.subadmins = false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove user from group
|
||||||
|
*
|
||||||
|
* @param {object} group Group object
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
removeUserSubAdmin(group) {
|
||||||
|
this.loading.subadmins = true;
|
||||||
|
let userid = this.user.id;
|
||||||
|
let gid = group.id;
|
||||||
|
return this.$store.dispatch('removeUserSubAdmin', {userid, gid})
|
||||||
|
.then(() => this.loading.subadmins = false);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch quota set request
|
||||||
|
*
|
||||||
|
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
setUserQuota(quota = 'none') {
|
||||||
|
this.loading.quota = true;
|
||||||
|
// ensure we only send the preset id
|
||||||
|
quota = quota.id ? quota.id : quota;
|
||||||
|
this.$store.dispatch('setUserData', {
|
||||||
|
userid: this.user.id,
|
||||||
|
key: 'quota',
|
||||||
|
value: quota
|
||||||
|
}).then(() => this.loading.quota = false);
|
||||||
|
return quota;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate quota string to make sure it's a valid human file size
|
||||||
|
*
|
||||||
|
* @param {string} quota Quota in readable format '5 GB'
|
||||||
|
* @returns {Promise|boolean}
|
||||||
|
*/
|
||||||
|
validateQuota(quota) {
|
||||||
|
// only used for new presets sent through @Tag
|
||||||
|
let validQuota = OC.Util.computerFileSize(quota);
|
||||||
|
if (validQuota === 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 do not change
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch language set request
|
||||||
|
*
|
||||||
|
* @param {Object} lang language object {code:'en', name:'English'}
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
setUserLanguage(lang) {
|
||||||
|
this.loading.languages = true;
|
||||||
|
// ensure we only send the preset id
|
||||||
|
this.$store.dispatch('setUserData', {
|
||||||
|
userid: this.user.id,
|
||||||
|
key: 'language',
|
||||||
|
value: lang.code
|
||||||
|
}).then(() => this.loading.languages = false);
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,22 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { sync } from 'vuex-router-sync';
|
||||||
|
import App from './App.vue';
|
||||||
|
import router from './router';
|
||||||
|
import store from './store';
|
||||||
|
require("babel-polyfill");
|
||||||
|
|
||||||
|
|
||||||
|
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,36 @@
|
||||||
|
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',
|
||||||
|
// if index.php is in the url AND we got this far, then it's working:
|
||||||
|
// let's keep using index.php in the url
|
||||||
|
base: OC.generateUrl(''),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/:index(index.php/)?settings/users',
|
||||||
|
component: Users,
|
||||||
|
props: true,
|
||||||
|
name: 'users',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: ':selectedGroup',
|
||||||
|
name: 'group',
|
||||||
|
component: Users
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
});
|
|
@ -0,0 +1,99 @@
|
||||||
|
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 url slash
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This Promise is used to chain a request that require an admin password confirmation
|
||||||
|
* Since chaining Promise have a very precise behavior concerning catch and then,
|
||||||
|
* you'll need to be careful when using it.
|
||||||
|
* e.g
|
||||||
|
* // store
|
||||||
|
* action(context) {
|
||||||
|
* return api.requireAdmin().then((response) => {
|
||||||
|
* return api.get('url')
|
||||||
|
* .then((response) => {API success})
|
||||||
|
* .catch((error) => {API failure});
|
||||||
|
* }).catch((error) => {requireAdmin failure});
|
||||||
|
* }
|
||||||
|
* // vue
|
||||||
|
* this.$store.dispatch('action').then(() => {always executed})
|
||||||
|
*
|
||||||
|
* Since Promise.then().catch().then() will always execute the last then
|
||||||
|
* this.$store.dispatch('action').then will always be executed
|
||||||
|
*
|
||||||
|
* If you want requireAdmin failure to also catch the API request failure
|
||||||
|
* you will need to throw a new error in the api.get.catch()
|
||||||
|
*
|
||||||
|
* e.g
|
||||||
|
* api.requireAdmin().then((response) => {
|
||||||
|
* api.get('url')
|
||||||
|
* .then((response) => {API success})
|
||||||
|
* .catch((error) => {throw error;});
|
||||||
|
* }).catch((error) => {requireAdmin OR API failure});
|
||||||
|
*
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
requireAdmin() {
|
||||||
|
return new Promise(function(resolve, reject) {
|
||||||
|
// TODO: migrate the OC.dialog to Vue and avoid this mess
|
||||||
|
// wait for password confirmation
|
||||||
|
let passwordTimeout;
|
||||||
|
let waitForpassword = function() {
|
||||||
|
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||||
|
passwordTimeout = setTimeout(waitForpassword, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearTimeout(passwordTimeout);
|
||||||
|
clearTimeout(promiseTimeout);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
// automatically reject after 5s if not resolved
|
||||||
|
let promiseTimeout = setTimeout(() => {
|
||||||
|
clearTimeout(passwordTimeout);
|
||||||
|
// close dialog
|
||||||
|
if (document.getElementsByClassName('oc-dialog-close').length>0) {
|
||||||
|
document.getElementsByClassName('oc-dialog-close')[0].click();
|
||||||
|
}
|
||||||
|
OC.Notification.showTemporary(t('settings', 'You did not enter the password in time'));
|
||||||
|
reject('Password request cancelled');
|
||||||
|
}, 7000);
|
||||||
|
|
||||||
|
// request password
|
||||||
|
OC.PasswordConfirmation.requirePasswordConfirmation();
|
||||||
|
waitForpassword();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
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, tokenHeaders)
|
||||||
|
.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,28 @@
|
||||||
|
import Vue from 'vue';
|
||||||
|
import Vuex from 'vuex';
|
||||||
|
import users from './users';
|
||||||
|
import settings from './settings';
|
||||||
|
import oc from './oc';
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
const debug = process.env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
API_FAILURE(state, error) {
|
||||||
|
let message = error.error.response.data.ocs.meta.message;
|
||||||
|
OC.Notification.showHtml(t('settings','An error occured during the request. Unable to proceed.')+'<br>'+message, {timeout: 7});
|
||||||
|
console.log(state, error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default new Vuex.Store({
|
||||||
|
modules: {
|
||||||
|
users,
|
||||||
|
settings,
|
||||||
|
oc
|
||||||
|
},
|
||||||
|
strict: debug,
|
||||||
|
|
||||||
|
mutations
|
||||||
|
});
|
|
@ -0,0 +1,25 @@
|
||||||
|
import api from './api';
|
||||||
|
|
||||||
|
const state = {};
|
||||||
|
const mutations = {};
|
||||||
|
const getters = {};
|
||||||
|
const actions = {
|
||||||
|
/**
|
||||||
|
* Set application config in database
|
||||||
|
*
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {Object} options
|
||||||
|
* @param {string} options.app Application name
|
||||||
|
* @param {boolean} options.key Config key
|
||||||
|
* @param {boolean} options.value Value to set
|
||||||
|
* @returns{Promise}
|
||||||
|
*/
|
||||||
|
setAppConfig(context, {app, key, value}) {
|
||||||
|
return api.requireAdmin().then((response) => {
|
||||||
|
return api.post(OC.linkToOCS(`apps/provisioning_api/api/v1/config/apps/${app}/${key}`, 2), {value: value})
|
||||||
|
.catch((error) => {throw error;});
|
||||||
|
}).catch((error) => context.commit('API_FAILURE', { app, key, value, error }));;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {state, mutations, getters, actions};
|
|
@ -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,420 @@
|
||||||
|
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,
|
||||||
|
userCount: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
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, userCount}) {
|
||||||
|
state.groups = groups;
|
||||||
|
state.orderBy = orderBy;
|
||||||
|
state.userCount = userCount;
|
||||||
|
state.groups = orderGroups(state.groups, state.orderBy);
|
||||||
|
},
|
||||||
|
addGroup(state, gid) {
|
||||||
|
try {
|
||||||
|
state.groups.push({
|
||||||
|
id: gid,
|
||||||
|
name: gid,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeGroup(state, gid) {
|
||||||
|
let groupIndex = state.groups.findIndex(groupSearch => groupSearch.id == gid);
|
||||||
|
if (groupIndex >= 0) {
|
||||||
|
state.groups.splice(groupIndex, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addUserGroup(state, { userid, gid }) {
|
||||||
|
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 }) {
|
||||||
|
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;
|
||||||
|
// increment or not
|
||||||
|
state.groups.find(group => group.id == 'disabled').usercount += enabled ? -1 : 1;
|
||||||
|
state.userCount += enabled ? 1 : -1;
|
||||||
|
console.log(enabled);
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset users list
|
||||||
|
*/
|
||||||
|
resetUsers(state) {
|
||||||
|
state.users = [];
|
||||||
|
state.usersOffset = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
getUserCount(state) {
|
||||||
|
return state.userCount;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
* @param {string} options.search Search amongst users
|
||||||
|
* @param {string} options.group Get users from group
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
getUsers(context, { offset, limit, search, group }) {
|
||||||
|
search = typeof search === 'string' ? search : '';
|
||||||
|
group = typeof group === 'string' ? group : '';
|
||||||
|
if (group !== '') {
|
||||||
|
return api.get(OC.linkToOCS(`cloud/groups/${group}/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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if(oc_capabilities.password_policy && oc_capabilities.password_policy.minLength) {
|
||||||
|
context.commit('setPasswordPolicyMinLength', oc_capabilities.password_policy.minLength);
|
||||||
|
return oc_capabilities.password_policy.minLength;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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) => {throw error;});
|
||||||
|
}).catch((error) => context.commit('API_FAILURE', { userid, error }));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove group
|
||||||
|
*
|
||||||
|
* @param {Object} context
|
||||||
|
* @param {string} gid Group id
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
removeGroup(context, gid) {
|
||||||
|
return api.requireAdmin().then((response) => {
|
||||||
|
return api.delete(OC.linkToOCS(`cloud/groups/${gid}`, 2))
|
||||||
|
.then((response) => context.commit('removeGroup', gid))
|
||||||
|
.catch((error) => {throw error;});
|
||||||
|
}).catch((error) => context.commit('API_FAILURE', { gid, 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) => {throw error;});
|
||||||
|
}).catch((error) => context.commit('API_FAILURE', { userid, 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) => {throw error;});
|
||||||
|
}).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) => {throw error;});
|
||||||
|
}).catch((error) => context.commit('API_FAILURE', { userid, 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) => {throw error;});
|
||||||
|
}).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) => {throw error;});
|
||||||
|
}).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
|
||||||
|
* @param {string} options.groups User groups
|
||||||
|
* @param {string} options.subadmin User subadmin groups
|
||||||
|
* @param {string} options.quota User email
|
||||||
|
* @returns {Promise}
|
||||||
|
*/
|
||||||
|
addUser({commit, dispatch}, { userid, password, email, groups, subadmin, quota, language }) {
|
||||||
|
return api.requireAdmin().then((response) => {
|
||||||
|
return api.post(OC.linkToOCS(`cloud/users`, 2), { userid, password, email, groups, subadmin, quota, language })
|
||||||
|
.then((response) => dispatch('addUserData', userid))
|
||||||
|
.catch((error) => {throw error;});
|
||||||
|
}).catch((error) => 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) => {throw error;});
|
||||||
|
}).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) => {throw error;});
|
||||||
|
}).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 }) {
|
||||||
|
let allowedEmpty = ['email', 'displayname'];
|
||||||
|
if (['email', 'language', 'quota', 'displayname', 'password'].indexOf(key) !== -1) {
|
||||||
|
// We allow empty email or displayname
|
||||||
|
if (typeof value === 'string' &&
|
||||||
|
(
|
||||||
|
(allowedEmpty.indexOf(key) === -1 && value.length > 0) ||
|
||||||
|
allowedEmpty.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) => {throw error;});
|
||||||
|
}).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,301 @@
|
||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<app-navigation :menu="menu">
|
||||||
|
<template slot="settings-content">
|
||||||
|
<div>
|
||||||
|
<p>{{t('settings', 'Default quota :')}}</p>
|
||||||
|
<multiselect :value="defaultQuota" :options="quotaOptions"
|
||||||
|
tag-placeholder="create" :placeholder="t('settings', 'Select default quota')"
|
||||||
|
label="label" track-by="id" class="multiselect-vue"
|
||||||
|
:allowEmpty="false" :taggable="true"
|
||||||
|
@tag="validateQuota" @input="setDefaultQuota">
|
||||||
|
</multiselect>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="showLanguages" class="checkbox" v-model="showLanguages">
|
||||||
|
<label for="showLanguages">{{t('settings', 'Show Languages')}}</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="showLastLogin" class="checkbox" v-model="showLastLogin">
|
||||||
|
<label for="showLastLogin">{{t('settings', 'Show last login')}}</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="showUserBackend" class="checkbox" v-model="showUserBackend">
|
||||||
|
<label for="showUserBackend">{{t('settings', 'Show user backend')}}</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input type="checkbox" id="showStoragePath" class="checkbox" v-model="showStoragePath">
|
||||||
|
<label for="showStoragePath">{{t('settings', 'Show storage path')}}</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</app-navigation>
|
||||||
|
<user-list :users="users" :showConfig="showConfig" :selectedGroup="selectedGroup" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import appNavigation from '../components/appNavigation';
|
||||||
|
import userList from '../components/userList';
|
||||||
|
import Vue from 'vue';
|
||||||
|
import VueLocalStorage from 'vue-localstorage'
|
||||||
|
import Multiselect from 'vue-multiselect';
|
||||||
|
import api from '../store/api';
|
||||||
|
|
||||||
|
Vue.use(VueLocalStorage)
|
||||||
|
Vue.use(VueLocalStorage)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Users',
|
||||||
|
props: ['selectedGroup'],
|
||||||
|
components: {
|
||||||
|
appNavigation,
|
||||||
|
userList,
|
||||||
|
Multiselect
|
||||||
|
},
|
||||||
|
beforeMount() {
|
||||||
|
this.$store.commit('initGroups', {
|
||||||
|
groups: this.$store.getters.getServerData.groups,
|
||||||
|
orderBy: this.$store.getters.getServerData.sortGroups,
|
||||||
|
userCount: this.$store.getters.getServerData.userCount
|
||||||
|
});
|
||||||
|
this.$store.dispatch('getPasswordPolicyMinLength');
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// default quota is unlimited
|
||||||
|
unlimitedQuota: {id:'default', label:t('settings', 'Unlimited')},
|
||||||
|
// temporary value used for multiselect change
|
||||||
|
selectedQuota: false,
|
||||||
|
showConfig: {
|
||||||
|
showStoragePath: false,
|
||||||
|
showUserBackend: false,
|
||||||
|
showLastLogin: false,
|
||||||
|
showNewUserForm: false,
|
||||||
|
showLanguages: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleNewUserMenu() {
|
||||||
|
this.showConfig.showNewUserForm = !this.showConfig.showNewUserForm;
|
||||||
|
if (this.showConfig.showNewUserForm) {
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
window.newusername.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLocalstorage(key) {
|
||||||
|
// force initialization
|
||||||
|
let localConfig = this.$localStorage.get(key);
|
||||||
|
// if localstorage is null, fallback to original values
|
||||||
|
this.showConfig[key] = localConfig !== null ? localConfig === 'true' : this.showConfig[key];
|
||||||
|
return this.showConfig[key];
|
||||||
|
},
|
||||||
|
setLocalStorage(key, status) {
|
||||||
|
this.showConfig[key] = status;
|
||||||
|
this.$localStorage.set(key, status);
|
||||||
|
return status;
|
||||||
|
},
|
||||||
|
removeGroup(groupid) {
|
||||||
|
let self = this;
|
||||||
|
// TODO migrate to a vue js confirm dialog component
|
||||||
|
OC.dialogs.confirm(
|
||||||
|
t('settings', 'You are about to remove the group {group}. The users will NOT be deleted.', {group: groupid}),
|
||||||
|
t('settings','Please confirm the group removal '),
|
||||||
|
function (success) {
|
||||||
|
if (success) {
|
||||||
|
self.$store.dispatch('removeGroup', groupid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatch default quota set request
|
||||||
|
*
|
||||||
|
* @param {string|Object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
setDefaultQuota(quota = 'none') {
|
||||||
|
this.$store.dispatch('setAppConfig', {
|
||||||
|
app: 'files',
|
||||||
|
key: 'default_quota',
|
||||||
|
// ensure we only send the preset id
|
||||||
|
value: quota.id ? quota.id : quota
|
||||||
|
}).then(() => {
|
||||||
|
if (typeof quota !== 'object') {
|
||||||
|
quota = {id: quota, label: quota};
|
||||||
|
}
|
||||||
|
this.defaultQuota = 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.setDefaultQuota('none');
|
||||||
|
} else if (validQuota !== null) {
|
||||||
|
// unify format output
|
||||||
|
return this.setDefaultQuota(OC.Util.humanFileSize(OC.Util.computerFileSize(quota)));
|
||||||
|
}
|
||||||
|
// if no valid do not change
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Local settings
|
||||||
|
showLanguages: {
|
||||||
|
get: function() {return this.getLocalstorage('showLanguages')},
|
||||||
|
set: function(status) {
|
||||||
|
this.setLocalStorage('showLanguages', status);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
userCount() {
|
||||||
|
return this.$store.getters.getUserCount;
|
||||||
|
},
|
||||||
|
settings() {
|
||||||
|
return this.$store.getters.getServerData;
|
||||||
|
},
|
||||||
|
|
||||||
|
// default quota
|
||||||
|
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);
|
||||||
|
return quotaPreset;
|
||||||
|
},
|
||||||
|
// mapping saved values to objects
|
||||||
|
defaultQuota: {
|
||||||
|
get: function() {
|
||||||
|
if (this.selectedQuota !== false) {
|
||||||
|
return this.selectedQuota;
|
||||||
|
}
|
||||||
|
if (OC.Util.computerFileSize(this.settings.defaultQuota) > 0) {
|
||||||
|
// if value is valid, let's map the quotaOptions or return custom quota
|
||||||
|
return {id:this.settings.defaultQuota, label:this.settings.defaultQuota};
|
||||||
|
}
|
||||||
|
return this.unlimitedQuota; // unlimited
|
||||||
|
},
|
||||||
|
set: function(quota) {
|
||||||
|
this.selectedQuota = quota;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
// BUILD APP NAVIGATION MENU OBJECT
|
||||||
|
menu() {
|
||||||
|
// 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 = []; // empty classes, active will be set later
|
||||||
|
item.router = { // router link to
|
||||||
|
name: 'group',
|
||||||
|
params: {selectedGroup: group.id}
|
||||||
|
};
|
||||||
|
item.text = group.name; // group name
|
||||||
|
item.utils = {counter: group.usercount}; // users count
|
||||||
|
|
||||||
|
if (item.id !== 'admin' && item.id !== 'disabled' && this.settings.isAdmin) {
|
||||||
|
// add delete button on real groups
|
||||||
|
let self = this;
|
||||||
|
item.utils.actions = [{
|
||||||
|
icon: 'icon-delete',
|
||||||
|
text: t('settings', 'Remove group'),
|
||||||
|
action: function() {self.removeGroup(group.id)}
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Adjust data
|
||||||
|
let adminGroup = groups.find(group => group.id == 'admin');
|
||||||
|
let disabledGroupIndex = groups.findIndex(group => group.id == 'disabled');
|
||||||
|
let disabledGroup = groups[disabledGroupIndex];
|
||||||
|
if (adminGroup && adminGroup.text) {
|
||||||
|
adminGroup.text = t('settings', 'Admins'); // rename admin group
|
||||||
|
}
|
||||||
|
if (disabledGroup && disabledGroup.text) {
|
||||||
|
disabledGroup.text = t('settings', 'Disabled users'); // rename disabled group
|
||||||
|
if (disabledGroup.utils.counter === 0) {
|
||||||
|
groups.splice(disabledGroupIndex, 1); // remove disabled if empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add everyone group
|
||||||
|
groups.unshift({
|
||||||
|
id: 'everyone',
|
||||||
|
classes: [],
|
||||||
|
router: {name:'users'},
|
||||||
|
text: t('settings', 'Everyone'),
|
||||||
|
utils: {counter: this.userCount}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set current group as active
|
||||||
|
let activeGroup = groups.findIndex(group => group.id === this.selectedGroup);
|
||||||
|
if (activeGroup >= 0) {
|
||||||
|
groups[activeGroup].classes.push('active');
|
||||||
|
} else {
|
||||||
|
groups[0].classes.push('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return
|
||||||
|
return {
|
||||||
|
id: 'usergrouplist',
|
||||||
|
new: {
|
||||||
|
id:'new-user-button',
|
||||||
|
text: t('settings','New user'),
|
||||||
|
icon: 'icon-add',
|
||||||
|
action: this.toggleNewUserMenu
|
||||||
|
},
|
||||||
|
items: groups
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,9 +1,24 @@
|
||||||
<?php /**
|
<?php
|
||||||
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com>
|
/**
|
||||||
* This file is licensed under the Affero General Public License version 3 or later.
|
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||||
* See the COPYING-README file.
|
* @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
|
||||||
|
*
|
||||||
|
* return new TemplateResponse('settings', 'settings', ['serverData' => $serverData]);
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
<?php foreach($_['forms'] as $form) {
|
script('settings', 'main');
|
||||||
print_unescaped($form);
|
style('settings', 'settings');
|
||||||
}
|
|
||||||
|
// Did we have some data to inject ?
|
||||||
|
if(is_array($_['serverData'])) {
|
||||||
|
?>
|
||||||
|
<span id="serverData" data-server="<?php p(json_encode($_['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();
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,149 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
|
||||||
*
|
|
||||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
|
||||||
* @author Bart Visscher <bartv@thisnet.nl>
|
|
||||||
* @author Clark Tomlinson <fallen013@gmail.com>
|
|
||||||
* @author Daniel Molkentin <daniel@molkentin.de>
|
|
||||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
|
||||||
* @author Jakob Sack <mail@jakobsack.de>
|
|
||||||
* @author Joas Schilling <coding@schilljs.com>
|
|
||||||
* @author Jörn Friedrich Dreyer <jfd@butonic.de>
|
|
||||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
|
||||||
* @author Morris Jobke <hey@morrisjobke.de>
|
|
||||||
* @author Robin Appelman <robin@icewind.nl>
|
|
||||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
|
||||||
* @author Stephan Peijnik <speijnik@anexia-it.com>
|
|
||||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
|
||||||
* @author Thomas Pulzer <t.pulzer@kniel.de>
|
|
||||||
*
|
|
||||||
* @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/>
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
OC_Util::checkSubAdminUser();
|
|
||||||
|
|
||||||
\OC::$server->getNavigationManager()->setActiveEntry('core_users');
|
|
||||||
|
|
||||||
$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();
|
|
||||||
|
|
||||||
if ($config->getSystemValue('sort_groups_by_name', false)) {
|
|
||||||
$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
|
|
||||||
} else {
|
|
||||||
$isLDAPUsed = false;
|
|
||||||
if ($appManager->isEnabledForUser('user_ldap')) {
|
|
||||||
$isLDAPUsed =
|
|
||||||
$groupManager->isBackendUsed('\OCA\User_LDAP\Group_LDAP')
|
|
||||||
|| $groupManager->isBackendUsed('\OCA\User_LDAP\Group_Proxy');
|
|
||||||
if ($isLDAPUsed) {
|
|
||||||
// LDAP user count can be slow, so we sort by group name here
|
|
||||||
$sortGroupsBy = \OC\Group\MetaData::SORT_GROUPNAME;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$uid = \OC_User::getUser();
|
|
||||||
$isAdmin = OC_User::isAdminUser($uid);
|
|
||||||
|
|
||||||
$isDisabled = true;
|
|
||||||
$user = $userManager->get($uid);
|
|
||||||
if ($user) {
|
|
||||||
$isDisabled = !$user->isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
$groupsInfo = new \OC\Group\MetaData(
|
|
||||||
$uid,
|
|
||||||
$isAdmin,
|
|
||||||
$groupManager,
|
|
||||||
\OC::$server->getUserSession()
|
|
||||||
);
|
|
||||||
|
|
||||||
$groupsInfo->setSorting($sortGroupsBy);
|
|
||||||
list($adminGroup, $groups) = $groupsInfo->get();
|
|
||||||
|
|
||||||
$recoveryAdminEnabled = $appManager->isEnabledForUser('encryption') &&
|
|
||||||
$config->getAppValue( 'encryption', 'recoveryAdminEnabled', '0');
|
|
||||||
|
|
||||||
if($isAdmin) {
|
|
||||||
$subAdmins = \OC::$server->getGroupManager()->getSubAdmin()->getAllSubAdmins();
|
|
||||||
// New class returns IUser[] so convert back
|
|
||||||
$result = [];
|
|
||||||
foreach ($subAdmins as $subAdmin) {
|
|
||||||
$result[] = [
|
|
||||||
'gid' => $subAdmin['group']->getGID(),
|
|
||||||
'uid' => $subAdmin['user']->getUID(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$subAdmins = $result;
|
|
||||||
}else{
|
|
||||||
/* Retrieve group IDs from $groups array, so we can pass that information into OC_Group::displayNamesInGroups() */
|
|
||||||
$gids = array();
|
|
||||||
foreach($groups as $group) {
|
|
||||||
if (isset($group['id'])) {
|
|
||||||
$gids[] = $group['id'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$subAdmins = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$disabledUsers = $isLDAPUsed ? 0 : $userManager->countDisabledUsers();
|
|
||||||
$disabledUsersGroup = [
|
|
||||||
'id' => '_disabledUsers',
|
|
||||||
'name' => '_disabledUsers',
|
|
||||||
'usercount' => $disabledUsers
|
|
||||||
];
|
|
||||||
|
|
||||||
// load preset quotas
|
|
||||||
$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'));
|
|
||||||
|
|
||||||
$tmpl->printPage();
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
const path = require('path')
|
||||||
|
|
||||||
|
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']
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
const merge = require('webpack-merge');
|
||||||
|
const common = require('./webpack.common.js');
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'development',
|
||||||
|
devServer: {
|
||||||
|
historyApiFallback: true,
|
||||||
|
noInfo: true,
|
||||||
|
overlay: true
|
||||||
|
},
|
||||||
|
devtool: '#eval-source-map',
|
||||||
|
})
|
|
@ -0,0 +1,7 @@
|
||||||
|
const merge = require('webpack-merge')
|
||||||
|
const common = require('./webpack.common.js')
|
||||||
|
|
||||||
|
module.exports = merge(common, {
|
||||||
|
mode: 'production',
|
||||||
|
devtool: '#source-map'
|
||||||
|
})
|
|
@ -72,7 +72,6 @@ class ApplicationTest extends TestCase {
|
||||||
[AuthSettingsController::class, Controller::class],
|
[AuthSettingsController::class, Controller::class],
|
||||||
// Needs session: [CertificateController::class, Controller::class],
|
// Needs session: [CertificateController::class, Controller::class],
|
||||||
[CheckSetupController::class, Controller::class],
|
[CheckSetupController::class, Controller::class],
|
||||||
[GroupsController::class, Controller::class],
|
|
||||||
[LogSettingsController::class, Controller::class],
|
[LogSettingsController::class, Controller::class],
|
||||||
[MailSettingsController::class, Controller::class],
|
[MailSettingsController::class, Controller::class],
|
||||||
[UsersController::class, Controller::class],
|
[UsersController::class, Controller::class],
|
||||||
|
|
|
@ -1,381 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* @author Lukas Reschke
|
|
||||||
* @copyright 2014 Lukas Reschke lukas@owncloud.com
|
|
||||||
*
|
|
||||||
* This file is licensed under the Affero General Public License version 3 or
|
|
||||||
* later.
|
|
||||||
* See the COPYING-README file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Tests\Settings\Controller;
|
|
||||||
|
|
||||||
use OC\Group\Group;
|
|
||||||
use OC\Group\MetaData;
|
|
||||||
use OC\Settings\Controller\GroupsController;
|
|
||||||
use OC\User\User;
|
|
||||||
use OCP\AppFramework\Http;
|
|
||||||
use OCP\AppFramework\Http\DataResponse;
|
|
||||||
use OCP\IGroupManager;
|
|
||||||
use OCP\IL10N;
|
|
||||||
use OCP\IRequest;
|
|
||||||
use OCP\IUserSession;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Tests\Settings\Controller
|
|
||||||
*/
|
|
||||||
class GroupsControllerTest extends \Test\TestCase {
|
|
||||||
|
|
||||||
/** @var IGroupManager|\PHPUnit_Framework_MockObject_MockObject */
|
|
||||||
private $groupManager;
|
|
||||||
|
|
||||||
/** @var IUserSession|\PHPUnit_Framework_MockObject_MockObject */
|
|
||||||
private $userSession;
|
|
||||||
|
|
||||||
/** @var GroupsController */
|
|
||||||
private $groupsController;
|
|
||||||
|
|
||||||
protected function setUp() {
|
|
||||||
parent::setUp();
|
|
||||||
|
|
||||||
$this->groupManager = $this->createMock(IGroupManager::class);
|
|
||||||
$this->userSession = $this->createMock(IUserSession::class);
|
|
||||||
$l = $this->createMock(IL10N::class);
|
|
||||||
$l->method('t')
|
|
||||||
->will($this->returnCallback(function($text, $parameters = []) {
|
|
||||||
return vsprintf($text, $parameters);
|
|
||||||
}));
|
|
||||||
$this->groupsController = new GroupsController(
|
|
||||||
'settings',
|
|
||||||
$this->createMock(IRequest::class),
|
|
||||||
$this->groupManager,
|
|
||||||
$this->userSession,
|
|
||||||
true,
|
|
||||||
$l
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Since GroupManager uses the static OC_Subadmin class it can't be mocked
|
|
||||||
* to test for subadmins. Thus the test always assumes you have admin permissions...
|
|
||||||
*/
|
|
||||||
public function testIndexSortByName() {
|
|
||||||
$firstGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$firstGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('firstGroup'));
|
|
||||||
$firstGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('First group'));
|
|
||||||
$firstGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(12));
|
|
||||||
$secondGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$secondGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('secondGroup'));
|
|
||||||
$secondGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Second group'));
|
|
||||||
$secondGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(25));
|
|
||||||
$thirdGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$thirdGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('thirdGroup'));
|
|
||||||
$thirdGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Third group'));
|
|
||||||
$thirdGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(14));
|
|
||||||
$fourthGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$fourthGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('admin'));
|
|
||||||
$fourthGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Admin'));
|
|
||||||
$fourthGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(18));
|
|
||||||
/** @var \OC\Group\Group[] $groups */
|
|
||||||
$groups = array();
|
|
||||||
$groups[] = $firstGroup;
|
|
||||||
$groups[] = $secondGroup;
|
|
||||||
$groups[] = $thirdGroup;
|
|
||||||
$groups[] = $fourthGroup;
|
|
||||||
|
|
||||||
$user = $this->getMockBuilder(User::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$this->userSession
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getUser')
|
|
||||||
->will($this->returnValue($user));
|
|
||||||
$user
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getUID')
|
|
||||||
->will($this->returnValue('MyAdminUser'));
|
|
||||||
$this->groupManager->method('search')
|
|
||||||
->will($this->returnValue($groups));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'data' => array(
|
|
||||||
'adminGroups' => array(
|
|
||||||
0 => array(
|
|
||||||
'id' => 'admin',
|
|
||||||
'name' => 'Admin',
|
|
||||||
'usercount' => 0,//User count disabled 18,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'groups' =>
|
|
||||||
array(
|
|
||||||
0 => array(
|
|
||||||
'id' => 'firstGroup',
|
|
||||||
'name' => 'First group',
|
|
||||||
'usercount' => 0,//User count disabled 12,
|
|
||||||
),
|
|
||||||
1 => array(
|
|
||||||
'id' => 'secondGroup',
|
|
||||||
'name' => 'Second group',
|
|
||||||
'usercount' => 0,//User count disabled 25,
|
|
||||||
),
|
|
||||||
2 => array(
|
|
||||||
'id' => 'thirdGroup',
|
|
||||||
'name' => 'Third group',
|
|
||||||
'usercount' => 0,//User count disabled 14,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->index('', false, MetaData::SORT_GROUPNAME);
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO: Since GroupManager uses the static OC_Subadmin class it can't be mocked
|
|
||||||
* to test for subadmins. Thus the test always assumes you have admin permissions...
|
|
||||||
*/
|
|
||||||
public function testIndexSortbyCount() {
|
|
||||||
$firstGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$firstGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('firstGroup'));
|
|
||||||
$firstGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('First group'));
|
|
||||||
$firstGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(12));
|
|
||||||
$secondGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$secondGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('secondGroup'));
|
|
||||||
$secondGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Second group'));
|
|
||||||
$secondGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(25));
|
|
||||||
$thirdGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$thirdGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('thirdGroup'));
|
|
||||||
$thirdGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Third group'));
|
|
||||||
$thirdGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(14));
|
|
||||||
$fourthGroup = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$fourthGroup
|
|
||||||
->method('getGID')
|
|
||||||
->will($this->returnValue('admin'));
|
|
||||||
$fourthGroup
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('Admin'));
|
|
||||||
$fourthGroup
|
|
||||||
->method('count')
|
|
||||||
->will($this->returnValue(18));
|
|
||||||
/** @var \OC\Group\Group[] $groups */
|
|
||||||
$groups = array();
|
|
||||||
$groups[] = $firstGroup;
|
|
||||||
$groups[] = $secondGroup;
|
|
||||||
$groups[] = $thirdGroup;
|
|
||||||
$groups[] = $fourthGroup;
|
|
||||||
|
|
||||||
$user = $this->getMockBuilder(User::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$this->userSession
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getUser')
|
|
||||||
->will($this->returnValue($user));
|
|
||||||
$user
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getUID')
|
|
||||||
->will($this->returnValue('MyAdminUser'));
|
|
||||||
$this->groupManager
|
|
||||||
->method('search')
|
|
||||||
->will($this->returnValue($groups));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'data' => array(
|
|
||||||
'adminGroups' => array(
|
|
||||||
0 => array(
|
|
||||||
'id' => 'admin',
|
|
||||||
'name' => 'Admin',
|
|
||||||
'usercount' => 18,
|
|
||||||
)
|
|
||||||
),
|
|
||||||
'groups' =>
|
|
||||||
array(
|
|
||||||
0 => array(
|
|
||||||
'id' => 'secondGroup',
|
|
||||||
'name' => 'Second group',
|
|
||||||
'usercount' => 25,
|
|
||||||
),
|
|
||||||
1 => array(
|
|
||||||
'id' => 'thirdGroup',
|
|
||||||
'name' => 'Third group',
|
|
||||||
'usercount' => 14,
|
|
||||||
),
|
|
||||||
2 => array(
|
|
||||||
'id' => 'firstGroup',
|
|
||||||
'name' => 'First group',
|
|
||||||
'usercount' => 12,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->index();
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreateWithExistingGroup() {
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('groupExists')
|
|
||||||
->with('ExistingGroup')
|
|
||||||
->will($this->returnValue(true));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'message' => 'Group already exists.'
|
|
||||||
),
|
|
||||||
Http::STATUS_CONFLICT
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->create('ExistingGroup');
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreateSuccessful() {
|
|
||||||
$group = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('groupExists')
|
|
||||||
->with('NewGroup')
|
|
||||||
->will($this->returnValue(false));
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('createGroup')
|
|
||||||
->with('NewGroup')
|
|
||||||
->will($this->returnValue($group));
|
|
||||||
$group
|
|
||||||
->expects($this->once())
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('NewGroup'));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'groupname' => 'NewGroup'
|
|
||||||
),
|
|
||||||
Http::STATUS_CREATED
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->create('NewGroup');
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testCreateUnsuccessful() {
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('groupExists')
|
|
||||||
->with('NewGroup')
|
|
||||||
->will($this->returnValue(false));
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('createGroup')
|
|
||||||
->with('NewGroup')
|
|
||||||
->will($this->returnValue(false));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'error',
|
|
||||||
'data' => array('message' => 'Unable to add group.')
|
|
||||||
),
|
|
||||||
Http::STATUS_FORBIDDEN
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->create('NewGroup');
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDestroySuccessful() {
|
|
||||||
$group = $this->getMockBuilder(Group::class)
|
|
||||||
->disableOriginalConstructor()->getMock();
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('get')
|
|
||||||
->with('ExistingGroup')
|
|
||||||
->will($this->returnValue($group));
|
|
||||||
$group
|
|
||||||
->expects($this->once())
|
|
||||||
->method('delete')
|
|
||||||
->will($this->returnValue(true));
|
|
||||||
$group
|
|
||||||
->method('getDisplayName')
|
|
||||||
->will($this->returnValue('ExistingGroup'));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'success',
|
|
||||||
'data' => array('groupname' => 'ExistingGroup')
|
|
||||||
),
|
|
||||||
Http::STATUS_NO_CONTENT
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->destroy('ExistingGroup');
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function testDestroyUnsuccessful() {
|
|
||||||
$this->groupManager
|
|
||||||
->expects($this->once())
|
|
||||||
->method('get')
|
|
||||||
->with('ExistingGroup')
|
|
||||||
->will($this->returnValue(null));
|
|
||||||
|
|
||||||
$expectedResponse = new DataResponse(
|
|
||||||
array(
|
|
||||||
'status' => 'error',
|
|
||||||
'data' => array('message' => 'Unable to delete group.')
|
|
||||||
),
|
|
||||||
Http::STATUS_FORBIDDEN
|
|
||||||
);
|
|
||||||
$response = $this->groupsController->destroy('ExistingGroup');
|
|
||||||
$this->assertEquals($expectedResponse, $response);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -8,8 +8,10 @@ default:
|
||||||
- NextcloudTestServerContext
|
- NextcloudTestServerContext
|
||||||
|
|
||||||
- AppNavigationContext
|
- AppNavigationContext
|
||||||
|
- AppSettingsContext
|
||||||
- CommentsAppContext
|
- CommentsAppContext
|
||||||
- ContactsMenuContext
|
- ContactsMenuContext
|
||||||
|
- DialogContext
|
||||||
- FeatureContext
|
- FeatureContext
|
||||||
- FileListContext
|
- FileListContext
|
||||||
- FilesAppContext
|
- FilesAppContext
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
|
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
|
||||||
|
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||||
*
|
*
|
||||||
* @license GNU AGPL version 3 or any later version
|
* @license GNU AGPL version 3 or any later version
|
||||||
*
|
*
|
||||||
|
@ -32,24 +33,43 @@ class AppNavigationContext implements Context, ActorAwareInterface {
|
||||||
*/
|
*/
|
||||||
public static function appNavigation() {
|
public static function appNavigation() {
|
||||||
return Locator::forThe()->id("app-navigation")->
|
return Locator::forThe()->id("app-navigation")->
|
||||||
describedAs("App navigation");
|
describedAs("App navigation");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function appNavigationSectionItemFor($sectionText) {
|
public static function appNavigationSectionItemFor($sectionText) {
|
||||||
return Locator::forThe()->xpath("//li[normalize-space() = '$sectionText']")->
|
return Locator::forThe()->xpath("//li/a[normalize-space() = '$sectionText']/..")->
|
||||||
descendantOf(self::appNavigation())->
|
descendantOf(self::appNavigation())->
|
||||||
describedAs($sectionText . " section item in App Navigation");
|
describedAs($sectionText . " section item in App Navigation");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function appNavigationCurrentSectionItem() {
|
public static function appNavigationCurrentSectionItem() {
|
||||||
return Locator::forThe()->css(".active")->descendantOf(self::appNavigation())->
|
return Locator::forThe()->css(".active")->
|
||||||
describedAs("Current section item in App Navigation");
|
descendantOf(self::appNavigation())->
|
||||||
|
describedAs("Current section item in App Navigation");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function buttonForTheSection($class, $section) {
|
||||||
|
return Locator::forThe()->css("." . $class)->
|
||||||
|
descendantOf(self::appNavigationSectionItemFor($section))->
|
||||||
|
describedAs("The $class button on the $section section in App Navigation");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function counterForTheSection($section) {
|
||||||
|
return Locator::forThe()->css(".app-navigation-entry-utils-counter")->
|
||||||
|
descendantOf(self::appNavigationSectionItemFor($section))->
|
||||||
|
describedAs("The counter for the $section section in App Navigation");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -59,6 +79,13 @@ class AppNavigationContext implements Context, ActorAwareInterface {
|
||||||
$this->actor->find(self::appNavigationSectionItemFor($section), 10)->click();
|
$this->actor->find(self::appNavigationSectionItemFor($section), 10)->click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given I click the :class button on the :section section
|
||||||
|
*/
|
||||||
|
public function iClickTheButtonInTheSection($class, $section) {
|
||||||
|
$this->actor->find(self::buttonForTheSection($class, $section), 10)->click();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then I see that the current section is :section
|
* @Then I see that the current section is :section
|
||||||
*/
|
*/
|
||||||
|
@ -66,4 +93,25 @@ class AppNavigationContext implements Context, ActorAwareInterface {
|
||||||
PHPUnit_Framework_Assert::assertEquals($this->actor->find(self::appNavigationCurrentSectionItem(), 10)->getText(), $section);
|
PHPUnit_Framework_Assert::assertEquals($this->actor->find(self::appNavigationCurrentSectionItem(), 10)->getText(), $section);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the section :section is shown
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheSectionIsShown($section) {
|
||||||
|
WaitFor::elementToBeEventuallyShown($this->actor, self::appNavigationSectionItemFor($section));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the section :section is not shown
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheSectionIsNotShown($section) {
|
||||||
|
WaitFor::elementToBeEventuallyNotShown($this->actor, self::appNavigationSectionItemFor($section));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the section :section has a count of :count
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheSectionHasACountOf($section, $count) {
|
||||||
|
PHPUnit_Framework_Assert::assertEquals($this->actor->find(self::counterForTheSection($section), 10)->getText(), $count);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
|
||||||
|
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Behat\Behat\Context\Context;
|
||||||
|
|
||||||
|
class AppSettingsContext implements Context, ActorAwareInterface {
|
||||||
|
|
||||||
|
use ActorAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function appSettings() {
|
||||||
|
return Locator::forThe()->id("app-settings")->
|
||||||
|
describedAs("App settings");
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function appSettingsContent() {
|
||||||
|
return Locator::forThe()->id("app-settings-content")->
|
||||||
|
descendantOf(self::appSettings())->
|
||||||
|
describedAs("App settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function appSettingsOpenButton() {
|
||||||
|
return Locator::forThe()->xpath("//div[@id = 'app-settings-header']/button")->
|
||||||
|
descendantOf(self::appSettings())->
|
||||||
|
describedAs("The button to open the app settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function checkboxInTheSettings($id) {
|
||||||
|
return Locator::forThe()->xpath("//input[@id = '$id']")->
|
||||||
|
descendantOf(self::appSettingsContent())->
|
||||||
|
describedAs("The $id checkbox in the settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function checkboxLabelInTheSettings($id) {
|
||||||
|
return Locator::forThe()->xpath("//label[@for = '$id']")->
|
||||||
|
descendantOf(self::appSettingsContent())->
|
||||||
|
describedAs("The label for the $id checkbox in the settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given I open the settings
|
||||||
|
*/
|
||||||
|
public function iOpenTheSettings() {
|
||||||
|
$this->actor->find(self::appSettingsOpenButton())->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given I toggle the :id checkbox in the settings
|
||||||
|
*/
|
||||||
|
public function iToggleTheCheckboxInTheSettingsTo($id) {
|
||||||
|
$locator = self::CheckboxInTheSettings($id);
|
||||||
|
|
||||||
|
// If locator is not visible, fallback to label
|
||||||
|
if (!$this->actor->find(self::CheckboxInTheSettings($id))->isVisible()) {
|
||||||
|
$locator = self::checkboxLabelInTheSettings($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->actor->find($locator)->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the settings are opened
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheSettingsAreOpened() {
|
||||||
|
WaitFor::elementToBeEventuallyShown($this->actor, self::appSettingsContent());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||||
|
*
|
||||||
|
* @license GNU AGPL version 3 or any later version
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
use Behat\Behat\Context\Context;
|
||||||
|
|
||||||
|
class DialogContext implements Context, ActorAwareInterface {
|
||||||
|
|
||||||
|
use ActorAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function theDialog() {
|
||||||
|
return Locator::forThe()->css(".oc-dialog")->
|
||||||
|
describedAs("The dialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function theDialogButton($text) {
|
||||||
|
return Locator::forThe()->xpath("//button[normalize-space() = '$text']")->
|
||||||
|
descendantOf(self::theDialog())->
|
||||||
|
describedAs($text . " button of the dialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Given I click the :text button of the confirmation dialog
|
||||||
|
*/
|
||||||
|
public function iClickTheDialogButton($text) {
|
||||||
|
$this->actor->find(self::theDialogButton($text), 10)->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the confirmation dialog is shown
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheConfirmationDialogIsShown() {
|
||||||
|
WaitFor::elementToBeEventuallyShown($this->actor, self::theDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the confirmation dialog is not shown
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheConfirmationDialogIsNotShown() {
|
||||||
|
WaitFor::elementToBeEventuallyNotShown($this->actor, self::theDialog());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,6 +3,7 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
|
* @copyright Copyright (c) 2017, Daniel Calviño Sánchez (danxuliu@gmail.com)
|
||||||
|
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
|
||||||
*
|
*
|
||||||
* @license GNU AGPL version 3 or any later version
|
* @license GNU AGPL version 3 or any later version
|
||||||
*
|
*
|
||||||
|
@ -31,7 +32,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function newUserForm() {
|
public static function newUserForm() {
|
||||||
return Locator::forThe()->id("newuserHeader")->
|
return Locator::forThe()->id("new-user")->
|
||||||
describedAs("New user form in Users Settings");
|
describedAs("New user form in Users Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function createNewUserButton() {
|
public static function createNewUserButton() {
|
||||||
return Locator::forThe()->xpath("//form[@id = 'newuser']//input[@type = 'submit']")->
|
return Locator::forThe()->xpath("//form[@id = 'new-user']//input[@type = 'submit']")->
|
||||||
describedAs("Create user button in Users Settings");
|
describedAs("Create user button in Users Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,24 +72,72 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function rowForUser($user) {
|
public static function rowForUser($user) {
|
||||||
return Locator::forThe()->xpath("//table[@id = 'userlist']//td[normalize-space() = '$user']/..")->
|
return Locator::forThe()->xpath("//div[@id='app-content']/div/div[normalize-space() = '$user']/..")->
|
||||||
describedAs("Row for user $user in Users Settings");
|
describedAs("Row for user $user in Users Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Warning: you need to watch out for the proper classes order
|
||||||
|
*
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function passwordCellForUser($user) {
|
public static function classCellForUser($class, $user) {
|
||||||
return Locator::forThe()->css(".password")->descendantOf(self::rowForUser($user))->
|
return Locator::forThe()->xpath("//*[@class='$class']")->
|
||||||
describedAs("Password cell for user $user in Users Settings");
|
descendantOf(self::rowForUser($user))->
|
||||||
|
describedAs("$class cell for user $user in Users Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Locator
|
* @return Locator
|
||||||
*/
|
*/
|
||||||
public static function passwordInputForUser($user) {
|
public static function inputForUserInCell($cell, $user) {
|
||||||
return Locator::forThe()->css("input")->descendantOf(self::passwordCellForUser($user))->
|
return Locator::forThe()->css("input")->
|
||||||
describedAs("Password input for user $user in Users Settings");
|
descendantOf(self::classCellForUser($cell, $user))->
|
||||||
|
describedAs("$cell input for user $user in Users Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function optionInInputForUser($cell, $user) {
|
||||||
|
return Locator::forThe()->css(".multiselect__option--highlight")->
|
||||||
|
descendantOf(self::classCellForUser($cell, $user))->
|
||||||
|
describedAs("Selected $cell option in $cell input for user $user in Users Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function actionsMenuOf($user) {
|
||||||
|
return Locator::forThe()->css(".icon-more")->
|
||||||
|
descendantOf(self::rowForUser($user))->
|
||||||
|
describedAs("Actions menu for user $user in Users Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function theAction($action, $user) {
|
||||||
|
return Locator::forThe()->xpath("//button[normalize-space() = '$action']")->
|
||||||
|
descendantOf(self::rowForUser($user))->
|
||||||
|
describedAs("$action action for the user $user row in Users Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function theColumn($column) {
|
||||||
|
return Locator::forThe()->xpath("//div[@class='user-list-grid']//div[normalize-space() = '$column']")->
|
||||||
|
describedAs("The $column column in Users Settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Locator
|
||||||
|
*/
|
||||||
|
public static function selectedSelectOption($cell, $user) {
|
||||||
|
return Locator::forThe()->css(".multiselect__single")->
|
||||||
|
descendantOf(self::classCellForUser($cell, $user))->
|
||||||
|
describedAs("The selected option of the $cell select for the user $user in Users Settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,6 +147,20 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
$this->actor->find(self::newUserButton())->click();
|
$this->actor->find(self::newUserButton())->click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I click the :action action in the :user actions menu
|
||||||
|
*/
|
||||||
|
public function iClickTheAction($action, $user) {
|
||||||
|
$this->actor->find(self::theAction($action, $user))->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I open the actions menu for the user :user
|
||||||
|
*/
|
||||||
|
public function iOpenTheActionsMenuOf($user) {
|
||||||
|
$this->actor->find(self::actionsMenuOf($user))->click();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @When I create user :user with password :password
|
* @When I create user :user with password :password
|
||||||
*/
|
*/
|
||||||
|
@ -108,18 +171,40 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @When I set the password for :user to :password
|
* @When I set the :field for :user to :value
|
||||||
*/
|
*/
|
||||||
public function iSetThePasswordForUserTo($user, $password) {
|
public function iSetTheFieldForUserTo($field, $user, $value) {
|
||||||
$this->actor->find(self::passwordCellForUser($user), 10)->click();
|
$this->actor->find(self::inputForUserInCell($field, $user), 2)->setValue($value . "\r");
|
||||||
$this->actor->find(self::passwordInputForUser($user), 2)->setValue($password . "\r");
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I assign the user :user to the group :group
|
||||||
|
*/
|
||||||
|
public function iAssignTheUserToTheGroup($user, $group) {
|
||||||
|
$this->actor->find(self::inputForUserInCell('groups', $user))->setValue($group);
|
||||||
|
$this->actor->find(self::optionInInputForUser('groups', $user))->click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @When I set the user :user quota to :quota
|
||||||
|
*/
|
||||||
|
public function iSetTheUserQuotaTo($user, $quota) {
|
||||||
|
$this->actor->find(self::inputForUserInCell('quota', $user))->setValue($quota);
|
||||||
|
$this->actor->find(self::optionInInputForUser('quota', $user))->click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Then I see that the list of users contains the user :user
|
* @Then I see that the list of users contains the user :user
|
||||||
*/
|
*/
|
||||||
public function iSeeThatTheListOfUsersContainsTheUser($user) {
|
public function iSeeThatTheListOfUsersContainsTheUser($user) {
|
||||||
PHPUnit_Framework_Assert::assertNotNull($this->actor->find(self::rowForUser($user), 10));
|
WaitFor::elementToBeEventuallyShown($this->actor, self::rowForUser($user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the list of users does not contains the user :user
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheListOfUsersDoesNotContainsTheUser($user) {
|
||||||
|
WaitFor::elementToBeEventuallyNotShown($this->actor, self::rowForUser($user));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -130,4 +215,45 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
||||||
$this->actor->find(self::newUserForm(), 10)->isVisible());
|
$this->actor->find(self::newUserForm(), 10)->isVisible());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the :action action in the :user actions menu is shown
|
||||||
|
*/
|
||||||
|
public function iSeeTheAction($action, $user) {
|
||||||
|
PHPUnit_Framework_Assert::assertTrue(
|
||||||
|
$this->actor->find(self::theAction($action, $user), 10)->isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the :column column is shown
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheColumnIsShown($column) {
|
||||||
|
PHPUnit_Framework_Assert::assertTrue(
|
||||||
|
$this->actor->find(self::theColumn($column), 10)->isVisible());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the :field of :user is :value
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheFieldOfUserIs($field, $user, $value) {
|
||||||
|
PHPUnit_Framework_Assert::assertEquals(
|
||||||
|
$this->actor->find(self::inputForUserInCell($field, $user), 10)->getValue(), $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the :cell cell for user :user is done loading
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheCellForUserIsDoneLoading($cell, $user) {
|
||||||
|
WaitFor::elementToBeEventuallyShown($this->actor, self::classCellForUser($cell.' icon-loading-small', $user));
|
||||||
|
WaitFor::elementToBeEventuallyNotShown($this->actor, self::classCellForUser($cell.' icon-loading-small', $user));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Then I see that the user quota of :user is :quota
|
||||||
|
*/
|
||||||
|
public function iSeeThatTheuserQuotaIs($user, $quota) {
|
||||||
|
PHPUnit_Framework_Assert::assertEquals(
|
||||||
|
$this->actor->find(self::selectedSelectOption('quota', $user), 2)->getText(), $quota);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -147,6 +147,18 @@ class ElementWrapper {
|
||||||
return $this->executeCommand($commandCallback, "visibility could not be got");
|
return $this->executeCommand($commandCallback, "visibility could not be got");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the wrapped element is checked or not.
|
||||||
|
*
|
||||||
|
* @return bool true if the wrapped element is checked, false otherwise.
|
||||||
|
*/
|
||||||
|
public function isChecked() {
|
||||||
|
$commandCallback = function() {
|
||||||
|
return $this->element->isChecked();
|
||||||
|
};
|
||||||
|
return $this->executeCommand($commandCallback, "check state could not be got");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the text of the wrapped element.
|
* Returns the text of the wrapped element.
|
||||||
*
|
*
|
||||||
|
@ -205,6 +217,32 @@ class ElementWrapper {
|
||||||
$this->executeCommandOnVisibleElement($commandCallback, "could not be clicked");
|
$this->executeCommandOnVisibleElement($commandCallback, "could not be clicked");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the wrapped element.
|
||||||
|
*
|
||||||
|
* If automatically waits for the wrapped element to be visible (up to the
|
||||||
|
* timeout set when finding it).
|
||||||
|
*/
|
||||||
|
public function check() {
|
||||||
|
$commandCallback = function() {
|
||||||
|
$this->element->check();
|
||||||
|
};
|
||||||
|
$this->executeCommand($commandCallback, "could not be checked");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uncheck the wrapped element.
|
||||||
|
*
|
||||||
|
* If automatically waits for the wrapped element to be visible (up to the
|
||||||
|
* timeout set when finding it).
|
||||||
|
*/
|
||||||
|
public function uncheck() {
|
||||||
|
$commandCallback = function() {
|
||||||
|
$this->element->uncheck();
|
||||||
|
};
|
||||||
|
$this->executeCommand($commandCallback, "could not be unchecked");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes the given command.
|
* Executes the given command.
|
||||||
*
|
*
|
||||||
|
|
|
@ -18,7 +18,6 @@ Feature: login
|
||||||
And I am logged in as the admin
|
And I am logged in as the admin
|
||||||
And I open the User settings
|
And I open the User settings
|
||||||
And I set the password for user0 to 654321
|
And I set the password for user0 to 654321
|
||||||
And I see that the "Password successfully changed" notification is shown
|
|
||||||
And I act as John
|
And I act as John
|
||||||
And I log in with user user0 and password 654321
|
And I log in with user user0 and password 654321
|
||||||
Then I see that the current page is the Files app
|
Then I see that the current page is the Files app
|
||||||
|
|
|
@ -0,0 +1,115 @@
|
||||||
|
Feature: users
|
||||||
|
|
||||||
|
Scenario: create a new user
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I click the New user button
|
||||||
|
And I see that the new user form is shown
|
||||||
|
When I create user unknownUser with password 123456acb
|
||||||
|
Then I see that the list of users contains the user unknownUser
|
||||||
|
|
||||||
|
Scenario: delete a user
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I open the actions menu for the user user0
|
||||||
|
And I see that the "Delete user" action in the user0 actions menu is shown
|
||||||
|
When I click the "Delete user" action in the user0 actions menu
|
||||||
|
Then I see that the list of users does not contains the user user0
|
||||||
|
|
||||||
|
Scenario: disable a user
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I open the actions menu for the user user0
|
||||||
|
And I see that the "Disable user" action in the user0 actions menu is shown
|
||||||
|
When I click the "Disable user" action in the user0 actions menu
|
||||||
|
Then I see that the list of users does not contains the user user0
|
||||||
|
When I open the "Disabled users" section
|
||||||
|
Then I see that the list of users contains the user user0
|
||||||
|
|
||||||
|
Scenario: assign user to a group
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
# disabled because we need the TAB patch:
|
||||||
|
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||||
|
# When I assign the user user0 to the group admin
|
||||||
|
# Then I see that the section Admins is shown
|
||||||
|
# And I see that the section Admins has a count of 2
|
||||||
|
|
||||||
|
Scenario: create and delete a group
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
# disabled because we need the TAB patch:
|
||||||
|
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||||
|
# And I assign the user user0 to the group Group1
|
||||||
|
# And I see that the section Group1 is shown
|
||||||
|
# And I click the "icon-delete" button on the Group1 section
|
||||||
|
# And I see that the confirmation dialog is shown
|
||||||
|
# When I click the "Yes" button of the confirmation dialog
|
||||||
|
# Then I see that the section Group1 is not shown
|
||||||
|
|
||||||
|
Scenario: change columns visibility
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I open the settings
|
||||||
|
And I see that the settings are opened
|
||||||
|
When I toggle the showLanguages checkbox in the settings
|
||||||
|
Then I see that the "Languages" column is shown
|
||||||
|
When I toggle the showLastLogin checkbox in the settings
|
||||||
|
Then I see that the "Last login" column is shown
|
||||||
|
When I toggle the showStoragePath checkbox in the settings
|
||||||
|
Then I see that the "Storage location" column is shown
|
||||||
|
When I toggle the showUserBackend checkbox in the settings
|
||||||
|
Then I see that the "User backend" column is shown
|
||||||
|
|
||||||
|
Scenario: change display name
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I see that the displayName of user0 is user0
|
||||||
|
When I set the displayName for user0 to user1
|
||||||
|
And I see that the displayName cell for user user0 is done loading
|
||||||
|
Then I see that the displayName of user0 is user1
|
||||||
|
|
||||||
|
Scenario: change password
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I see that the password of user0 is ""
|
||||||
|
When I set the password for user0 to 123456
|
||||||
|
And I see that the password cell for user user0 is done loading
|
||||||
|
# password input is emptied on change
|
||||||
|
Then I see that the password of user0 is ""
|
||||||
|
|
||||||
|
Scenario: change email
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I see that the mailAddress of user0 is ""
|
||||||
|
When I set the mailAddress for user0 to "test@nextcloud.com"
|
||||||
|
And I see that the mailAddress cell for user user0 is done loading
|
||||||
|
Then I see that the mailAddress of user0 is "test@nextcloud.com"
|
||||||
|
|
||||||
|
Scenario: change user quota
|
||||||
|
Given I act as Jane
|
||||||
|
And I am logged in as the admin
|
||||||
|
And I open the User settings
|
||||||
|
And I see that the list of users contains the user user0
|
||||||
|
And I see that the user quota of user0 is Unlimited
|
||||||
|
# disabled because we need the TAB patch:
|
||||||
|
# https://github.com/minkphp/MinkSelenium2Driver/pull/244
|
||||||
|
# When I set the user user0 quota to 1GB
|
||||||
|
# And I see that the quota cell for user user0 is done loading
|
||||||
|
# Then I see that the user quota of user0 is "1 GB"
|
Loading…
Reference in New Issue