Vue migration: settingstags/v14.0.0beta1
@@ -594,6 +594,13 @@ pipeline: | |||
when: | |||
matrix: | |||
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: | |||
image: nextcloudci/php7.0:php7.0-19 | |||
commands: | |||
@@ -761,6 +768,8 @@ matrix: | |||
TESTS-ACCEPTANCE: header | |||
- TESTS: acceptance | |||
TESTS-ACCEPTANCE: login | |||
- TESTS: acceptance | |||
TESTS-ACCEPTANCE: users | |||
- TESTS: jsunit | |||
- TESTS: syntax-php7.0 | |||
- TESTS: syntax-php7.1 |
@@ -67,6 +67,13 @@ div[contenteditable=true], | |||
cursor: default; | |||
opacity: 0.5; | |||
} | |||
&:required { | |||
box-shadow: none; | |||
} | |||
&:invalid { | |||
box-shadow: none !important; | |||
border-color: $color-error; | |||
} | |||
/* Primary action button, use sparingly */ | |||
&.primary { | |||
background-color: $color-primary-element; | |||
@@ -216,7 +223,8 @@ input { | |||
margin-left: -8px !important; | |||
border-left-color: transparent !important; | |||
border-radius: 0 $border-radius $border-radius 0 !important; | |||
background-clip: padding-box; /* Avoid background under border */ | |||
background-clip: padding-box; | |||
/* Avoid background under border */ | |||
background-color: $color-main-background !important; | |||
opacity: 1; | |||
width: 34px; | |||
@@ -227,6 +235,7 @@ input { | |||
background-image: url('../img/actions/confirm-fade.svg?v=2') !important; | |||
} | |||
} | |||
/* only show confirm borders if input is not focused */ | |||
&:not(:active):not(:hover):not(:focus){ | |||
+ .icon-confirm { | |||
@@ -244,14 +253,19 @@ input { | |||
&:active, | |||
&:hover, | |||
&:focus { | |||
&:invalid { | |||
+ .icon-confirm { | |||
border-color: $color-error; | |||
} | |||
} | |||
+ .icon-confirm { | |||
border-color: $color-primary-element !important; | |||
border-left-color: transparent !important; | |||
z-index: 2; /* above previous input */ | |||
/* above previous input */ | |||
z-index: 2; | |||
} | |||
} | |||
} | |||
} | |||
@@ -606,6 +620,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 */ | |||
progress { | |||
display: block; |
@@ -75,8 +75,9 @@ ul.multiselectoptions { | |||
} | |||
} | |||
div.multiselect, | |||
select.multiselect { | |||
/* TODO drop old legacy multiselect! */ | |||
div.multiselect:not(.multiselect-vue), | |||
select.multiselect:not(.multiselect-vue) { | |||
display: inline-block; | |||
max-width: 200px; | |||
min-width: 150px !important; |
@@ -11,119 +11,128 @@ | |||
*/ | |||
.tooltip { | |||
position: absolute; | |||
display: block; | |||
font-family: 'Open Sans', Frutiger, Calibri, 'Myriad Pro', Myriad, sans-serif; | |||
font-style: normal; | |||
font-weight: 400; | |||
letter-spacing: normal; | |||
line-break: auto; | |||
line-height: 1.6; | |||
text-align: left; | |||
text-align: start; | |||
text-decoration: none; | |||
text-shadow: none; | |||
text-transform: none; | |||
white-space: normal; | |||
word-break: normal; | |||
word-spacing: normal; | |||
word-wrap: normal; | |||
font-size: 12px; | |||
opacity: 0; | |||
z-index: 100000; | |||
filter: drop-shadow(0 1px 10px $color-box-shadow); | |||
&.in { | |||
opacity: 1; | |||
} | |||
&.top { | |||
margin-top: -3px; | |||
padding: 10px 0; | |||
} | |||
&.bottom { | |||
margin-top: 3px; | |||
padding: 10px 0; | |||
} | |||
&.right { | |||
margin-left: 3px; | |||
padding: 0 10px; | |||
.tooltip-arrow { | |||
top: 50%; | |||
left: 0; | |||
margin-top: -10px; | |||
border-width: 10px 10px 10px 0; | |||
border-right-color: $color-main-background; | |||
} | |||
} | |||
&.left { | |||
margin-left: -3px; | |||
padding: 0 5px; | |||
.tooltip-arrow { | |||
top: 50%; | |||
right: 0; | |||
margin-top: -10px; | |||
border-width: 10px 0 10px 10px; | |||
border-left-color: $color-main-background; | |||
} | |||
} | |||
/* TOP */ | |||
&.top .tooltip-arrow, | |||
&.top-left .tooltip-arrow, | |||
&.top-right .tooltip-arrow { | |||
bottom: 0; | |||
border-width: 10px 10px 0; | |||
border-top-color: $color-main-background; | |||
} | |||
&.top .tooltip-arrow { | |||
left: 50%; | |||
margin-left: -10px; | |||
} | |||
&.top-left .tooltip-arrow { | |||
right: 10px; | |||
margin-bottom: -10px; | |||
} | |||
&.top-right .tooltip-arrow { | |||
left: 10px; | |||
margin-bottom: -10px; | |||
} | |||
/* BOTTOM */ | |||
&.bottom .tooltip-arrow, | |||
&.bottom-left .tooltip-arrow, | |||
&.bottom-right .tooltip-arrow { | |||
top: 0; | |||
border-width: 0 10px 10px; | |||
border-bottom-color: $color-main-background; | |||
} | |||
&.bottom .tooltip-arrow { | |||
left: 50%; | |||
margin-left: -10px; | |||
} | |||
&.bottom-left .tooltip-arrow { | |||
right: 10px; | |||
margin-top: -10px; | |||
} | |||
&.bottom-right .tooltip-arrow { | |||
left: 10px; | |||
margin-top: -10px; | |||
} | |||
position: absolute; | |||
display: block; | |||
font-family: 'Open Sans', Frutiger, Calibri, 'Myriad Pro', Myriad, sans-serif; | |||
font-style: normal; | |||
font-weight: 400; | |||
letter-spacing: normal; | |||
line-break: auto; | |||
line-height: 1.6; | |||
text-align: left; | |||
text-align: start; | |||
text-decoration: none; | |||
text-shadow: none; | |||
text-transform: none; | |||
white-space: normal; | |||
word-break: normal; | |||
word-spacing: normal; | |||
word-wrap: normal; | |||
font-size: 12px; | |||
opacity: 0; | |||
z-index: 100000; | |||
/* default to top */ | |||
margin-top: -3px; | |||
padding: 10px 0; | |||
filter: drop-shadow(0 1px 10px $color-box-shadow); | |||
&.in, | |||
&.tooltip[aria-hidden='false'] { | |||
visibility: visible; | |||
opacity: 1; | |||
transition: opacity .15s; | |||
} | |||
&.top .tooltip-arrow, | |||
&[x-placement^='top'] { | |||
left: 50%; | |||
margin-left: -10px; | |||
} | |||
&.bottom, | |||
&[x-placement^='bottom'] { | |||
margin-top: 3px; | |||
padding: 10px 0; | |||
} | |||
&.right, | |||
&[x-placement^='right'] { | |||
margin-left: 3px; | |||
padding: 0 10px; | |||
.tooltip-arrow { | |||
top: 50%; | |||
left: 0; | |||
margin-top: -10px; | |||
border-width: 10px 10px 10px 0; | |||
border-right-color: $color-main-background; | |||
} | |||
} | |||
&.left, | |||
&[x-placement^='left'] { | |||
margin-left: -3px; | |||
padding: 0 5px; | |||
.tooltip-arrow { | |||
top: 50%; | |||
right: 0; | |||
margin-top: -10px; | |||
border-width: 10px 0 10px 10px; | |||
border-left-color: $color-main-background; | |||
} | |||
} | |||
/* TOP */ | |||
&.top, | |||
&.top-left, | |||
&[x-placement^='top'], | |||
&.top-right { | |||
.tooltip-arrow { | |||
bottom: 0; | |||
border-width: 10px 10px 0; | |||
border-top-color: $color-main-background; | |||
} | |||
} | |||
&.top-left .tooltip-arrow { | |||
right: 10px; | |||
margin-bottom: -10px; | |||
} | |||
&.top-right .tooltip-arrow { | |||
left: 10px; | |||
margin-bottom: -10px; | |||
} | |||
/* BOTTOM */ | |||
&.bottom, | |||
&[x-placement^='bottom'], | |||
&.bottom-left, | |||
&.bottom-right { | |||
.tooltip-arrow { | |||
top: 0; | |||
border-width: 0 10px 10px; | |||
border-bottom-color: $color-main-background; | |||
} | |||
} | |||
&[x-placement^='bottom'] .tooltip-arrow, | |||
&.bottom .tooltip-arrow { | |||
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 { | |||
max-width: 350px; | |||
padding: 5px 8px; | |||
background-color: $color-main-background; | |||
color: $color-main-text; | |||
text-align: center; | |||
border-radius: $border-radius; | |||
max-width: 350px; | |||
padding: 5px 8px; | |||
background-color: $color-main-background; | |||
color: $color-main-text; | |||
text-align: center; | |||
border-radius: $border-radius; | |||
} | |||
.tooltip-arrow { | |||
position: absolute; | |||
width: 0; | |||
height: 0; | |||
border-color: transparent; | |||
border-style: solid; | |||
} | |||
position: absolute; | |||
width: 0; | |||
height: 0; | |||
border-color: transparent; | |||
border-style: solid; | |||
} |
@@ -913,7 +913,6 @@ return array( | |||
'OC\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/settings/Controller/ChangePasswordController.php', | |||
'OC\\Settings\\Controller\\CheckSetupController' => $baseDir . '/settings/Controller/CheckSetupController.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\\MailSettingsController' => $baseDir . '/settings/Controller/MailSettingsController.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\\CheckSetupController' => __DIR__ . '/../../..' . '/settings/Controller/CheckSetupController.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\\MailSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/MailSettingsController.php', | |||
'OC\\Settings\\Controller\\PersonalSettingsController' => __DIR__ . '/../../..' . '/settings/Controller/PersonalSettingsController.php', |
@@ -59,6 +59,11 @@ class Factory implements IFactory { | |||
*/ | |||
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 */ | |||
protected $config; | |||
@@ -137,9 +142,9 @@ class Factory implements IFactory { | |||
* | |||
* @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; | |||
if(!is_null($userId)) { | |||
if (!is_null($userId)) { | |||
$userLang = $this->config->getUserValue($userId, 'core', 'lang', null); | |||
} else { | |||
$userLang = null; | |||
@@ -310,7 +315,7 @@ class Factory implements IFactory { | |||
*/ | |||
private function isSubDirectory($sub, $parent) { | |||
// Check whether $sub contains no ".." | |||
if(strpos($sub, '..') !== false) { | |||
if (strpos($sub, '..') !== false) { | |||
return false; | |||
} | |||
@@ -441,4 +446,74 @@ class Factory implements IFactory { | |||
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', | |||
'id' => 'core_users', | |||
'order' => 4, | |||
'href' => $this->urlGenerator->linkToRoute('settings_users'), | |||
'href' => $this->urlGenerator->linkToRoute('settings.Users.usersList'), | |||
'name' => $l->t('Users'), | |||
'icon' => $this->urlGenerator->imagePath('settings', 'users.svg'), | |||
]); |
@@ -39,6 +39,7 @@ use OCP\L10N\IFactory; | |||
use OCP\Settings\ISettings; | |||
class PersonalInfo implements ISettings { | |||
/** @var IConfig */ | |||
private $config; | |||
/** @var IUserManager */ | |||
@@ -51,12 +52,6 @@ class PersonalInfo implements ISettings { | |||
private $appManager; | |||
/** @var IFactory */ | |||
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 */ | |||
private $l; | |||
@@ -198,64 +193,29 @@ class PersonalInfo implements ISettings { | |||
$uid = $user->getUID(); | |||
$userLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); | |||
$languageCodes = $this->l10nFactory->findAvailableLanguages(); | |||
$commonLanguages = []; | |||
$languages = []; | |||
$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); | |||
$languages = $this->l10nFactory->getLanguages(); | |||
foreach($languageCodes as $lang) { | |||
$l = \OC::$server->getL10N('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); | |||
} 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; | |||
} | |||
// associate the user language with the proper array | |||
$userLangIndex = array_search($userConfLang, array_column($languages['commonlanguages'], 'code')); | |||
$userLang = $languages['commonlanguages'][$userLangIndex]; | |||
// search in the other languages | |||
if ($userLangIndex === false) { | |||
$userLangIndex = array_search($userConfLang, array_column($languages['languages'], 'code')); | |||
$userLang = $languages['languages'][$userLangIndex]; | |||
} | |||
// if user language is not available but set somehow: show the actual code as name | |||
if (!is_array($userLang)) { | |||
$userLang = [ | |||
'code' => $userLang, | |||
'name' => $userLang, | |||
'code' => $userConfLang, | |||
'name' => $userConfLang, | |||
]; | |||
} | |||
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 [ | |||
'activelanguage' => $userLang, | |||
'commonlanguages' => $commonLanguages, | |||
'languages' => $languages | |||
]; | |||
return array_merge( | |||
array('activelanguage' => $userLang), | |||
$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 | |||
); | |||
} | |||
} |
@@ -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 */ | |||
#userlist tr.row-warning { | |||
@@ -1350,3 +1255,178 @@ doesnotexist:-o-prefocus, .strengthify-wrapper { | |||
margin-top: 22px; | |||
} | |||
} | |||
/* USERS LIST -------------------------------------------------------------- */ | |||
#body-settings { | |||
#app-navigation { | |||
/* Hack to override the javascript orderBy */ | |||
#usergrouplist > li { | |||
order: 4; | |||
&#everyone { | |||
order:1; | |||
} | |||
&#admin { | |||
order:2; | |||
} | |||
&#disabled { | |||
order:3; | |||
} | |||
} | |||
} | |||
$grid-row-height: 46px; | |||
#app-content.user-list-grid { | |||
display: grid; | |||
grid-auto-columns: 1fr; | |||
grid-auto-rows: $grid-row-height; | |||
grid-column-gap: 20px; | |||
.row { | |||
// 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; | |||
} | |||
} | |||
} |
@@ -1,213 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/** | |||
* takes care of deleting things represented by an ID | |||
* | |||
* @class | |||
* @param {string} endpoint the corresponding ajax PHP script. Currently limited | |||
* to settings - ajax path. | |||
* @param {string} paramID the by the script expected parameter name holding the | |||
* ID of the object to delete | |||
* @param {markCallback} markCallback function to be called after successfully | |||
* marking the object for deletion. | |||
* @param {removeCallback} removeCallback the function to be called after | |||
* successful delete. | |||
*/ | |||
/* globals escapeHTML */ | |||
function DeleteHandler(endpoint, paramID, markCallback, removeCallback) { | |||
this.oidToDelete = false; | |||
this.canceled = false; | |||
this.ajaxEndpoint = endpoint; | |||
this.ajaxParamID = paramID; | |||
this.markCallback = markCallback; | |||
this.removeCallback = removeCallback; | |||
this.undoCallback = false; | |||
this.notifier = false; | |||
this.notificationDataID = false; | |||
this.notificationMessage = false; | |||
this.notificationPlaceholder = '%oid'; | |||
} | |||
/** | |||
* Number of milliseconds after which the operation is performed. | |||
*/ | |||
DeleteHandler.TIMEOUT_MS = 7000; | |||
/** | |||
* Timer after which the action will be performed anyway. | |||
*/ | |||
DeleteHandler.prototype._timeout = null; | |||
/** | |||
* The function to be called after successfully marking the object for deletion | |||
* @callback markCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* The function to be called after successful delete. The id of the object will | |||
* be passed as argument. Unsuccessful operations will display an error using | |||
* OC.dialogs, no callback is fired. | |||
* @callback removeCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* This callback is fired after "undo" was clicked so the consumer can update | |||
* the web interface | |||
* @callback undoCallback | |||
* @param {string} oid the ID of the specific user or group | |||
*/ | |||
/** | |||
* enabled the notification system. Required for undo UI. | |||
* | |||
* @param {object} notifier Usually OC.Notification | |||
* @param {string} dataID an identifier for the notifier, e.g. 'deleteuser' | |||
* @param {string} message the message that should be shown upon delete. %oid | |||
* will be replaced with the affected id of the item to be deleted | |||
* @param {undoCallback} undoCallback called after "undo" was clicked | |||
*/ | |||
DeleteHandler.prototype.setNotification = function(notifier, dataID, message, undoCallback) { | |||
this.notifier = notifier; | |||
this.notificationDataID = dataID; | |||
this.notificationMessage = message; | |||
this.undoCallback = undoCallback; | |||
var dh = this; | |||
$('#notification') | |||
.off('click.deleteHandler_' + dataID) | |||
.on('click.deleteHandler_' + dataID, '.undo', function () { | |||
if ($('#notification').data(dh.notificationDataID)) { | |||
var oid = dh.oidToDelete; | |||
dh.cancel(); | |||
if(typeof dh.undoCallback !== 'undefined') { | |||
dh.undoCallback(oid); | |||
} | |||
} | |||
dh.notifier.hide(); | |||
}); | |||
}; | |||
/** | |||
* shows the Undo Notification (if configured) | |||
*/ | |||
DeleteHandler.prototype.showNotification = function() { | |||
if(this.notifier !== false) { | |||
if(!this.notifier.isHidden()) { | |||
this.hideNotification(); | |||
} | |||
$('#notification').data(this.notificationDataID, true); | |||
var msg = this.notificationMessage.replace( | |||
this.notificationPlaceholder, escapeHTML(this.oidToDelete)); | |||
this.notifier.showHtml(msg); | |||
} | |||
}; | |||
/** | |||
* hides the Undo Notification | |||
*/ | |||
DeleteHandler.prototype.hideNotification = function() { | |||
if(this.notifier !== false) { | |||
$('#notification').removeData(this.notificationDataID); | |||
this.notifier.hide(); | |||
} | |||
}; | |||
/** | |||
* initializes the delete operation for a given object id | |||
* | |||
* @param {string} oid the object id | |||
*/ | |||
DeleteHandler.prototype.mark = function(oid) { | |||
if(this.oidToDelete !== false) { | |||
// passing true to avoid hiding the notification | |||
// twice and causing the second notification | |||
// to disappear immediately | |||
this.deleteEntry(true); | |||
} | |||
this.oidToDelete = oid; | |||
this.canceled = false; | |||
this.markCallback(oid); | |||
this.showNotification(); | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
if (DeleteHandler.TIMEOUT_MS > 0) { | |||
this._timeout = window.setTimeout( | |||
_.bind(this.deleteEntry, this), | |||
DeleteHandler.TIMEOUT_MS | |||
); | |||
} | |||
}; | |||
/** | |||
* cancels a delete operation | |||
*/ | |||
DeleteHandler.prototype.cancel = function() { | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
this.canceled = true; | |||
this.oidToDelete = false; | |||
}; | |||
/** | |||
* executes a delete operation. Requires that the operation has been | |||
* initialized by mark(). On error, it will show a message via | |||
* OC.dialogs.alert. On success, a callback is fired so that the client can | |||
* update the web interface accordingly. | |||
* | |||
* @param {boolean} [keepNotification] true to keep the notification, false to hide | |||
* it, defaults to false | |||
*/ | |||
DeleteHandler.prototype.deleteEntry = function(keepNotification) { | |||
var deferred = $.Deferred(); | |||
if(this.canceled || this.oidToDelete === false) { | |||
return deferred.resolve().promise(); | |||
} | |||
var dh = this; | |||
if(!keepNotification && $('#notification').data(this.notificationDataID) === true) { | |||
dh.hideNotification(); | |||
} | |||
if (this._timeout) { | |||
clearTimeout(this._timeout); | |||
this._timeout = null; | |||
} | |||
var payload = {}; | |||
payload[dh.ajaxParamID] = dh.oidToDelete; | |||
return $.ajax({ | |||
type: 'DELETE', | |||
url: OC.generateUrl(dh.ajaxEndpoint+'/{oid}',{oid: this.oidToDelete}), | |||
// FIXME: do not use synchronous ajax calls as they block the browser ! | |||
async: false, | |||
success: function (result) { | |||
// Remove undo option, & remove user from table | |||
//TODO: following line | |||
dh.removeCallback(dh.oidToDelete); | |||
dh.canceled = true; | |||
}, | |||
error: function (jqXHR) { | |||
OC.dialogs.alert(jqXHR.responseJSON.data.message, t('settings', 'Unable to delete {objName}', {objName: dh.oidToDelete})); | |||
dh.undoCallback(dh.oidToDelete); | |||
} | |||
}); | |||
}; |
@@ -1,78 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/** | |||
* @brief this object takes care of the filter functionality on the user | |||
* management page | |||
* @param {UserList} userList the UserList object | |||
* @param {GroupList} groupList the GroupList object | |||
*/ | |||
function UserManagementFilter (userList, groupList) { | |||
this.userList = userList; | |||
this.groupList = groupList; | |||
this.oldFilter = ''; | |||
this.init(); | |||
} | |||
/** | |||
* @brief sets up when the filter action shall be triggered | |||
*/ | |||
UserManagementFilter.prototype.init = function () { | |||
OC.Plugins.register('OCA.Search', this); | |||
}; | |||
/** | |||
* @brief the filter action needs to be done, here the accurate steps are being | |||
* taken care of | |||
*/ | |||
UserManagementFilter.prototype.run = _.debounce(function (filter) { | |||
if (filter === this.oldFilter) { | |||
return; | |||
} | |||
this.oldFilter = filter; | |||
this.userList.filter = filter; | |||
this.userList.empty(); | |||
this.userList.update(GroupList.getCurrentGID()); | |||
if (this.groupList.filterGroups) { | |||
// user counts are being updated nevertheless | |||
this.groupList.empty(); | |||
} | |||
this.groupList.update(); | |||
}, | |||
300 | |||
); | |||
/** | |||
* @brief returns the filter String | |||
* @returns string | |||
*/ | |||
UserManagementFilter.prototype.getPattern = function () { | |||
var input = this.filterInput.val(), | |||
html = $('html'), | |||
isIE8or9 = html.hasClass('lte9'); | |||
// FIXME - TODO - once support for IE8 and IE9 is dropped | |||
if (isIE8or9 && input == this.filterInput.attr('placeholder')) { | |||
input = ''; | |||
} | |||
return input; | |||
}; | |||
/** | |||
* @brief adds reset functionality to an HTML element | |||
* @param jQuery the jQuery representation of that element | |||
*/ | |||
UserManagementFilter.prototype.addResetButton = function (button) { | |||
var umf = this; | |||
button.click(function () { | |||
umf.filterInput.val(''); | |||
umf.run(); | |||
}); | |||
}; | |||
UserManagementFilter.prototype.attach = function (search) { | |||
search.setFilter('settings', this.run.bind(this)); | |||
}; |
@@ -1,385 +0,0 @@ | |||
/** | |||
* Copyright (c) 2014, Raghu Nayyar <beingminimal@gmail.com> | |||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/ | |||
/* globals escapeHTML, UserList, DeleteHandler */ | |||
var $userGroupList, | |||
$sortGroupBy; | |||
var GroupList; | |||
GroupList = { | |||
activeGID: '', | |||
everyoneGID: '_everyone', | |||
filter: '', | |||
filterGroups: false, | |||
addGroup: function (gid, displayName, usercount) { | |||
if (_.isUndefined(displayName)) { | |||
displayName = gid; | |||
} | |||
var $li = $userGroupList.find('.isgroup:last-child').clone(); | |||
$li | |||
.data('gid', gid) | |||
.find('.groupname').text(displayName); | |||
GroupList.setUserCount($li, usercount); | |||
$li.appendTo($userGroupList); | |||
GroupList.sortGroups(); | |||
return $li; | |||
}, | |||
setUserCount: function (groupLiElement, usercount) { | |||
if ($sortGroupBy !== 1) { | |||
// If we don't sort by group count we don't display them either | |||
return; | |||
} | |||
var $groupLiElement = $(groupLiElement); | |||
if (usercount === undefined || usercount === 0 || usercount < 0) { | |||
usercount = ''; | |||
$groupLiElement.data('usercount', 0); | |||
} else { | |||
$groupLiElement.data('usercount', usercount); | |||
} | |||
$groupLiElement.find('.usercount').text(usercount); | |||
}, | |||
getUserCount: function ($groupLiElement) { | |||
var count = parseInt($groupLiElement.data('usercount'), 10); | |||
return isNaN(count) ? 0 : count; | |||
}, | |||
modGroupCount: function(gid, diff) { | |||
var $li = GroupList.getGroupLI(gid); | |||
var count = GroupList.getUserCount($li) + diff; | |||
GroupList.setUserCount($li, count); | |||
}, | |||
incEveryoneCount: function() { | |||
GroupList.modGroupCount(GroupList.everyoneGID, 1); | |||
}, | |||
decEveryoneCount: function() { | |||
GroupList.modGroupCount(GroupList.everyoneGID, -1); | |||
}, | |||
incGroupCount: function(gid) { | |||
GroupList.modGroupCount(gid, 1); | |||
}, | |||
decGroupCount: function(gid) { | |||
GroupList.modGroupCount(gid, -1); | |||
}, | |||
getCurrentGID: function () { | |||
return GroupList.activeGID; | |||
}, | |||
sortGroups: function () { | |||
var lis = $userGroupList.find('.isgroup').get(); | |||
lis.sort(function (a, b) { | |||
// "Everyone" always at the top | |||
if ($(a).data('gid') === '_everyone') { | |||
return -1; | |||
} else if ($(b).data('gid') === '_everyone') { | |||
return 1; | |||
} | |||
// "admin" always as second | |||
if ($(a).data('gid') === 'admin') { | |||
return -1; | |||
} else if ($(b).data('gid') === 'admin') { | |||
return 1; | |||
} | |||
if ($sortGroupBy === 1) { | |||
// Sort by user count first | |||
var $usersGroupA = $(a).data('usercount'), | |||
$usersGroupB = $(b).data('usercount'); | |||
if ($usersGroupA > 0 && $usersGroupA > $usersGroupB) { | |||
return -1; | |||
} | |||
if ($usersGroupB > 0 && $usersGroupB > $usersGroupA) { | |||
return 1; | |||
} | |||
} | |||
// Fallback or sort by group name | |||
return UserList.alphanum( | |||
$(a).find('a span').text(), | |||
$(b).find('a span').text() | |||
); | |||
}); | |||
var items = []; | |||
$.each(lis, function (index, li) { | |||
items.push(li); | |||
if (items.length === 100) { | |||
$userGroupList.append(items); | |||
items = []; | |||
} | |||
}); | |||
if (items.length > 0) { | |||
$userGroupList.append(items); | |||
} | |||
}, | |||
createGroup: function (groupid) { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.createGroup, this, groupid)); | |||
return; | |||
} | |||
$.post( | |||
OC.generateUrl('/settings/users/groups'), | |||
{ | |||
id: groupid | |||
}, | |||
function (result) { | |||
if (result.groupname) { | |||
var addedGroup = result.groupname; | |||
UserList.availableGroups[groupid] = {displayName: result.groupname}; | |||
GroupList.addGroup(groupid, result.groupname); | |||
} | |||
GroupList.toggleAddGroup(); | |||
}).fail(function(result) { | |||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', {message: result.responseJSON.message})); | |||
}); | |||
}, | |||
update: function () { | |||
if (GroupList.updating) { | |||
return; | |||
} | |||
GroupList.updating = true; | |||
$.get( | |||
OC.generateUrl('/settings/users/groups'), | |||
{ | |||
pattern: this.filter, | |||
filterGroups: this.filterGroups ? 1 : 0, | |||
sortGroups: $sortGroupBy | |||
}, | |||
function (result) { | |||
var lis = []; | |||
if (result.status === 'success') { | |||
$.each(result.data, function (i, subset) { | |||
$.each(subset, function (index, group) { | |||
if (GroupList.getGroupLI(group.name).length > 0) { | |||
GroupList.setUserCount(GroupList.getGroupLI(group.name).first(), group.usercount); | |||
} | |||
else { | |||
var $li = GroupList.addGroup(group.id, group.name, group.usercount); | |||
$li.addClass('appear transparent'); | |||
lis.push($li); | |||
} | |||
}); | |||
}); | |||
if (result.data.length > 0) { | |||
GroupList.doSort(); | |||
} | |||
else { | |||
GroupList.noMoreEntries = true; | |||
} | |||
_.defer(function () { | |||
$(lis).each(function () { | |||
this.removeClass('transparent'); | |||
}); | |||
}); | |||
} | |||
GroupList.updating = false; | |||
} | |||
); | |||
}, | |||
elementBelongsToAddGroup: function (el) { | |||
return !(el !== $('#newgroup-form').get(0) && | |||
$('#newgroup-form').find($(el)).length === 0); | |||
}, | |||
hasAddGroupNameText: function () { | |||
var name = $('#newgroupname').val(); | |||
return $.trim(name) !== ''; | |||
}, | |||
showDisabledUsers: function () { | |||
UserList.empty(); | |||
UserList.update('_disabledUsers'); | |||
$userGroupList.find('li').removeClass('active'); | |||
GroupList.getGroupLI('_disabledUsers').addClass('active'); | |||
}, | |||
showGroup: function (gid) { | |||
GroupList.activeGID = gid; | |||
UserList.empty(); | |||
UserList.update(gid === '_everyone' ? '' : gid); | |||
$userGroupList.find('li').removeClass('active'); | |||
if (gid !== undefined) { | |||
GroupList.getGroupLI(gid).addClass('active'); | |||
} | |||
}, | |||
isAddGroupButtonVisible: function () { | |||
return !$('#newgroup-entry').hasClass('editing'); | |||
}, | |||
toggleAddGroup: function (event) { | |||
if (GroupList.isAddGroupButtonVisible()) { | |||
if (event) { | |||
event.stopPropagation(); | |||
} | |||
$('#newgroup-entry').addClass('editing'); | |||
$('#newgroupname').select(); | |||
GroupList.handleAddGroupInput(''); | |||
} | |||
else { | |||
$('#newgroup-entry').removeClass('editing'); | |||
$('#newgroupname').val(''); | |||
} | |||
}, | |||
handleAddGroupInput: function (input) { | |||
if(input.length) { | |||
$('#newgroup-form input[type="submit"]').attr('disabled', null); | |||
} else { | |||
$('#newgroup-form input[type="submit"]').attr('disabled', 'disabled'); | |||
} | |||
}, | |||
isGroupNameValid: function (groupname) { | |||
if ($.trim(groupname) === '') { | |||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', { | |||
message: t('settings', 'A valid group name must be provided') | |||
})); | |||
return false; | |||
} | |||
return true; | |||
}, | |||
hide: function (gid) { | |||
GroupList.getGroupLI(gid).hide(); | |||
}, | |||
show: function (gid) { | |||
GroupList.getGroupLI(gid).show(); | |||
}, | |||
remove: function (gid) { | |||
GroupList.getGroupLI(gid).remove(); | |||
}, | |||
empty: function () { | |||
$userGroupList.find('.isgroup').filter(function(index, item){ | |||
return $(item).data('gid') !== ''; | |||
}).remove(); | |||
}, | |||
initDeleteHandling: function () { | |||
//set up handler | |||
var GroupDeleteHandler = new DeleteHandler('/settings/users/groups', 'groupname', | |||
GroupList.hide, GroupList.remove); | |||
//configure undo | |||
OC.Notification.hide(); | |||
var msg = escapeHTML(t('settings', 'deleted {groupName}', {groupName: '%oid'})) + '<span class="undo">' + | |||
escapeHTML(t('settings', 'undo')) + '</span>'; | |||
GroupDeleteHandler.setNotification(OC.Notification, 'deletegroup', msg, | |||
GroupList.show); | |||
//when to mark user for delete | |||
var deleteAction = function () { | |||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) { | |||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(deleteAction, this)); | |||
return; | |||
} | |||
// Call function for handling delete/undo | |||
GroupDeleteHandler.mark(GroupList.getElementGID($(this).parent())); | |||
}; | |||
$userGroupList.on('click', '.delete', deleteAction); | |||
//delete a marked user when leaving the page | |||
$(window).on('beforeunload', function () { | |||
GroupDeleteHandler.deleteEntry(); | |||
}); | |||
}, | |||
getGroupLI: function (gid) { | |||
return $userGroupList.find('li.isgroup').filter(function () { | |||
return GroupList.getElementGID(this) === gid; | |||
}); | |||
}, | |||
getElementGID: function (element) { | |||
return ($(element).closest('li').data('gid') || '').toString(); | |||
}, | |||
getEveryoneCount: function () { | |||
$.ajax({ | |||
type: "GET", | |||
dataType: "json", | |||
url: OC.generateUrl('/settings/users/stats') | |||
}).success(function (data) { | |||
$('#everyonegroup').data('usercount', data.totalUsers); | |||
$('#everyonecount').text(data.totalUsers); | |||
}); | |||
} | |||
}; | |||
$(document).ready( function () { | |||
$userGroupList = $('#usergrouplist'); | |||
GroupList.initDeleteHandling(); | |||
$sortGroupBy = $userGroupList.data('sort-groups'); | |||
if ($sortGroupBy === 1) { | |||
// Disabled due to performance issues, when we don't need it for sorting | |||
GroupList.getEveryoneCount(); | |||
} | |||
// Display or hide of Create Group List Element | |||
$('#newgroup-init').on('click', function (e) { | |||
GroupList.toggleAddGroup(e); | |||
}); | |||
$(document).on('click keydown keyup', function(event) { | |||
if(!GroupList.isAddGroupButtonVisible() && | |||
!GroupList.elementBelongsToAddGroup(event.target) && | |||
!GroupList.hasAddGroupNameText()) { | |||
GroupList.toggleAddGroup(); | |||
} | |||
// Escape | |||
if(!GroupList.isAddGroupButtonVisible() && event.keyCode && event.keyCode === 27) { | |||
GroupList.toggleAddGroup(); | |||
} | |||
}); | |||
// Responsible for Creating Groups. | |||
$('#newgroup-form form').submit(function (event) { | |||
event.preventDefault(); | |||
if(GroupList.isGroupNameValid($('#newgroupname').val())) { | |||
GroupList.createGroup($('#newgroupname').val()); | |||
} | |||
}); | |||
// click on group name | |||
$userGroupList.on('click', '.isgroup', function () { | |||
GroupList.showGroup(GroupList.getElementGID(this)); | |||
}); | |||
// show disabled users | |||
$userGroupList.on('click', '.disabledusers', function () { | |||
GroupList.showDisabledUsers(); | |||
}); | |||
$('#newgroupname').on('input', function(){ | |||
GroupList.handleAddGroupInput(this.value); | |||
}); | |||
// highlight `everyone` group at DOMReady by default | |||
GroupList.showGroup('_everyone'); | |||
}); |
@@ -0,0 +1,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->registerRoutes($this, [ | |||
'resources' => [ | |||
'users' => ['url' => '/settings/users/users'], | |||
'AuthSettings' => ['url' => '/settings/personal/authtokens'], | |||
], | |||
'routes' => [ | |||
@@ -50,12 +49,10 @@ $application->registerRoutes($this, [ | |||
['name' => 'AppSettings#listCategories', 'url' => '/settings/apps/categories', 'verb' => 'GET'], | |||
['name' => 'AppSettings#viewApps', 'url' => '/settings/apps', 'verb' => 'GET'], | |||
['name' => 'AppSettings#listApps', 'url' => '/settings/apps/list', 'verb' => 'GET'], | |||
['name' => 'Users#setDisplayName', 'url' => '/settings/users/{username}/displayName', 'verb' => 'POST'], | |||
['name' => 'Users#setEMailAddress', 'url' => '/settings/users/{id}/mailAddress', 'verb' => 'PUT'], | |||
['name' => 'Users#setUserSettings', 'url' => '/settings/users/{username}/settings', 'verb' => 'PUT'], | |||
['name' => 'Users#getVerificationCode', 'url' => '/settings/users/{account}/verify', 'verb' => 'GET'], | |||
['name' => 'Users#setEnabled', 'url' => '/settings/users/{id}/setEnabled', 'verb' => 'POST'], | |||
['name' => 'Users#stats', 'url' => '/settings/users/stats', 'verb' => 'GET'], | |||
['name' => 'Users#usersList', 'url' => '/settings/users', 'verb' => 'GET'], | |||
['name' => 'Users#usersListByGroup', 'url' => '/settings/users/{group}', 'verb' => 'GET'], | |||
['name' => 'LogSettings#setLogLevel', 'url' => '/settings/admin/log/level', 'verb' => 'POST'], | |||
['name' => 'LogSettings#getEntries', 'url' => '/settings/admin/log/entries', '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#form', 'url' => '/settings/admin/{section}', 'verb' => 'GET'], | |||
['name' => 'ChangePassword#changePersonalPassword', 'url' => '/settings/personal/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'], | |||
['name' => 'ChangePassword#changeUserPassword', 'url' => '/settings/users/changepassword', 'verb' => 'POST'] | |||
] | |||
]); | |||
@@ -84,18 +76,7 @@ $application->registerRoutes($this, [ | |||
// Settings pages | |||
$this->create('settings_help', '/settings/help') | |||
->actionInclude('settings/help.php'); | |||
$this->create('settings_users', '/settings/users') | |||
->actionInclude('settings/users.php'); | |||
// 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 | |||
$this->create('settings_ajax_enableapp', '/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'); | |||
$this->create('settings_ajax_uninstallapp', '/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 /** | |||
* Copyright (c) 2011, Robin Appelman <icewind1991@gmail.com> | |||
* This file is licensed under the Affero General Public License version 3 or later. | |||
* See the COPYING-README file. | |||
*/?> | |||
<?php | |||
/** | |||
* @copyright Copyright (c) 2018, John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* @author John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com> | |||
* | |||
* @license GNU AGPL version 3 or any later version | |||
* | |||
* This is the default empty template to load Vue! | |||
* Do your cbackend computations into a php files | |||
* then serve this file as template and include your data into | |||
* the $serverData template variable | |||
* | |||
* return new TemplateResponse('settings', 'settings', ['serverData' => $serverData]); | |||
* | |||
*/ | |||
<?php foreach($_['forms'] as $form) { | |||
print_unescaped($form); | |||
} | |||
script('settings', 'main'); | |||
style('settings', 'settings'); | |||
// Did we have some data to inject ? | |||
if(is_array($_['serverData'])) { | |||
?> | |||
<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], | |||
// Needs session: [CertificateController::class, Controller::class], | |||
[CheckSetupController::class, Controller::class], | |||
[GroupsController::class, Controller::class], | |||
[LogSettingsController::class, Controller::class], | |||
[MailSettingsController::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); | |||
} | |||
} |
@@ -8,8 +8,10 @@ default: | |||
- NextcloudTestServerContext | |||
- AppNavigationContext | |||
- AppSettingsContext | |||
- CommentsAppContext | |||
- ContactsMenuContext | |||
- DialogContext | |||
- FeatureContext | |||
- FileListContext | |||
- FilesAppContext |
@@ -1,9 +1,10 @@ | |||
<?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 | |||
@@ -32,24 +33,43 @@ class AppNavigationContext implements Context, ActorAwareInterface { | |||
*/ | |||
public static function appNavigation() { | |||
return Locator::forThe()->id("app-navigation")-> | |||
describedAs("App navigation"); | |||
describedAs("App navigation"); | |||
} | |||
/** | |||
* @return Locator | |||
*/ | |||
public static function appNavigationSectionItemFor($sectionText) { | |||
return Locator::forThe()->xpath("//li[normalize-space() = '$sectionText']")-> | |||
descendantOf(self::appNavigation())-> | |||
describedAs($sectionText . " section item in App Navigation"); | |||
return Locator::forThe()->xpath("//li/a[normalize-space() = '$sectionText']/..")-> | |||
descendantOf(self::appNavigation())-> | |||
describedAs($sectionText . " section item in App Navigation"); | |||
} | |||
/** | |||
* @return Locator | |||
*/ | |||
public static function appNavigationCurrentSectionItem() { | |||
return Locator::forThe()->css(".active")->descendantOf(self::appNavigation())-> | |||
describedAs("Current section item in App Navigation"); | |||
return Locator::forThe()->css(".active")-> | |||
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(); | |||
} | |||
/** | |||
* @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 | |||
*/ | |||
@@ -66,4 +93,25 @@ class AppNavigationContext implements Context, ActorAwareInterface { | |||
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()); | |||
} | |||
} |
@@ -1,8 +1,9 @@ | |||
<?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 | |||
* | |||
@@ -31,7 +32,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface { | |||
* @return Locator | |||
*/ | |||
public static function newUserForm() { | |||
return Locator::forThe()->id("newuserHeader")-> | |||
return Locator::forThe()->id("new-user")-> | |||
describedAs("New user form in Users Settings"); | |||
} | |||
@@ -63,7 +64,7 @@ class UsersSettingsContext implements Context, ActorAwareInterface { | |||
* @return Locator | |||
*/ | |||
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"); | |||
} | |||
@@ -71,24 +72,72 @@ class UsersSettingsContext implements Context, ActorAwareInterface { | |||
* @return Locator | |||
*/ | |||
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"); | |||
} | |||
/** | |||
* Warning: you need to watch out for the proper classes order | |||
* | |||
* @return Locator | |||
*/ | |||
public static function classCellForUser($class, $user) { | |||
return Locator::forThe()->xpath("//*[@class='$class']")-> | |||
descendantOf(self::rowForUser($user))-> | |||
describedAs("$class cell for user $user in Users Settings"); | |||
} | |||
/** | |||
* @return Locator | |||
*/ | |||
public static function inputForUserInCell($cell, $user) { | |||
return Locator::forThe()->css("input")-> | |||
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 passwordCellForUser($user) { | |||
return Locator::forThe()->css(".password")->descendantOf(self::rowForUser($user))-> | |||
describedAs("Password cell for user $user in Users Settings"); | |||
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 passwordInputForUser($user) { | |||
return Locator::forThe()->css("input")->descendantOf(self::passwordCellForUser($user))-> | |||
describedAs("Password input for user $user in Users Settings"); | |||
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(); | |||
} | |||
/** | |||
* @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 | |||
*/ | |||
@@ -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) { | |||
$this->actor->find(self::passwordCellForUser($user), 10)->click(); | |||
$this->actor->find(self::passwordInputForUser($user), 2)->setValue($password . "\r"); | |||
public function iSetTheFieldForUserTo($field, $user, $value) { | |||
$this->actor->find(self::inputForUserInCell($field, $user), 2)->setValue($value . "\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 | |||
*/ | |||
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()); | |||
} | |||
/** | |||
* @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"); | |||
} | |||
/** | |||
* 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. | |||
* | |||
@@ -205,6 +217,32 @@ class ElementWrapper { | |||
$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. | |||
* |
@@ -18,7 +18,6 @@ Feature: login | |||
And I am logged in as the admin | |||
And I open the User settings | |||
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 log in with user user0 and password 654321 | |||
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" |