Users.vue 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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. <Fragment>
  24. <NcContent app-name="settings" :navigation-class="{ 'icon-loading': loadingAddGroup }">
  25. <NcAppNavigation>
  26. <NcAppNavigationNew button-id="new-user-button"
  27. :text="t('settings','New user')"
  28. button-class="icon-add"
  29. @click="showNewUserMenu"
  30. @keyup.enter="showNewUserMenu"
  31. @keyup.space="showNewUserMenu">
  32. <template #icon>
  33. <Plus :size="20" />
  34. </template>
  35. </NcAppNavigationNew>
  36. <template #list>
  37. <NcAppNavigationNewItem id="addgroup"
  38. ref="addGroup"
  39. :edit-placeholder="t('settings', 'Enter group name')"
  40. :editable="true"
  41. :loading="loadingAddGroup"
  42. :name="t('settings', 'Add group')"
  43. @click="showAddGroupForm"
  44. @new-item="createGroup">
  45. <template #icon>
  46. <Plus :size="20" />
  47. </template>
  48. </NcAppNavigationNewItem>
  49. <NcAppNavigationItem id="everyone"
  50. :exact="true"
  51. :name="t('settings', 'Active users')"
  52. :to="{ name: 'users' }">
  53. <template #icon>
  54. <AccountGroup :size="20" />
  55. </template>
  56. <template #counter>
  57. <NcCounterBubble :type="!selectedGroupDecoded ? 'highlighted' : undefined">
  58. {{ userCount }}
  59. </NcCounterBubble>
  60. </template>
  61. </NcAppNavigationItem>
  62. <NcAppNavigationItem v-if="settings.isAdmin"
  63. id="admin"
  64. :exact="true"
  65. :name="t('settings', 'Admins')"
  66. :to="{ name: 'group', params: { selectedGroup: 'admin' } }">
  67. <template #icon>
  68. <ShieldAccount :size="20" />
  69. </template>
  70. <template v-if="adminGroupMenu.count > 0" #counter>
  71. <NcCounterBubble :type="selectedGroupDecoded === 'admin' ? 'highlighted' : undefined">
  72. {{ adminGroupMenu.count }}
  73. </NcCounterBubble>
  74. </template>
  75. </NcAppNavigationItem>
  76. <!-- Hide the disabled if none, if we don't have the data (-1) show it -->
  77. <NcAppNavigationItem v-if="disabledGroupMenu.usercount > 0 || disabledGroupMenu.usercount === -1"
  78. id="disabled"
  79. :exact="true"
  80. :name="t('settings', 'Disabled users')"
  81. :to="{ name: 'group', params: { selectedGroup: 'disabled' } }">
  82. <template #icon>
  83. <AccountOff :size="20" />
  84. </template>
  85. <template v-if="disabledGroupMenu.usercount > 0" #counter>
  86. <NcCounterBubble :type="selectedGroupDecoded === 'disabled' ? 'highlighted' : undefined">
  87. {{ disabledGroupMenu.usercount }}
  88. </NcCounterBubble>
  89. </template>
  90. </NcAppNavigationItem>
  91. <NcAppNavigationCaption v-if="groupList.length > 0" :name="t('settings', 'Groups')" />
  92. <GroupListItem v-for="group in groupList"
  93. :id="group.id"
  94. :key="group.id"
  95. :active="selectedGroupDecoded === group.id"
  96. :name="group.title"
  97. :count="group.count" />
  98. </template>
  99. <template #footer>
  100. <ul class="app-navigation-entry__settings">
  101. <NcAppNavigationItem :name="t('settings', 'User management settings')"
  102. @click="isDialogOpen = true">
  103. <template #icon>
  104. <Cog :size="20" />
  105. </template>
  106. </NcAppNavigationItem>
  107. </ul>
  108. </template>
  109. </NcAppNavigation>
  110. <NcAppContent>
  111. <UserList :selected-group="selectedGroupDecoded"
  112. :external-actions="externalActions" />
  113. </NcAppContent>
  114. </NcContent>
  115. <UserSettingsDialog :open.sync="isDialogOpen" />
  116. </Fragment>
  117. </template>
  118. <script>
  119. import Vue from 'vue'
  120. import VueLocalStorage from 'vue-localstorage'
  121. import { Fragment } from 'vue-frag'
  122. import NcAppContent from '@nextcloud/vue/dist/Components/NcAppContent.js'
  123. import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation.js'
  124. import NcAppNavigationCaption from '@nextcloud/vue/dist/Components/NcAppNavigationCaption.js'
  125. import NcAppNavigationItem from '@nextcloud/vue/dist/Components/NcAppNavigationItem.js'
  126. import NcAppNavigationNew from '@nextcloud/vue/dist/Components/NcAppNavigationNew.js'
  127. import NcAppNavigationNewItem from '@nextcloud/vue/dist/Components/NcAppNavigationNewItem.js'
  128. import NcContent from '@nextcloud/vue/dist/Components/NcContent.js'
  129. import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble.js'
  130. import AccountGroup from 'vue-material-design-icons/AccountGroup.vue'
  131. import AccountOff from 'vue-material-design-icons/AccountOff.vue'
  132. import Cog from 'vue-material-design-icons/Cog.vue'
  133. import Plus from 'vue-material-design-icons/Plus.vue'
  134. import ShieldAccount from 'vue-material-design-icons/ShieldAccount.vue'
  135. import GroupListItem from '../components/GroupListItem.vue'
  136. import UserList from '../components/UserList.vue'
  137. import UserSettingsDialog from '../components/Users/UserSettingsDialog.vue'
  138. Vue.use(VueLocalStorage)
  139. export default {
  140. name: 'Users',
  141. components: {
  142. AccountGroup,
  143. AccountOff,
  144. Cog,
  145. Fragment,
  146. GroupListItem,
  147. NcAppContent,
  148. NcAppNavigation,
  149. NcAppNavigationCaption,
  150. NcAppNavigationItem,
  151. NcAppNavigationNew,
  152. NcAppNavigationNewItem,
  153. NcContent,
  154. NcCounterBubble,
  155. Plus,
  156. ShieldAccount,
  157. UserList,
  158. UserSettingsDialog,
  159. },
  160. props: {
  161. selectedGroup: {
  162. type: String,
  163. default: null,
  164. },
  165. },
  166. data() {
  167. return {
  168. // temporary value used for multiselect change
  169. externalActions: [],
  170. loadingAddGroup: false,
  171. isDialogOpen: false,
  172. }
  173. },
  174. computed: {
  175. showConfig() {
  176. return this.$store.getters.getShowConfig
  177. },
  178. selectedGroupDecoded() {
  179. return this.selectedGroup ? decodeURIComponent(this.selectedGroup) : null
  180. },
  181. users() {
  182. return this.$store.getters.getUsers
  183. },
  184. groups() {
  185. return this.$store.getters.getGroups
  186. },
  187. usersOffset() {
  188. return this.$store.getters.getUsersOffset
  189. },
  190. usersLimit() {
  191. return this.$store.getters.getUsersLimit
  192. },
  193. userCount() {
  194. return this.$store.getters.getUserCount
  195. },
  196. settings() {
  197. return this.$store.getters.getServerData
  198. },
  199. groupList() {
  200. const groups = Array.isArray(this.groups) ? this.groups : []
  201. return groups
  202. // filter out disabled and admin
  203. .filter(group => group.id !== 'disabled' && group.id !== 'admin')
  204. .map(group => this.formatGroupMenu(group))
  205. },
  206. adminGroupMenu() {
  207. return this.formatGroupMenu(this.groups.find(group => group.id === 'admin'))
  208. },
  209. disabledGroupMenu() {
  210. return this.formatGroupMenu(this.groups.find(group => group.id === 'disabled'))
  211. },
  212. },
  213. beforeMount() {
  214. this.$store.commit('initGroups', {
  215. groups: this.$store.getters.getServerData.groups,
  216. orderBy: this.$store.getters.getServerData.sortGroups,
  217. userCount: this.$store.getters.getServerData.userCount,
  218. })
  219. this.$store.dispatch('getPasswordPolicyMinLength')
  220. },
  221. created() {
  222. // init the OCA.Settings.UserList object
  223. // and add the registerAction method
  224. Object.assign(OCA, {
  225. Settings: {
  226. UserList: {
  227. registerAction: this.registerAction,
  228. },
  229. },
  230. })
  231. },
  232. methods: {
  233. showNewUserMenu() {
  234. this.$store.commit('setShowConfig', {
  235. key: 'showNewUserForm',
  236. value: true,
  237. })
  238. },
  239. /**
  240. * Register a new action for the user menu
  241. *
  242. * @param {string} icon the icon class
  243. * @param {string} text the text to display
  244. * @param {Function} action the function to run
  245. * @return {Array}
  246. */
  247. registerAction(icon, text, action) {
  248. this.externalActions.push({
  249. icon,
  250. text,
  251. action,
  252. })
  253. return this.externalActions
  254. },
  255. /**
  256. * Create a new group
  257. *
  258. * @param {string} gid The group id
  259. */
  260. async createGroup(gid) {
  261. // group is not valid
  262. if (gid.trim() === '') {
  263. return
  264. }
  265. try {
  266. this.loadingAddGroup = true
  267. await this.$store.dispatch('addGroup', gid.trim())
  268. this.hideAddGroupForm()
  269. await this.$router.push({
  270. name: 'group',
  271. params: {
  272. selectedGroup: encodeURIComponent(gid.trim()),
  273. },
  274. })
  275. } catch {
  276. this.showAddGroupForm()
  277. } finally {
  278. this.loadingAddGroup = false
  279. }
  280. },
  281. showAddGroupForm() {
  282. this.$refs.addGroup.newItemActive = true
  283. this.$nextTick(() => {
  284. this.$refs.addGroup.$refs.newItemInput.focusInput()
  285. })
  286. },
  287. hideAddGroupForm() {
  288. this.$refs.addGroup.newItemActive = false
  289. this.$refs.addGroup.newItemValue = ''
  290. },
  291. /**
  292. * Format a group to a menu entry
  293. *
  294. * @param {object} group the group
  295. * @return {object}
  296. */
  297. formatGroupMenu(group) {
  298. const item = {}
  299. if (typeof group === 'undefined') {
  300. return {}
  301. }
  302. item.id = group.id
  303. item.title = group.name
  304. item.usercount = group.usercount
  305. // users count for all groups
  306. if (group.usercount - group.disabled > 0) {
  307. item.count = group.usercount - group.disabled
  308. }
  309. return item
  310. },
  311. },
  312. }
  313. </script>
  314. <style lang="scss" scoped>
  315. .app-content {
  316. // Virtual list needs to be full height and is scrollable
  317. display: flex;
  318. overflow: hidden;
  319. flex-direction: column;
  320. max-height: 100%;
  321. }
  322. // force hiding the editing action for the add group entry
  323. .app-navigation__list #addgroup::v-deep .app-navigation-entry__utils {
  324. display: none;
  325. }
  326. .app-navigation-entry__settings {
  327. height: auto !important;
  328. // Prevent shrinking or growing
  329. flex: 0 0 auto;
  330. }
  331. </style>