Skip to content
Snippets Groups Projects
Verified Commit 1aa53d83 authored by Lorenz van Herwaarden's avatar Lorenz van Herwaarden :two: Committed by GitLab
Browse files

Merge branch...

Merge branch '510322-ai-resolution-on-the-mr-widget-use-mutation-observer-for-scroll-trigger' into 'master' 

AI-Resolution on MR widget: Improve scroll UX

See merge request !177883



Merged-by: Lorenz van Herwaarden's avatarLorenz van Herwaarden <lvanherwaarden@gitlab.com>
Approved-by: Lorenz van Herwaarden's avatarLorenz van Herwaarden <lvanherwaarden@gitlab.com>
Reviewed-by: default avatarSamantha Ming <sming@gitlab.com>
Reviewed-by: default avatarDavid Pisek <dpisek@gitlab.com>
Reviewed-by: Lorenz van Herwaarden's avatarLorenz van Herwaarden <lvanherwaarden@gitlab.com>
Co-authored-by: default avatarDave Pisek <dpisek@gitlab.com>
parents 06c59b21 7266094a
No related branches found
No related tags found
3 merge requests!181325Fix ambiguous `created_at` in project.rb,!180187Draft: Update dashboard editing to save visualizations directly to the dashboard file,!177883AI-Resolution on MR widget: Improve scroll UX
Pipeline #1653387319 failed
<script>
import { GlBadge, GlButton, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/constants';
import { visitUrl } from '~/lib/utils/url_utility';
import { historyPushState } from '~/lib/utils/common_utils';
import MrWidget from '~/vue_merge_request_widget/components/widget/widget.vue';
import MrWidgetRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
......@@ -53,7 +54,6 @@ export default {
collapsedData: {},
};
},
computed: {
reports() {
return this.endpoints
......@@ -195,17 +195,56 @@ export default {
return !this.mr.isPipelineActive && this.endpoints.length > 0;
},
},
beforeDestroy() {
this.cleanUpResolveWithAiHandlers();
},
methods: {
handleResolveWithAiSuccess(commentUrl) {
this.clearModalData();
// the note's id is the hash of the url and also the DOM id which we want to scroll to
const [, commentNoteId] = commentUrl.split('#');
const isCommentOnPage = () => document.getElementById(commentNoteId) !== null;
const closeModalAndScrollToComment = () => {
this.clearModalData();
visitUrl(commentUrl);
};
const [, resultHash] = commentUrl.split('#');
if (resultHash && document.getElementById(resultHash)) {
window.location.assign(commentUrl);
} else {
// the comment has not been added to the DOM yet, so we need to hard-reload the page
if (isCommentOnPage()) {
closeModalAndScrollToComment();
return;
}
// as a fallback we set a timeout and then manually do a hard page reload
this.commentNotefallBackTimeout = setTimeout(() => {
this.cleanUpResolveWithAiHandlers();
historyPushState(commentUrl);
window.location.reload();
}, 3000);
// observe the DOM and scroll to the comment when it's added
this.commentMutationObserver = new MutationObserver((mutationList) => {
for (const mutation of mutationList) {
// check if the added notes within the mutation contains the comment we're looking for
if (mutation.addedNodes.length > 0 && isCommentOnPage()) {
this.cleanUpResolveWithAiHandlers();
closeModalAndScrollToComment();
return;
}
}
});
this.commentMutationObserver.observe(document.getElementById('notes') || document.body, {
childList: true,
subtree: true,
});
},
cleanUpResolveWithAiHandlers() {
if (this.commentNotefallBackTimeout) {
clearTimeout(this.commentNotefallBackTimeout);
}
if (this.commentMutationObserver) {
this.commentMutationObserver.disconnect();
}
},
......@@ -494,11 +533,11 @@ export default {
variant="link"
class="gl-ml-2 gl-overflow-hidden gl-text-ellipsis gl-whitespace-nowrap"
@click="setModalData(vuln)"
>{{ vuln.name }}</gl-button
>
<gl-badge v-if="isDismissed(vuln)" class="gl-ml-3">{{
$options.i18n.dismissed
}}</gl-badge>
>{{ vuln.name }}
</gl-button>
<gl-badge v-if="isDismissed(vuln)" class="gl-ml-3"
>{{ $options.i18n.dismissed }}
</gl-badge>
<template v-if="isAiResolvable(vuln)">
<gl-badge
:id="getAiResolvableBadgeId(vuln.uuid)"
......@@ -516,9 +555,9 @@ export default {
:data-testid="`ai-resolvable-badge-popover-${vuln.uuid}`"
>
{{ $options.aiResolutionHelpPopOver.text }}
<gl-link :href="$options.aiResolutionHelpPopOver.learnMorePath">{{
__('Learn more')
}}</gl-link>
<gl-link :href="$options.aiResolutionHelpPopOver.learnMorePath"
>{{ __('Learn more') }}
</gl-link>
</gl-popover>
</template>
</template>
......
......@@ -781,44 +781,72 @@ describe('MR Widget Security Reports', () => {
});
describe('resolve with AI', () => {
beforeEach(async () => {
await createComponentExpandWidgetAndOpenModal();
});
jest.useFakeTimers();
useMockLocationHelper();
const aiCommentUrl = `${TEST_HOST}/project/merge_requests/2#note_1`;
const addCommentToDOM = () => {
const comment = document.createElement('div');
comment.id = 'note_1';
document.body.appendChild(comment);
return nextTick();
};
useMockLocationHelper();
beforeEach(async () => {
await createComponentExpandWidgetAndOpenModal();
});
afterEach(() => {
// remove the comment from the DOM
document.getElementById('note_1')?.remove();
});
it('scrolls to the comment when the comment note that is added by the AI-action is already on the page', async () => {
expect(window.location.assign).not.toHaveBeenCalled();
expect(findStandaloneModal().exists()).toBe(true);
it('closes the modal when the "resolveWithAiSuccess" event is emitted', async () => {
findStandaloneModal().vm.$emit('resolveWithAiSuccess', aiCommentUrl);
await addCommentToDOM();
expect(window.location.assign).toHaveBeenCalledWith(aiCommentUrl);
expect(window.location.reload).not.toHaveBeenCalled();
await nextTick();
expect(findStandaloneModal().exists()).toBe(false);
});
it('does a hard-reload when the comment note that is added by the AI-action is not yet on the page', async () => {
expect(window.location.reload).not.toHaveBeenCalled();
it('scrolls to the comment when the comment note that is added by the AI-action is on the page', async () => {
expect(window.location.assign).not.toHaveBeenCalled();
expect(findStandaloneModal().exists()).toBe(true);
findStandaloneModal().vm.$emit('resolveWithAiSuccess', aiCommentUrl);
// at this point the comment is not yet within the DOM
expect(window.location.assign).not.toHaveBeenCalledWith(aiCommentUrl);
await addCommentToDOM();
expect(window.location.assign).toHaveBeenCalledWith(aiCommentUrl);
expect(window.location.reload).not.toHaveBeenCalled();
await nextTick();
expect(historyPushState).toHaveBeenCalledWith(aiCommentUrl);
expect(window.location.reload).toHaveBeenCalled();
expect(findStandaloneModal().exists()).toBe(false);
});
it('scrolls to the comment with no hard-reload when the comment note that is added by the AI-action is on the page', async () => {
addCommentToDOM();
expect(window.location.assign).not.toHaveBeenCalled();
it('does a hard-reload when the comment note that is added by the AI-action is not on the page within 3 seconds', async () => {
expect(window.location.reload).not.toHaveBeenCalled();
findStandaloneModal().vm.$emit('resolveWithAiSuccess', aiCommentUrl);
await nextTick();
expect(window.location.assign).toHaveBeenCalledWith(aiCommentUrl);
jest.advanceTimersByTime(3000);
expect(historyPushState).toHaveBeenCalledWith(aiCommentUrl);
expect(window.location.reload).toHaveBeenCalled();
});
});
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment