diff options
Diffstat (limited to 'app/assets/javascripts/jira_connect')
10 files changed, 294 insertions, 88 deletions
diff --git a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue index 66fcb8e10eb..46c27c33f56 100644 --- a/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue +++ b/app/assets/javascripts/jira_connect/branches/components/new_branch_form.vue @@ -1,5 +1,6 @@ <script> -import { GlFormGroup, GlButton, GlFormInput, GlForm, GlAlert } from '@gitlab/ui'; +import { GlFormGroup, GlButton, GlFormInput, GlForm, GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { helpPagePath } from '~/helpers/help_page_helper'; import { CREATE_BRANCH_ERROR_GENERIC, CREATE_BRANCH_ERROR_WITH_CONTEXT, @@ -7,6 +8,7 @@ import { I18N_NEW_BRANCH_LABEL_BRANCH, I18N_NEW_BRANCH_LABEL_SOURCE, I18N_NEW_BRANCH_SUBMIT_BUTTON_TEXT, + I18N_NEW_BRANCH_PERMISSION_ALERT, } from '../constants'; import createBranchMutation from '../graphql/mutations/create_branch.mutation.graphql'; import ProjectDropdown from './project_dropdown.vue'; @@ -17,6 +19,8 @@ const DEFAULT_ALERT_PARAMS = { title: '', message: '', variant: DEFAULT_ALERT_VARIANT, + link: undefined, + dismissible: true, }; export default { @@ -27,10 +31,16 @@ export default { GlFormInput, GlForm, GlAlert, + GlSprintf, + GlLink, ProjectDropdown, SourceBranchDropdown, }, - inject: ['initialBranchName'], + inject: { + initialBranchName: { + default: '', + }, + }, data() { return { selectedProject: null, @@ -40,6 +50,7 @@ export default { alertParams: { ...DEFAULT_ALERT_PARAMS, }, + hasPermission: false, }; }, computed: { @@ -49,19 +60,38 @@ export default { showAlert() { return Boolean(this.alertParams?.message); }, + isBranchNameValid() { + return (this.branchName ?? '').trim().length > 0; + }, disableSubmitButton() { - return !(this.selectedProject && this.selectedSourceBranchName && this.branchName); + return !(this.selectedProject && this.selectedSourceBranchName && this.isBranchNameValid); }, }, methods: { - displayAlert({ title, message, variant = DEFAULT_ALERT_VARIANT } = {}) { + displayAlert({ + title, + message, + variant = DEFAULT_ALERT_VARIANT, + link, + dismissible = true, + } = {}) { this.alertParams = { title, message, variant, + link, + dismissible, }; }, - onAlertDismiss() { + setPermissionAlert() { + this.displayAlert({ + message: I18N_NEW_BRANCH_PERMISSION_ALERT, + variant: 'warning', + link: helpPagePath('user/permissions', { anchor: 'project-members-permissions' }), + dismissible: false, + }); + }, + dismissAlert() { this.alertParams = { ...DEFAULT_ALERT_PARAMS, }; @@ -69,6 +99,14 @@ export default { onProjectSelect(project) { this.selectedProject = project; this.selectedSourceBranchName = null; // reset branch selection + this.hasPermission = this.selectedProject.userPermissions.pushCode; + + if (!this.hasPermission) { + this.setPermissionAlert(); + } else { + // clear alert if the user has permissions for the newly-selected project. + this.dismissAlert(); + } }, onSourceBranchSelect(branchName) { this.selectedSourceBranchName = branchName; @@ -127,10 +165,18 @@ export default { class="gl-mb-5" :variant="alertParams.variant" :title="alertParams.title" - @dismiss="onAlertDismiss" + :dismissible="alertParams.dismissible" + @dismiss="dismissAlert" > - {{ alertParams.message }} + <gl-sprintf :message="alertParams.message"> + <template #link="{ content }"> + <gl-link :href="alertParams.link" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> </gl-alert> + <gl-form-group :label="$options.i18n.I18N_NEW_BRANCH_LABEL_DROPDOWN" label-for="project-select"> <project-dropdown id="project-select" @@ -140,25 +186,28 @@ export default { /> </gl-form-group> - <gl-form-group - :label="$options.i18n.I18N_NEW_BRANCH_LABEL_BRANCH" - label-for="branch-name-input" - > - <gl-form-input id="branch-name-input" v-model="branchName" type="text" required /> - </gl-form-group> + <template v-if="selectedProject && hasPermission"> + <gl-form-group + :label="$options.i18n.I18N_NEW_BRANCH_LABEL_SOURCE" + label-for="source-branch-select" + > + <source-branch-dropdown + id="source-branch-select" + :selected-project="selectedProject" + :selected-branch-name="selectedSourceBranchName" + @change="onSourceBranchSelect" + @error="onError" + /> + </gl-form-group> - <gl-form-group - :label="$options.i18n.I18N_NEW_BRANCH_LABEL_SOURCE" - label-for="source-branch-select" - > - <source-branch-dropdown - id="source-branch-select" - :selected-project="selectedProject" - :selected-branch-name="selectedSourceBranchName" - @change="onSourceBranchSelect" - @error="onError" - /> - </gl-form-group> + <gl-form-group + :label="$options.i18n.I18N_NEW_BRANCH_LABEL_BRANCH" + label-for="branch-name-input" + class="gl-max-w-62" + > + <gl-form-input id="branch-name-input" v-model="branchName" type="text" required /> + </gl-form-group> + </template> <div class="form-actions"> <gl-button diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue index 751f3e9639d..88005cccd89 100644 --- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue +++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue @@ -1,5 +1,11 @@ <script> -import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui'; +import { + GlDropdown, + GlSearchBoxByType, + GlLoadingIcon, + GlDropdownItem, + GlAvatarLabeled, +} from '@gitlab/ui'; import { __ } from '~/locale'; import { PROJECTS_PER_PAGE } from '../constants'; import getProjectsQuery from '../graphql/queries/get_projects.query.graphql'; @@ -14,6 +20,7 @@ export default { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, + GlAvatarLabeled, }, props: { selectedProject: { @@ -56,7 +63,7 @@ export default { return Boolean(this.$apollo.queries.projects.loading); }, projectDropdownText() { - return this.selectedProject?.nameWithNamespace || __('Select a project'); + return this.selectedProject?.nameWithNamespace || this.$options.i18n.selectProjectText; }, }, methods: { @@ -70,11 +77,19 @@ export default { return project.id === this.selectedProject?.id; }, }, + i18n: { + selectProjectText: __('Select a project'), + }, }; </script> <template> - <gl-dropdown :text="projectDropdownText" :loading="initialProjectsLoading"> + <gl-dropdown + :text="projectDropdownText" + :loading="initialProjectsLoading" + menu-class="gl-w-auto!" + :header-text="$options.i18n.selectProjectText" + > <template #header> <gl-search-box-by-type v-model.trim="projectSearchQuery" :debounce="250" /> </template> @@ -85,10 +100,20 @@ export default { v-for="project in projects" :key="project.id" is-check-item + is-check-centered :is-checked="isProjectSelected(project)" + :data-testid="`test-project-${project.id}`" @click="onProjectSelect(project)" > - {{ project.nameWithNamespace }} + <gl-avatar-labeled + class="gl-text-truncate" + shape="rect" + :size="32" + :src="project.avatarUrl" + :label="project.name" + :entity-name="project.name" + :sub-label="project.nameWithNamespace" + /> </gl-dropdown-item> </template> </gl-dropdown> diff --git a/app/assets/javascripts/jira_connect/branches/constants.js b/app/assets/javascripts/jira_connect/branches/constants.js index ab9d3b2c110..43be774ce7c 100644 --- a/app/assets/javascripts/jira_connect/branches/constants.js +++ b/app/assets/javascripts/jira_connect/branches/constants.js @@ -23,3 +23,6 @@ export const I18N_NEW_BRANCH_SUCCESS_TITLE = s__( export const I18N_NEW_BRANCH_SUCCESS_MESSAGE = s__( 'JiraConnect|You can now close this window and return to Jira.', ); +export const I18N_NEW_BRANCH_PERMISSION_ALERT = s__( + "JiraConnect|You don't have permission to create branches for this project. Select a different project or contact the project owner for access. %{linkStart}Learn more.%{linkEnd}", +); diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql index 32fbc1113bc..03e8e3e986b 100644 --- a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql +++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql @@ -26,6 +26,9 @@ query jiraGetProjects( repository { empty } + userPermissions { + pushCode + } } pageInfo { ...PageInfo diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue index 5a49d7c1a90..7f035dddafe 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/add_namespace_modal/groups_list.vue @@ -30,7 +30,8 @@ export default { page: 1, totalItems: 0, errorMessage: null, - searchTerm: '', + userSearchTerm: '', + searchValue: '', }; }, computed: { @@ -45,16 +46,11 @@ export default { }, methods: { loadGroups() { - // fetchGroups returns no results for search terms 0 < {length} < 3. - // The desired UX is to return the unfiltered results for searches {length} < 3. - // Here, we set the search to an empty string if {length} < 3 - const search = this.searchTerm?.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.searchTerm; - this.isLoadingMore = true; return fetchGroups(this.groupsPath, { page: this.page, perPage: this.$options.DEFAULT_GROUPS_PER_PAGE, - search, + search: this.searchValue, }) .then((response) => { const { page, total } = parseIntPagination(normalizeHeaders(response.headers)); @@ -69,12 +65,24 @@ export default { this.isLoadingMore = false; }); }, - onGroupSearch(searchTerm) { - // keep a copy of the search term for pagination - this.searchTerm = searchTerm; - // reset the current page + onGroupSearch(userSearchTerm = '') { + this.userSearchTerm = userSearchTerm; + + // fetchGroups returns no results for search terms 0 < {length} < 3. + // The desired UX is to return the unfiltered results for searches {length} < 3. + // Here, we set the search to an empty string '' if {length} < 3 + const newSearchValue = + this.userSearchTerm.length < MINIMUM_SEARCH_TERM_LENGTH ? '' : this.userSearchTerm; + + // don't fetch new results if the search value didn't change. + if (newSearchValue === this.searchValue) { + return; + } + + // reset the page. this.page = 1; - return this.loadGroups(); + this.searchValue = newSearchValue; + this.loadGroups(); }, }, DEFAULT_GROUPS_PER_PAGE, @@ -92,7 +100,7 @@ export default { debounce="500" :placeholder="__('Search by name')" :is-loading="isLoadingMore" - :value="searchTerm" + :value="userSearchTerm" @input="onGroupSearch" /> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 7fd4cc38f11..905e242e977 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -1,13 +1,13 @@ <script> -import { GlAlert, GlLink, GlSprintf, GlEmptyState } from '@gitlab/ui'; +import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui'; import { isEmpty } from 'lodash'; import { mapState, mapMutations } from 'vuex'; import { retrieveAlert } from '~/jira_connect/subscriptions/utils'; import { SET_ALERT } from '../store/mutation_types'; -import SubscriptionsList from './subscriptions_list.vue'; -import AddNamespaceButton from './add_namespace_button.vue'; -import SignInButton from './sign_in_button.vue'; +import SignInPage from '../pages/sign_in.vue'; +import SubscriptionsPage from '../pages/subscriptions.vue'; import UserLink from './user_link.vue'; +import CompatibilityAlert from './compatibility_alert.vue'; export default { name: 'JiraConnectApp', @@ -15,11 +15,10 @@ export default { GlAlert, GlLink, GlSprintf, - GlEmptyState, - SubscriptionsList, - AddNamespaceButton, - SignInButton, UserLink, + CompatibilityAlert, + SignInPage, + SubscriptionsPage, }, inject: { usersPath: { @@ -58,11 +57,14 @@ export default { <template> <div> + <compatibility-alert /> + <gl-alert v-if="shouldShowAlert" class="gl-mb-7" :variant="alert.variant" :title="alert.title" + data-testid="jira-connect-persisted-alert" @dismiss="setAlert" > <gl-sprintf v-if="alert.linkUrl" :message="alert.message"> @@ -79,43 +81,9 @@ export default { <user-link :user-signed-in="userSignedIn" :has-subscriptions="hasSubscriptions" /> <h2 class="gl-text-center gl-mb-7">{{ s__('JiraService|GitLab for Jira Configuration') }}</h2> - <div class="jira-connect-app-body gl-mx-auto gl-px-5 gl-mb-7"> - <template v-if="hasSubscriptions"> - <div class="gl-display-flex gl-justify-content-end"> - <sign-in-button v-if="!userSignedIn" :users-path="usersPath" /> - <add-namespace-button v-else /> - </div> - - <subscriptions-list /> - </template> - <template v-else> - <div v-if="!userSignedIn" class="gl-text-center"> - <p class="gl-mb-7">{{ s__('JiraService|Sign in to GitLab.com to get started.') }}</p> - <sign-in-button class="gl-mb-7" :users-path="usersPath"> - {{ __('Sign in to GitLab') }} - </sign-in-button> - <p> - {{ - s__( - 'Integrations|Note: this integration only works with accounts on GitLab.com (SaaS).', - ) - }} - </p> - </div> - <gl-empty-state - v-else - :title="s__('Integrations|No linked namespaces')" - :description=" - s__( - 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.', - ) - " - > - <template #actions> - <add-namespace-button /> - </template> - </gl-empty-state> - </template> + <div class="gl-layout-w-limited gl-mx-auto gl-px-5 gl-mb-7"> + <sign-in-page v-if="!userSignedIn" :has-subscriptions="hasSubscriptions" /> + <subscriptions-page v-else :has-subscriptions="hasSubscriptions" /> </div> </div> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue b/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue new file mode 100644 index 00000000000..3cfbd87ac53 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/components/compatibility_alert.vue @@ -0,0 +1,63 @@ +<script> +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; + +const COMPATIBILITY_ALERT_STATE_KEY = 'compatibility_alert_dismissed'; + +export default { + name: 'CompatibilityAlert', + components: { + GlAlert, + GlSprintf, + GlLink, + LocalStorageSync, + }, + data() { + return { + alertDismissed: false, + }; + }, + computed: { + shouldShowAlert() { + return !this.alertDismissed; + }, + }, + methods: { + dismissAlert() { + this.alertDismissed = true; + }, + }, + i18n: { + title: s__('Integrations|Known limitations'), + body: s__( + 'Integrations|This integration only works with GitLab.com. Adding a namespace only works in browsers that allow cross-site cookies. %{linkStart}Learn more%{linkEnd}.', + ), + }, + DOCS_LINK_URL: helpPagePath('integration/jira/connect-app'), + COMPATIBILITY_ALERT_STATE_KEY, +}; +</script> +<template> + <local-storage-sync + v-model="alertDismissed" + :storage-key="$options.COMPATIBILITY_ALERT_STATE_KEY" + > + <gl-alert + v-if="shouldShowAlert" + class="gl-mb-7" + variant="info" + :title="$options.i18n.title" + @dismiss="dismissAlert" + > + <gl-sprintf :message="$options.i18n.body"> + <template #link="{ content }"> + <gl-link :href="$options.DOCS_LINK_URL" target="_blank" rel="noopener noreferrer">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </gl-alert> + </local-storage-sync> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue index dc0a77e99c2..627abcdd4a0 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/sign_in_button.vue @@ -1,6 +1,7 @@ <script> import { GlButton } from '@gitlab/ui'; import { getGitlabSignInURL } from '~/jira_connect/subscriptions/utils'; +import { s__ } from '~/locale'; export default { components: { @@ -25,12 +26,15 @@ export default { this.signInURL = await getGitlabSignInURL(this.usersPath); }, }, + i18n: { + defaultButtonText: s__('Integrations|Sign in to GitLab'), + }, }; </script> <template> <gl-button category="primary" variant="info" :href="signInURL" target="_blank"> <slot> - {{ s__('Integrations|Sign in to add namespaces') }} + {{ $options.i18n.defaultButtonText }} </slot> </gl-button> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue new file mode 100644 index 00000000000..2bce5afc72b --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in.vue @@ -0,0 +1,40 @@ +<script> +import { s__ } from '~/locale'; +import SubscriptionsList from '../components/subscriptions_list.vue'; +import SignInButton from '../components/sign_in_button.vue'; + +export default { + name: 'SignInPage', + components: { + SubscriptionsList, + SignInButton, + }, + inject: ['usersPath'], + props: { + hasSubscriptions: { + type: Boolean, + required: true, + }, + }, + i18n: { + signinButtonTextWithSubscriptions: s__('Integrations|Sign in to add namespaces'), + signInText: s__('JiraService|Sign in to GitLab.com to get started.'), + }, +}; +</script> + +<template> + <div v-if="hasSubscriptions"> + <div class="gl-display-flex gl-justify-content-end"> + <sign-in-button :users-path="usersPath"> + {{ $options.i18n.signinButtonTextWithSubscriptions }} + </sign-in-button> + </div> + + <subscriptions-list /> + </div> + <div v-else class="gl-text-center"> + <p class="gl-mb-7">{{ $options.i18n.signInText }}</p> + <sign-in-button class="gl-mb-7" :users-path="usersPath" /> + </div> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue new file mode 100644 index 00000000000..426f2999370 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/subscriptions.vue @@ -0,0 +1,43 @@ +<script> +import { GlEmptyState } from '@gitlab/ui'; +import SubscriptionsList from '../components/subscriptions_list.vue'; +import AddNamespaceButton from '../components/add_namespace_button.vue'; + +export default { + name: 'SubscriptionsPage', + components: { + GlEmptyState, + SubscriptionsList, + AddNamespaceButton, + }, + props: { + hasSubscriptions: { + type: Boolean, + required: true, + }, + }, +}; +</script> + +<template> + <div v-if="hasSubscriptions"> + <div class="gl-display-flex gl-justify-content-end"> + <add-namespace-button /> + </div> + + <subscriptions-list /> + </div> + <gl-empty-state + v-else + :title="s__('Integrations|No linked namespaces')" + :description=" + s__( + 'Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.', + ) + " + > + <template #actions> + <add-namespace-button /> + </template> + </gl-empty-state> +</template> |