diff options
Diffstat (limited to 'app/assets/javascripts/issuable_list')
3 files changed, 346 insertions, 0 deletions
diff --git a/app/assets/javascripts/issuable_list/components/issuable_item.vue b/app/assets/javascripts/issuable_list/components/issuable_item.vue new file mode 100644 index 00000000000..d8cb1ab07cd --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_item.vue @@ -0,0 +1,140 @@ +<script> +import { GlLink, GlLabel, GlTooltipDirective } from '@gitlab/ui'; + +import { __, sprintf } from '~/locale'; +import { getTimeago } from '~/lib/utils/datetime_utility'; +import { isScopedLabel } from '~/lib/utils/common_utils'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; + +export default { + components: { + GlLink, + GlLabel, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [timeagoMixin], + props: { + issuableSymbol: { + type: String, + required: true, + }, + issuable: { + type: Object, + required: true, + }, + }, + computed: { + author() { + return this.issuable.author; + }, + authorId() { + const id = parseInt(this.author.id, 10); + + if (Number.isNaN(id)) { + return this.author.id.includes('gid') + ? this.author.id.split('gid://gitlab/User/').pop() + : ''; + } + + return id; + }, + labels() { + return this.issuable.labels?.nodes || this.issuable.labels || []; + }, + createdAt() { + return sprintf(__('created %{timeAgo}'), { + timeAgo: getTimeago().format(this.issuable.createdAt), + }); + }, + updatedAt() { + return sprintf(__('updated %{timeAgo}'), { + timeAgo: getTimeago().format(this.issuable.updatedAt), + }); + }, + }, + methods: { + scopedLabel(label) { + return isScopedLabel(label); + }, + /** + * This is needed as an independent method since + * when user changes current page, `$refs.authorLink` + * will be null until next page results are loaded & rendered. + */ + getAuthorPopoverTarget() { + if (this.$refs.authorLink) { + return this.$refs.authorLink.$el; + } + return ''; + }, + }, +}; +</script> + +<template> + <li class="issue"> + <div class="issue-box"> + <div class="issuable-info-container"> + <div class="issuable-main-info"> + <div data-testid="issuable-title" class="issue-title title"> + <span class="issue-title-text" dir="auto"> + <gl-link :href="issuable.webUrl">{{ issuable.title }}</gl-link> + </span> + </div> + <div class="issuable-info"> + <span data-testid="issuable-reference" class="issuable-reference" + >{{ issuableSymbol }}{{ issuable.iid }}</span + > + <span class="issuable-authored d-none d-sm-inline-block"> + · + <span + v-gl-tooltip:tooltipcontainer.bottom + data-testid="issuable-created-at" + :title="tooltipTitle(issuable.createdAt)" + >{{ createdAt }}</span + > + {{ __('by') }} + <gl-link + :data-user-id="authorId" + :data-username="author.username" + :data-name="author.name" + :data-avatar-url="author.avatarUrl" + :href="author.webUrl" + data-testid="issuable-author" + class="author-link js-user-link" + > + <span class="author">{{ author.name }}</span> + </gl-link> + </span> + + <gl-label + v-for="(label, index) in labels" + :key="index" + :background-color="label.color" + :title="label.title" + :description="label.description" + :scoped="scopedLabel(label)" + :class="{ 'gl-ml-2': index }" + size="sm" + /> + </div> + </div> + <div class="issuable-meta"> + <div + data-testid="issuable-updated-at" + class="float-right issuable-updated-at d-none d-sm-inline-block" + > + <span + v-gl-tooltip:tooltipcontainer.bottom + :title="tooltipTitle(issuable.updatedAt)" + class="issuable-updated-at" + >{{ updatedAt }}</span + > + </div> + </div> + </div> + </div> + </li> +</template> diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue new file mode 100644 index 00000000000..7535203dea1 --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue @@ -0,0 +1,153 @@ +<script> +import { GlLoadingIcon, GlPagination } from '@gitlab/ui'; + +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; + +import IssuableTabs from './issuable_tabs.vue'; +import IssuableItem from './issuable_item.vue'; + +export default { + components: { + GlLoadingIcon, + IssuableTabs, + FilteredSearchBar, + IssuableItem, + GlPagination, + }, + props: { + namespace: { + type: String, + required: true, + }, + recentSearchesStorageKey: { + type: String, + required: true, + }, + searchInputPlaceholder: { + type: String, + required: true, + }, + searchTokens: { + type: Array, + required: true, + }, + sortOptions: { + type: Array, + required: true, + }, + initialFilterValue: { + type: Array, + required: false, + default: () => [], + }, + initialSortBy: { + type: String, + required: false, + default: 'created_desc', + }, + issuables: { + type: Array, + required: true, + }, + tabs: { + type: Array, + required: true, + }, + tabCounts: { + type: Object, + required: true, + }, + currentTab: { + type: String, + required: true, + }, + issuableSymbol: { + type: String, + required: false, + default: '#', + }, + issuablesLoading: { + type: Boolean, + required: false, + default: false, + }, + showPaginationControls: { + type: Boolean, + required: false, + default: false, + }, + defaultPageSize: { + type: Number, + required: false, + default: 20, + }, + currentPage: { + type: Number, + required: false, + default: 1, + }, + previousPage: { + type: Number, + required: false, + default: 0, + }, + nextPage: { + type: Number, + required: false, + default: 2, + }, + }, +}; +</script> + +<template> + <div class="issuable-list-container"> + <issuable-tabs + :tabs="tabs" + :tab-counts="tabCounts" + :current-tab="currentTab" + @click="$emit('click-tab', $event)" + > + <template #nav-actions> + <slot name="nav-actions"></slot> + </template> + </issuable-tabs> + <filtered-search-bar + :namespace="namespace" + :recent-searches-storage-key="recentSearchesStorageKey" + :search-input-placeholder="searchInputPlaceholder" + :tokens="searchTokens" + :sort-options="sortOptions" + :initial-filter-value="initialFilterValue" + :initial-sort-by="initialSortBy" + class="gl-flex-grow-1 row-content-block" + @onFilter="$emit('filter', $event)" + @onSort="$emit('sort', $event)" + /> + <div class="issuables-holder"> + <gl-loading-icon v-if="issuablesLoading" size="md" class="gl-mt-5" /> + <ul + v-if="!issuablesLoading && issuables.length" + class="content-list issuable-list issues-list" + > + <issuable-item + v-for="issuable in issuables" + :key="issuable.id" + :issuable-symbol="issuableSymbol" + :issuable="issuable" + /> + </ul> + <slot v-if="!issuablesLoading && !issuables.length" name="empty-state"></slot> + <gl-pagination + v-if="showPaginationControls" + :per-page="defaultPageSize" + :value="currentPage" + :prev-page="previousPage" + :next-page="nextPage" + align="center" + class="gl-pagination gl-mt-3" + @input="$emit('page-change', $event)" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/issuable_list/components/issuable_tabs.vue b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue new file mode 100644 index 00000000000..df544ce69e7 --- /dev/null +++ b/app/assets/javascripts/issuable_list/components/issuable_tabs.vue @@ -0,0 +1,53 @@ +<script> +import { GlTabs, GlTab, GlBadge } from '@gitlab/ui'; + +export default { + components: { + GlTabs, + GlTab, + GlBadge, + }, + props: { + tabs: { + type: Array, + required: true, + }, + tabCounts: { + type: Object, + required: true, + }, + currentTab: { + type: String, + required: true, + }, + }, + methods: { + isTabActive(tabName) { + return tabName === this.currentTab; + }, + }, +}; +</script> + +<template> + <div class="top-area"> + <gl-tabs class="nav-links mobile-separator issuable-state-filters"> + <gl-tab + v-for="tab in tabs" + :key="tab.id" + :active="isTabActive(tab.name)" + @click="$emit('click', tab.name)" + > + <template #title> + <span :title="tab.titleTooltip">{{ tab.title }}</span> + <gl-badge variant="neutral" size="sm" class="gl-px-2 gl-py-1!">{{ + tabCounts[tab.name] + }}</gl-badge> + </template> + </gl-tab> + </gl-tabs> + <div class="nav-controls"> + <slot name="nav-actions"></slot> + </div> + </div> +</template> |