UserRow.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. <!--
  2. - @copyright Copyright (c) 2018 John Molakvoæ <skjnldsv@protonmail.com>
  3. - @copyright Copyright (c) 2019 Gary Kim <gary@garykim.dev>
  4. -
  5. - @author John Molakvoæ <skjnldsv@protonmail.com>
  6. - @author Gary Kim <gary@garykim.dev>
  7. -
  8. - @license GNU AGPL version 3 or any later version
  9. -
  10. - This program is free software: you can redistribute it and/or modify
  11. - it under the terms of the GNU Affero General Public License as
  12. - published by the Free Software Foundation, either version 3 of the
  13. - License, or (at your option) any later version.
  14. -
  15. - This program is distributed in the hope that it will be useful,
  16. - but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. - GNU Affero General Public License for more details.
  19. -
  20. - You should have received a copy of the GNU Affero General Public License
  21. - along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. -
  23. -->
  24. <template>
  25. <!-- Obfuscated user: Logged in user does not have permissions to see all of the data -->
  26. <div v-if="Object.keys(user).length ===1" :data-id="user.id" class="row">
  27. <div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
  28. class="avatar">
  29. <img v-if="!loading.delete && !loading.disable && !loading.wipe"
  30. :src="generateAvatar(user.id, isDarkTheme)"
  31. alt=""
  32. height="32"
  33. width="32">
  34. </div>
  35. <div class="name">
  36. {{ user.id }}
  37. </div>
  38. <div class="obfuscated">
  39. {{ t('settings','You do not have permissions to see the details of this user') }}
  40. </div>
  41. </div>
  42. <!-- User full data -->
  43. <UserRowSimple v-else-if="!editing"
  44. :editing.sync="editing"
  45. :feedback-message="feedbackMessage"
  46. :groups="groups"
  47. :languages="languages"
  48. :loading="loading"
  49. :opened-menu.sync="openedMenu"
  50. :settings="settings"
  51. :show-config="showConfig"
  52. :sub-admins-groups="subAdminsGroups"
  53. :user-actions="userActions"
  54. :user="user"
  55. :is-dark-theme="isDarkTheme"
  56. :class="{'row--menu-opened': openedMenu}" />
  57. <div v-else
  58. :class="{
  59. 'disabled': loading.delete || loading.disable,
  60. 'row--menu-opened': openedMenu
  61. }"
  62. :data-id="user.id"
  63. class="row row--editable">
  64. <div :class="{'icon-loading-small': loading.delete || loading.disable || loading.wipe}"
  65. class="avatar">
  66. <img v-if="!loading.delete && !loading.disable && !loading.wipe"
  67. :src="generateAvatar(user.id, isDarkTheme)"
  68. alt=""
  69. height="32"
  70. width="32">
  71. </div>
  72. <!-- dirty hack to ellipsis on two lines -->
  73. <div v-if="user.backendCapabilities.setDisplayName" class="displayName">
  74. <label class="hidden-visually" :for="'displayName'+user.id+rand">{{ t('settings', 'Edit display name') }}</label>
  75. <NcTextField :id="'displayName'+user.id+rand"
  76. :show-trailing-button="true"
  77. class="user-row-text-field"
  78. :class="{'icon-loading-small': loading.displayName}"
  79. :disabled="loading.displayName||loading.all"
  80. trailing-button-icon="arrowRight"
  81. :value.sync="editedDisplayName"
  82. autocapitalize="off"
  83. autocomplete="off"
  84. autocorrect="off"
  85. spellcheck="false"
  86. type="text"
  87. @trailing-button-click="updateDisplayName" />
  88. </div>
  89. <div v-else class="name">
  90. {{ user.id }}
  91. <div class="displayName subtitle">
  92. <div :title="user.displayname.length > 20 ? user.displayname : ''" class="cellText">
  93. {{ user.displayname }}
  94. </div>
  95. </div>
  96. </div>
  97. <div v-if="settings.canChangePassword && user.backendCapabilities.setPassword" class="password">
  98. <label class="hidden-visually" :for="'password'+user.id+rand">{{ t('settings', 'Add new password') }}</label>
  99. <NcTextField :id="'password'+user.id+rand"
  100. :show-trailing-button="true"
  101. class="user-row-text-field"
  102. :class="{'icon-loading-small': loading.password}"
  103. :disabled="loading.password || loading.all"
  104. :minlength="minPasswordLength"
  105. maxlength="469"
  106. :placeholder="t('settings', 'Add new password')"
  107. trailing-button-icon="arrowRight"
  108. :value.sync="editedPassword"
  109. autocapitalize="off"
  110. autocomplete="new-password"
  111. autocorrect="off"
  112. required
  113. spellcheck="false"
  114. type="password"
  115. @trailing-button-click="updatePassword" />
  116. </div>
  117. <div v-else />
  118. <div class="mailAddress">
  119. <label class="hidden-visually" :for="'mailAddress'+user.id+rand">{{ t('settings', 'Add new email address') }}</label>
  120. <NcTextField :id="'mailAddress'+user.id+rand"
  121. :show-trailing-button="true"
  122. class="user-row-text-field"
  123. :class="{'icon-loading-small': loading.mailAddress}"
  124. :disabled="loading.mailAddress||loading.all"
  125. :placeholder="t('settings', 'Add new email address')"
  126. trailing-button-icon="arrowRight"
  127. :value.sync="editedMail"
  128. autocapitalize="off"
  129. autocomplete="new-password"
  130. autocorrect="off"
  131. spellcheck="false"
  132. type="email"
  133. @trailing-button-click="updateEmail" />
  134. </div>
  135. <div :class="{'icon-loading-small': loading.groups}" class="groups">
  136. <label class="hidden-visually" :for="'groups'+user.id+rand">{{ t('settings', 'Add user to group') }}</label>
  137. <NcSelect :input-id="'groups'+user.id+rand"
  138. :close-on-select="false"
  139. :disabled="loading.groups||loading.all"
  140. :multiple="true"
  141. :options="availableGroups"
  142. :placeholder="t('settings', 'Add user to group')"
  143. :taggable="settings.isAdmin"
  144. :value="userGroups"
  145. class="select-vue"
  146. label="name"
  147. :no-wrap="true"
  148. :selectable="() => userGroups.length < 2"
  149. :create-option="(value) => ({ name: value, isCreating: true })"
  150. @option:created="createGroup"
  151. @option:selected="options => addUserGroup(options.at(-1))"
  152. @option:deselected="removeUserGroup" />
  153. </div>
  154. <div v-if="subAdminsGroups.length>0 && settings.isAdmin"
  155. :class="{'icon-loading-small': loading.subadmins}"
  156. class="subadmins">
  157. <label class="hidden-visually" :for="'subadmins'+user.id+rand">{{ t('settings', 'Set user as admin for') }}</label>
  158. <NcSelect :id="'subadmins'+user.id+rand"
  159. :close-on-select="false"
  160. :disabled="loading.subadmins||loading.all"
  161. label="name"
  162. :multiple="true"
  163. :no-wrap="true"
  164. :selectable="() => userSubAdminsGroups.length < 2"
  165. :options="subAdminsGroups"
  166. :placeholder="t('settings', 'Set user as admin for')"
  167. :value="userSubAdminsGroups"
  168. class="select-vue"
  169. @option:deselected="removeUserSubAdmin"
  170. @option:selected="options => addUserSubAdmin(options.at(-1))" />
  171. </div>
  172. <div :title="usedSpace"
  173. :class="{'icon-loading-small': loading.quota}"
  174. class="quota">
  175. <label class="hidden-visually" :for="'quota'+user.id+rand">{{ t('settings', 'Select user quota') }}</label>
  176. <NcSelect v-model="userQuota"
  177. :close-on-select="true"
  178. :create-option="validateQuota"
  179. :disabled="loading.quota||loading.all"
  180. :input-id="'quota'+user.id+rand"
  181. class="select-vue"
  182. :options="quotaOptions"
  183. :placeholder="t('settings', 'Select user quota')"
  184. :taggable="true"
  185. @option:selected="setUserQuota" />
  186. </div>
  187. <div v-if="showConfig.showLanguages"
  188. :class="{'icon-loading-small': loading.languages}"
  189. class="languages">
  190. <label class="hidden-visually" :for="'language'+user.id+rand">{{ t('settings', 'Set the language') }}</label>
  191. <NcSelect :id="'language'+user.id+rand"
  192. :allow-empty="false"
  193. :disabled="loading.languages||loading.all"
  194. :options="availableLanguages"
  195. :placeholder="t('settings', 'No language set')"
  196. :value="userLanguage"
  197. label="name"
  198. class="select-vue"
  199. @input="setUserLanguage" />
  200. </div>
  201. <div v-if="showConfig.showStoragePath || showConfig.showUserBackend"
  202. class="storageLocation" />
  203. <div v-if="showConfig.showLastLogin" />
  204. <div :class="{'icon-loading-small': loading.manager}" class="managers">
  205. <label class="hidden-visually" :for="'manager'+user.id+rand">{{ t('settings', 'Set the language') }}</label>
  206. <NcSelect v-model="currentManager"
  207. :input-id="'manager'+user.id+rand"
  208. :close-on-select="true"
  209. label="displayname"
  210. :options="possibleManagers"
  211. :placeholder="t('settings', 'Select manager')"
  212. class="select-vue"
  213. @search="searchUserManager"
  214. @option:selected="updateUserManager"
  215. @input="updateUserManager" />
  216. </div>
  217. <div class="userActions">
  218. <div v-if="!loading.all"
  219. class="toggleUserActions">
  220. <NcActions>
  221. <NcActionButton icon="icon-checkmark"
  222. :title="t('settings', 'Done')"
  223. :aria-label="t('settings', 'Done')"
  224. @click="handleDoneButton" />
  225. </NcActions>
  226. <div v-click-outside="hideMenu" class="userPopoverMenuWrapper">
  227. <button class="icon-more"
  228. :aria-expanded="openedMenu"
  229. :aria-label="t('settings', 'Toggle user actions menu')"
  230. @click.prevent="toggleMenu" />
  231. <div :class="{ 'open': openedMenu }" class="popovermenu">
  232. <NcPopoverMenu :menu="userActions" />
  233. </div>
  234. </div>
  235. </div>
  236. <div :style="{opacity: feedbackMessage !== '' ? 1 : 0}"
  237. class="feedback">
  238. <div class="icon-checkmark" />
  239. {{ feedbackMessage }}
  240. </div>
  241. </div>
  242. </div>
  243. </template>
  244. <script>
  245. import ClickOutside from 'vue-click-outside'
  246. import NcPopoverMenu from '@nextcloud/vue/dist/Components/NcPopoverMenu.js'
  247. import NcSelect from '@nextcloud/vue/dist/Components/NcSelect.js'
  248. import NcActions from '@nextcloud/vue/dist/Components/NcActions.js'
  249. import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js'
  250. import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
  251. import UserRowSimple from './UserRowSimple.vue'
  252. import UserRowMixin from '../../mixins/UserRowMixin.js'
  253. import { showSuccess, showError } from '@nextcloud/dialogs'
  254. export default {
  255. name: 'UserRow',
  256. components: {
  257. UserRowSimple,
  258. NcPopoverMenu,
  259. NcActions,
  260. NcActionButton,
  261. NcSelect,
  262. NcTextField,
  263. },
  264. directives: {
  265. ClickOutside,
  266. },
  267. mixins: [UserRowMixin],
  268. props: {
  269. users: {
  270. type: Array,
  271. required: true,
  272. },
  273. user: {
  274. type: Object,
  275. required: true,
  276. },
  277. settings: {
  278. type: Object,
  279. default: () => ({}),
  280. },
  281. groups: {
  282. type: Array,
  283. default: () => [],
  284. },
  285. subAdminsGroups: {
  286. type: Array,
  287. default: () => [],
  288. },
  289. quotaOptions: {
  290. type: Array,
  291. default: () => [],
  292. },
  293. showConfig: {
  294. type: Object,
  295. default: () => ({}),
  296. },
  297. languages: {
  298. type: Array,
  299. required: true,
  300. },
  301. externalActions: {
  302. type: Array,
  303. default: () => [],
  304. },
  305. isDarkTheme: {
  306. type: Boolean,
  307. required: true,
  308. },
  309. },
  310. data() {
  311. return {
  312. // default quota is set to unlimited
  313. unlimitedQuota: { id: 'none', label: t('settings', 'Unlimited') },
  314. // temporary value used for multiselect change
  315. selectedQuota: false,
  316. rand: parseInt(Math.random() * 1000),
  317. openedMenu: false,
  318. feedbackMessage: '',
  319. possibleManagers: [],
  320. currentManager: '',
  321. editing: false,
  322. loading: {
  323. all: false,
  324. displayName: false,
  325. password: false,
  326. mailAddress: false,
  327. groups: false,
  328. subadmins: false,
  329. quota: false,
  330. delete: false,
  331. disable: false,
  332. languages: false,
  333. wipe: false,
  334. manager: false,
  335. },
  336. editedDisplayName: this.user.displayname,
  337. editedPassword: '',
  338. editedMail: this.user.email ?? '',
  339. }
  340. },
  341. computed: {
  342. /* USER POPOVERMENU ACTIONS */
  343. userActions() {
  344. const actions = [
  345. {
  346. icon: 'icon-delete',
  347. text: t('settings', 'Delete user'),
  348. action: this.deleteUser,
  349. },
  350. {
  351. icon: 'icon-delete',
  352. text: t('settings', 'Wipe all devices'),
  353. action: this.wipeUserDevices,
  354. },
  355. {
  356. icon: this.user.enabled ? 'icon-close' : 'icon-add',
  357. text: this.user.enabled ? t('settings', 'Disable user') : t('settings', 'Enable user'),
  358. action: this.enableDisableUser,
  359. },
  360. ]
  361. if (this.user.email !== null && this.user.email !== '') {
  362. actions.push({
  363. icon: 'icon-mail',
  364. text: t('settings', 'Resend welcome email'),
  365. action: this.sendWelcomeMail,
  366. })
  367. }
  368. return actions.concat(this.externalActions)
  369. },
  370. // mapping saved values to objects
  371. userQuota: {
  372. get() {
  373. if (this.selectedQuota !== false) {
  374. return this.selectedQuota
  375. }
  376. if (this.settings.defaultQuota !== this.unlimitedQuota.id && OC.Util.computerFileSize(this.settings.defaultQuota) >= 0) {
  377. // if value is valid, let's map the quotaOptions or return custom quota
  378. return { id: this.settings.defaultQuota, label: this.settings.defaultQuota }
  379. }
  380. return this.unlimitedQuota // unlimited
  381. },
  382. set(quota) {
  383. this.selectedQuota = quota
  384. },
  385. },
  386. availableLanguages() {
  387. return this.languages[0].languages.concat(this.languages[1].languages)
  388. },
  389. },
  390. async beforeMount() {
  391. await this.searchUserManager()
  392. if (this.user.manager) {
  393. await this.initManager(this.user.manager)
  394. }
  395. },
  396. methods: {
  397. /* MENU HANDLING */
  398. toggleMenu() {
  399. this.openedMenu = !this.openedMenu
  400. },
  401. hideMenu() {
  402. this.openedMenu = false
  403. },
  404. wipeUserDevices() {
  405. const userid = this.user.id
  406. OC.dialogs.confirmDestructive(
  407. t('settings', 'In case of lost device or exiting the organization, this can remotely wipe the Nextcloud data from all devices associated with {userid}. Only works if the devices are connected to the internet.', { userid }),
  408. t('settings', 'Remote wipe of devices'),
  409. {
  410. type: OC.dialogs.YES_NO_BUTTONS,
  411. confirm: t('settings', 'Wipe {userid}\'s devices', { userid }),
  412. confirmClasses: 'error',
  413. cancel: t('settings', 'Cancel'),
  414. },
  415. (result) => {
  416. if (result) {
  417. this.loading.wipe = true
  418. this.loading.all = true
  419. this.$store.dispatch('wipeUserDevices', userid)
  420. .then(() => {
  421. this.loading.wipe = false
  422. this.loading.all = false
  423. })
  424. }
  425. },
  426. true
  427. )
  428. },
  429. filterManagers(managers) {
  430. return managers.filter((manager) => manager.id !== this.user.id)
  431. },
  432. async initManager(userId) {
  433. await this.$store.dispatch('getUser', userId).then(response => {
  434. this.currentManager = response?.data.ocs.data
  435. })
  436. },
  437. async searchUserManager(query) {
  438. await this.$store.dispatch('searchUsers', { offset: 0, limit: 10, search: query }).then(response => {
  439. const users = response?.data ? this.filterManagers(Object.values(response?.data.ocs.data.users)) : []
  440. if (users.length > 0) {
  441. this.possibleManagers = users
  442. }
  443. })
  444. },
  445. updateUserManager(manager) {
  446. if (manager === null) {
  447. this.currentManager = ''
  448. }
  449. this.loading.manager = true
  450. try {
  451. this.$store.dispatch('setUserData', {
  452. userid: this.user.id,
  453. key: 'manager',
  454. value: this.currentManager ? this.currentManager.id : '',
  455. })
  456. } catch (error) {
  457. showError(t('setting', 'Update of user manager was failed'))
  458. console.error(error)
  459. } finally {
  460. this.loading.manager = false
  461. }
  462. },
  463. deleteUser() {
  464. const userid = this.user.id
  465. OC.dialogs.confirmDestructive(
  466. t('settings', 'Fully delete {userid}\'s account including all their personal files, app data, etc.', { userid }),
  467. t('settings', 'Account deletion'),
  468. {
  469. type: OC.dialogs.YES_NO_BUTTONS,
  470. confirm: t('settings', 'Delete {userid}\'s account', { userid }),
  471. confirmClasses: 'error',
  472. cancel: t('settings', 'Cancel'),
  473. },
  474. (result) => {
  475. if (result) {
  476. this.loading.delete = true
  477. this.loading.all = true
  478. return this.$store.dispatch('deleteUser', userid)
  479. .then(() => {
  480. this.loading.delete = false
  481. this.loading.all = false
  482. })
  483. }
  484. },
  485. true
  486. )
  487. },
  488. enableDisableUser() {
  489. this.loading.delete = true
  490. this.loading.all = true
  491. const userid = this.user.id
  492. const enabled = !this.user.enabled
  493. return this.$store.dispatch('enableDisableUser', {
  494. userid,
  495. enabled,
  496. })
  497. .then(() => {
  498. this.loading.delete = false
  499. this.loading.all = false
  500. })
  501. },
  502. /**
  503. * Set user displayName
  504. *
  505. * @param {string} displayName The display name
  506. */
  507. updateDisplayName() {
  508. this.loading.displayName = true
  509. this.$store.dispatch('setUserData', {
  510. userid: this.user.id,
  511. key: 'displayname',
  512. value: this.editedDisplayName,
  513. }).then(() => {
  514. this.loading.displayName = false
  515. if (this.editedDisplayName === this.user.displayname) {
  516. showSuccess(t('setting', 'Display name was successfully changed'))
  517. }
  518. })
  519. },
  520. /**
  521. * Set user password
  522. *
  523. * @param {string} password The email address
  524. */
  525. updatePassword() {
  526. this.loading.password = true
  527. if (this.editedPassword.length === 0) {
  528. showError(t('setting', "Password can't be empty"))
  529. this.loading.password = false
  530. } else {
  531. this.$store.dispatch('setUserData', {
  532. userid: this.user.id,
  533. key: 'password',
  534. value: this.editedPassword,
  535. }).then(() => {
  536. this.loading.password = false
  537. this.editedPassword = ''
  538. showSuccess(t('setting', 'Password was successfully changed'))
  539. })
  540. }
  541. },
  542. /**
  543. * Set user mailAddress
  544. *
  545. * @param {string} mailAddress The email address
  546. */
  547. updateEmail() {
  548. this.loading.mailAddress = true
  549. if (this.editedMail === '') {
  550. showError(t('setting', "Email can't be empty"))
  551. this.loading.mailAddress = false
  552. this.editedMail = this.user.email
  553. } else {
  554. this.$store.dispatch('setUserData', {
  555. userid: this.user.id,
  556. key: 'email',
  557. value: this.editedMail,
  558. }).then(() => {
  559. this.loading.mailAddress = false
  560. if (this.editedMail === this.user.email) {
  561. showSuccess(t('setting', 'Email was successfully changed'))
  562. }
  563. })
  564. }
  565. },
  566. /**
  567. * Create a new group and add user to it
  568. *
  569. * @param {string} gid Group id
  570. */
  571. async createGroup({ name: gid }) {
  572. this.loading = { groups: true, subadmins: true }
  573. try {
  574. await this.$store.dispatch('addGroup', gid)
  575. const userid = this.user.id
  576. await this.$store.dispatch('addUserGroup', { userid, gid })
  577. } catch (error) {
  578. console.error(error)
  579. } finally {
  580. this.loading = { groups: false, subadmins: false }
  581. }
  582. return this.$store.getters.getGroups[this.groups.length]
  583. },
  584. /**
  585. * Add user to group
  586. *
  587. * @param {object} group Group object
  588. */
  589. async addUserGroup(group) {
  590. if (group.isCreating) {
  591. // This is NcSelect's internal value for a new inputted group name
  592. // Ignore
  593. return
  594. }
  595. this.loading.groups = true
  596. const userid = this.user.id
  597. const gid = group.id
  598. if (group.canAdd === false) {
  599. return false
  600. }
  601. try {
  602. await this.$store.dispatch('addUserGroup', { userid, gid })
  603. } catch (error) {
  604. console.error(error)
  605. } finally {
  606. this.loading.groups = false
  607. }
  608. },
  609. /**
  610. * Remove user from group
  611. *
  612. * @param {object} group Group object
  613. */
  614. async removeUserGroup(group) {
  615. if (group.canRemove === false) {
  616. return false
  617. }
  618. this.loading.groups = true
  619. const userid = this.user.id
  620. const gid = group.id
  621. try {
  622. await this.$store.dispatch('removeUserGroup', {
  623. userid,
  624. gid,
  625. })
  626. this.loading.groups = false
  627. // remove user from current list if current list is the removed group
  628. if (this.$route.params.selectedGroup === gid) {
  629. this.$store.commit('deleteUser', userid)
  630. }
  631. } catch {
  632. this.loading.groups = false
  633. }
  634. },
  635. /**
  636. * Add user to group
  637. *
  638. * @param {object} group Group object
  639. */
  640. async addUserSubAdmin(group) {
  641. this.loading.subadmins = true
  642. const userid = this.user.id
  643. const gid = group.id
  644. try {
  645. await this.$store.dispatch('addUserSubAdmin', {
  646. userid,
  647. gid,
  648. })
  649. this.loading.subadmins = false
  650. } catch (error) {
  651. console.error(error)
  652. }
  653. },
  654. /**
  655. * Remove user from group
  656. *
  657. * @param {object} group Group object
  658. */
  659. async removeUserSubAdmin(group) {
  660. this.loading.subadmins = true
  661. const userid = this.user.id
  662. const gid = group.id
  663. try {
  664. await this.$store.dispatch('removeUserSubAdmin', {
  665. userid,
  666. gid,
  667. })
  668. } catch (error) {
  669. console.error(error)
  670. } finally {
  671. this.loading.subadmins = false
  672. }
  673. },
  674. /**
  675. * Dispatch quota set request
  676. *
  677. * @param {string | object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
  678. * @return {string}
  679. */
  680. async setUserQuota(quota = 'none') {
  681. // Make sure correct label is set for unlimited quota
  682. if (quota === 'none') {
  683. quota = this.unlimitedQuota
  684. }
  685. this.loading.quota = true
  686. // ensure we only send the preset id
  687. quota = quota.id ? quota.id : quota
  688. try {
  689. await this.$store.dispatch('setUserData', {
  690. userid: this.user.id,
  691. key: 'quota',
  692. value: quota,
  693. })
  694. } catch (error) {
  695. console.error(error)
  696. } finally {
  697. this.loading.quota = false
  698. }
  699. return quota
  700. },
  701. /**
  702. * Validate quota string to make sure it's a valid human file size
  703. *
  704. * @param {string | object} quota Quota in readable format '5 GB' or Object {id: '5 GB', label: '5GB'}
  705. * @return {object} The validated quota object or unlimited quota if input is invalid
  706. */
  707. validateQuota(quota) {
  708. if (typeof quota === 'object') {
  709. quota = quota?.id || quota.label
  710. }
  711. // only used for new presets sent through @Tag
  712. const validQuota = OC.Util.computerFileSize(quota)
  713. if (validQuota === null) {
  714. return this.unlimitedQuota
  715. } else {
  716. // unify format output
  717. quota = OC.Util.humanFileSize(OC.Util.computerFileSize(quota))
  718. return { id: quota, label: quota }
  719. }
  720. },
  721. /**
  722. * Dispatch language set request
  723. *
  724. * @param {object} lang language object {code:'en', name:'English'}
  725. * @return {object}
  726. */
  727. async setUserLanguage(lang) {
  728. this.loading.languages = true
  729. // ensure we only send the preset id
  730. try {
  731. await this.$store.dispatch('setUserData', {
  732. userid: this.user.id,
  733. key: 'language',
  734. value: lang.code,
  735. })
  736. this.loading.languages = false
  737. } catch (error) {
  738. console.error(error)
  739. }
  740. return lang
  741. },
  742. /**
  743. * Dispatch new welcome mail request
  744. */
  745. sendWelcomeMail() {
  746. this.loading.all = true
  747. this.$store.dispatch('sendWelcomeMail', this.user.id)
  748. .then(success => {
  749. if (success) {
  750. // Show feedback to indicate the success
  751. this.feedbackMessage = t('setting', 'Welcome mail sent!')
  752. setTimeout(() => {
  753. this.feedbackMessage = ''
  754. }, 2000)
  755. }
  756. this.loading.all = false
  757. })
  758. },
  759. handleDoneButton() {
  760. this.editing = false
  761. if (this.editedDisplayName !== this.user.displayname) {
  762. this.editedDisplayName = this.user.displayname
  763. } else if (this.editedMail !== this.user.email) {
  764. this.editedMail = this.user.email
  765. }
  766. },
  767. },
  768. }
  769. </script>
  770. <style scoped lang="scss">
  771. // Force menu to be above other rows
  772. .row--menu-opened {
  773. z-index: 1 !important;
  774. }
  775. .row :deep() {
  776. .mailAddress,
  777. .password,
  778. .displayName {
  779. .input-field,
  780. .input-field__input {
  781. height: 48px!important;
  782. }
  783. .button-vue--icon-only {
  784. height: 44px!important;
  785. }
  786. }
  787. }
  788. </style>