Commit c95b671b authored by Mike Greiling's avatar Mike Greiling

Merge branch '63247-add-conf-toast-and-link' into 'master'

Resolve "Including a link to the posted comment for visual review app review feature"

Closes #63247

See merge request gitlab-org/gitlab-ce!29719
parents 0f045c42 63fdd2c6
import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants'; import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants';
import { clearNote, note, postError } from './note'; import { clearNote, postError } from './note';
import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } from './utils'; import {
buttonClearStyles,
selectCommentBox,
selectCommentButton,
selectNote,
selectNoteContainer,
} from './utils';
const comment = ` const comment = `
<div> <div>
<textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea> <textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea>
${note}
<p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p> <p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
</div> </div>
<div class="gitlab-button-wrapper"> <div class="gitlab-button-wrapper">
<button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Logout </button> <button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Log out </button>
<button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button> <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
</div> </div>
`; `;
const resetCommentBox = () => { const resetCommentButton = () => {
const commentBox = selectCommentBox();
const commentButton = selectCommentButton(); const commentButton = selectCommentButton();
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
commentButton.innerText = 'Send feedback'; commentButton.innerText = 'Send feedback';
commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
commentButton.style.opacity = 1; commentButton.style.opacity = 1;
};
const resetCommentBox = () => {
const commentBox = selectCommentBox();
commentBox.style.pointerEvents = 'auto'; commentBox.style.pointerEvents = 'auto';
commentBox.style.color = BLACK; commentBox.style.color = BLACK;
}; };
const resetCommentButton = () => { const resetCommentText = () => {
const commentBox = selectCommentBox(); const commentBox = selectCommentBox();
const currentNote = selectNote();
commentBox.value = ''; commentBox.value = '';
currentNote.innerText = '';
}; };
const resetComment = () => { const resetComment = () => {
resetCommentBox();
resetCommentButton(); resetCommentButton();
resetCommentBox();
resetCommentText();
}; };
const confirmAndClear = mergeRequestId => { const confirmAndClear = feedbackInfo => {
const commentButton = selectCommentButton(); const commentButton = selectCommentButton();
const currentNote = selectNote(); const currentNote = selectNote();
const noteContainer = selectNoteContainer();
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
commentButton.innerText = 'Feedback sent'; commentButton.innerText = 'Feedback sent';
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ noteContainer.style.visibility = 'visible';
currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`; currentNote.insertAdjacentHTML('beforeend', feedbackInfo);
setTimeout(resetComment, 2000);
setTimeout(resetComment, 1000);
setTimeout(clearNote, 6000);
}; };
const setInProgressState = () => { const setInProgressState = () => {
...@@ -71,6 +79,7 @@ const postComment = ({ ...@@ -71,6 +79,7 @@ const postComment = ({
innerWidth, innerWidth,
innerHeight, innerHeight,
projectId, projectId,
projectPath,
mergeRequestId, mergeRequestId,
mrUrl, mrUrl,
token, token,
...@@ -86,6 +95,7 @@ const postComment = ({ ...@@ -86,6 +95,7 @@ const postComment = ({
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
postError('Your comment appears to be empty.', COMMENT_BOX); postError('Your comment appears to be empty.', COMMENT_BOX);
resetCommentBox(); resetCommentBox();
resetCommentButton();
return; return;
} }
...@@ -114,18 +124,24 @@ const postComment = ({ ...@@ -114,18 +124,24 @@ const postComment = ({
}) })
.then(response => { .then(response => {
if (response.ok) { if (response.ok) {
confirmAndClear(mergeRequestId); return response.json();
return;
} }
throw new Error(`${response.status}: ${response.statusText}`); throw new Error(`${response.status}: ${response.statusText}`);
}) })
.then(data => {
const commentId = data.notes[0].id;
const feedbackLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}#note_${commentId}`;
const feedbackInfo = `Feedback sent. View at <a class="gitlab-link" href="${feedbackLink}">${projectPath} #${mergeRequestId} (comment ${commentId})</a>`;
confirmAndClear(feedbackInfo);
})
.catch(err => { .catch(err => {
postError( postError(
`Your comment could not be sent. Please try again. Error: ${err.message}`, `Your comment could not be sent. Please try again. Error: ${err.message}`,
COMMENT_BOX, COMMENT_BOX,
); );
resetCommentBox(); resetCommentBox();
resetCommentButton();
}); });
}; };
......
...@@ -2,10 +2,12 @@ ...@@ -2,10 +2,12 @@
const COLLAPSE_BUTTON = 'gitlab-collapse'; const COLLAPSE_BUTTON = 'gitlab-collapse';
const COMMENT_BOX = 'gitlab-comment'; const COMMENT_BOX = 'gitlab-comment';
const COMMENT_BUTTON = 'gitlab-comment-button'; const COMMENT_BUTTON = 'gitlab-comment-button';
const FORM = 'gitlab-form-wrapper'; const FORM = 'gitlab-form';
const FORM_CONTAINER = 'gitlab-form-wrapper';
const LOGIN = 'gitlab-login'; const LOGIN = 'gitlab-login';
const LOGOUT = 'gitlab-logout-button'; const LOGOUT = 'gitlab-logout-button';
const NOTE = 'gitlab-validation-note'; const NOTE = 'gitlab-validation-note';
const NOTE_CONTAINER = 'gitlab-note-wrapper';
const REMEMBER_TOKEN = 'gitlab-remember_token'; const REMEMBER_TOKEN = 'gitlab-remember_token';
const REVIEW_CONTAINER = 'gitlab-review-container'; const REVIEW_CONTAINER = 'gitlab-review-container';
const TOKEN_BOX = 'gitlab-token'; const TOKEN_BOX = 'gitlab-token';
...@@ -16,16 +18,18 @@ const BLACK = 'rgba(46, 46, 46, 1)'; ...@@ -16,16 +18,18 @@ const BLACK = 'rgba(46, 46, 46, 1)';
const CLEAR = 'rgba(255, 255, 255, 0)'; const CLEAR = 'rgba(255, 255, 255, 0)';
const MUTED = 'rgba(223, 223, 223, 0.5)'; const MUTED = 'rgba(223, 223, 223, 0.5)';
const RED = 'rgba(219, 59, 33, 1)'; const RED = 'rgba(219, 59, 33, 1)';
const WHITE = 'rgba(255, 255, 255, 1)'; const WHITE = 'rgba(250, 250, 250, 1)';
export { export {
COLLAPSE_BUTTON, COLLAPSE_BUTTON,
COMMENT_BOX, COMMENT_BOX,
COMMENT_BUTTON, COMMENT_BUTTON,
FORM, FORM,
FORM_CONTAINER,
LOGIN, LOGIN,
LOGOUT, LOGOUT,
NOTE, NOTE,
NOTE_CONTAINER,
REMEMBER_TOKEN, REMEMBER_TOKEN,
REVIEW_CONTAINER, REVIEW_CONTAINER,
TOKEN_BOX, TOKEN_BOX,
......
import { comment, postComment } from './comment'; import { comment, postComment } from './comment';
import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants'; import {
COLLAPSE_BUTTON,
COMMENT_BUTTON,
FORM_CONTAINER,
LOGIN,
LOGOUT,
REVIEW_CONTAINER,
} from './constants';
import { authorizeUser, login } from './login'; import { authorizeUser, login } from './login';
import { note } from './note';
import { selectContainer } from './utils'; import { selectContainer } from './utils';
import { form, logoutUser, toggleForm } from './wrapper'; import { buttonAndForm, logoutUser, toggleForm } from './wrapper';
import { collapseButton } from './wrapper_icons'; import { collapseButton } from './wrapper_icons';
export { export {
authorizeUser, authorizeUser,
buttonAndForm,
collapseButton, collapseButton,
comment, comment,
form,
login, login,
logoutUser, logoutUser,
note,
postComment, postComment,
selectContainer, selectContainer,
toggleForm, toggleForm,
COLLAPSE_BUTTON, COLLAPSE_BUTTON,
COMMENT_BUTTON, COMMENT_BUTTON,
FORM_CONTAINER,
LOGIN, LOGIN,
LOGOUT, LOGOUT,
REVIEW_CONTAINER, REVIEW_CONTAINER,
......
import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants'; import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants';
import { clearNote, note, postError } from './note'; import { clearNote, postError } from './note';
import { buttonClearStyles, selectRemember, selectToken } from './utils'; import { buttonClearStyles, selectRemember, selectToken } from './utils';
import { addCommentForm } from './wrapper'; import { addCommentForm } from './wrapper';
...@@ -7,7 +7,6 @@ const login = ` ...@@ -7,7 +7,6 @@ const login = `
<div> <div>
<label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label> <label for="${TOKEN_BOX}" class="gitlab-label">Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a></label>
<input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password"> <input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password">
${note}
</div> </div>
<div class="gitlab-checkbox-wrapper"> <div class="gitlab-checkbox-wrapper">
<input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember"> <input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember">
......
import { NOTE, RED } from './constants'; import { NOTE, NOTE_CONTAINER, RED } from './constants';
import { selectById, selectNote } from './utils'; import { selectById, selectNote, selectNoteContainer } from './utils';
const note = ` const note = `
<p id=${NOTE} class='gitlab-message'></p> <div id="${NOTE_CONTAINER}" style="visibility: hidden;">
<p id="${NOTE}" class="gitlab-message"></p>
</div>
`; `;
const clearNote = inputId => { const clearNote = inputId => {
const currentNote = selectNote(); const currentNote = selectNote();
const noteContainer = selectNoteContainer();
currentNote.innerText = ''; currentNote.innerText = '';
currentNote.style.color = ''; currentNote.style.color = '';
noteContainer.style.visibility = 'hidden';
if (inputId) { if (inputId) {
const field = document.getElementById(inputId); const field = document.getElementById(inputId);
...@@ -18,10 +23,13 @@ const clearNote = inputId => { ...@@ -18,10 +23,13 @@ const clearNote = inputId => {
const postError = (message, inputId) => { const postError = (message, inputId) => {
const currentNote = selectNote(); const currentNote = selectNote();
const noteContainer = selectNoteContainer();
const field = selectById(inputId); const field = selectById(inputId);
field.style.borderColor = RED; field.style.borderColor = RED;
currentNote.style.color = RED; currentNote.style.color = RED;
currentNote.innerText = message; currentNote.innerText = message;
noteContainer.style.visibility = 'visible';
setTimeout(clearNote.bind(null, inputId), 5000);
}; };
export { clearNote, note, postError }; export { clearNote, note, postError };
...@@ -5,7 +5,9 @@ import { ...@@ -5,7 +5,9 @@ import {
COMMENT_BOX, COMMENT_BOX,
COMMENT_BUTTON, COMMENT_BUTTON,
FORM, FORM,
FORM_CONTAINER,
NOTE, NOTE,
NOTE_CONTAINER,
REMEMBER_TOKEN, REMEMBER_TOKEN,
REVIEW_CONTAINER, REVIEW_CONTAINER,
TOKEN_BOX, TOKEN_BOX,
...@@ -24,7 +26,9 @@ const selectCommentBox = () => document.getElementById(COMMENT_BOX); ...@@ -24,7 +26,9 @@ const selectCommentBox = () => document.getElementById(COMMENT_BOX);
const selectCommentButton = () => document.getElementById(COMMENT_BUTTON); const selectCommentButton = () => document.getElementById(COMMENT_BUTTON);
const selectContainer = () => document.getElementById(REVIEW_CONTAINER); const selectContainer = () => document.getElementById(REVIEW_CONTAINER);
const selectForm = () => document.getElementById(FORM); const selectForm = () => document.getElementById(FORM);
const selectFormContainer = () => document.getElementById(FORM_CONTAINER);
const selectNote = () => document.getElementById(NOTE); const selectNote = () => document.getElementById(NOTE);
const selectNoteContainer = () => document.getElementById(NOTE_CONTAINER);
const selectRemember = () => document.getElementById(REMEMBER_TOKEN); const selectRemember = () => document.getElementById(REMEMBER_TOKEN);
const selectToken = () => document.getElementById(TOKEN_BOX); const selectToken = () => document.getElementById(TOKEN_BOX);
...@@ -36,7 +40,9 @@ export { ...@@ -36,7 +40,9 @@ export {
selectCommentBox, selectCommentBox,
selectCommentButton, selectCommentButton,
selectForm, selectForm,
selectFormContainer,
selectNote, selectNote,
selectNoteContainer,
selectRemember, selectRemember,
selectToken, selectToken,
}; };
import { comment } from './comment'; import { comment } from './comment';
import { CLEAR, FORM, WHITE } from './constants'; import { CLEAR, FORM, FORM_CONTAINER, WHITE } from './constants';
import { login } from './login'; import { login } from './login';
import { selectCollapseButton, selectContainer, selectForm } from './utils'; import { clearNote } from './note';
import {
selectCollapseButton,
selectForm,
selectFormContainer,
selectNoteContainer,
} from './utils';
import { commentIcon, compressIcon } from './wrapper_icons'; import { commentIcon, compressIcon } from './wrapper_icons';
const form = content => ` const form = content => `
<form id=${FORM}> <form id="${FORM}">
${content} ${content}
</form> </form>
`; `;
const buttonAndForm = ({ content, toggleButton }) => `
<div id="${FORM_CONTAINER}" class="gitlab-form-open">
${toggleButton}
${form(content)}
</div>
`;
const addCommentForm = () => { const addCommentForm = () => {
const formWrapper = selectForm(); const formWrapper = selectForm();
formWrapper.innerHTML = comment; formWrapper.innerHTML = comment;
...@@ -31,13 +44,15 @@ function logoutUser() { ...@@ -31,13 +44,15 @@ function logoutUser() {
return; return;
} }
clearNote();
addLoginForm(); addLoginForm();
} }
function toggleForm() { function toggleForm() {
const container = selectContainer();
const collapseButton = selectCollapseButton(); const collapseButton = selectCollapseButton();
const currentForm = selectForm(); const currentForm = selectForm();
const formContainer = selectFormContainer();
const noteContainer = selectNoteContainer();
const OPEN = 'open'; const OPEN = 'open';
const CLOSED = 'closed'; const CLOSED = 'closed';
...@@ -49,7 +64,7 @@ function toggleForm() { ...@@ -49,7 +64,7 @@ function toggleForm() {
const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open']; const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open'];
const closedButtonClasses = [...openButtonClasses].reverse(); const closedButtonClasses = [...openButtonClasses].reverse();
const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper']; const openContainerClasses = ['gitlab-wrapper-closed', 'gitlab-wrapper-open'];
const closedContainerClasses = [...openContainerClasses].reverse(); const closedContainerClasses = [...openContainerClasses].reverse();
const stateVals = { const stateVals = {
...@@ -72,11 +87,16 @@ function toggleForm() { ...@@ -72,11 +87,16 @@ function toggleForm() {
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
const currentVals = stateVals[nextState]; const currentVals = stateVals[nextState];
container.classList.replace(...currentVals.containerClasses); formContainer.classList.replace(...currentVals.containerClasses);
container.style.backgroundColor = currentVals.backgroundColor; formContainer.style.backgroundColor = currentVals.backgroundColor;
formContainer.classList.toggle('gitlab-form-open');
currentForm.style.display = currentVals.display; currentForm.style.display = currentVals.display;
collapseButton.classList.replace(...currentVals.buttonClasses); collapseButton.classList.replace(...currentVals.buttonClasses);
collapseButton.innerHTML = currentVals.icon; collapseButton.innerHTML = currentVals.icon;
if (noteContainer && noteContainer.innerText.length > 0) {
noteContainer.style.display = currentVals.display;
}
} }
export { addCommentForm, addLoginForm, form, logoutUser, toggleForm }; export { addCommentForm, addLoginForm, buttonAndForm, logoutUser, toggleForm };
import './styles/toolbar.css'; import './styles/toolbar.css';
import { form, selectContainer, REVIEW_CONTAINER } from './components'; import { buttonAndForm, note, selectContainer, REVIEW_CONTAINER } from './components';
import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store'; import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store';
/* /*
...@@ -20,12 +20,11 @@ import { debounce, eventLookup, getInitialView, initializeState, updateWindowSiz ...@@ -20,12 +20,11 @@ import { debounce, eventLookup, getInitialView, initializeState, updateWindowSiz
window.addEventListener('load', () => { window.addEventListener('load', () => {
initializeState(window, document); initializeState(window, document);
const { content, toggleButton } = getInitialView(window); const mainContent = buttonAndForm(getInitialView(window));
const container = document.createElement('div'); const container = document.createElement('div');
container.setAttribute('id', REVIEW_CONTAINER); container.setAttribute('id', REVIEW_CONTAINER);
container.insertAdjacentHTML('beforeend', toggleButton); container.insertAdjacentHTML('beforeend', note);
container.insertAdjacentHTML('beforeend', form(content)); container.insertAdjacentHTML('beforeend', mainContent);
document.body.insertBefore(container, document.body.firstChild); document.body.insertBefore(container, document.body.firstChild);
......
...@@ -34,7 +34,7 @@ const initializeState = (wind, doc) => { ...@@ -34,7 +34,7 @@ const initializeState = (wind, doc) => {
const browser = getBrowserId(userAgent); const browser = getBrowserId(userAgent);
const scriptEl = doc.getElementById('review-app-toolbar-script'); const scriptEl = doc.getElementById('review-app-toolbar-script');
const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset; const { projectId, mergeRequestId, mrUrl, projectPath } = scriptEl.dataset;
// This mutates our default state object above. It's weird but it makes the linter happy. // This mutates our default state object above. It's weird but it makes the linter happy.
Object.assign(state, { Object.assign(state, {
...@@ -46,6 +46,7 @@ const initializeState = (wind, doc) => { ...@@ -46,6 +46,7 @@ const initializeState = (wind, doc) => {
mrUrl, mrUrl,
platform, platform,
projectId, projectId,
projectPath,
userAgent, userAgent,
}); });
}; };
......
...@@ -6,23 +6,42 @@ ...@@ -6,23 +6,42 @@
pointer-events: none; pointer-events: none;
} }
#gitlab-form-wrapper { #gitlab-comment {
background-color: #fafafa;
}
#gitlab-form {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 0;
}
#gitlab-note-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100% background-color: #fafafa;
border-radius: 4px;
margin-bottom: .5rem;
padding: 1rem;
}
#gitlab-form-wrapper {
overflow: auto;
display: flex;
flex-direction: row-reverse;
border-radius: 4px;
} }
#gitlab-review-container { #gitlab-review-container {
max-width: 22rem; max-width: 22rem;
max-height: 22rem; max-height: 22rem;
overflow: scroll; overflow: auto;
display: flex;
flex-direction: column;
position: fixed; position: fixed;
bottom: 1rem; bottom: 1rem;
right: 1rem; right: 1rem;
display: flex;
flex-direction: row-reverse;
padding: 1rem;
background-color: #fff;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell,
'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji'; 'Noto Color Emoji';
...@@ -31,12 +50,12 @@ ...@@ -31,12 +50,12 @@
color: #2e2e2e; color: #2e2e2e;
} }
.gitlab-open-wrapper { .gitlab-wrapper-open {
max-width: 22rem; max-width: 22rem;
max-height: 22rem; max-height: 22rem;
} }
.gitlab-closed-wrapper { .gitlab-wrapper-closed {
max-width: 3.4rem; max-width: 3.4rem;
max-height: 3.4rem; max-height: 3.4rem;
} }
...@@ -47,7 +66,7 @@ ...@@ -47,7 +66,7 @@
} }
.gitlab-button-secondary { .gitlab-button-secondary {
background: none #fff; background: none #fafafa;
margin: 0 .5rem; margin: 0 .5rem;
border: 1px solid #e3e3e3; border: 1px solid #e3e3e3;
} }
...@@ -113,6 +132,11 @@ ...@@ -113,6 +132,11 @@
align-items: baseline; align-items: baseline;
} }
.gitlab-form-open {
padding: 1rem;
background-color: #fafafa;
}
.gitlab-label { .gitlab-label {
font-weight: 600; font-weight: 600;
display: inline-block; display: inline-block;
...@@ -126,6 +150,10 @@ ...@@ -126,6 +150,10 @@
background-image: none; background-image: none;
} }
.gitlab-link:hover {
text-decoration: underline;
}
.gitlab-message { .gitlab-message {
padding: .25rem 0; padding: .25rem 0;
margin: 0; margin: 0;
......
---
title: Include a link back to the MR for Visual Review feedback form
merge_request: 29719
author:
type: changed
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment