diff options
Diffstat (limited to 'app/assets/javascripts/groups')
8 files changed, 236 insertions, 62 deletions
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue index 0bd7371d39b..15f5a3518a5 100644 --- a/app/assets/javascripts/groups/components/app.vue +++ b/app/assets/javascripts/groups/components/app.vue @@ -1,6 +1,6 @@ <script> import { GlLoadingIcon, GlModal } from '@gitlab/ui'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility'; import { HIDDEN_CLASS } from '~/lib/utils/constants'; import { __, s__, sprintf } from '~/locale'; @@ -51,7 +51,6 @@ export default { isModalVisible: false, isLoading: true, isSearchEmpty: false, - searchEmptyMessage: '', targetGroup: null, targetParentGroup: null, showEmptyState: false, @@ -88,15 +87,12 @@ export default { }, }, created() { - this.searchEmptyMessage = this.hideProjects - ? COMMON_STR.GROUP_SEARCH_EMPTY - : COMMON_STR.GROUP_PROJECT_SEARCH_EMPTY; - eventHub.$on(`${this.action}fetchPage`, this.fetchPage); eventHub.$on(`${this.action}toggleChildren`, this.toggleChildren); eventHub.$on(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal); eventHub.$on(`${this.action}updatePagination`, this.updatePagination); eventHub.$on(`${this.action}updateGroups`, this.updateGroups); + eventHub.$on(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups); }, mounted() { this.fetchAllGroups(); @@ -111,6 +107,7 @@ export default { eventHub.$off(`${this.action}showLeaveGroupModal`, this.showLeaveGroupModal); eventHub.$off(`${this.action}updatePagination`, this.updatePagination); eventHub.$off(`${this.action}updateGroups`, this.updateGroups); + eventHub.$off(`${this.action}fetchFilteredAndSortedGroups`, this.fetchFilteredAndSortedGroups); }, methods: { hideModal() { @@ -132,7 +129,7 @@ export default { this.isLoading = false; window.scrollTo({ top: 0, behavior: 'smooth' }); - createFlash({ message: COMMON_STR.FAILURE }); + createAlert({ message: COMMON_STR.FAILURE }); }); }, fetchAllGroups() { @@ -153,6 +150,18 @@ export default { this.updateGroups(res, Boolean(this.filterGroupsBy)); }); }, + fetchFilteredAndSortedGroups({ filterGroupsBy, sortBy }) { + this.isLoading = true; + + return this.fetchGroups({ + filterGroupsBy, + sortBy, + updatePagination: true, + }).then((res) => { + this.isLoading = false; + this.updateGroups(res, Boolean(filterGroupsBy)); + }); + }, fetchPage({ page, filterGroupsBy, sortBy, archived }) { this.isLoading = true; @@ -218,7 +227,7 @@ export default { if (err.status === 403) { message = COMMON_STR.LEAVE_FORBIDDEN; } - createFlash({ message }); + createAlert({ message }); this.targetGroup.isBeingRemoved = false; }); }, @@ -245,7 +254,7 @@ export default { const hasGroups = groups && groups.length > 0; if (this.renderEmptyState) { - this.isSearchEmpty = this.filterGroupsBy !== null && !hasGroups; + this.isSearchEmpty = fromSearch && !hasGroups; } else { this.isSearchEmpty = !hasGroups; } @@ -280,7 +289,6 @@ export default { v-else :groups="groups" :search-empty="isSearchEmpty" - :search-empty-message="searchEmptyMessage" :page-info="pageInfo" :action="action" /> diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue index 3a05c308a2a..43aa0753082 100644 --- a/app/assets/javascripts/groups/components/groups.vue +++ b/app/assets/javascripts/groups/components/groups.vue @@ -1,11 +1,18 @@ <script> +import { GlEmptyState } from '@gitlab/ui'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { getParameterByName } from '~/lib/utils/url_utility'; +import { __ } from '~/locale'; import eventHub from '../event_hub'; export default { + i18n: { + emptyStateTitle: __('No results found'), + emptyStateDescription: __('Edit your search and try again'), + }, components: { PaginationLinks, + GlEmptyState, }, props: { groups: { @@ -20,10 +27,6 @@ export default { type: Boolean, required: true, }, - searchEmptyMessage: { - type: String, - required: true, - }, action: { type: String, required: false, @@ -43,12 +46,11 @@ export default { <template> <div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container"> - <div + <gl-empty-state v-if="searchEmpty" - class="has-no-search-results gl-font-style-italic gl-text-center gl-text-gray-600 gl-p-5" - > - {{ searchEmptyMessage }} - </div> + :title="$options.i18n.emptyStateTitle" + :description="$options.i18n.emptyStateDescription" + /> <template v-else> <group-folder :groups="groups" :action="action" /> <pagination-links diff --git a/app/assets/javascripts/groups/components/new_top_level_group_alert.vue b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue new file mode 100644 index 00000000000..c6af6cdb59f --- /dev/null +++ b/app/assets/javascripts/groups/components/new_top_level_group_alert.vue @@ -0,0 +1,40 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +export default { + name: 'NewTopLevelGroupAlert', + components: { + GlAlert, + UserCalloutDismisser, + }, + i18n: { + titleText: s__("Groups|You're creating a new top-level group"), + bodyText: s__( + 'Groups|Members, projects, trials, and paid subscriptions are tied to a specific top-level group. If you are already a member of a top-level group, you can create a subgroup so your new work is part of your existing top-level group. Do you want to create a subgroup instead?', + ), + primaryBtnText: s__('Groups|Learn more about subgroups'), + }, + subgroupsDocsPath: helpPagePath('user/group/subgroups/index'), +}; +</script> + +<template> + <user-callout-dismisser feature-name="new_top_level_group_alert"> + <template #default="{ dismiss, shouldShowCallout }"> + <gl-alert + v-if="shouldShowCallout" + ref="newTopLevelAlert" + data-testid="new-top-level-alert" + :title="$options.i18n.titleText" + :primary-button-text="$options.i18n.primaryBtnText" + :primary-button-link="$options.subgroupsDocsPath" + @dismiss="dismiss" + > + {{ $options.i18n.bodyText }} + </gl-alert> + </template> + </user-callout-dismisser> +</template> diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue index 325e42af0f8..d0c5846ac88 100644 --- a/app/assets/javascripts/groups/components/overview_tabs.vue +++ b/app/assets/javascripts/groups/components/overview_tabs.vue @@ -1,58 +1,77 @@ <script> -import { GlTabs, GlTab } from '@gitlab/ui'; -import { isString } from 'lodash'; +import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui'; +import { isString, debounce } from 'lodash'; import { __ } from '~/locale'; +import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants'; import GroupsStore from '../store/groups_store'; import GroupsService from '../service/groups_service'; import { ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED, + OVERVIEW_TABS_SORTING_ITEMS, } from '../constants'; +import eventHub from '../event_hub'; import GroupsApp from './app.vue'; +const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS; + export default { - components: { GlTabs, GlTab, GroupsApp }, - inject: ['endpoints'], + components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem }, + inject: ['endpoints', 'initialSort'], data() { + const tabs = [ + { + title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS], + key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + renderEmptyState: true, + lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, + service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), + store: new GroupsStore({ showSchemaMarkup: true }), + }, + { + title: this.$options.i18n[ACTIVE_TAB_SHARED], + key: ACTIVE_TAB_SHARED, + renderEmptyState: false, + lazy: this.$route.name !== ACTIVE_TAB_SHARED, + service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]), + store: new GroupsStore(), + }, + { + title: this.$options.i18n[ACTIVE_TAB_ARCHIVED], + key: ACTIVE_TAB_ARCHIVED, + renderEmptyState: false, + lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED, + service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]), + store: new GroupsStore(), + }, + ]; return { - tabs: [ - { - title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS], - key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS, - renderEmptyState: true, - lazy: false, - service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]), - store: new GroupsStore({ showSchemaMarkup: true }), - }, - { - title: this.$options.i18n[ACTIVE_TAB_SHARED], - key: ACTIVE_TAB_SHARED, - renderEmptyState: false, - lazy: true, - service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]), - store: new GroupsStore(), - }, - { - title: this.$options.i18n[ACTIVE_TAB_ARCHIVED], - key: ACTIVE_TAB_ARCHIVED, - renderEmptyState: false, - lazy: true, - service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]), - store: new GroupsStore(), - }, - ], - activeTabIndex: 0, + tabs, + activeTabIndex: tabs.findIndex((tab) => tab.key === this.$route.name), + sort: SORTING_ITEM_NAME, + isAscending: true, + search: '', }; }, + computed: { + activeTab() { + return this.tabs[this.activeTabIndex]; + }, + sortQueryStringValue() { + return this.isAscending ? this.sort.asc : this.sort.desc; + }, + }, mounted() { - const activeTabIndex = this.tabs.findIndex((tab) => tab.key === this.$route.name); - - if (activeTabIndex === -1) { - return; - } + this.search = this.$route.query?.filter || ''; - this.activeTabIndex = activeTabIndex; + const sortQueryStringValue = this.$route.query?.sort || this.initialSort; + const sort = + OVERVIEW_TABS_SORTING_ITEMS.find((sortOption) => + [sortOption.asc, sortOption.desc].includes(sortQueryStringValue), + ) || SORTING_ITEM_NAME; + this.sort = sort; + this.isAscending = sort.asc === sortQueryStringValue; }, methods: { handleTabInput(tabIndex) { @@ -72,14 +91,64 @@ export default { ? this.$route.params.group.split('/') : this.$route.params.group; - this.$router.push({ name: tab.key, params: { group: groupParam } }); + this.$router.push({ name: tab.key, params: { group: groupParam }, query: this.$route.query }); + }, + handleSearchOrSortChange() { + // Update query string + const query = {}; + if (this.sortQueryStringValue !== this.initialSort) { + query.sort = this.isAscending ? this.sort.asc : this.sort.desc; + } + if (this.search) { + query.filter = this.search; + } + this.$router.push({ query }); + + // Reset `lazy` prop so that groups/projects are fetched with updated `sort` and `filter` params when switching tabs + this.tabs.forEach((tab, index) => { + if (index === this.activeTabIndex) { + return; + } + // eslint-disable-next-line no-param-reassign + tab.lazy = true; + }); + + // Update data + eventHub.$emit(`${this.activeTab.key}fetchFilteredAndSortedGroups`, { + filterGroupsBy: this.search, + sortBy: this.sortQueryStringValue, + }); + }, + handleSortDirectionChange() { + this.isAscending = !this.isAscending; + + this.handleSearchOrSortChange(); + }, + handleSortingItemClick(sortingItem) { + if (sortingItem === this.sort) { + return; + } + + this.sort = sortingItem; + + this.handleSearchOrSortChange(); + }, + handleSearchInput(value) { + this.search = value; + + this.debouncedSearch(); }, + debouncedSearch: debounce(async function debouncedSearch() { + this.handleSearchOrSortChange(); + }, DEBOUNCE_DELAY), }, i18n: { [ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]: __('Subgroups and projects'), [ACTIVE_TAB_SHARED]: __('Shared projects'), [ACTIVE_TAB_ARCHIVED]: __('Archived projects'), + searchPlaceholder: __('Search'), }, + OVERVIEW_TABS_SORTING_ITEMS, }; </script> @@ -99,5 +168,37 @@ export default { :render-empty-state="renderEmptyState" /> </gl-tab> + <template #tabs-end> + <li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2"> + <div class="gl-lg-display-flex gl-justify-content-end gl-mx-n2 gl-my-n2"> + <div class="gl-p-2 gl-lg-form-input-md gl-w-full"> + <gl-search-box-by-type + :value="search" + :placeholder="$options.i18n.searchPlaceholder" + data-qa-selector="groups_filter_field" + @input="handleSearchInput" + /> + </div> + <div class="gl-p-2 gl-w-full gl-lg-w-auto"> + <gl-sorting + class="gl-w-full" + dropdown-class="gl-w-full" + data-testid="group_sort_by_dropdown" + :text="sort.label" + :is-ascending="isAscending" + @sortDirectionChange="handleSortDirectionChange" + > + <gl-sorting-item + v-for="sortingItem in $options.OVERVIEW_TABS_SORTING_ITEMS" + :key="sortingItem.label" + :active="sortingItem === sort" + @click="handleSortingItemClick(sortingItem)" + >{{ sortingItem.label }}</gl-sorting-item + > + </gl-sorting> + </div> + </div> + </li> + </template> </gl-tabs> </template> diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue index 7e7282a27b0..e28459811d7 100644 --- a/app/assets/javascripts/groups/components/transfer_group_form.vue +++ b/app/assets/javascripts/groups/components/transfer_group_form.vue @@ -2,7 +2,7 @@ import { GlFormGroup } from '@gitlab/ui'; import { __, s__ } from '~/locale'; import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; -import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue'; +import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select_deprecated.vue'; export const i18n = { confirmationMessage: __( diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js index 223c2975c11..6fb12cd6270 100644 --- a/app/assets/javascripts/groups/constants.js +++ b/app/assets/javascripts/groups/constants.js @@ -24,8 +24,6 @@ export const COMMON_STR = { EDIT_BTN_TITLE: s__('GroupsTree|Edit'), REMOVE_BTN_TITLE: s__('GroupsTree|Delete'), OPTIONS_DROPDOWN_TITLE: s__('GroupsTree|Options'), - GROUP_SEARCH_EMPTY: s__('GroupsTree|No groups matched your search'), - GROUP_PROJECT_SEARCH_EMPTY: s__('GroupsTree|No groups or projects matched your search'), }; export const ITEM_TYPE = { @@ -62,3 +60,26 @@ export const VISIBILITY_TYPE_ICON = { [VISIBILITY_LEVEL_INTERNAL_STRING]: 'shield', [VISIBILITY_LEVEL_PRIVATE_STRING]: 'lock', }; + +export const OVERVIEW_TABS_SORTING_ITEMS = [ + { + label: __('Name'), + asc: 'name_asc', + desc: 'name_desc', + }, + { + label: __('Created'), + asc: 'created_asc', + desc: 'created_desc', + }, + { + label: __('Updated'), + asc: 'latest_activity_asc', + desc: 'latest_activity_desc', + }, + { + label: __('Stars'), + asc: 'stars_asc', + desc: 'stars_desc', + }, +]; diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js index 4fa3682c729..664d07ca13d 100644 --- a/app/assets/javascripts/groups/init_overview_tabs.js +++ b/app/assets/javascripts/groups/init_overview_tabs.js @@ -51,6 +51,7 @@ export const initGroupOverviewTabs = () => { subgroupsAndProjectsEndpoint, sharedProjectsEndpoint, archivedProjectsEndpoint, + initialSort, } = el.dataset; return new Vue({ @@ -70,6 +71,7 @@ export const initGroupOverviewTabs = () => { [ACTIVE_TAB_SHARED]: sharedProjectsEndpoint, [ACTIVE_TAB_ARCHIVED]: archivedProjectsEndpoint, }, + initialSort, }, render(createElement) { return createElement(OverviewTabs); diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue index 28f059fa23e..db8e424e166 100644 --- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue +++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue @@ -1,7 +1,7 @@ <script> import { GlDropdown, GlDropdownItem, GlDropdownSectionHeader, GlSearchBoxByType } from '@gitlab/ui'; import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __, s__, n__ } from '~/locale'; import { getSubGroups } from '../api/access_dropdown_api'; import { LEVEL_TYPES } from '../constants'; @@ -98,7 +98,7 @@ export default { this.consolidateData(groupsResponse.data); this.setSelected({ initial }); }) - .catch(() => createFlash({ message: __('Failed to load groups.') })) + .catch(() => createAlert({ message: __('Failed to load groups.') })) .finally(() => { this.initialLoading = false; this.loading = false; |