You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

userList.vue 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. <!--
  2. - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  3. -
  4. - @author John Molakvoæ <skjnldsv@protonmail.com>
  5. -
  6. - @license GNU AGPL version 3 or any later version
  7. -
  8. - This program is free software: you can redistribute it and/or modify
  9. - it under the terms of the GNU Affero General Public License as
  10. - published by the Free Software Foundation, either version 3 of the
  11. - License, or (at your option) any later version.
  12. -
  13. - This program is distributed in the hope that it will be useful,
  14. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. - GNU Affero General Public License for more details.
  17. -
  18. - You should have received a copy of the GNU Affero General Public License
  19. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  20. -
  21. -->
  22. <template>
  23. <div id="app-content" class="user-list-grid" v-on:scroll.passive="onScroll">
  24. <div class="row" id="grid-header" :class="{'sticky': scrolled && !showConfig.showNewUserForm}">
  25. <div id="headerAvatar" class="avatar"></div>
  26. <div id="headerName" class="name">{{ t('settings', 'Username') }}</div>
  27. <div id="headerDisplayName" class="displayName">{{ t('settings', 'Full name') }}</div>
  28. <div id="headerPassword" class="password">{{ t('settings', 'Password') }}</div>
  29. <div id="headerAddress" class="mailAddress">{{ t('settings', 'Email') }}</div>
  30. <div id="headerGroups" class="groups">{{ t('settings', 'Groups') }}</div>
  31. <div id="headerSubAdmins" class="subadmins"
  32. v-if="subAdminsGroups.length>0 && settings.isAdmin">{{ t('settings', 'Group admin for') }}</div>
  33. <div id="headerQuota" class="quota">{{ t('settings', 'Quota') }}</div>
  34. <div id="headerLanguages" class="languages"
  35. v-if="showConfig.showLanguages">{{ t('settings', 'Languages') }}</div>
  36. <div class="headerStorageLocation storageLocation"
  37. v-if="showConfig.showStoragePath">{{ t('settings', 'Storage location') }}</div>
  38. <div class="headerUserBackend userBackend"
  39. v-if="showConfig.showUserBackend">{{ t('settings', 'User backend') }}</div>
  40. <div class="headerLastLogin lastLogin"
  41. v-if="showConfig.showLastLogin">{{ t('settings', 'Last login') }}</div>
  42. <div class="userActions"></div>
  43. </div>
  44. <form class="row" id="new-user" v-show="showConfig.showNewUserForm"
  45. v-on:submit.prevent="createUser" :disabled="loading"
  46. :class="{'sticky': scrolled && showConfig.showNewUserForm}">
  47. <div :class="loading?'icon-loading-small':'icon-add'"></div>
  48. <div class="name">
  49. <input id="newusername" type="text" required v-model="newUser.id"
  50. :placeholder="t('settings', 'User name')" name="username"
  51. autocomplete="off" autocapitalize="none" autocorrect="off"
  52. pattern="[a-zA-Z0-9 _\.@\-']+">
  53. </div>
  54. <div class="displayName">
  55. <input id="newdisplayname" type="text" v-model="newUser.displayName"
  56. :placeholder="t('settings', 'Display name')" name="displayname"
  57. autocomplete="off" autocapitalize="none" autocorrect="off">
  58. </div>
  59. <div class="password">
  60. <input id="newuserpassword" type="password" v-model="newUser.password"
  61. :required="newUser.mailAddress===''"
  62. :placeholder="t('settings', 'Password')" name="password"
  63. autocomplete="new-password" autocapitalize="none" autocorrect="off"
  64. :minlength="minPasswordLength">
  65. </div>
  66. <div class="mailAddress">
  67. <input id="newemail" type="email" v-model="newUser.mailAddress"
  68. :required="newUser.password===''"
  69. :placeholder="t('settings', 'Mail address')" name="email"
  70. autocomplete="off" autocapitalize="none" autocorrect="off">
  71. </div>
  72. <div class="groups">
  73. <!-- hidden input trick for vanilla html5 form validation -->
  74. <input type="text" :value="newUser.groups" v-if="!settings.isAdmin"
  75. tabindex="-1" id="newgroups" :required="!settings.isAdmin" />
  76. <multiselect :options="groups" v-model="newUser.groups"
  77. :placeholder="t('settings', 'Add user in group')"
  78. label="name" track-by="id" class="multiselect-vue"
  79. :multiple="true" :close-on-select="false"
  80. :allowEmpty="settings.isAdmin">
  81. <!-- If user is not admin, he is a subadmin.
  82. Subadmins can't create users outside their groups
  83. Therefore, empty select is forbidden -->
  84. <span slot="noResult">{{t('settings', 'No results')}}</span>
  85. </multiselect>
  86. </div>
  87. <div class="subadmins" v-if="subAdminsGroups.length>0 && settings.isAdmin">
  88. <multiselect :options="subAdminsGroups" v-model="newUser.subAdminsGroups"
  89. :placeholder="t('settings', 'Set user as admin for')"
  90. label="name" track-by="id" class="multiselect-vue"
  91. :multiple="true" :close-on-select="false">
  92. <span slot="noResult">{{t('settings', 'No results')}}</span>
  93. </multiselect>
  94. </div>
  95. <div class="quota">
  96. <multiselect :options="quotaOptions" v-model="newUser.quota"
  97. :placeholder="t('settings', 'Select user quota')"
  98. label="label" track-by="id" class="multiselect-vue"
  99. :allowEmpty="false" :taggable="true"
  100. @tag="validateQuota" >
  101. </multiselect>
  102. </div>
  103. <div class="languages" v-if="showConfig.showLanguages">
  104. <multiselect :options="languages" v-model="newUser.language"
  105. :placeholder="t('settings', 'Default language')"
  106. label="name" track-by="code" class="multiselect-vue"
  107. :allowEmpty="false" group-values="languages" group-label="label">
  108. </multiselect>
  109. </div>
  110. <div class="storageLocation" v-if="showConfig.showStoragePath"></div>
  111. <div class="userBackend" v-if="showConfig.showUserBackend"></div>
  112. <div class="lastLogin" v-if="showConfig.showLastLogin"></div>
  113. <div class="userActions">
  114. <input type="submit" id="newsubmit" class="button primary icon-checkmark-white has-tooltip"
  115. value="" :title="t('settings', 'Add a new user')">
  116. <input type="reset" id="newreset" class="button icon-close has-tooltip" @click="resetForm"
  117. value="" :title="t('settings', 'Cancel and reset the form')">
  118. </div>
  119. </form>
  120. <user-row v-for="(user, key) in filteredUsers" :user="user" :key="key" :settings="settings" :showConfig="showConfig"
  121. :groups="groups" :subAdminsGroups="subAdminsGroups" :quotaOptions="quotaOptions" :languages="languages" />
  122. <infinite-loading @infinite="infiniteHandler" ref="infiniteLoading">
  123. <div slot="spinner"><div class="users-icon-loading icon-loading"></div></div>
  124. <div slot="no-more"><div class="users-list-end">— {{t('settings', 'no more results')}} —</div></div>
  125. <div slot="no-results">
  126. <div id="emptycontent">
  127. <div class="icon-contacts-dark"></div>
  128. <h2>{{t('settings', 'No users in here')}}</h2>
  129. </div>
  130. </div>
  131. </infinite-loading>
  132. </div>
  133. </template>
  134. <script>
  135. import userRow from './userList/userRow';
  136. import Multiselect from 'vue-multiselect';
  137. import InfiniteLoading from 'vue-infinite-loading';
  138. import Vue from 'vue';
  139. export default {
  140. name: 'userList',
  141. props: ['users', 'showConfig', 'selectedGroup'],
  142. components: {
  143. userRow,
  144. Multiselect,
  145. InfiniteLoading
  146. },
  147. data() {
  148. let unlimitedQuota = {id:'none', label:t('settings', 'Unlimited')},
  149. defaultQuota = {id:'default', label:t('settings', 'Default quota')};
  150. return {
  151. unlimitedQuota: unlimitedQuota,
  152. defaultQuota: defaultQuota,
  153. loading: false,
  154. scrolled: false,
  155. newUser: {
  156. id:'',
  157. displayName:'',
  158. password:'',
  159. mailAddress:'',
  160. groups: [],
  161. subAdminsGroups: [],
  162. quota: defaultQuota,
  163. language: {code: 'en', name: t('settings', 'Default language')}
  164. }
  165. };
  166. },
  167. mounted() {
  168. if (!this.settings.canChangePassword) {
  169. OC.Notification.showTemporary(t('settings', 'Password change is disabled because the master key is disabled'));
  170. }
  171. /**
  172. * Init default language from server data. The use of this.settings
  173. * requires a computed variable, which break the v-model binding of the form,
  174. * this is a much easier solution than getter and setter on a computed var
  175. */
  176. Vue.set(this.newUser.language, 'code', this.settings.defaultLanguage);
  177. /**
  178. * In case the user directly loaded the user list within a group
  179. * the watch won't be triggered. We need to initialize it.
  180. */
  181. this.setNewUserDefaultGroup(this.$route.params.selectedGroup);
  182. },
  183. computed: {
  184. settings() {
  185. return this.$store.getters.getServerData;
  186. },
  187. filteredUsers() {
  188. if (this.selectedGroup === 'disabled') {
  189. let disabledUsers = this.users.filter(user => user.enabled === false);
  190. if (disabledUsers.length===0 && this.$refs.infiniteLoading && this.$refs.infiniteLoading.isComplete) {
  191. // disabled group is empty, redirection to all users
  192. this.$router.push({name: 'users'});
  193. this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
  194. }
  195. return disabledUsers;
  196. }
  197. if (!this.settings.isAdmin) {
  198. // We don't want subadmins to edit themselves
  199. return this.users.filter(user => user.enabled !== false && user.id !== oc_current_user);
  200. }
  201. return this.users.filter(user => user.enabled !== false);
  202. },
  203. groups() {
  204. // data provided php side + remove the disabled group
  205. return this.$store.getters.getGroups
  206. .filter(group => group.id !== 'disabled')
  207. .sort((a, b) => a.name.localeCompare(b.name));
  208. },
  209. subAdminsGroups() {
  210. // data provided php side
  211. return this.$store.getters.getSubadminGroups;
  212. },
  213. quotaOptions() {
  214. // convert the preset array into objects
  215. let quotaPreset = this.settings.quotaPreset.reduce((acc, cur) => acc.concat({id:cur, label:cur}), []);
  216. // add default presets
  217. quotaPreset.unshift(this.unlimitedQuota);
  218. quotaPreset.unshift(this.defaultQuota);
  219. return quotaPreset;
  220. },
  221. minPasswordLength() {
  222. return this.$store.getters.getPasswordPolicyMinLength;
  223. },
  224. usersOffset() {
  225. return this.$store.getters.getUsersOffset;
  226. },
  227. usersLimit() {
  228. return this.$store.getters.getUsersLimit;
  229. },
  230. /* LANGUAGES */
  231. languages() {
  232. return Array(
  233. {
  234. label: t('settings', 'Common languages'),
  235. languages: this.settings.languages.commonlanguages
  236. },
  237. {
  238. label: t('settings', 'All languages'),
  239. languages: this.settings.languages.languages
  240. }
  241. );
  242. }
  243. },
  244. watch: {
  245. // watch url change and group select
  246. selectedGroup: function (val, old) {
  247. this.$store.commit('resetUsers');
  248. this.$refs.infiniteLoading.$emit('$InfiniteLoading:reset');
  249. this.setNewUserDefaultGroup(val);
  250. }
  251. },
  252. methods: {
  253. onScroll(event) {
  254. this.scrolled = event.target.scrollTop>0;
  255. },
  256. /**
  257. * Validate quota string to make sure it's a valid human file size
  258. *
  259. * @param {string} quota Quota in readable format '5 GB'
  260. * @returns {Object}
  261. */
  262. validateQuota(quota) {
  263. // only used for new presets sent through @Tag
  264. let validQuota = OC.Util.computerFileSize(quota);
  265. if (validQuota !== null && validQuota >= 0) {
  266. // unify format output
  267. quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota));
  268. return this.newUser.quota = {id: quota, label: quota};
  269. }
  270. // Default is unlimited
  271. return this.newUser.quota = this.quotaOptions[0];
  272. },
  273. infiniteHandler($state) {
  274. this.$store.dispatch('getUsers', {
  275. offset: this.usersOffset,
  276. limit: this.usersLimit,
  277. group: this.selectedGroup !== 'disabled' ? this.selectedGroup : ''
  278. })
  279. .then((response) => { response ? $state.loaded() : $state.complete() });
  280. },
  281. resetForm() {
  282. // revert form to original state
  283. Object.assign(this.newUser, this.$options.data.call(this).newUser);
  284. this.loading = false;
  285. },
  286. createUser() {
  287. this.loading = true;
  288. this.$store.dispatch('addUser', {
  289. userid: this.newUser.id,
  290. password: this.newUser.password,
  291. email: this.newUser.mailAddress,
  292. groups: this.newUser.groups.map(group => group.id),
  293. subadmin: this.newUser.subAdminsGroups.map(group => group.id),
  294. quota: this.newUser.quota.id,
  295. language: this.newUser.language.code,
  296. }).then(() => this.resetForm())
  297. .catch(() => this.loading = false);
  298. },
  299. setNewUserDefaultGroup(value) {
  300. if (value && value.length > 0) {
  301. // setting new user default group to the current selected one
  302. let currentGroup = this.groups.find(group => group.id === value);
  303. if (currentGroup) {
  304. this.newUser.groups = [currentGroup];
  305. return;
  306. }
  307. }
  308. // fallback, empty selected group
  309. this.newUser.groups = [];
  310. }
  311. }
  312. }
  313. </script>