Fix role pickers to have limit and be searchable

This commit is contained in:
Jože Fortun
2024-09-24 12:20:57 +02:00
parent d6aa8ef935
commit c1ddf43f80
20 changed files with 495 additions and 351 deletions
+44 -69
View File
@@ -1,26 +1,24 @@
<template>
<div
data-test-id="role-picker"
class="d-flex flex-column"
>
<c-input-select
ref="picker"
<c-input-role
data-test-id="input-role-picker"
:options="filtered"
:get-option-key="r => r.value"
:get-option-label="r => getRoleLabel(r)"
:selectable="r => !value.includes(r.roleID)"
:placeholder="$t('admin:picker.role.placeholder')"
:filterable="false"
@search="search"
@input="updateValue($event)"
:visible="isRoleVisible"
clear-on-select
@input="addRole($event)"
/>
<b-spinner
v-if="preloading"
class="mx-auto my-4"
/>
<b-form-text
v-if="$slots['description']"
>
<slot name="description" />
</b-form-text>
<b-table-simple
v-if="selected"
v-else-if="getSelectedRoles.length"
responsive
small
hover
@@ -28,7 +26,7 @@
>
<tbody>
<tr
v-for="role in selected"
v-for="role in getSelectedRoles"
:key="role.roleID"
data-test-id="selected-row-list"
>
@@ -39,7 +37,7 @@
<c-input-confirm
data-test-id="button-remove-role"
show-icon
@confirmed="removeRole(role)"
@confirmed="removeRole(role.roleID)"
/>
</td>
</tr>
@@ -49,19 +47,15 @@
</template>
<script>
import { debounce } from 'lodash'
function roleSorter (a, b) {
return `${a.name} ${a.handle} ${a.roleID}`.localeCompare(`${b.name} ${b.handle} ${b.roleID}`)
}
import { components } from '@cortezaproject/corteza-vue'
const { CInputRole } = components
export default {
props: {
label: {
type: String,
default: 'count',
},
components: {
CInputRole,
},
props: {
// list of role IDs
value: {
type: Array,
@@ -71,77 +65,58 @@ export default {
data () {
return {
roles: [],
fetching: false,
preloading: false,
filter: '',
selectedRoles: [],
}
},
computed: {
selected () {
return this.roles
.filter(({ roleID }) => (this.value || []).includes(roleID))
.sort(roleSorter)
},
filtered () {
const match = ({ name = '', handle = '', roleID = '' }) => {
return `${name} ${handle} ${roleID}`.toLocaleLowerCase().indexOf(this.filter.toLocaleLowerCase()) > -1
}
const fits = ({ isClosed, meta = {} }) => {
return !(isClosed || (meta.context && meta.context.resourceTypes))
}
return this.roles.filter(r => !(this.value || []).includes(r.roleID) && fits(r) && match(r))
getSelectedRoles () {
return this.selectedRoles.filter(({ roleID }) => this.value.includes(roleID))
},
},
mounted () {
this.preload()
this.preloadSelected()
},
methods: {
addRole (role) {
if (!this.value.includes(role.roleID)) {
this.value.push(role.roleID)
this.$emit('input', this.value)
this.selectedRoles.push(role)
this.$emit('input', [...this.value, role.roleID])
}
},
removeRole (r) {
this.value.splice(this.value.indexOf(r.roleID), 1)
this.filter = ''
removeRole (roleID) {
this.selectedRoles = this.selectedRoles.filter(({ roleID: rID }) => rID !== roleID)
this.$emit('input', this.value.filter(v => v !== roleID))
},
preload () {
return this.$SystemAPI.roleList({ query: this.filter })
.then(({ set }) => { this.roles = set || [] })
preloadSelected () {
this.preloading = true
return this.$SystemAPI.roleList({ memberID: this.$auth.user.userID })
.then(({ set }) => { this.selectedRoles = set || [] })
.finally(() => { this.preloading = false })
.catch(this.toastErrorHandler(this.$t('notification:role.fetch.error')))
},
search: debounce(function (query = '') {
if (query !== this.filter) {
this.filter = query
}
this.preload()
}, 300),
updateValue (role) {
// reset picker value for better value presentation
if (this.$refs.picker) {
this.$refs.picker._data._value = undefined
}
this.addRole(role)
},
getRoleLabel ({ name, handle, roleID }) {
return name || handle || roleID
},
isRoleVisible ({ isClosed, meta = {} }) {
return !(isClosed || (meta.context && meta.context.resourceTypes))
},
},
}
</script>
<style lang="scss">
.results {
z-index: 100;
@@ -19,23 +19,19 @@
centered
:title="$t('ui.clone.title')"
:ok-title="$t('ui.clone.clone')"
:ok-disabled="!selectedRoles.length || processingRoles || processingSubmit"
@ok="clonePermissions()"
:ok-disabled="!selectedRoles.length || processingSubmit"
@ok="clonePermissions"
>
<b-form-group
:description="$t('ui.clone.description')"
class="mb-0"
>
<c-input-select
<c-input-role
v-model="selectedRoles"
data-test-id="select-role-list"
label="name"
:options="roles"
:get-option-key="getOptionKey"
:reduce="role => role.roleID"
:loading="processingRoles"
multiple
:selectable="r => !selectedRoles.some(rr => rr.roleID === r.roleID)"
:placeholder="$t('ui.clone.pick-role')"
multiple
/>
</b-form-group>
</b-modal>
@@ -43,11 +39,18 @@
</template>
<script>
import { components } from '@cortezaproject/corteza-vue'
const { CInputRole } = components
export default {
i18nOptions: {
namespaces: 'permissions',
},
components: {
CInputRole,
},
props: {
roleId: {
type: String,
@@ -60,32 +63,19 @@ export default {
return {
showModal: false,
roles: [],
selectedRoles: [],
processingSubmit: false,
processingRoles: false,
}
},
mounted () {
this.processingRoles = true
this.$SystemAPI.roleList()
.then(({ set: roles = [] }) => {
this.roles = roles
})
.catch(this.toastErrorHandler(this.$t('notification:role.fetch.error')))
.finally(() => {
this.processingRoles = false
})
},
methods: {
clonePermissions () {
this.processingSubmit = true
this.$SystemAPI.roleCloneRules({ roleID: this.roleId, cloneToRoleID: this.selectedRoles })
const cloneToRoleID = this.selectedRoles.map(({ roleID }) => roleID)
this.$SystemAPI.roleCloneRules({ roleID: this.roleId, cloneToRoleID })
.then(() => {
this.selectedRoles = []
this.toastSuccess(this.$t('notification:permissions.clone.success'))
@@ -93,13 +83,8 @@ export default {
.catch(this.toastErrorHandler(this.$t('notification:permissions.clone.error')))
.finally(() => {
this.processingSubmit = false
this.showModal = false
})
},
getOptionKey ({ roleID }) {
return roleID
},
},
}
</script>
@@ -217,15 +217,13 @@
label-class="text-primary"
class="mb-0"
>
<c-input-select
<c-input-role
v-model="add.roleID"
:data-test-id="`select-${add.mode}-roles`"
:options="availableRoles"
:get-option-key="getOptionRoleKey"
label="name"
:placeholder="$t('ui.add.role.placeholder')"
:visible="isRoleVisible"
:multiple="add.mode === 'eval'"
:disabled="add.mode === 'eval' && !!add.userID"
:placeholder="$t('ui.add.role.placeholder')"
/>
</b-form-group>
@@ -252,23 +250,24 @@
<script>
import _ from 'lodash'
import { components } from '@cortezaproject/corteza-vue'
const { CInputRole } = components
export default {
i18nOptions: {
namespaces: 'permissions',
},
components: {
CInputRole,
},
props: {
roles: {
type: Array,
required: true,
},
allRoles: {
type: Array,
required: true,
},
permissions: {
type: Object,
required: true,
@@ -331,20 +330,6 @@ export default {
},
computed: {
editableRoles () {
return this.roles.filter(({ mode }) => mode !== 'eval').map(({ roleID }) => roleID.roleID)
},
availableRoles () {
if (this.add.mode === 'edit') {
return this.allRoles.filter(({ roleID, isBypass }) => !isBypass && !this.editableRoles.includes(roleID))
} else if (this.add.mode === 'eval') {
return this.allRoles
}
return []
},
sortedPermissions () {
return Object.keys(this.permissions).sort()
},
@@ -444,6 +429,10 @@ export default {
})
},
isRoleVisible ({ isBypass }) {
return this.add.mode === 'edit' || !isBypass
},
getUserLabel (userID) {
return this.fetchedUsers[userID]
},
@@ -472,7 +461,14 @@ export default {
},
onAdd () {
this.$emit('add', { ...this.add, userID: { userID: this.add.userID, name: this.fetchedUsers[this.add.userID] } })
let { userID } = this.add
if (userID) {
userID = { userID: this.add.userID, name: this.fetchedUsers[this.add.userID] }
}
this.$emit('add', { ...this.add, userID })
this.add = {
mode: 'edit',
roleID: [],
@@ -484,10 +480,6 @@ export default {
this.$emit('hide', role)
},
getOptionRoleKey ({ roleID }) {
return roleID
},
setDefaultValues () {
this.add = {}
this.modeOptions = []
@@ -8,7 +8,10 @@
<b-form
@submit.prevent="$emit('submit')"
>
<c-role-picker v-model="value" />
<c-role-picker
:value="value"
@input="$emit('input', $event)"
/>
</b-form>
<template #header>
@@ -59,17 +62,5 @@ export default {
value: false,
},
},
computed: {
roles: {
get () {
return this.currentRoles
},
set (roles) {
this.$emit('update:current-roles', roles)
},
},
},
}
</script>
@@ -59,25 +59,19 @@ export default {
},
methods: {
fetchRoles () {
prepareRoles () {
this.incLoader()
return this.$SystemAPI.roleList()
.then(({ set }) => {
this.allRoles = set
this.rolePermissions = []
const roleIDs = this.allRoles.map(({ roleID }) => roleID)
this.rolePermissions = []
// We read permissions for included roles
return Promise.all(getIncludedRoles().filter(({ roleID, mode }) => mode === 'eval' || roleIDs.includes(roleID)).map(({ mode, name, roleID, userID }) => {
if (mode === 'edit') {
return this.readPermissions({ name, roleID })
} else {
return this.evaluatePermissions({ name, roleID, userID })
}
}))
})
.catch(this.toastErrorHandler(this.$t('notification:user.roles.error')))
// We read permissions for included roles
return Promise.all(getIncludedRoles().map(({ mode, name, roleID, userID }) => {
if (mode === 'edit') {
return this.readPermissions({ name, roleID })
} else {
return this.evaluatePermissions({ name, roleID, userID })
}
})).catch(this.toastErrorHandler(this.$t('notification:user.roles.error')))
.finally(() => {
this.loaded.roles = true
this.decLoader()
@@ -101,7 +95,7 @@ export default {
return map
}, {})
})
.then(() => this.fetchRoles())
.then(() => this.prepareRoles())
.catch(this.toastErrorHandler(this.$t('notification:permissions.fetch.system')))
.finally(() => {
this.loaded.permissions = true
@@ -9,7 +9,6 @@
<c-permission-list
:roles="sortedRoles"
:all-roles="allRoles"
:permissions="permissions"
:role-permissions="rolePermissions"
:can-grant="canGrant"
@@ -9,7 +9,6 @@
<c-permission-list
:roles="sortedRoles"
:all-roles="allRoles"
:permissions="permissions"
:role-permissions="rolePermissions"
:can-grant="canGrant"
@@ -9,7 +9,6 @@
<c-permission-list
:roles="sortedRoles"
:all-roles="allRoles"
:permissions="permissions"
:role-permissions="rolePermissions"
:can-grant="canGrant"
@@ -9,7 +9,6 @@
<c-permission-list
:roles="sortedRoles"
:all-roles="allRoles"
:permissions="permissions"
:role-permissions="rolePermissions"
:can-grant="canGrant"
@@ -7,19 +7,19 @@
</b-form-group>
<b-form-group
v-if="f.options.roles"
:label="$t('kind.user.roles.label')"
label-class="text-primary"
>
<c-input-select
v-model="f.options.roles"
:options="roleOptions"
:get-option-key="getOptionKey"
:reduce="role => role.roleID"
:close-on-select="false"
<b-spinner
v-if="preloadingRoles"
/>
<c-input-role
v-else
v-model="currentRoles"
:placeholder="$t('kind.user.roles.placeholder')"
multiple
label="name"
@input="f.options.roles = $event.map(r => r.roleID)"
/>
</b-form-group>
@@ -52,13 +52,20 @@
</template>
<script>
import { components } from '@cortezaproject/corteza-vue'
import base from './base'
const { CInputRole } = components
export default {
i18nOptions: {
namespaces: 'field',
},
components: {
CInputRole,
},
extends: base,
data () {
@@ -68,7 +75,9 @@ export default {
{ text: this.$t('kind.select.optionType.multiple'), value: 'multiple' },
{ text: this.$t('kind.select.optionType.each'), value: 'each', allowDuplicates: true },
],
roleOptions: [],
preloadingRoles: true,
currentRoles: [],
}
},
@@ -82,9 +91,17 @@ export default {
},
mounted () {
this.$SystemAPI.roleList().then(({ set: roles = [] }) => {
this.roleOptions = roles
})
if (this.f.options.roles.length) {
this.preloadingRoles = true
Promise.all(this.f.options.roles.map(roleID => {
return this.$SystemAPI.roleRead({ roleID }).then(role => {
this.currentRoles.push(role)
})
})).finally(() => {
this.preloadingRoles = false
})
}
},
beforeDestroy () {
@@ -291,101 +291,103 @@
label-class="text-primary"
class="mb-0"
>
<b-table-simple
v-if="recordListModule"
borderless
small
responsive="lg"
class="mb-0"
>
<draggable
:list.sync="options.filterPresets"
group="sort"
handle=".grab"
tag="tbody"
<b-spinner v-if="fetchingRoles" />
<template v-else>
<b-table-simple
v-if="recordListModule"
borderless
small
responsive="lg"
class="mb-0"
>
<b-tr
v-for="(filter, index) in options.filterPresets"
:key="index"
<draggable
:list.sync="options.filterPresets"
group="sort"
handle=".grab"
tag="tbody"
>
<b-td
class="grab text-center align-middle"
style="width: 40px;"
<b-tr
v-for="(filter, index) in options.filterPresets"
:key="index"
>
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-secondary"
/>
</b-td>
<b-td
class="align-middle"
style="min-width: 150px;"
>
<b-input-group>
<b-form-input
v-model="filter.name"
:placeholder="$t('recordList.filter.name.placeholder')"
<b-td
class="grab text-center align-middle"
style="width: 40px;"
>
<font-awesome-icon
:icon="['fas', 'bars']"
class="text-secondary"
/>
</b-td>
<b-input-group-append>
<record-list-filter
class="d-print-none"
:target="`record-filter-${index}`"
:namespace="namespace"
:module="recordListModule"
:selected-field="recordListModule.fields[0]"
:record-list-filter="filter.filter"
variant="extra-light"
inactive-icon-class="text-light"
button-class="px-2 pt-2"
button-style="border-top-left-radius: 0; border-bottom-left-radius: 0;"
@filter="(filter) => onFilter(filter, index)"
<b-td
class="align-middle"
style="min-width: 150px;"
>
<b-input-group>
<b-form-input
v-model="filter.name"
:placeholder="$t('recordList.filter.name.placeholder')"
/>
</b-input-group-append>
</b-input-group>
</b-td>
<b-td
class="text-center align-middle"
style="min-width: 200px;"
>
<c-input-select
v-model="filter.roles"
:options="roleOptions"
:get-option-label="getRoleLabel"
:get-option-key="getOptionKey"
:placeholder="$t('recordList.filter.role.placeholder')"
:reduce="role => role.roleID"
multiple
/>
</b-td>
<b-input-group-append>
<record-list-filter
class="d-print-none"
:target="`record-filter-${index}`"
:namespace="namespace"
:module="recordListModule"
:selected-field="recordListModule.fields[0]"
:record-list-filter="filter.filter"
variant="extra-light"
inactive-icon-class="text-light"
button-class="px-2 pt-2"
button-style="border-top-left-radius: 0; border-bottom-left-radius: 0;"
@filter="(filter) => onFilter(filter, index)"
/>
</b-input-group-append>
</b-input-group>
</b-td>
<b-td
class="text-right align-middle"
style="min-width: 80px; width: 80px;"
>
<c-input-confirm
show-icon
@confirmed="options.filterPresets.splice(index, 1)"
/>
</b-td>
</b-tr>
</draggable>
</b-table-simple>
<b-td
class="text-center align-middle"
style="min-width: 200px;"
>
<c-input-role
:value="getFilterRoles(filter)"
:placeholder="$t('recordList.filter.role.placeholder')"
:visible="isRoleVisible"
multiple
@input="onFilterRoleChange(filter, $event)"
/>
</b-td>
<b-button
variant="primary"
size="sm"
class="mt-1"
@click="addFilterPreset"
>
<font-awesome-icon
:icon="['fas', 'plus']"
class="mr-1"
/>
{{ $t('general:label.add') }}
</b-button>
<b-td
class="text-right align-middle"
style="min-width: 80px; width: 80px;"
>
<c-input-confirm
show-icon
@confirmed="options.filterPresets.splice(index, 1)"
/>
</b-td>
</b-tr>
</draggable>
</b-table-simple>
<b-button
variant="primary"
size="sm"
class="mt-1"
@click="addFilterPreset"
>
<font-awesome-icon
:icon="['fas', 'plus']"
class="mr-1"
/>
{{ $t('general:label.add') }}
</b-button>
</template>
</b-form-group>
</div>
</b-col>
@@ -792,7 +794,7 @@ import AutomationTab from './Shared/AutomationTab'
import FieldPicker from 'corteza-webapp-compose/src/components/Common/FieldPicker'
import RecordListFilter from 'corteza-webapp-compose/src/components/Common/RecordListFilter'
import { components } from '@cortezaproject/corteza-vue'
const { CInputPresort } = components
const { CInputPresort, CInputRole } = components
export default {
i18nOptions: {
@@ -807,6 +809,7 @@ export default {
CInputPresort,
RecordListFilter,
Draggable,
CInputRole,
},
extends: base,
@@ -817,7 +820,9 @@ export default {
on: this.$t('general:label.yes'),
off: this.$t('general:label.no'),
},
roleOptions: [],
fetchingRoles: false,
resolvedRoles: {},
}
},
@@ -969,14 +974,44 @@ export default {
},
methods: {
getRoleLabel ({ name }) {
return name
async fetchRoles () {
this.fetchingRoles = true
if (this.options.filterPresets.length) {
const rolesToResolve = this.options.filterPresets.reduce((acc, { roles }) => {
return acc.concat(roles)
}, [])
Promise.all(rolesToResolve.map(roleID => {
if (this.resolvedRoles[roleID]) {
return Promise.resolve()
}
return this.$SystemAPI.roleRead({ roleID }).then(role => {
this.resolvedRoles[roleID] = role
})
})).finally(() => {
this.fetchingRoles = false
})
}
},
async fetchRoles () {
this.$SystemAPI.roleList().then(({ set: roles = [] }) => {
this.roleOptions = roles.filter(({ meta }) => !(meta.context && meta.context.resourceTypes))
onFilterRoleChange (filter, roles) {
roles.forEach(r => {
if (!this.resolvedRoles[r.roleID]) {
this.resolvedRoles[r.roleID] = r
}
})
filter.roles = roles.map(({ roleID }) => roleID)
},
getFilterRoles (filter) {
return filter.roles.map(roleID => this.resolvedRoles[roleID])
},
isRoleVisible ({ meta }) {
return !(meta.context && meta.context.resourceTypes)
},
onFilter (filter = [], index) {
@@ -997,7 +1032,7 @@ export default {
setDefaultValues () {
this.checkboxLabel = {}
this.roleOptions = []
this.resolvedRoles = {}
},
},
}
@@ -539,15 +539,14 @@
:label="$t('page-layout.roles.label')"
label-class="text-primary"
>
<c-input-select
v-model="currentLayoutRoles"
:options="roles.options"
:loading="roles.processing"
<b-spinner v-if="resolvingLayoutRoles" />
<c-input-role
v-else
:value="getLayoutRoles()"
:placeholder="$t('page-layout.roles.placeholder')"
:get-option-label="role => role.name"
:reduce="role => role.roleID"
:selectable="role => !currentLayoutRoles.includes(role.roleID)"
multiple
@input="onLayoutRoleChange"
/>
</b-form-group>
@@ -952,7 +951,8 @@ import pages from 'corteza-webapp-compose/src/mixins/pages'
import Uploader from 'corteza-webapp-compose/src/components/Public/Page/Attachment/Uploader'
import Draggable from 'vuedraggable'
import { compose, NoID } from '@cortezaproject/corteza-js'
import { handle } from '@cortezaproject/corteza-vue'
import { handle, components } from '@cortezaproject/corteza-vue'
const { CInputRole } = components
export default {
i18nOptions: {
@@ -967,6 +967,7 @@ export default {
PageLayoutTranslator,
Uploader,
Draggable,
CInputRole,
},
mixins: [
@@ -1017,19 +1018,15 @@ export default {
layout: undefined,
},
removedLayouts: new Set(),
resolvingLayoutRoles: false,
resolvedRoles: {},
roles: {
processing: false,
options: [],
},
removedLayouts: new Set(),
checkboxLabel: {
on: this.$t('general:label.yes'),
off: this.$t('general:label.no'),
},
abortableRequests: [],
}
},
@@ -1200,12 +1197,7 @@ export default {
},
},
created () {
this.fetchRoles()
},
beforeDestroy () {
this.abortRequests()
this.setDefaultValues()
},
@@ -1229,20 +1221,40 @@ export default {
})
},
async fetchRoles () {
this.roles.processing = true
async resolveLayoutRoles () {
this.resolvingLayoutRoles = true
const { response, cancel } = this.$SystemAPI
.roleListCancellable({})
if (this.currentLayoutRoles.length) {
Promise.all(this.currentLayoutRoles.map(roleID => {
if (this.resolvedRoles[roleID]) {
return Promise.resolve()
}
this.abortableRequests.push(cancel)
response()
.then(({ set: roles = [] }) => {
this.roles.options = roles.filter(({ meta }) => !(meta.context && meta.context.resourceTypes))
}).finally(() => {
this.roles.processing = false
return this.$SystemAPI.roleRead({ roleID }).then(role => {
this.resolvedRoles[roleID] = role
})
})).finally(() => {
this.resolvingLayoutRoles = false
})
}
},
getLayoutRoles () {
return this.currentLayoutRoles.map(roleID => this.resolvedRoles[roleID])
},
onLayoutRoleChange (roles) {
roles.forEach(r => {
if (!this.resolvedRoles[r.roleID]) {
this.resolvedRoles[r.roleID] = r
}
})
this.currentLayoutRoles = roles.map(r => r.roleID)
},
isRoleVisible ({ meta }) {
return !(meta.context && meta.context.resourceTypes)
},
addLayout () {
@@ -1268,6 +1280,8 @@ export default {
configureLayout (index) {
this.layoutEditor.index = index
this.layoutEditor.layout = new compose.PageLayout(this.layouts[index])
this.resolveLayoutRoles()
},
async handleSaveLayouts () {
@@ -1490,17 +1504,11 @@ export default {
this.linkUrl = ''
this.layouts = []
this.layoutEditor = {}
this.resolvedRoles = {}
this.removedLayouts.clear()
this.roles = {}
this.checkboxLabel = {}
this.abortableRequests = []
},
abortRequests () {
this.abortableRequests.forEach((cancel) => {
cancel()
})
},
},
}
</script>
+1
View File
@@ -21,6 +21,7 @@ export {
CAceEditor,
CButtonSubmit,
CInputSelect,
CInputRole,
} from './input'
export {
+120
View File
@@ -0,0 +1,120 @@
<template>
<c-input-select
ref="picker"
:value="value"
:options="roles"
:placeholder="placeholder"
:get-option-key="r => r.roleID"
:get-option-label="r => getRoleLabel(r)"
:filterable="false"
:selectable="selectable"
:multiple="multiple"
:clearable="clearable"
:loading="loading"
@search="search"
@input="updateValue"
/>
</template>
<script>
import { debounce } from 'lodash'
export default {
props: {
value: {
type: Array | String | Object,
default: '',
},
visible: {
type: Function,
default: () => true,
},
placeholder: {
type: String,
default: 'Start typing to search for roles',
},
multiple: {
type: Boolean,
default: false,
},
clearOnSelect: {
type: Boolean,
default: false,
},
selectable: {
type: Function,
default: () => true,
},
clearable: {
type: Boolean,
default: true,
},
preselect: {
type: Boolean,
default: false,
}
},
data () {
return {
loading: false,
roles: [],
filter: '',
}
},
mounted () {
this.fetchRoles()
},
methods: {
fetchRoles () {
this.loading = true
return this.$SystemAPI.roleList({ query: this.filter, limit: 20 })
.then(({ set }) => {
this.roles = set.filter(this.visible)
if (this.preselect && (!this.value || !this.value.length)) {
this.updateValue(this.roles[0])
}
}).finally(() => {
const timeout = this.filter ? 300 : 0
setTimeout(() => {
this.loading = false
}, timeout)
})
},
search: debounce(function (query = '') {
if (query !== this.filter) {
this.filter = query
}
this.fetchRoles()
}, 400),
updateValue (role) {
// reset picker value for better value presentation
if (this.$refs.picker && this.clearOnSelect) {
this.$refs.picker._data._value = undefined
}
this.$emit('input', role)
},
getRoleLabel ({ name, handle, roleID }) {
return name || handle || roleID
},
},
}
</script>
+41 -7
View File
@@ -2,12 +2,15 @@
<vue-select
v-model="_value"
v-bind="$attrs"
ref="vueSelect"
data-test-id="select"
:clearable="clearable"
:options="options"
:searchable="searchable"
:disabled="disabled"
:selectable="selectable"
:multiple="multiple"
:loading="loading"
:calculate-position="calculateDropdownPosition"
:append-to-body="appendToBody"
class="bg-white rounded"
@@ -40,7 +43,7 @@ export default {
props: {
value: {
type: [String, Array],
type: [String, Array, Object],
default: () => '',
},
@@ -67,7 +70,7 @@ export default {
},
defaultValue: {
type: [String, Array],
type: [String, Array, Object],
default: () => '',
},
@@ -85,12 +88,28 @@ export default {
type: Function,
default: o => !o.disabled,
},
multiple: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
},
data () {
return {
query: '',
}
},
computed: {
_value: {
get () {
const fallbackValue = this.$attrs.multiple ? [] : ''
const fallbackValue = this.multiple ? [] : ''
return !!this.defaultValue && (this.value === this.defaultValue) ? fallbackValue : this.value
},
@@ -148,8 +167,20 @@ export default {
return () => popper.destroy()
},
onSearch (search, loading) {
this.$emit('search', search, loading)
onSearch (query, loading) {
if (this.loading) {
if (this.$refs.vueSelect) {
this.$refs.vueSelect._data.search = this.query
}
return
}
if (query !== this.query) {
this.query = query
}
this.$emit('search', query, loading)
},
},
}
@@ -207,10 +238,8 @@ export default {
// force this to not use any space
// we still need it to be rendered for the focus
width: 0;
padding: 0;
margin: 0;
border: none;
height: 0;
}
.vs__dropdown-toggle {
@@ -267,6 +296,11 @@ export default {
}
}
.vs__spinner {
border: .7em solid var(--dark);
border-left-color: var(--white);
}
.vs__spinner, .vs__spinner::after {
width: 4em;
height: 4em;
+1
View File
@@ -12,3 +12,4 @@ export {
CButtonSubmit,
} from './button'
export { default as CInputSelect } from './CInputSelect.vue'
export { default as CInputRole } from './CInputRole.vue'
@@ -43,16 +43,12 @@
:label="labels.edit.label"
label-class="text-primary"
>
<c-input-select
v-model="currentRoleID"
<c-input-role
data-test-id="select-user-list-roles"
label="name"
:disabled="!currentRoleID"
v-model="currentRoleID"
:visible="isRoleVisible"
:clearable="false"
:options="roles"
:get-option-key="getOptionRoleKey"
:reduce="o => o.roleID"
append-to-body
preselect
@input="onRoleChange"
/>
</b-form-group>
@@ -195,15 +191,12 @@
label-class="text-primary"
class="mb-0"
>
<c-input-select
<c-input-role
data-test-id="select-role"
:placeholder="labels.add.role.placeholder"
v-model="add.roleID"
:options="roles"
:get-option-key="getOptionRoleKey"
label="name"
multiple
:disabled="!!add.userID"
:placeholder="labels.add.role.placeholder"
/>
</b-form-group>
@@ -240,6 +233,7 @@
<script lang="js">
import { modalOpenEventName, split } from './def.ts'
import CInputSelect from '../input/CInputSelect.vue'
import CInputRole from '../input/CInputRole.vue'
import Rules from './form/Rules.vue'
export default {
@@ -250,6 +244,7 @@ export default {
components: {
Rules,
CInputSelect,
CInputRole,
},
props: {
@@ -280,9 +275,6 @@ export default {
// List of rules for the current role
rules: [],
// List of all available roles
roles: [],
currentRoleID: undefined,
evaluate: [],
@@ -363,10 +355,9 @@ export default {
this.backendComponentName = resource.split(':')[2]
this.fetchPermissions().then(() => {
if (!this.roles.length) {
return this.fetchRoles()
} else if (this.currentRoleID) {
return this.reEvaluatePermissions(this.currentRoleID)
if (this.currentRoleID) {
const { roleID } = this.currentRoleID
return this.reEvaluatePermissions(roleID)
}
}).finally(() => {
this.processing = false
@@ -379,7 +370,7 @@ export default {
this.target = undefined
},
onRoleChange (roleID) {
onRoleChange ({ roleID }) {
this.processing = true
this.fetchRules(roleID).finally(() => {
@@ -391,7 +382,7 @@ export default {
this.submitting = true
const rules = this.collectChangedRules()
const roleID = this.currentRoleID
const { roleID } = this.currentRoleID
this.api.permissionsUpdate({ roleID, rules }).then(() => {
this.reEvaluatePermissions(roleID)
@@ -420,18 +411,8 @@ export default {
})
},
async fetchRoles () {
// Roles are always fetched from $SystemAPI.
return this.$SystemAPI.roleList().then(({ set }) => {
this.roles = set
.filter(({ isBypass }) => !isBypass)
.sort((a, b) => a.roleID.localeCompare(b.roleID))
if (this.roles.length > 0) {
this.currentRoleID = this.roles[0].roleID
this.onRoleChange(this.currentRoleID)
}
})
isRoleVisible ({ isBypass }) {
return !isBypass
},
async evaluatePermissions ({ resource = this.resource, roleID, userID }) {
@@ -607,7 +588,6 @@ export default {
this.userOptions = []
this.permissions = []
this.rules = []
this.roles = []
this.currentRoleID = undefined
this.evaluate = []
this.add = {}
@@ -8,11 +8,15 @@ ui:
set-for: Set permissions for {{target}}
loading: Loading permissions
notification:
save:
success: Permissions saved
failed: Failed to save permissions
edit:
title: Edit permissions
label: User roles
description: Select role to set permissions
edit-or-eval: Edit or evaluate permissions
evaluate:
title: Evaluated permissions
@@ -46,6 +50,7 @@ ui:
#############################################################################
# Inline permission table UI:
click-on-cell-to-allow: Click on permission/role cell to allow a specific operation
edit-or-eval: Edit or evaluate permissions
title:
automation: Automation permissions
@@ -56,8 +61,8 @@ ui:
clone:
clone: Clone
label: Clone permissions
pick-role: Pick a role
title: Clone permissions to
pick-role: Start typing to search for roles
title: Apply permissions to
description: This will clone all the permissions of the current role and apply them to the selected
resources:
@@ -5,6 +5,11 @@ ui:
loading: Loading permissions
label: Permissions
notification:
save:
success: Permissions saved
failed: Failed to save permissions
edit:
label: User roles
description: Select role to set permissions
@@ -5,6 +5,11 @@ ui:
loading: Loading permissions
label: Permissions
notification:
save:
success: Permissions saved
failed: Failed to save permissions
edit:
label: User roles
description: Select role to set permissions