diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components')
3 files changed, 197 insertions, 0 deletions
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js new file mode 100644 index 00000000000..c9db79581d1 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js @@ -0,0 +1,7 @@ +import { __ } from '~/locale'; + +// Note, we can extend this config in future to make the component work in other contexts +// https://gitlab.com/gitlab-org/gitlab/-/issues/428865 +export const CONFIG = { + users: { title: __('Users'), icon: 'user', filterKey: 'username' }, +}; diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue new file mode 100644 index 00000000000..237369f5900 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue @@ -0,0 +1,135 @@ +<script> +import { GlCard, GlIcon, GlCollapsibleListbox, GlSearchBoxByType } from '@gitlab/ui'; +import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql'; +import User from './user.vue'; +import { CONFIG } from './constants'; + +export default { + name: 'ListSelector', + components: { + GlCard, + GlIcon, + GlSearchBoxByType, + GlCollapsibleListbox, + User, + }, + props: { + title: { + type: String, + required: true, + }, + type: { + type: String, + required: true, + }, + selectedItems: { + type: Array, + required: false, + default: () => [], + }, + projectPath: { + type: String, + required: false, + default: null, + }, + }, + data() { + return { + searchValue: '', + isProject: true, // TODO: implement a way to distinguish between project/group + selected: [], + items: [], + }; + }, + computed: { + config() { + return CONFIG[this.type]; + }, + searchItems() { + return ( + this.items?.map((item) => ({ + value: item.username, + text: item.name, + ...item, + })) || [] + ); + }, + component() { + // Note, we can extend this for the component to support other contexts + // https://gitlab.com/gitlab-org/gitlab/-/issues/428865 + return User; + }, + }, + methods: { + async handleSearchInput(search) { + this.$refs.results.open(); + this.items = await this.fetchUsersBySearchTerm(search); + }, + fetchUsersBySearchTerm(search) { + const namespace = this.isProject ? 'project' : 'group'; + return this.$apollo + .query({ + query: usersAutocompleteQuery, + variables: { fullPath: this.projectPath, search, isProject: this.isProject }, + }) + .then(({ data }) => data[namespace]?.autocompleteUsers); + }, + getItemByKey(key) { + return this.searchItems.find((item) => item[this.config.filterKey] === key); + }, + handleSelectItem(key) { + this.$emit('select', this.getItemByKey(key)); + }, + handleDeleteItem(key) { + this.$emit('delete', key); + }, + }, +}; +</script> + +<template> + <gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer"> + <template #header + ><strong + >{{ title }} + <span class="gl-text-gray-500" + ><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span + ></strong + ></template + > + + <gl-collapsible-listbox + ref="results" + v-model="selected" + class="list-selector gl-mb-4 gl-display-block" + :items="searchItems" + multiple + @shown="$refs.search.focusInput()" + > + <template #toggle> + <gl-search-box-by-type + ref="search" + v-model="searchValue" + autofocus + debounce="500" + @input="handleSearchInput" + /> + </template> + + <template #list-item="{ item }"> + <component :is="component" :data="item" @select="handleSelectItem" /> + </template> + </gl-collapsible-listbox> + + <component + :is="component" + v-for="(item, index) of selectedItems" + :key="index" + :class="{ 'gl-border-t': index > 0 }" + class="gl-p-3" + :data="item" + can-delete + @delete="handleDeleteItem" + /> + </gl-card> +</template> diff --git a/app/assets/javascripts/vue_shared/components/list_selector/user.vue b/app/assets/javascripts/vue_shared/components/list_selector/user.vue new file mode 100644 index 00000000000..fdbc767db81 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/list_selector/user.vue @@ -0,0 +1,55 @@ +<script> +import { GlAvatar, GlButton } from '@gitlab/ui'; +import { sprintf, __ } from '~/locale'; + +export default { + name: 'UserItem', + components: { + GlAvatar, + GlButton, + }, + props: { + data: { + type: Object, + required: true, + }, + canDelete: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + deleteButtonLabel() { + return sprintf(__('Delete %{name}'), { name: this.name }); + }, + name() { + return this.data.name; + }, + username() { + return this.data.username; + }, + avatarUrl() { + return this.data.avatarUrl; + }, + }, +}; +</script> + +<template> + <span class="gl-display-flex gl-align-items-center gl-gap-3" @click="$emit('select', username)"> + <gl-avatar :alt="name" :size="32" :src="avatarUrl" /> + <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1"> + <span class="gl-font-weight-bold">{{ name }}</span> + <span class="gl-text-gray-600">@{{ username }}</span> + </span> + + <gl-button + v-if="canDelete" + icon="remove" + :aria-label="deleteButtonLabel" + category="tertiary" + @click="$emit('delete', username)" + /> + </span> +</template> |