Merge pull request #8824 from nextcloud/settings-vue
Vue migration: settings
This commit is contained in:
commit
a2c518ee5a
|
@ -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;
|
||||
|
|
|
@ -31,21 +31,28 @@
|
|||
font-size: 12px;
|
||||
opacity: 0;
|
||||
z-index: 100000;
|
||||
filter: drop-shadow(0 1px 10px $color-box-shadow);
|
||||
&.in {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.top {
|
||||
/* 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;
|
||||
}
|
||||
&.bottom {
|
||||
&.top .tooltip-arrow,
|
||||
&[x-placement^='top'] {
|
||||
left: 50%;
|
||||
margin-left: -10px;
|
||||
}
|
||||
&.bottom,
|
||||
&[x-placement^='bottom'] {
|
||||
margin-top: 3px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
&.right,
|
||||
&[x-placement^='right'] {
|
||||
margin-left: 3px;
|
||||
padding: 0 10px;
|
||||
.tooltip-arrow {
|
||||
|
@ -56,7 +63,8 @@
|
|||
border-right-color: $color-main-background;
|
||||
}
|
||||
}
|
||||
&.left {
|
||||
&.left,
|
||||
&[x-placement^='left'] {
|
||||
margin-left: -3px;
|
||||
padding: 0 5px;
|
||||
.tooltip-arrow {
|
||||
|
@ -67,18 +75,16 @@
|
|||
border-left-color: $color-main-background;
|
||||
}
|
||||
}
|
||||
|
||||
/* TOP */
|
||||
&.top .tooltip-arrow,
|
||||
&.top-left .tooltip-arrow,
|
||||
&.top-right .tooltip-arrow {
|
||||
&.top,
|
||||
&.top-left,
|
||||
&[x-placement^='top'],
|
||||
&.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;
|
||||
|
@ -88,15 +94,18 @@
|
|||
left: 10px;
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
/* BOTTOM */
|
||||
&.bottom .tooltip-arrow,
|
||||
&.bottom-left .tooltip-arrow,
|
||||
&.bottom-right .tooltip-arrow {
|
||||
&.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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
$userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage());
|
||||
$languages = $this->l10nFactory->getLanguages();
|
||||
|
||||
$commonLanguages = [];
|
||||
$languages = [];
|
||||
|
||||
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);
|
||||
// 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];
|
||||
}
|
||||
|
||||
// put appropriate languages into appropriate arrays, to print them sorted
|
||||
// used language -> common languages -> divider -> other languages
|
||||
if ($lang === $userLang) {
|
||||
$userLang = $ln;
|
||||
} elseif (in_array($lang, self::COMMON_LANGUAGE_CODES)) {
|
||||
$commonLanguages[array_search($lang, self::COMMON_LANGUAGE_CODES)]=$ln;
|
||||
} else {
|
||||
$languages[]=$ln;
|
||||
}
|
||||
}
|
||||
|
||||
// if user language is not available but set somehow: show the actual code as name
|
||||
if (!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
|
||||
);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
|||
all: dev-setup build-js-production
|
||||
|
||||
dev-setup: clean clean-dev npm-init
|
||||
|
||||
npm-init:
|
||||
npm install
|
||||
|
||||
npm-update:
|
||||
npm update
|
||||
|
||||
build-js:
|
||||
npm run dev
|
||||
|
||||
build-js-production:
|
||||
npm run build
|
||||
|
||||
watch-js:
|
||||
npm run watch
|
||||
|
||||
clean:
|
||||
rm -f js/main.js
|
||||
rm -f js/main.js.map
|
||||
|
||||
clean-dev:
|
||||
rm -rf node_modules
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Settings section
|
||||
|
||||
> Nextcloud settings with Vue
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
# install dependencies
|
||||
make dev-setup
|
||||
|
||||
# build for development
|
||||
make build-js
|
||||
|
||||
# build for development and watch edits
|
||||
make watch-js
|
||||
|
||||
# build for production with minification
|
||||
make build-js-production
|
||||
```
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
* @author Bart Visscher <bartv@thisnet.nl>
|
||||
* @author Björn Schießle <bjoern@schiessle.org>
|
||||
* @author Christopher Schäpers <kondou@ts.unde.re>
|
||||
* @author Felix Moeller <mail@felixmoeller.de>
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Morris Jobke <hey@morrisjobke.de>
|
||||
* @author Robin Appelman <robin@icewind.nl>
|
||||
* @author Thomas Müller <thomas.mueller@tmit.eu>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
OC_JSON::checkSubAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
||||
exit();
|
||||
}
|
||||
|
||||
$username = isset($_POST["username"]) ? (string)$_POST["username"] : '';
|
||||
|
||||
$isUserAccessible = false;
|
||||
$currentUserObject = \OC::$server->getUserSession()->getUser();
|
||||
$targetUserObject = \OC::$server->getUserManager()->get($username);
|
||||
if($targetUserObject !== null && $currentUserObject !== null) {
|
||||
$isUserAccessible = \OC::$server->getGroupManager()->getSubAdmin()->isUserAccessible($currentUserObject, $targetUserObject);
|
||||
}
|
||||
|
||||
if(($username === '' && !OC_User::isAdminUser(OC_User::getUser()))
|
||||
|| (!OC_User::isAdminUser(OC_User::getUser())
|
||||
&& !$isUserAccessible)) {
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Authentication error') )));
|
||||
exit();
|
||||
}
|
||||
|
||||
//make sure the quota is in the expected format
|
||||
$quota= (string)$_POST["quota"];
|
||||
if($quota !== 'none' and $quota !== 'default') {
|
||||
$quota= OC_Helper::computerFileSize($quota);
|
||||
$quota=OC_Helper::humanFileSize($quota);
|
||||
}
|
||||
|
||||
// Return Success story
|
||||
if($username) {
|
||||
$targetUserObject->setQuota($quota);
|
||||
}else{//set the default quota when no username is specified
|
||||
if($quota === 'default') {//'default' as default quota makes no sense
|
||||
$quota='none';
|
||||
}
|
||||
\OC::$server->getConfig()->setAppValue('files', 'default_quota', $quota);
|
||||
}
|
||||
OC_JSON::success(array("data" => array( "username" => $username , 'quota' => $quota)));
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Bart Visscher <bartv@thisnet.nl>
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author Lukas Reschke <lukas@statuscode.ch>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
OC_JSON::checkAdminUser();
|
||||
\OC_JSON::callCheck();
|
||||
|
||||
$lastConfirm = (int) \OC::$server->getSession()->get('last-password-confirm');
|
||||
if ($lastConfirm < (time() - 30 * 60 + 15)) { // allow 15 seconds delay
|
||||
$l = \OC::$server->getL10N('core');
|
||||
OC_JSON::error(array( 'data' => array( 'message' => $l->t('Password confirmation is required'))));
|
||||
exit();
|
||||
}
|
||||
|
||||
$username = (string)$_POST['username'];
|
||||
$group = (string)$_POST['group'];
|
||||
|
||||
$subAdminManager = \OC::$server->getGroupManager()->getSubAdmin();
|
||||
$targetUserObject = \OC::$server->getUserManager()->get($username);
|
||||
$targetGroupObject = \OC::$server->getGroupManager()->get($group);
|
||||
|
||||
$isSubAdminOfGroup = false;
|
||||
if($targetUserObject !== null && $targetGroupObject !== null) {
|
||||
$isSubAdminOfGroup = $subAdminManager->isSubAdminOfGroup($targetUserObject, $targetGroupObject);
|
||||
}
|
||||
|
||||
// Toggle group
|
||||
if($isSubAdminOfGroup) {
|
||||
$subAdminManager->deleteSubAdmin($targetUserObject, $targetGroupObject);
|
||||
} else {
|
||||
$subAdminManager->createSubAdmin($targetUserObject, $targetGroupObject);
|
||||
}
|
||||
|
||||
OC_JSON::success();
|
|
@ -675,101 +675,6 @@ span.usersLastLoginTooltip {
|
|||
}
|
||||
}
|
||||
|
||||
tr:hover > td {
|
||||
&.password > span, &.displayName > span, &.mailAddress > span {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
&.password > img, &.displayName > img, &.mailAddress > img {
|
||||
visibility: visible;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
td.userActions {
|
||||
.toggleUserActions {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
.action {
|
||||
display: block;
|
||||
padding: 14px;
|
||||
opacity: 0.5;
|
||||
.icon-more {
|
||||
display: inline-block;
|
||||
}
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.recoveryPassword {
|
||||
left: 50em;
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
input#recoveryPassword {
|
||||
width: 15em;
|
||||
}
|
||||
|
||||
#controls select.quota {
|
||||
margin: 3px;
|
||||
margin-right: 10px;
|
||||
height: 37px;
|
||||
}
|
||||
|
||||
#userlist td.quota {
|
||||
position: relative;
|
||||
width: 10em;
|
||||
progress.quota-user-progress {
|
||||
position: absolute;
|
||||
width: calc(10em + 0px);
|
||||
margin-top: -7px;
|
||||
z-index: 0;
|
||||
margin-left: 1px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
&.quota-user {
|
||||
width: 10em;
|
||||
height: 34px;
|
||||
z-index: 50;
|
||||
position: relative;
|
||||
}
|
||||
+ progress.quota-user-progress {
|
||||
position: absolute;
|
||||
width: calc(10em + 0px);
|
||||
margin-top: -7px;
|
||||
z-index: 0;
|
||||
margin-left: 1px;
|
||||
height: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
input.userFilter {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
#newusergroups + input[type='submit'] {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
#headerGroups, #headerSubAdmins, #headerQuota {
|
||||
padding-left: 18px;
|
||||
}
|
||||
|
||||
#headerAvatar {
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
/* used to highlight a user row in red */
|
||||
|
||||
#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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,213 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com>
|
||||
* This file is licensed under the Affero General Public License version 3 or later.
|
||||
* See the COPYING-README file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* takes care of deleting things represented by an ID
|
||||
*
|
||||
* @class
|
||||
* @param {string} endpoint the corresponding ajax PHP script. Currently limited
|
||||
* to settings - ajax path.
|
||||
* @param {string} paramID the by the script expected parameter name holding the
|
||||
* ID of the object to delete
|
||||
* @param {markCallback} markCallback function to be called after successfully
|
||||
* marking the object for deletion.
|
||||
* @param {removeCallback} removeCallback the function to be called after
|
||||
* successful delete.
|
||||
*/
|
||||
|
||||
/* globals escapeHTML */
|
||||
|
||||
function DeleteHandler(endpoint, paramID, markCallback, removeCallback) {
|
||||
this.oidToDelete = false;
|
||||
this.canceled = false;
|
||||
|
||||
this.ajaxEndpoint = endpoint;
|
||||
this.ajaxParamID = paramID;
|
||||
|
||||
this.markCallback = markCallback;
|
||||
this.removeCallback = removeCallback;
|
||||
this.undoCallback = false;
|
||||
|
||||
this.notifier = false;
|
||||
this.notificationDataID = false;
|
||||
this.notificationMessage = false;
|
||||
this.notificationPlaceholder = '%oid';
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of milliseconds after which the operation is performed.
|
||||
*/
|
||||
DeleteHandler.TIMEOUT_MS = 7000;
|
||||
|
||||
/**
|
||||
* Timer after which the action will be performed anyway.
|
||||
*/
|
||||
DeleteHandler.prototype._timeout = null;
|
||||
|
||||
/**
|
||||
* The function to be called after successfully marking the object for deletion
|
||||
* @callback markCallback
|
||||
* @param {string} oid the ID of the specific user or group
|
||||
*/
|
||||
|
||||
/**
|
||||
* The function to be called after successful delete. The id of the object will
|
||||
* be passed as argument. Unsuccessful operations will display an error using
|
||||
* OC.dialogs, no callback is fired.
|
||||
* @callback removeCallback
|
||||
* @param {string} oid the ID of the specific user or group
|
||||
*/
|
||||
|
||||
/**
|
||||
* This callback is fired after "undo" was clicked so the consumer can update
|
||||
* the web interface
|
||||
* @callback undoCallback
|
||||
* @param {string} oid the ID of the specific user or group
|
||||
*/
|
||||
|
||||
/**
|
||||
* enabled the notification system. Required for undo UI.
|
||||
*
|
||||
* @param {object} notifier Usually OC.Notification
|
||||
* @param {string} dataID an identifier for the notifier, e.g. 'deleteuser'
|
||||
* @param {string} message the message that should be shown upon delete. %oid
|
||||
* will be replaced with the affected id of the item to be deleted
|
||||
* @param {undoCallback} undoCallback called after "undo" was clicked
|
||||
*/
|
||||
DeleteHandler.prototype.setNotification = function(notifier, dataID, message, undoCallback) {
|
||||
this.notifier = notifier;
|
||||
this.notificationDataID = dataID;
|
||||
this.notificationMessage = message;
|
||||
this.undoCallback = undoCallback;
|
||||
|
||||
var dh = this;
|
||||
|
||||
$('#notification')
|
||||
.off('click.deleteHandler_' + dataID)
|
||||
.on('click.deleteHandler_' + dataID, '.undo', function () {
|
||||
if ($('#notification').data(dh.notificationDataID)) {
|
||||
var oid = dh.oidToDelete;
|
||||
dh.cancel();
|
||||
if(typeof dh.undoCallback !== 'undefined') {
|
||||
dh.undoCallback(oid);
|
||||
}
|
||||
}
|
||||
dh.notifier.hide();
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* shows the Undo Notification (if configured)
|
||||
*/
|
||||
DeleteHandler.prototype.showNotification = function() {
|
||||
if(this.notifier !== false) {
|
||||
if(!this.notifier.isHidden()) {
|
||||
this.hideNotification();
|
||||
}
|
||||
$('#notification').data(this.notificationDataID, true);
|
||||
var msg = this.notificationMessage.replace(
|
||||
this.notificationPlaceholder, escapeHTML(this.oidToDelete));
|
||||
this.notifier.showHtml(msg);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* hides the Undo Notification
|
||||
*/
|
||||
DeleteHandler.prototype.hideNotification = function() {
|
||||
if(this.notifier !== false) {
|
||||
$('#notification').removeData(this.notificationDataID);
|
||||
this.notifier.hide();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* initializes the delete operation for a given object id
|
||||
*
|
||||
* @param {string} oid the object id
|
||||
*/
|
||||
DeleteHandler.prototype.mark = function(oid) {
|
||||
if(this.oidToDelete !== false) {
|
||||
// passing true to avoid hiding the notification
|
||||
// twice and causing the second notification
|
||||
// to disappear immediately
|
||||
this.deleteEntry(true);
|
||||
}
|
||||
this.oidToDelete = oid;
|
||||
this.canceled = false;
|
||||
this.markCallback(oid);
|
||||
this.showNotification();
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
if (DeleteHandler.TIMEOUT_MS > 0) {
|
||||
this._timeout = window.setTimeout(
|
||||
_.bind(this.deleteEntry, this),
|
||||
DeleteHandler.TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* cancels a delete operation
|
||||
*/
|
||||
DeleteHandler.prototype.cancel = function() {
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
this.canceled = true;
|
||||
this.oidToDelete = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* executes a delete operation. Requires that the operation has been
|
||||
* initialized by mark(). On error, it will show a message via
|
||||
* OC.dialogs.alert. On success, a callback is fired so that the client can
|
||||
* update the web interface accordingly.
|
||||
*
|
||||
* @param {boolean} [keepNotification] true to keep the notification, false to hide
|
||||
* it, defaults to false
|
||||
*/
|
||||
DeleteHandler.prototype.deleteEntry = function(keepNotification) {
|
||||
var deferred = $.Deferred();
|
||||
if(this.canceled || this.oidToDelete === false) {
|
||||
return deferred.resolve().promise();
|
||||
}
|
||||
|
||||
var dh = this;
|
||||
if(!keepNotification && $('#notification').data(this.notificationDataID) === true) {
|
||||
dh.hideNotification();
|
||||
}
|
||||
|
||||
if (this._timeout) {
|
||||
clearTimeout(this._timeout);
|
||||
this._timeout = null;
|
||||
}
|
||||
|
||||
var payload = {};
|
||||
payload[dh.ajaxParamID] = dh.oidToDelete;
|
||||
return $.ajax({
|
||||
type: 'DELETE',
|
||||
url: OC.generateUrl(dh.ajaxEndpoint+'/{oid}',{oid: this.oidToDelete}),
|
||||
// FIXME: do not use synchronous ajax calls as they block the browser !
|
||||
async: false,
|
||||
success: function (result) {
|
||||
// Remove undo option, & remove user from table
|
||||
|
||||
//TODO: following line
|
||||
dh.removeCallback(dh.oidToDelete);
|
||||
dh.canceled = true;
|
||||
},
|
||||
error: function (jqXHR) {
|
||||
OC.dialogs.alert(jqXHR.responseJSON.data.message, t('settings', 'Unable to delete {objName}', {objName: dh.oidToDelete}));
|
||||
dh.undoCallback(dh.oidToDelete);
|
||||
|
||||
}
|
||||
});
|
||||
};
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com>
|
||||
* This file is licensed under the Affero General Public License version 3 or later.
|
||||
* See the COPYING-README file.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief this object takes care of the filter functionality on the user
|
||||
* management page
|
||||
* @param {UserList} userList the UserList object
|
||||
* @param {GroupList} groupList the GroupList object
|
||||
*/
|
||||
function UserManagementFilter (userList, groupList) {
|
||||
this.userList = userList;
|
||||
this.groupList = groupList;
|
||||
this.oldFilter = '';
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief sets up when the filter action shall be triggered
|
||||
*/
|
||||
UserManagementFilter.prototype.init = function () {
|
||||
OC.Plugins.register('OCA.Search', this);
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief the filter action needs to be done, here the accurate steps are being
|
||||
* taken care of
|
||||
*/
|
||||
UserManagementFilter.prototype.run = _.debounce(function (filter) {
|
||||
if (filter === this.oldFilter) {
|
||||
return;
|
||||
}
|
||||
this.oldFilter = filter;
|
||||
this.userList.filter = filter;
|
||||
this.userList.empty();
|
||||
this.userList.update(GroupList.getCurrentGID());
|
||||
if (this.groupList.filterGroups) {
|
||||
// user counts are being updated nevertheless
|
||||
this.groupList.empty();
|
||||
}
|
||||
this.groupList.update();
|
||||
},
|
||||
300
|
||||
);
|
||||
|
||||
/**
|
||||
* @brief returns the filter String
|
||||
* @returns string
|
||||
*/
|
||||
UserManagementFilter.prototype.getPattern = function () {
|
||||
var input = this.filterInput.val(),
|
||||
html = $('html'),
|
||||
isIE8or9 = html.hasClass('lte9');
|
||||
// FIXME - TODO - once support for IE8 and IE9 is dropped
|
||||
if (isIE8or9 && input == this.filterInput.attr('placeholder')) {
|
||||
input = '';
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief adds reset functionality to an HTML element
|
||||
* @param jQuery the jQuery representation of that element
|
||||
*/
|
||||
UserManagementFilter.prototype.addResetButton = function (button) {
|
||||
var umf = this;
|
||||
button.click(function () {
|
||||
umf.filterInput.val('');
|
||||
umf.run();
|
||||
});
|
||||
};
|
||||
|
||||
UserManagementFilter.prototype.attach = function (search) {
|
||||
search.setFilter('settings', this.run.bind(this));
|
||||
};
|
|
@ -1,385 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014, Raghu Nayyar <beingminimal@gmail.com>
|
||||
* Copyright (c) 2014, Arthur Schiwon <blizzz@owncloud.com>
|
||||
* This file is licensed under the Affero General Public License version 3 or later.
|
||||
* See the COPYING-README file.
|
||||
*/
|
||||
|
||||
/* globals escapeHTML, UserList, DeleteHandler */
|
||||
|
||||
var $userGroupList,
|
||||
$sortGroupBy;
|
||||
|
||||
var GroupList;
|
||||
GroupList = {
|
||||
activeGID: '',
|
||||
everyoneGID: '_everyone',
|
||||
filter: '',
|
||||
filterGroups: false,
|
||||
|
||||
addGroup: function (gid, displayName, usercount) {
|
||||
if (_.isUndefined(displayName)) {
|
||||
displayName = gid;
|
||||
}
|
||||
var $li = $userGroupList.find('.isgroup:last-child').clone();
|
||||
$li
|
||||
.data('gid', gid)
|
||||
.find('.groupname').text(displayName);
|
||||
GroupList.setUserCount($li, usercount);
|
||||
|
||||
$li.appendTo($userGroupList);
|
||||
|
||||
GroupList.sortGroups();
|
||||
|
||||
return $li;
|
||||
},
|
||||
|
||||
setUserCount: function (groupLiElement, usercount) {
|
||||
if ($sortGroupBy !== 1) {
|
||||
// If we don't sort by group count we don't display them either
|
||||
return;
|
||||
}
|
||||
|
||||
var $groupLiElement = $(groupLiElement);
|
||||
if (usercount === undefined || usercount === 0 || usercount < 0) {
|
||||
usercount = '';
|
||||
$groupLiElement.data('usercount', 0);
|
||||
} else {
|
||||
$groupLiElement.data('usercount', usercount);
|
||||
}
|
||||
$groupLiElement.find('.usercount').text(usercount);
|
||||
},
|
||||
|
||||
getUserCount: function ($groupLiElement) {
|
||||
var count = parseInt($groupLiElement.data('usercount'), 10);
|
||||
return isNaN(count) ? 0 : count;
|
||||
},
|
||||
|
||||
modGroupCount: function(gid, diff) {
|
||||
var $li = GroupList.getGroupLI(gid);
|
||||
var count = GroupList.getUserCount($li) + diff;
|
||||
GroupList.setUserCount($li, count);
|
||||
},
|
||||
|
||||
incEveryoneCount: function() {
|
||||
GroupList.modGroupCount(GroupList.everyoneGID, 1);
|
||||
},
|
||||
|
||||
decEveryoneCount: function() {
|
||||
GroupList.modGroupCount(GroupList.everyoneGID, -1);
|
||||
},
|
||||
|
||||
incGroupCount: function(gid) {
|
||||
GroupList.modGroupCount(gid, 1);
|
||||
},
|
||||
|
||||
decGroupCount: function(gid) {
|
||||
GroupList.modGroupCount(gid, -1);
|
||||
},
|
||||
|
||||
getCurrentGID: function () {
|
||||
return GroupList.activeGID;
|
||||
},
|
||||
|
||||
sortGroups: function () {
|
||||
var lis = $userGroupList.find('.isgroup').get();
|
||||
|
||||
lis.sort(function (a, b) {
|
||||
// "Everyone" always at the top
|
||||
if ($(a).data('gid') === '_everyone') {
|
||||
return -1;
|
||||
} else if ($(b).data('gid') === '_everyone') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// "admin" always as second
|
||||
if ($(a).data('gid') === 'admin') {
|
||||
return -1;
|
||||
} else if ($(b).data('gid') === 'admin') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($sortGroupBy === 1) {
|
||||
// Sort by user count first
|
||||
var $usersGroupA = $(a).data('usercount'),
|
||||
$usersGroupB = $(b).data('usercount');
|
||||
if ($usersGroupA > 0 && $usersGroupA > $usersGroupB) {
|
||||
return -1;
|
||||
}
|
||||
if ($usersGroupB > 0 && $usersGroupB > $usersGroupA) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback or sort by group name
|
||||
return UserList.alphanum(
|
||||
$(a).find('a span').text(),
|
||||
$(b).find('a span').text()
|
||||
);
|
||||
});
|
||||
|
||||
var items = [];
|
||||
$.each(lis, function (index, li) {
|
||||
items.push(li);
|
||||
if (items.length === 100) {
|
||||
$userGroupList.append(items);
|
||||
items = [];
|
||||
}
|
||||
});
|
||||
if (items.length > 0) {
|
||||
$userGroupList.append(items);
|
||||
}
|
||||
},
|
||||
|
||||
createGroup: function (groupid) {
|
||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(this.createGroup, this, groupid));
|
||||
return;
|
||||
}
|
||||
|
||||
$.post(
|
||||
OC.generateUrl('/settings/users/groups'),
|
||||
{
|
||||
id: groupid
|
||||
},
|
||||
function (result) {
|
||||
if (result.groupname) {
|
||||
var addedGroup = result.groupname;
|
||||
UserList.availableGroups[groupid] = {displayName: result.groupname};
|
||||
GroupList.addGroup(groupid, result.groupname);
|
||||
}
|
||||
GroupList.toggleAddGroup();
|
||||
}).fail(function(result) {
|
||||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', {message: result.responseJSON.message}));
|
||||
});
|
||||
},
|
||||
|
||||
update: function () {
|
||||
if (GroupList.updating) {
|
||||
return;
|
||||
}
|
||||
GroupList.updating = true;
|
||||
$.get(
|
||||
OC.generateUrl('/settings/users/groups'),
|
||||
{
|
||||
pattern: this.filter,
|
||||
filterGroups: this.filterGroups ? 1 : 0,
|
||||
sortGroups: $sortGroupBy
|
||||
},
|
||||
function (result) {
|
||||
|
||||
var lis = [];
|
||||
if (result.status === 'success') {
|
||||
$.each(result.data, function (i, subset) {
|
||||
$.each(subset, function (index, group) {
|
||||
if (GroupList.getGroupLI(group.name).length > 0) {
|
||||
GroupList.setUserCount(GroupList.getGroupLI(group.name).first(), group.usercount);
|
||||
}
|
||||
else {
|
||||
var $li = GroupList.addGroup(group.id, group.name, group.usercount);
|
||||
|
||||
$li.addClass('appear transparent');
|
||||
lis.push($li);
|
||||
}
|
||||
});
|
||||
});
|
||||
if (result.data.length > 0) {
|
||||
GroupList.doSort();
|
||||
}
|
||||
else {
|
||||
GroupList.noMoreEntries = true;
|
||||
}
|
||||
_.defer(function () {
|
||||
$(lis).each(function () {
|
||||
this.removeClass('transparent');
|
||||
});
|
||||
});
|
||||
}
|
||||
GroupList.updating = false;
|
||||
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
elementBelongsToAddGroup: function (el) {
|
||||
return !(el !== $('#newgroup-form').get(0) &&
|
||||
$('#newgroup-form').find($(el)).length === 0);
|
||||
},
|
||||
|
||||
hasAddGroupNameText: function () {
|
||||
var name = $('#newgroupname').val();
|
||||
return $.trim(name) !== '';
|
||||
|
||||
},
|
||||
|
||||
showDisabledUsers: function () {
|
||||
UserList.empty();
|
||||
UserList.update('_disabledUsers');
|
||||
$userGroupList.find('li').removeClass('active');
|
||||
GroupList.getGroupLI('_disabledUsers').addClass('active');
|
||||
},
|
||||
|
||||
showGroup: function (gid) {
|
||||
GroupList.activeGID = gid;
|
||||
UserList.empty();
|
||||
UserList.update(gid === '_everyone' ? '' : gid);
|
||||
$userGroupList.find('li').removeClass('active');
|
||||
if (gid !== undefined) {
|
||||
GroupList.getGroupLI(gid).addClass('active');
|
||||
}
|
||||
},
|
||||
|
||||
isAddGroupButtonVisible: function () {
|
||||
return !$('#newgroup-entry').hasClass('editing');
|
||||
},
|
||||
|
||||
toggleAddGroup: function (event) {
|
||||
if (GroupList.isAddGroupButtonVisible()) {
|
||||
if (event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
$('#newgroup-entry').addClass('editing');
|
||||
$('#newgroupname').select();
|
||||
GroupList.handleAddGroupInput('');
|
||||
}
|
||||
else {
|
||||
$('#newgroup-entry').removeClass('editing');
|
||||
$('#newgroupname').val('');
|
||||
}
|
||||
},
|
||||
|
||||
handleAddGroupInput: function (input) {
|
||||
if(input.length) {
|
||||
$('#newgroup-form input[type="submit"]').attr('disabled', null);
|
||||
} else {
|
||||
$('#newgroup-form input[type="submit"]').attr('disabled', 'disabled');
|
||||
}
|
||||
},
|
||||
|
||||
isGroupNameValid: function (groupname) {
|
||||
if ($.trim(groupname) === '') {
|
||||
OC.Notification.showTemporary(t('settings', 'Error creating group: {message}', {
|
||||
message: t('settings', 'A valid group name must be provided')
|
||||
}));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
hide: function (gid) {
|
||||
GroupList.getGroupLI(gid).hide();
|
||||
},
|
||||
show: function (gid) {
|
||||
GroupList.getGroupLI(gid).show();
|
||||
},
|
||||
remove: function (gid) {
|
||||
GroupList.getGroupLI(gid).remove();
|
||||
},
|
||||
empty: function () {
|
||||
$userGroupList.find('.isgroup').filter(function(index, item){
|
||||
return $(item).data('gid') !== '';
|
||||
}).remove();
|
||||
},
|
||||
initDeleteHandling: function () {
|
||||
//set up handler
|
||||
var GroupDeleteHandler = new DeleteHandler('/settings/users/groups', 'groupname',
|
||||
GroupList.hide, GroupList.remove);
|
||||
|
||||
//configure undo
|
||||
OC.Notification.hide();
|
||||
var msg = escapeHTML(t('settings', 'deleted {groupName}', {groupName: '%oid'})) + '<span class="undo">' +
|
||||
escapeHTML(t('settings', 'undo')) + '</span>';
|
||||
GroupDeleteHandler.setNotification(OC.Notification, 'deletegroup', msg,
|
||||
GroupList.show);
|
||||
|
||||
//when to mark user for delete
|
||||
var deleteAction = function () {
|
||||
if (OC.PasswordConfirmation.requiresPasswordConfirmation()) {
|
||||
OC.PasswordConfirmation.requirePasswordConfirmation(_.bind(deleteAction, this));
|
||||
return;
|
||||
}
|
||||
|
||||
// Call function for handling delete/undo
|
||||
GroupDeleteHandler.mark(GroupList.getElementGID($(this).parent()));
|
||||
};
|
||||
$userGroupList.on('click', '.delete', deleteAction);
|
||||
|
||||
//delete a marked user when leaving the page
|
||||
$(window).on('beforeunload', function () {
|
||||
GroupDeleteHandler.deleteEntry();
|
||||
});
|
||||
},
|
||||
|
||||
getGroupLI: function (gid) {
|
||||
return $userGroupList.find('li.isgroup').filter(function () {
|
||||
return GroupList.getElementGID(this) === gid;
|
||||
});
|
||||
},
|
||||
|
||||
getElementGID: function (element) {
|
||||
return ($(element).closest('li').data('gid') || '').toString();
|
||||
},
|
||||
getEveryoneCount: function () {
|
||||
$.ajax({
|
||||
type: "GET",
|
||||
dataType: "json",
|
||||
url: OC.generateUrl('/settings/users/stats')
|
||||
}).success(function (data) {
|
||||
$('#everyonegroup').data('usercount', data.totalUsers);
|
||||
$('#everyonecount').text(data.totalUsers);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$(document).ready( function () {
|
||||
$userGroupList = $('#usergrouplist');
|
||||
GroupList.initDeleteHandling();
|
||||
$sortGroupBy = $userGroupList.data('sort-groups');
|
||||
if ($sortGroupBy === 1) {
|
||||
// Disabled due to performance issues, when we don't need it for sorting
|
||||
GroupList.getEveryoneCount();
|
||||
}
|
||||
|
||||
// Display or hide of Create Group List Element
|
||||
$('#newgroup-init').on('click', function (e) {
|
||||
GroupList.toggleAddGroup(e);
|
||||
});
|
||||
|
||||
$(document).on('click keydown keyup', function(event) {
|
||||
if(!GroupList.isAddGroupButtonVisible() &&
|
||||
!GroupList.elementBelongsToAddGroup(event.target) &&
|
||||
!GroupList.hasAddGroupNameText()) {
|
||||
GroupList.toggleAddGroup();
|
||||
}
|
||||
// Escape
|
||||
if(!GroupList.isAddGroupButtonVisible() && event.keyCode && event.keyCode === 27) {
|
||||
GroupList.toggleAddGroup();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Responsible for Creating Groups.
|
||||
$('#newgroup-form form').submit(function (event) {
|
||||
event.preventDefault();
|
||||
if(GroupList.isGroupNameValid($('#newgroupname').val())) {
|
||||
GroupList.createGroup($('#newgroupname').val());
|
||||
}
|
||||
});
|
||||
|
||||
// click on group name
|
||||
$userGroupList.on('click', '.isgroup', function () {
|
||||
GroupList.showGroup(GroupList.getElementGID(this));
|
||||
});
|
||||
|
||||
// show disabled users
|
||||
$userGroupList.on('click', '.disabledusers', function () {
|
||||
GroupList.showDisabledUsers();
|
||||
});
|
||||
|
||||
$('#newgroupname').on('input', function(){
|
||||
GroupList.handleAddGroupInput(this.value);
|
||||
});
|
||||
|
||||
// highlight `everyone` group at DOMReady by default
|
||||
GroupList.showGroup('_everyone');
|
||||
});
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,45 @@
|
|||
{
|
||||
"name": "settings",
|
||||
"description": "Nextcloud settings",
|
||||
"version": "1.0.0",
|
||||
"author": "John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>",
|
||||
"license": "AGPL3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "webpack --config webpack.dev.js",
|
||||
"watch": "webpack --progress --watch --config webpack.dev.js",
|
||||
"build": "webpack --progress --hide-modules --config webpack.prod.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.18.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"v-tooltip": "^2.0.0-rc.32",
|
||||
"vue": "^2.5.16",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-infinite-loading": "^2.3.1",
|
||||
"vue-localstorage": "^0.6.2",
|
||||
"vue-multiselect": "^2.1.0",
|
||||
"vue-router": "^3.0.1",
|
||||
"vuex": "^3.0.1",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie >= 11"
|
||||
],
|
||||
"devDependencies": {
|
||||
"babel-core": "^6.26.3",
|
||||
"babel-loader": "^7.1.4",
|
||||
"babel-preset-env": "^1.7.0",
|
||||
"babel-preset-stage-3": "^6.24.1",
|
||||
"css-loader": "^0.28.11",
|
||||
"file-loader": "^1.1.11",
|
||||
"node-sass": "^4.9.0",
|
||||
"sass-loader": "^7.0.1",
|
||||
"vue-loader": "^14.2.2",
|
||||
"vue-template-compiler": "^2.5.16",
|
||||
"webpack": "^4.8.3",
|
||||
"webpack-cli": "^2.1.3",
|
||||
"webpack-merge": "^4.1.2"
|
||||
}
|
||||
}
|
|
@ -39,7 +39,6 @@ namespace OC\Settings;
|
|||
$application = new Application();
|
||||
$application->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);
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -8,8 +8,10 @@ default:
|
|||
- NextcloudTestServerContext
|
||||
|
||||
- AppNavigationContext
|
||||
- AppSettingsContext
|
||||
- CommentsAppContext
|
||||
- ContactsMenuContext
|
||||
- DialogContext
|
||||
- FeatureContext
|
||||
- FileListContext
|
||||
- FilesAppContext
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
*
|
||||
* @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
|
||||
*
|
||||
|
@ -39,7 +40,7 @@ class AppNavigationContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function appNavigationSectionItemFor($sectionText) {
|
||||
return Locator::forThe()->xpath("//li[normalize-space() = '$sectionText']")->
|
||||
return Locator::forThe()->xpath("//li/a[normalize-space() = '$sectionText']/..")->
|
||||
descendantOf(self::appNavigation())->
|
||||
describedAs($sectionText . " section item in App Navigation");
|
||||
}
|
||||
|
@ -48,10 +49,29 @@ class AppNavigationContext implements Context, ActorAwareInterface {
|
|||
* @return Locator
|
||||
*/
|
||||
public static function appNavigationCurrentSectionItem() {
|
||||
return Locator::forThe()->css(".active")->descendantOf(self::appNavigation())->
|
||||
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");
|
||||
}
|
||||
|
||||
/**
|
||||
* @Given I open the :section section
|
||||
*/
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@
|
|||
/**
|
||||
*
|
||||
* @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 passwordCellForUser($user) {
|
||||
return Locator::forThe()->css(".password")->descendantOf(self::rowForUser($user))->
|
||||
describedAs("Password cell for user $user in Users Settings");
|
||||
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 passwordInputForUser($user) {
|
||||
return Locator::forThe()->css("input")->descendantOf(self::passwordCellForUser($user))->
|
||||
describedAs("Password input for user $user in Users Settings");
|
||||
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 theColumn($column) {
|
||||
return Locator::forThe()->xpath("//div[@class='user-list-grid']//div[normalize-space() = '$column']")->
|
||||
describedAs("The $column column in Users Settings");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Locator
|
||||
*/
|
||||
public static function selectedSelectOption($cell, $user) {
|
||||
return Locator::forThe()->css(".multiselect__single")->
|
||||
descendantOf(self::classCellForUser($cell, $user))->
|
||||
describedAs("The selected option of the $cell select for the user $user in Users Settings");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -98,6 +147,20 @@ class UsersSettingsContext implements Context, ActorAwareInterface {
|
|||
$this->actor->find(self::newUserButton())->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* @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"
|
Loading…
Reference in New Issue