diff options
Diffstat (limited to 'app/assets/javascripts/organizations')
9 files changed, 350 insertions, 45 deletions
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue index 2b42c821cd5..10471cc1fdd 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue +++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue @@ -1,53 +1,127 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; -import { __, s__ } from '~/locale'; -import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { createAlert } from '~/alert'; -import projectsQuery from '../graphql/queries/projects.query.graphql'; +import { GlCollapsibleListbox, GlSorting, GlSortingItem } from '@gitlab/ui'; +import { isEqual } from 'lodash'; +import { s__, __ } from '~/locale'; +import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; +import { + filterToQueryObject, + processFilters, + urlQueryToFilter, + prepareTokens, +} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; +import { + FILTERED_SEARCH_TERM, + TOKEN_EMPTY_SEARCH_TERM, +} from '~/vue_shared/components/filtered_search_bar/constants'; +import { + DISPLAY_QUERY_GROUPS, + DISPLAY_QUERY_PROJECTS, + DISPLAY_LISTBOX_ITEMS, + SORT_DIRECTION_ASC, + SORT_DIRECTION_DESC, + SORT_ITEMS, + SORT_ITEM_CREATED, + FILTERED_SEARCH_TERM_KEY, +} from '../constants'; +import GroupsPage from './groups_page.vue'; +import ProjectsPage from './projects_page.vue'; export default { i18n: { pageTitle: __('Groups and projects'), - errorMessage: s__( - 'Organization|An error occurred loading the projects. Please refresh the page to try again.', - ), + searchInputPlaceholder: s__('Organization|Search or filter list'), + displayListboxHeaderText: __('Display'), }, - components: { - ProjectsList, - GlLoadingIcon, + components: { FilteredSearchBar, GlCollapsibleListbox, GlSorting, GlSortingItem }, + filteredSearch: { + tokens: [], + namespace: 'organization_groups_and_projects', + recentSearchesStorageKey: 'organization_groups_and_projects', }, - data() { - return { - projects: [], - }; - }, - apollo: { - projects: { - query: projectsQuery, - update(data) { - return data.organization.projects.nodes; - }, - error(error) { - createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); - }, + displayListboxItems: DISPLAY_LISTBOX_ITEMS, + sortItems: SORT_ITEMS, + computed: { + routerView() { + const { display } = this.$route.query; + + switch (display) { + case DISPLAY_QUERY_GROUPS: + return GroupsPage; + + case DISPLAY_QUERY_PROJECTS: + return ProjectsPage; + + default: + return GroupsPage; + } + }, + activeSortItem() { + return this.$options.sortItems.find((sortItem) => sortItem.name === this.sortName); + }, + sortName() { + return this.$route.query.sort_name || SORT_ITEM_CREATED.name; + }, + isAscending() { + return this.$route.query.sort_direction !== SORT_DIRECTION_DESC; + }, + sortText() { + return this.activeSortItem.text; + }, + filteredSearchValue() { + const tokens = prepareTokens( + urlQueryToFilter(this.$route.query, { + filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, + filterNamesAllowList: [FILTERED_SEARCH_TERM], + }), + ); + + return tokens.length ? tokens : [TOKEN_EMPTY_SEARCH_TERM]; + }, + displayListboxSelected() { + const { display } = this.$route.query; + + return [DISPLAY_QUERY_GROUPS, DISPLAY_QUERY_PROJECTS].includes(display) + ? display + : DISPLAY_QUERY_GROUPS; }, }, - computed: { - formattedProjects() { - return this.projects.map(({ id, nameWithNamespace, accessLevel, ...project }) => ({ - ...project, - id: getIdFromGraphQLId(id), - name: nameWithNamespace, - permissions: { - projectAccess: { - accessLevel: accessLevel.integerValue, - }, - }, - })); - }, - isLoading() { - return this.$apollo.queries.projects?.loading; + methods: { + pushQuery(query) { + const currentQuery = this.$route.query; + + if (isEqual(currentQuery, query)) { + return; + } + + this.$router.push({ query }); + }, + onDisplayListboxSelect(display) { + this.pushQuery({ display }); + }, + onSortItemClick(sortItem) { + if (this.$route.query.sort_name === sortItem.name) { + return; + } + + this.pushQuery({ ...this.$route.query, sort_name: sortItem.name }); + }, + onSortDirectionChange(isAscending) { + this.pushQuery({ + ...this.$route.query, + sort_direction: isAscending ? SORT_DIRECTION_ASC : SORT_DIRECTION_DESC, + }); + }, + onFilter(filters) { + const { display, sort_name, sort_direction } = this.$route.query; + + this.pushQuery({ + display, + sort_name, + sort_direction, + ...filterToQueryObject(processFilters(filters), { + filteredSearchTermKey: FILTERED_SEARCH_TERM_KEY, + }), + }); }, }, }; @@ -56,7 +130,49 @@ export default { <template> <div> <h1 class="gl-font-size-h-display">{{ $options.i18n.pageTitle }}</h1> - <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> - <projects-list v-else :projects="formattedProjects" show-project-icon /> + <div class="gl-p-5 gl-bg-gray-10 gl-border-t gl-border-b"> + <div class="gl-mx-n2 gl-my-n2 gl-md-display-flex"> + <div class="gl-p-2 gl-flex-grow-1"> + <filtered-search-bar + :namespace="$options.filteredSearch.namespace" + :tokens="$options.filteredSearch.tokens" + :initial-filter-value="filteredSearchValue" + sync-filter-and-sort + :recent-searches-storage-key="$options.filteredSearch.recentSearchesStorageKey" + :search-input-placeholder="$options.i18n.searchInputPlaceholder" + @onFilter="onFilter" + /> + </div> + <div class="gl-p-2"> + <gl-collapsible-listbox + :selected="displayListboxSelected" + :items="$options.displayListboxItems" + :header-text="$options.i18n.displayListboxHeaderText" + block + toggle-class="gl-md-w-30" + @select="onDisplayListboxSelect" + /> + </div> + <div class="gl-p-2"> + <gl-sorting + class="gl-display-flex" + dropdown-class="gl-w-full" + :text="sortText" + :is-ascending="isAscending" + @sortDirectionChange="onSortDirectionChange" + > + <gl-sorting-item + v-for="sortItem in $options.sortItems" + :key="sortItem.name" + :active="activeSortItem.name === sortItem.name" + @click="onSortItemClick(sortItem)" + > + {{ sortItem.text }} + </gl-sorting-item> + </gl-sorting> + </div> + </div> + </div> + <component :is="routerView" /> </div> </template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue new file mode 100644 index 00000000000..20db38403f7 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/components/groups_page.vue @@ -0,0 +1,43 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { createAlert } from '~/alert'; +import { s__ } from '~/locale'; +import GroupsList from '~/vue_shared/components/groups_list/groups_list.vue'; +import groupsQuery from '../graphql/queries/groups.query.graphql'; +import { formatGroups } from '../utils'; + +export default { + i18n: { + errorMessage: s__( + 'Organization|An error occurred loading the groups. Please refresh the page to try again.', + ), + }, + components: { GlLoadingIcon, GroupsList }, + data() { + return { + groups: [], + }; + }, + apollo: { + groups: { + query: groupsQuery, + update(data) { + return formatGroups(data.organization.groups.nodes); + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.groups.loading; + }, + }, +}; +</script> + +<template> + <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> + <groups-list v-else :groups="groups" show-group-icon /> +</template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue new file mode 100644 index 00000000000..d6958ee996e --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/components/projects_page.vue @@ -0,0 +1,46 @@ +<script> +import { GlLoadingIcon } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; +import { createAlert } from '~/alert'; +import projectsQuery from '../graphql/queries/projects.query.graphql'; +import { formatProjects } from '../utils'; + +export default { + i18n: { + errorMessage: s__( + 'Organization|An error occurred loading the projects. Please refresh the page to try again.', + ), + }, + components: { + ProjectsList, + GlLoadingIcon, + }, + data() { + return { + projects: [], + }; + }, + apollo: { + projects: { + query: projectsQuery, + update(data) { + return formatProjects(data.organization.projects.nodes); + }, + error(error) { + createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true }); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.projects.loading; + }, + }, +}; +</script> + +<template> + <gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" /> + <projects-list v-else :projects="projects" show-project-icon /> +</template> diff --git a/app/assets/javascripts/organizations/groups_and_projects/constants.js b/app/assets/javascripts/organizations/groups_and_projects/constants.js new file mode 100644 index 00000000000..529caa666a0 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/constants.js @@ -0,0 +1,29 @@ +import { __ } from '~/locale'; + +export const DISPLAY_QUERY_GROUPS = 'groups'; +export const DISPLAY_QUERY_PROJECTS = 'projects'; + +export const ORGANIZATION_ROOT_ROUTE_NAME = 'root'; + +export const FILTERED_SEARCH_TERM_KEY = 'search'; + +export const DISPLAY_LISTBOX_ITEMS = [ + { + value: DISPLAY_QUERY_GROUPS, + text: __('Groups'), + }, + { + value: DISPLAY_QUERY_PROJECTS, + text: __('Projects'), + }, +]; + +export const SORT_DIRECTION_ASC = 'asc'; +export const SORT_DIRECTION_DESC = 'desc'; + +export const SORT_ITEM_CREATED = { + name: 'created', + text: __('Created'), +}; + +export const SORT_ITEMS = [SORT_ITEM_CREATED]; diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql new file mode 100644 index 00000000000..842c601e326 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/groups.query.graphql @@ -0,0 +1,22 @@ +query getOrganizationGroups { + organization @client { + id + groups { + nodes { + id + fullName + parent + webUrl + descriptionHtml + avatarUrl + descendantGroupsCount + projectsCount + groupMembersCount + visibility + accessLevel { + integerValue + } + } + } + } +} diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql index b4cb8c607d4..2a7971e1106 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/queries/projects.query.graphql @@ -15,6 +15,7 @@ query getOrganizationProjects { descriptionHtml issuesAccessLevel forkingAccessLevel + isForked accessLevel { integerValue } diff --git a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js index 794410c2a78..8a375b28797 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js +++ b/app/assets/javascripts/organizations/groups_and_projects/graphql/resolvers.js @@ -1,4 +1,8 @@ -import { organizationProjects } from 'jest/organizations/groups_and_projects/components/mock_data'; +import { + organization, + organizationProjects, + organizationGroups, +} from 'jest/organizations/groups_and_projects/mock_data'; export default { Query: { @@ -8,7 +12,11 @@ export default { setTimeout(resolve, 1000); }); - return organizationProjects; + return { + ...organization, + projects: organizationProjects, + groups: organizationGroups, + }; }, }, }; diff --git a/app/assets/javascripts/organizations/groups_and_projects/index.js b/app/assets/javascripts/organizations/groups_and_projects/index.js index d0790bcc040..f3f15c635f1 100644 --- a/app/assets/javascripts/organizations/groups_and_projects/index.js +++ b/app/assets/javascripts/organizations/groups_and_projects/index.js @@ -1,22 +1,39 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; +import VueRouter from 'vue-router'; import createDefaultClient from '~/lib/graphql'; import resolvers from './graphql/resolvers'; import App from './components/app.vue'; +import { ORGANIZATION_ROOT_ROUTE_NAME } from './constants'; + +export const createRouter = () => { + const routes = [{ path: '/', name: ORGANIZATION_ROOT_ROUTE_NAME }]; + + const router = new VueRouter({ + routes, + base: '/', + mode: 'history', + }); + + return router; +}; export const initOrganizationsGroupsAndProjects = () => { const el = document.getElementById('js-organizations-groups-and-projects'); if (!el) return false; + Vue.use(VueRouter); const apolloProvider = new VueApollo({ defaultClient: createDefaultClient(resolvers), }); + const router = createRouter(); return new Vue({ el, name: 'OrganizationsGroupsAndProjects', apolloProvider, + router, render(createElement) { return createElement(App); }, diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/groups_and_projects/utils.js new file mode 100644 index 00000000000..d2a4e05e806 --- /dev/null +++ b/app/assets/javascripts/organizations/groups_and_projects/utils.js @@ -0,0 +1,23 @@ +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants'; + +export const formatProjects = (projects) => + projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({ + ...project, + id: getIdFromGraphQLId(id), + name: nameWithNamespace, + permissions: { + projectAccess: { + accessLevel: accessLevel.integerValue, + }, + }, + webUrl, + editPath: `${webUrl}/edit`, + actions: [ACTION_EDIT, ACTION_DELETE], + })); + +export const formatGroups = (groups) => + groups.map(({ id, ...group }) => ({ + ...group, + id: getIdFromGraphQLId(id), + })); |