diff options
Diffstat (limited to 'app/assets/javascripts')
23 files changed, 342 insertions, 112 deletions
diff --git a/app/assets/javascripts/import/details/api.js b/app/assets/javascripts/import/details/api.js new file mode 100644 index 00000000000..1fb3ee526d7 --- /dev/null +++ b/app/assets/javascripts/import/details/api.js @@ -0,0 +1,11 @@ +import axios from '~/lib/utils/axios_utils'; + +export const fetchImportFailures = (failuresPath, { projectId, page, perPage }) => { + return axios.get(failuresPath, { + params: { + project_id: projectId, + page, + per_page: perPage, + }, + }); +}; diff --git a/app/assets/javascripts/import/details/components/import_details_app.vue b/app/assets/javascripts/import/details/components/import_details_app.vue index 86820663025..13483fa8ba2 100644 --- a/app/assets/javascripts/import/details/components/import_details_app.vue +++ b/app/assets/javascripts/import/details/components/import_details_app.vue @@ -4,13 +4,6 @@ import ImportDetailsTable from './import_details_table.vue'; export default { components: { ImportDetailsTable }, - props: { - project: { - type: Object, - required: false, - default: () => ({}), - }, - }, i18n: { pageTitle: s__('Import|GitHub import details'), }, diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue index 9ce58e8a9bc..b32b5778265 100644 --- a/app/assets/javascripts/import/details/components/import_details_table.vue +++ b/app/assets/javascripts/import/details/components/import_details_table.vue @@ -1,9 +1,13 @@ <script> -import { GlEmptyState, GlIcon, GlLink, GlTable } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlEmptyState, GlIcon, GlLink, GlLoadingIcon, GlTable } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import { createAlert } from '~/alert'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { getParameterValues } from '~/lib/utils/url_utility'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import { STATISTIC_ITEMS } from '../../constants'; +import { fetchImportFailures } from '../api'; const DEFAULT_PAGE_SIZE = 20; @@ -12,6 +16,7 @@ export default { GlEmptyState, GlIcon, GlLink, + GlLoadingIcon, GlTable, PaginationBar, }, @@ -38,40 +43,85 @@ export default { label: __('Details'), }, ], + + i18n: { + fetchErrorMessage: s__('Import|An error occurred while fetching import details.'), + emptyText: s__('Import|No import details'), + }, + + inject: { + failuresPath: { + default: undefined, + }, + }, + data() { return { + items: [], + loading: false, page: 1, perPage: DEFAULT_PAGE_SIZE, + totalPages: 0, + total: 0, }; }, - computed: { - items() { - return []; - }, + computed: { hasItems() { return this.items.length > 0; }, pageInfo() { - const mockPageInfo = { + return { page: this.page, perPage: this.perPage, - totalPages: this.page, - total: this.items.length, + totalPages: this.totalPages, + total: this.total, }; - return mockPageInfo; }, }, + mounted() { + this.loadImportFailures(); + }, + methods: { setPage(page) { this.page = page; + this.loadImportFailures(); }, setPageSize(size) { this.perPage = size; this.page = 1; + this.loadImportFailures(); + }, + + async loadImportFailures() { + if (!this.failuresPath) { + return; + } + + this.loading = true; + try { + const response = await fetchImportFailures(this.failuresPath, { + projectId: getParameterValues('project_id')[0], + page: this.page, + perPage: this.perPage, + }); + + const { page, perPage, totalPages, total } = parseIntPagination( + normalizeHeaders(response.headers), + ); + this.page = page; + this.perPage = perPage; + this.totalPages = totalPages; + this.total = total; + this.items = response.data; + } catch (error) { + createAlert({ message: this.$options.i18n.fetchErrorMessage }); + } + this.loading = false; }, }, }; @@ -79,9 +129,13 @@ export default { <template> <div> - <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" show-empty> + <gl-table :fields="$options.fields" :items="items" class="gl-mt-5" :busy="loading" show-empty> + <template #table-busy> + <gl-loading-icon size="lg" class="gl-my-5" /> + </template> + <template #empty> - <gl-empty-state :title="s__('Import|No import details')" /> + <gl-empty-state :title="$options.i18n.emptyText" /> </template> <template #cell(type)="{ item: { type } }"> diff --git a/app/assets/javascripts/import/details/index.js b/app/assets/javascripts/import/details/index.js index 70850d947e2..7421846f103 100644 --- a/app/assets/javascripts/import/details/index.js +++ b/app/assets/javascripts/import/details/index.js @@ -8,9 +8,14 @@ export default () => { return null; } + const { failuresPath } = el.dataset; + return new Vue({ el, name: 'ImportDetailsRoot', + provide: { + failuresPath, + }, render(createElement) { return createElement(ImportDetailsApp); }, diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue index 96d07803545..6c84684dedc 100644 --- a/app/assets/javascripts/import_entities/components/import_status.vue +++ b/app/assets/javascripts/import_entities/components/import_status.vue @@ -65,6 +65,11 @@ export default { }, }, props: { + projectId: { + type: Number, + required: false, + default: null, + }, status: { type: String, required: true, @@ -111,7 +116,19 @@ export default { }, showDetails() { - return Boolean(this.detailsPath) && this.glFeatures.importDetailsPage && this.isIncomplete; + return ( + Boolean(this.detailsPathForProject) && + this.glFeatures.importDetailsPage && + this.isIncomplete + ); + }, + + detailsPathForProject() { + if (!this.projectId || !this.detailsPath) { + return null; + } + + return `${this.detailsPath}?project_id=${this.projectId}`; }, }, @@ -163,7 +180,9 @@ export default { </div> </li> </ul> - <gl-link v-if="showDetails" :href="detailsPath">{{ $options.i18n.detailsLink }}</gl-link> + <gl-link v-if="showDetails" :href="detailsPathForProject">{{ + $options.i18n.detailsLink + }}</gl-link> </gl-accordion-item> </gl-accordion> </div> diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue index b20309baac7..735939f991f 100644 --- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue +++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue @@ -70,7 +70,7 @@ export default { ...mapGetters(['getImportTarget']), displayFullPath() { - return this.repo.importedProject.fullPath.replace(/^\//, ''); + return this.repo.importedProject?.fullPath.replace(/^\//, ''); }, isFinished() { @@ -105,6 +105,10 @@ export default { return this.getImportTarget(this.repo.importSource.id); }, + importedProjectId() { + return this.repo.importedProject?.id; + }, + importButtonText() { if (this.ciCdOnly) { return __('Connect'); @@ -220,7 +224,7 @@ export default { </div> </td> <td data-qa-selector="import_status_indicator"> - <import-status :status="importStatus" :stats="stats" /> + <import-status :project-id="importedProjectId" :status="importStatus" :stats="stats" /> </td> <td data-testid="actions" class="gl-white-space-nowrap"> <gl-tooltip :target="() => $refs.cancelButton.$el"> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue index 15e61ff1cd9..7e79572f76d 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/app.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/app.vue @@ -100,12 +100,7 @@ export default { <gl-link :href="gitlabUrl" target="_blank"> <img :src="gitlabLogo" class="gl-h-6" :alt="__('GitLab')" /> </gl-link> - <user-link - :user-signed-in="userSignedIn" - :has-subscriptions="hasSubscriptions" - :user="currentUser" - class="gl-fixed gl-right-4" - /> + <user-link v-if="userSignedIn" :user="currentUser" class="gl-fixed gl-right-4" /> </header> <main class="jira-connect-app gl-px-5 gl-pt-7 gl-mx-auto"> diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue index d05e0d8610e..cc0af0b9ab7 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/components/user_link.vue @@ -6,7 +6,6 @@ export default { components: { GlLink, GlSprintf, - SignInOauthButton: () => import('./sign_in_oauth_button.vue'), }, inject: { gitlabUserPath: { @@ -14,14 +13,6 @@ export default { }, }, props: { - userSignedIn: { - type: Boolean, - required: true, - }, - hasSubscriptions: { - type: Boolean, - required: true, - }, user: { type: Object, required: false, @@ -45,7 +36,6 @@ export default { }, }, i18n: { - signInText: __('Sign in to GitLab'), signedInAsUserText: __('Signed in to GitLab as %{user_link}'), signedInText: __('Signed in to GitLab'), }, @@ -53,18 +43,12 @@ export default { </script> <template> <div class="gl-font-base"> - <gl-sprintf v-if="userSignedIn" :message="signedInText"> + <gl-sprintf :message="signedInText"> <template #user_link> <gl-link data-testid="gitlab-user-link" :href="gitlabUserLink" target="_blank"> {{ gitlabUserHandle }} </gl-link> </template> </gl-sprintf> - - <template v-else-if="hasSubscriptions"> - <sign-in-oauth-button category="tertiary"> - {{ $options.i18n.signInText }} - </sign-in-oauth-button> - </template> </div> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue index 373c5970e64..8cc107930d1 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue @@ -12,7 +12,6 @@ import { import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types'; import SignInOauthButton from '../../../components/sign_in_oauth_button.vue'; -import SetupInstructions from './setup_instructions.vue'; import VersionSelectForm from './version_select_form.vue'; export default { @@ -20,31 +19,23 @@ export default { components: { GlButton, SignInOauthButton, - SetupInstructions, VersionSelectForm, }, data() { return { gitlabBasePath: null, loadingVersionSelect: false, - showSetupInstructions: false, }; }, computed: { hasSelectedVersion() { return this.gitlabBasePath !== null; }, - subtitle() { - return this.hasSelectedVersion - ? this.$options.i18n.signInSubtitle - : this.$options.i18n.versionSelectSubtitle; - }, }, mounted() { this.gitlabBasePath = retrieveBaseUrl(); if (this.gitlabBasePath !== GITLAB_COM_BASE_PATH) { setApiBaseURL(this.gitlabBasePath); - this.showSetupInstructions = true; } }, methods: { @@ -70,9 +61,6 @@ export default { this.loadingVersionSelect = false; }); }, - onSetupNext() { - this.showSetupInstructions = false; - }, onSignInError() { this.$emit('error'); }, @@ -80,7 +68,6 @@ export default { i18n: { title: s__('JiraService|Welcome to GitLab for Jira'), signInSubtitle: s__('JiraService|Sign in to GitLab to link namespaces.'), - versionSelectSubtitle: s__('JiraService|What version of GitLab are you using?'), changeVersionButtonText: s__('JiraService|Change GitLab version'), }, }; @@ -90,7 +77,6 @@ export default { <div> <div class="gl-text-center"> <h2>{{ $options.i18n.title }}</h2> - <p data-testid="subtitle">{{ subtitle }}</p> </div> <version-select-form @@ -101,9 +87,8 @@ export default { /> <template v-else> - <setup-instructions v-if="showSetupInstructions" @next="onSetupNext" /> - - <div v-else class="gl-text-center"> + <div class="gl-text-center"> + <p data-testid="subtitle">{{ $options.i18n.signInSubtitle }}</p> <sign-in-oauth-button class="gl-mb-5" :gitlab-base-path="gitlabBasePath" diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/self_managed_alert.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/self_managed_alert.vue new file mode 100644 index 00000000000..8ddbbffa708 --- /dev/null +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/self_managed_alert.vue @@ -0,0 +1,22 @@ +<script> +import { GlAlert } from '@gitlab/ui'; +import { s__ } from '~/locale'; + +export default { + components: { + GlAlert, + }, + i18n: { + title: s__('JiraService|Are you a GitLab administrator?'), + body: s__( + "JiraService|Setting up this integration is only possible if you're a GitLab administrator.", + ), + }, +}; +</script> + +<template> + <gl-alert variant="warning" :title="$options.i18n.title" :dismissible="false"> + {{ $options.i18n.body }} + </gl-alert> +</template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue index 00fa739b518..621bcccd19a 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue @@ -12,7 +12,7 @@ export default { </script> <template> - <div class="gl-max-w-62 gl-mx-auto gl-mt-7"> + <div class="gl-mt-5"> <h3>{{ s__('JiraService|Continue setup in GitLab') }}</h3> <p> {{ @@ -28,8 +28,9 @@ export default { > </p> - <gl-button variant="confirm" @click="$emit('next')"> - {{ __('Next') }} - </gl-button> + <div class="gl-display-flex gl-justify-content-space-between"> + <gl-button @click="$emit('back')">{{ __('Back') }}</gl-button> + <gl-button variant="confirm" @click="$emit('next')">{{ __('Next') }}</gl-button> + </div> </div> </template> diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue index 37a65946b3f..3a080afd3c5 100644 --- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue +++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue @@ -10,6 +10,8 @@ import { import { __, s__ } from '~/locale'; import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants'; +import SelfManagedAlert from './self_managed_alert.vue'; +import SetupInstructions from './setup_instructions.vue'; const RADIO_OPTIONS = { saas: 'saas', @@ -27,6 +29,8 @@ export default { GlFormInput, GlFormRadio, GlButton, + SelfManagedAlert, + SetupInstructions, }, props: { loading: { @@ -39,25 +43,54 @@ export default { return { selected: DEFAULT_RADIO_OPTION, selfManagedBasePathInput: '', + showSetupInstructions: false, + showSelfManagedInstanceInput: false, }; }, computed: { isSelfManagedSelected() { return this.selected === RADIO_OPTIONS.selfManaged; }, + submitText() { + return this.isSelfManagedSelected + ? this.$options.i18n.buttonNext + : this.$options.i18n.buttonSave; + }, + showVersonSelect() { + return !this.showSetupInstructions && !this.showSelfManagedInstanceInput; + }, }, methods: { onSubmit() { - const gitlabBasePath = - this.selected === RADIO_OPTIONS.saas ? GITLAB_COM_BASE_PATH : this.selfManagedBasePathInput; + if (this.isSelfManagedSelected && !this.showSelfManagedInstanceInput) { + this.showSetupInstructions = true; + return; + } + + const gitlabBasePath = this.isSelfManagedSelected + ? this.selfManagedBasePathInput + : GITLAB_COM_BASE_PATH; this.$emit('submit', gitlabBasePath); }, + + onSetupNext() { + this.showSetupInstructions = false; + this.showSelfManagedInstanceInput = true; + }, + + onSetupBack() { + this.showSetupInstructions = false; + this.showSelfManagedInstanceInput = false; + }, }, radioOptions: RADIO_OPTIONS, i18n: { + title: s__('JiraService|What version of GitLab are you using?'), saasRadioLabel: __('GitLab.com (SaaS)'), saasRadioHelp: __('Most common'), selfManagedRadioLabel: __('GitLab (self-managed)'), + buttonNext: __('Next'), + buttonSave: __('Save'), instanceURLInputLabel: s__('JiraService|GitLab instance URL'), instanceURLInputDescription: s__('JiraService|For example: https://gitlab.example.com'), }, @@ -66,30 +99,50 @@ export default { <template> <gl-form class="gl-max-w-62 gl-mx-auto" @submit.prevent="onSubmit"> - <gl-form-radio-group v-model="selected" class="gl-mb-3" name="gitlab_version"> - <gl-form-radio :value="$options.radioOptions.saas"> - {{ $options.i18n.saasRadioLabel }} - <template #help> - {{ $options.i18n.saasRadioHelp }} - </template> - </gl-form-radio> - <gl-form-radio :value="$options.radioOptions.selfManaged"> - {{ $options.i18n.selfManagedRadioLabel }} - </gl-form-radio> - </gl-form-radio-group> + <div v-if="showVersonSelect"> + <h5 class="gl-mb-5">{{ $options.i18n.title }}</h5> + <gl-form-radio-group v-model="selected" class="gl-mb-3" name="gitlab_version"> + <gl-form-radio :value="$options.radioOptions.saas"> + {{ $options.i18n.saasRadioLabel }} + <template #help> + {{ $options.i18n.saasRadioHelp }} + </template> + </gl-form-radio> + <gl-form-radio :value="$options.radioOptions.selfManaged"> + {{ $options.i18n.selfManagedRadioLabel }} + </gl-form-radio> + </gl-form-radio-group> + <self-managed-alert v-if="isSelfManagedSelected" /> + + <div class="gl-display-flex gl-justify-content-end gl-mt-5"> + <gl-button variant="confirm" type="submit" :loading="loading" data-testid="submit-button">{{ + submitText + }}</gl-button> + </div> + </div> - <gl-form-group - v-if="isSelfManagedSelected" - class="gl-ml-6" - :label="$options.i18n.instanceURLInputLabel" - :description="$options.i18n.instanceURLInputDescription" - label-for="self-managed-instance-input" - > - <gl-form-input id="self-managed-instance-input" v-model="selfManagedBasePathInput" required /> - </gl-form-group> + <setup-instructions v-else-if="showSetupInstructions" @next="onSetupNext" @back="onSetupBack" /> - <div class="gl-display-flex gl-justify-content-end"> - <gl-button variant="confirm" type="submit" :loading="loading">{{ __('Save') }}</gl-button> + <div v-else-if="showSelfManagedInstanceInput"> + <gl-form-group + :label="$options.i18n.instanceURLInputLabel" + :description="$options.i18n.instanceURLInputDescription" + label-for="self-managed-instance-input" + > + <gl-form-input + id="self-managed-instance-input" + v-model="selfManagedBasePathInput" + required + /> + </gl-form-group> + <div class="gl-display-flex gl-justify-content-space-between"> + <gl-button data-testid="back-button" @click.prevent="onSetupBack">{{ + __('Back') + }}</gl-button> + <gl-button variant="confirm" type="submit" :loading="loading">{{ + $options.i18n.buttonSave + }}</gl-button> + </div> </div> </gl-form> </template> diff --git a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue index 17766b4d162..f4061f3d375 100644 --- a/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue +++ b/app/assets/javascripts/jobs/components/table/cells/actions_cell.vue @@ -1,5 +1,12 @@ <script> -import { GlButton, GlButtonGroup, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui'; +import { + GlButton, + GlButtonGroup, + GlModal, + GlModalDirective, + GlSprintf, + GlTooltipDirective, +} from '@gitlab/ui'; import GlCountdown from '~/vue_shared/components/gl_countdown.vue'; import { redirectTo } from '~/lib/utils/url_utility'; import { @@ -49,6 +56,7 @@ export default { }, directives: { GlModalDirective, + GlTooltip: GlTooltipDirective, }, inject: { admin: { @@ -178,11 +186,12 @@ export default { <template v-if="canReadJob && canUpdateJob"> <gl-button v-if="isActive" - data-testid="cancel-button" + v-gl-tooltip icon="cancel" :title="$options.CANCEL" :aria-label="$options.CANCEL" :disabled="cancelBtnDisabled" + data-testid="cancel-button" @click="cancelJob()" /> <template v-else-if="isScheduled"> @@ -191,6 +200,7 @@ export default { </gl-button> <gl-button v-gl-modal-directive="$options.playJobModalId" + v-gl-tooltip icon="play" :title="$options.ACTIONS_START_NOW" :aria-label="$options.ACTIONS_START_NOW" @@ -206,6 +216,7 @@ export default { </gl-sprintf> </gl-modal> <gl-button + v-gl-tooltip icon="time-out" :title="$options.ACTIONS_UNSCHEDULE" :aria-label="$options.ACTIONS_UNSCHEDULE" @@ -218,6 +229,7 @@ export default { <!--Note: This is the manual job play button --> <gl-button v-if="manualJobPlayable" + v-gl-tooltip icon="play" :title="$options.ACTIONS_PLAY" :aria-label="$options.ACTIONS_PLAY" @@ -227,6 +239,7 @@ export default { /> <gl-button v-else-if="isRetryable" + v-gl-tooltip icon="retry" :title="retryButtonTitle" :aria-label="retryButtonTitle" @@ -239,6 +252,7 @@ export default { </template> <gl-button v-if="shouldDisplayArtifacts" + v-gl-tooltip icon="download" :title="$options.ACTIONS_DOWNLOAD_ARTIFACTS" :aria-label="$options.ACTIONS_DOWNLOAD_ARTIFACTS" diff --git a/app/assets/javascripts/labels/index.js b/app/assets/javascripts/labels/index.js index 0d4113bba4c..c7c17607af6 100644 --- a/app/assets/javascripts/labels/index.js +++ b/app/assets/javascripts/labels/index.js @@ -120,16 +120,15 @@ export function initAdminLabels() { const emptyState = document.querySelector('.js-admin-labels-empty-state'); function removeLabelSuccessCallback() { - this.closest('li').classList.add('gl-display-none!'); + this.closest('li.label-list-item').classList.add('gl-display-none!'); const labelsCount = document.querySelectorAll( - 'ul.manage-labels-list li:not(.gl-display-none\\!)', + 'ul.manage-labels-list li.label-list-item:not(.gl-display-none\\!)', ).length; // display the empty state if there are no more labels if (labelsCount < 1 && !pagination && emptyState) { emptyState.classList.remove('gl-display-none'); - labelsContainer.classList.add('gl-display-none'); } } diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js index 317c401e404..198f2da385c 100644 --- a/app/assets/javascripts/lib/utils/dom_utils.js +++ b/app/assets/javascripts/lib/utils/dom_utils.js @@ -39,7 +39,7 @@ export const toggleContainerClasses = (containerEl, classList) => { * Return a object mapping element dataset names to booleans. * * This is useful for data- attributes whose presense represent - * a truthiness, no matter the value of the attribute. The absense of the + * a truthiness, no matter the value of the attribute. The absence of the * attribute represents falsiness. * * This can be useful when Rails-provided boolean-like values are passed diff --git a/app/assets/javascripts/projects/commit_box/info/index.js b/app/assets/javascripts/projects/commit_box/info/index.js index 7500c152b6a..7c4b76fd62f 100644 --- a/app/assets/javascripts/projects/commit_box/info/index.js +++ b/app/assets/javascripts/projects/commit_box/info/index.js @@ -1,6 +1,5 @@ import { fetchCommitMergeRequests } from '~/commit_merge_requests'; import { initCommitPipelineMiniGraph } from './init_commit_pipeline_mini_graph'; -import { initDetailsButton } from './init_details_button'; import { loadBranches } from './load_branches'; import initCommitPipelineStatus from './init_commit_pipeline_status'; @@ -14,7 +13,5 @@ export const initCommitBoxInfo = () => { // Display pipeline mini graph for this commit initCommitPipelineMiniGraph(); - initDetailsButton(); - initCommitPipelineStatus(); }; diff --git a/app/assets/javascripts/projects/commit_box/info/init_details_button.js b/app/assets/javascripts/projects/commit_box/info/init_details_button.js index bc2c16b9e83..667d6bd0250 100644 --- a/app/assets/javascripts/projects/commit_box/info/init_details_button.js +++ b/app/assets/javascripts/projects/commit_box/info/init_details_button.js @@ -1,7 +1,17 @@ export const initDetailsButton = () => { - document.querySelector('.commit-info').addEventListener('click', function expand(e) { - e.preventDefault(); - this.querySelector('.js-details-content').classList.remove('hide'); - this.querySelector('.js-details-expand').classList.add('gl-display-none'); + const expandButton = document.querySelector('.js-details-expand'); + + if (!expandButton) { + return; + } + + expandButton.addEventListener('click', (event) => { + const btn = event.target; + const contentEl = btn.parentElement.querySelector('.js-details-content'); + + if (contentEl) { + contentEl.classList.remove('hide'); + btn.classList.add('gl-display-none'); + } }); }; diff --git a/app/assets/javascripts/projects/commit_box/info/load_branches.js b/app/assets/javascripts/projects/commit_box/info/load_branches.js index d1136817cb3..8333e70b951 100644 --- a/app/assets/javascripts/projects/commit_box/info/load_branches.js +++ b/app/assets/javascripts/projects/commit_box/info/load_branches.js @@ -1,6 +1,7 @@ import axios from 'axios'; import { sanitize } from '~/lib/dompurify'; import { __ } from '~/locale'; +import { initDetailsButton } from './init_details_button'; export const loadBranches = (containerSelector = '.js-commit-box-info') => { const containerEl = document.querySelector(containerSelector); @@ -14,6 +15,8 @@ export const loadBranches = (containerSelector = '.js-commit-box-info') => { .get(commitPath) .then(({ data }) => { branchesEl.innerHTML = sanitize(data); + + initDetailsButton(); }) .catch(() => { branchesEl.textContent = __('Failed to load branches. Please try again.'); diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue index 9dc26ad10f5..8382ae8987b 100644 --- a/app/assets/javascripts/super_sidebar/components/nav_item.vue +++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue @@ -7,6 +7,8 @@ import { TRACKING_UNKNOWN_ID, TRACKING_UNKNOWN_PANEL, } from '~/super_sidebar/constants'; +import NavItemLink from './nav_item_link.vue'; +import NavItemRouterLink from './nav_item_router_link.vue'; export default { i18n: { @@ -18,6 +20,8 @@ export default { GlButton, GlIcon, GlBadge, + NavItemLink, + NavItemRouterLink, }, inject: { pinnedItemIds: { default: { ids: [] } }, @@ -55,9 +59,6 @@ export default { (typeof this.pillData === 'string' && this.pillData !== '') ); }, - isActive() { - return this.item.is_active; - }, isPinnable() { return this.panelSupportsPins && !this.isStatic; }, @@ -86,27 +87,30 @@ export default { return { ...this.$attrs, ...this.trackingProps, - href: this.item.link, - 'aria-current': this.isActive ? 'page' : null, + item: this.item, 'data-qa-submenu-item': this.item.title, }; }, computedLinkClasses() { return { - 'gl-bg-t-gray-a-08': this.isActive, 'gl-py-2': this.isPinnable, 'gl-py-3': !this.isPinnable, [this.item.link_classes]: this.item.link_classes, ...this.linkClasses, }; }, + navItemLinkComponent() { + return this.item.to ? NavItemRouterLink : NavItemLink; + }, }, }; </script> <template> <li> - <a + <component + :is="navItemLinkComponent" + #default="{ isActive }" v-bind="linkProps" class="nav-item-link gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-0 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus" :class="computedLinkClasses" @@ -118,6 +122,7 @@ export default { class="gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow" aria-hidden="true" style="width: 3px; border-radius: 3px; margin-right: 1px" + data-testid="active-indicator" ></div> <div class="gl-flex-shrink-0 gl-w-6 gl-mx-3"> <slot name="icon"> @@ -162,6 +167,6 @@ export default { @click.prevent="$emit('pin-remove', item.id)" /> </span> - </a> + </component> </li> </template> diff --git a/app/assets/javascripts/super_sidebar/components/nav_item_link.vue b/app/assets/javascripts/super_sidebar/components/nav_item_link.vue new file mode 100644 index 00000000000..8358e96db94 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/nav_item_link.vue @@ -0,0 +1,35 @@ +<script> +import { NAV_ITEM_LINK_ACTIVE_CLASS } from '../constants'; +import { ariaCurrent } from '../utils'; + +export default { + props: { + item: { + type: Object, + required: true, + }, + }, + computed: { + isActive() { + return this.item.is_active; + }, + linkProps() { + return { + href: this.item.link, + 'aria-current': ariaCurrent(this.isActive), + }; + }, + computedLinkClasses() { + return { + [NAV_ITEM_LINK_ACTIVE_CLASS]: this.isActive, + }; + }, + }, +}; +</script> + +<template> + <a v-bind="linkProps" :class="computedLinkClasses"> + <slot :is-active="isActive"></slot> + </a> +</template> diff --git a/app/assets/javascripts/super_sidebar/components/nav_item_router_link.vue b/app/assets/javascripts/super_sidebar/components/nav_item_router_link.vue new file mode 100644 index 00000000000..78aca24d9a6 --- /dev/null +++ b/app/assets/javascripts/super_sidebar/components/nav_item_router_link.vue @@ -0,0 +1,37 @@ +<script> +import { NAV_ITEM_LINK_ACTIVE_CLASS } from '../constants'; +import { ariaCurrent } from '../utils'; + +export default { + NAV_ITEM_LINK_ACTIVE_CLASS, + props: { + item: { + type: Object, + required: true, + }, + }, + computed: { + linkProps() { + return { + to: this.item.to, + }; + }, + }, + methods: { + ariaCurrent, + }, +}; +</script> + +<template> + <router-link + #default="{ href, navigate, isActive }" + v-bind="linkProps" + :active-class="$options.NAV_ITEM_LINK_ACTIVE_CLASS" + custom + > + <a :href="href" :aria-current="ariaCurrent(isActive)" @click="navigate"> + <slot :is-active="isActive"></slot> + </a> + </router-link> +</template> diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js index 4a5d0bf637f..00ceaebe2cc 100644 --- a/app/assets/javascripts/super_sidebar/constants.js +++ b/app/assets/javascripts/super_sidebar/constants.js @@ -50,3 +50,5 @@ export const SIDEBAR_PINS_EXPANDED_COOKIE = 'sidebar_pinned_section_expanded'; export const SIDEBAR_COOKIE_EXPIRATION = 365 * 10; export const DROPDOWN_Y_OFFSET = 4; + +export const NAV_ITEM_LINK_ACTIVE_CLASS = 'gl-bg-t-gray-a-08'; diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js index 4bbef67742c..3b17a35c5bc 100644 --- a/app/assets/javascripts/super_sidebar/utils.js +++ b/app/assets/javascripts/super_sidebar/utils.js @@ -83,3 +83,5 @@ export const formatContextSwitcherItems = (items) => avatar, link, })); + +export const ariaCurrent = (isActive) => (isActive ? 'page' : null); |