Add MR form to runtime Visual Review configuration
* adds the ability to enter a merge request ID directly into the toolbar, * adds the option to save the ID to `localStorage` (as with the token), * adds a link to change the ID once entered * adds some more explanatory errors for 401 & 404 * saves the comment to session storage if the user navigates away without posting it
This commit is contained in:
parent
dbe3b9848b
commit
6bfdf529f5
|
@ -1,148 +1,39 @@
|
|||
import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants';
|
||||
import { clearNote, postError } from './note';
|
||||
import {
|
||||
buttonClearStyles,
|
||||
selectCommentBox,
|
||||
selectCommentButton,
|
||||
selectNote,
|
||||
selectNoteContainer,
|
||||
} from './utils';
|
||||
import { nextView } from '../store';
|
||||
import { localStorage, COMMENT_BOX, LOGOUT } from '../shared';
|
||||
import { clearNote } from './note';
|
||||
import { buttonClearStyles } from './utils';
|
||||
import { addForm } from './wrapper';
|
||||
import { changeSelectedMr, selectedMrNote } from './comment_mr_note';
|
||||
import postComment from './comment_post';
|
||||
import { saveComment, getSavedComment } from './comment_storage';
|
||||
|
||||
const comment = `
|
||||
<div>
|
||||
<textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true"></textarea>
|
||||
<p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
|
||||
</div>
|
||||
<div class="gitlab-button-wrapper">
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
const comment = state => {
|
||||
const savedComment = getSavedComment();
|
||||
|
||||
const resetCommentButton = () => {
|
||||
const commentButton = selectCommentButton();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Send feedback';
|
||||
commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
|
||||
commentButton.style.opacity = 1;
|
||||
};
|
||||
|
||||
const resetCommentBox = () => {
|
||||
const commentBox = selectCommentBox();
|
||||
commentBox.style.pointerEvents = 'auto';
|
||||
commentBox.style.color = BLACK;
|
||||
};
|
||||
|
||||
const resetCommentText = () => {
|
||||
const commentBox = selectCommentBox();
|
||||
commentBox.value = '';
|
||||
};
|
||||
|
||||
const resetComment = () => {
|
||||
resetCommentButton();
|
||||
resetCommentBox();
|
||||
resetCommentText();
|
||||
};
|
||||
|
||||
const confirmAndClear = feedbackInfo => {
|
||||
const commentButton = selectCommentButton();
|
||||
const currentNote = selectNote();
|
||||
const noteContainer = selectNoteContainer();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Feedback sent';
|
||||
noteContainer.style.visibility = 'visible';
|
||||
currentNote.insertAdjacentHTML('beforeend', feedbackInfo);
|
||||
|
||||
setTimeout(resetComment, 1000);
|
||||
setTimeout(clearNote, 6000);
|
||||
};
|
||||
|
||||
const setInProgressState = () => {
|
||||
const commentButton = selectCommentButton();
|
||||
const commentBox = selectCommentBox();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Sending feedback';
|
||||
commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary');
|
||||
commentButton.style.opacity = 0.5;
|
||||
commentBox.style.color = MUTED;
|
||||
commentBox.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
const postComment = ({
|
||||
href,
|
||||
platform,
|
||||
browser,
|
||||
userAgent,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
projectId,
|
||||
projectPath,
|
||||
mergeRequestId,
|
||||
mrUrl,
|
||||
token,
|
||||
}) => {
|
||||
// Clear any old errors
|
||||
clearNote(COMMENT_BOX);
|
||||
|
||||
setInProgressState();
|
||||
|
||||
const commentText = selectCommentBox().value.trim();
|
||||
|
||||
if (!commentText) {
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
postError('Your comment appears to be empty.', COMMENT_BOX);
|
||||
resetCommentBox();
|
||||
resetCommentButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const detailText = `
|
||||
\n
|
||||
<details>
|
||||
<summary>Metadata</summary>
|
||||
Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.
|
||||
<br /><br />
|
||||
<em>User agent: ${userAgent}</em>
|
||||
</details>
|
||||
return `
|
||||
<div>
|
||||
<textarea id="${COMMENT_BOX}" name="${COMMENT_BOX}" rows="3" placeholder="Enter your feedback or idea" class="gitlab-input" aria-required="true">${savedComment}</textarea>
|
||||
${selectedMrNote(state)}
|
||||
<p class="gitlab-metadata-note">Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.</p>
|
||||
</div>
|
||||
<div class="gitlab-button-wrapper">
|
||||
<button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button>
|
||||
<button class="gitlab-button gitlab-button-secondary" style="${buttonClearStyles}" type="button" id="${LOGOUT}"> Log out </button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const url = `
|
||||
${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
|
||||
|
||||
const body = `${commentText} ${detailText}`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ body }),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
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 => {
|
||||
postError(
|
||||
`Your comment could not be sent. Please try again. Error: ${err.message}`,
|
||||
COMMENT_BOX,
|
||||
);
|
||||
resetCommentBox();
|
||||
resetCommentButton();
|
||||
});
|
||||
};
|
||||
|
||||
export { comment, postComment };
|
||||
// This function is here becaause it is called only from the comment view
|
||||
// If we reach a design where we can logout from multiple views, promote this
|
||||
// to it's own package
|
||||
const logoutUser = state => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('mergeRequestId');
|
||||
state.token = '';
|
||||
state.mergeRequestId = '';
|
||||
|
||||
clearNote();
|
||||
addForm(nextView(state, COMMENT_BOX));
|
||||
};
|
||||
|
||||
export { changeSelectedMr, comment, logoutUser, postComment, saveComment };
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import { nextView } from '../store';
|
||||
import { localStorage, CHANGE_MR_ID_BUTTON, COMMENT_BOX } from '../shared';
|
||||
import { clearNote } from './note';
|
||||
import { buttonClearStyles } from './utils';
|
||||
import { addForm } from './wrapper';
|
||||
|
||||
const selectedMrNote = state => {
|
||||
const { mrUrl, projectPath, mergeRequestId } = state;
|
||||
|
||||
const mrLink = `${mrUrl}/${projectPath}/merge_requests/${mergeRequestId}`;
|
||||
|
||||
return `
|
||||
<p class="gitlab-metadata-note">
|
||||
This posts to merge request <a class="gitlab-link" href="${mrLink}">!${mergeRequestId}</a>.
|
||||
<button style="${buttonClearStyles}" type="button" id="${CHANGE_MR_ID_BUTTON}" class="gitlab-link gitlab-link-button">Change</button>
|
||||
</p>
|
||||
`;
|
||||
};
|
||||
|
||||
const clearMrId = state => {
|
||||
localStorage.removeItem('mergeRequestId');
|
||||
state.mergeRequestId = '';
|
||||
};
|
||||
|
||||
const changeSelectedMr = state => {
|
||||
clearMrId(state);
|
||||
clearNote();
|
||||
addForm(nextView(state, COMMENT_BOX));
|
||||
};
|
||||
|
||||
export { changeSelectedMr, selectedMrNote };
|
|
@ -0,0 +1,145 @@
|
|||
import { BLACK, COMMENT_BOX, MUTED } from '../shared';
|
||||
import { clearSavedComment } from './comment_storage';
|
||||
import { clearNote, postError } from './note';
|
||||
import { selectCommentBox, selectCommentButton, selectNote, selectNoteContainer } from './utils';
|
||||
|
||||
const resetCommentButton = () => {
|
||||
const commentButton = selectCommentButton();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Send feedback';
|
||||
commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success');
|
||||
commentButton.style.opacity = 1;
|
||||
};
|
||||
|
||||
const resetCommentBox = () => {
|
||||
const commentBox = selectCommentBox();
|
||||
commentBox.style.pointerEvents = 'auto';
|
||||
commentBox.style.color = BLACK;
|
||||
};
|
||||
|
||||
const resetCommentText = () => {
|
||||
const commentBox = selectCommentBox();
|
||||
commentBox.value = '';
|
||||
clearSavedComment();
|
||||
};
|
||||
|
||||
const resetComment = () => {
|
||||
resetCommentButton();
|
||||
resetCommentBox();
|
||||
resetCommentText();
|
||||
};
|
||||
|
||||
const confirmAndClear = feedbackInfo => {
|
||||
const commentButton = selectCommentButton();
|
||||
const currentNote = selectNote();
|
||||
const noteContainer = selectNoteContainer();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Feedback sent';
|
||||
noteContainer.style.visibility = 'visible';
|
||||
currentNote.insertAdjacentHTML('beforeend', feedbackInfo);
|
||||
|
||||
setTimeout(resetComment, 1000);
|
||||
setTimeout(clearNote, 6000);
|
||||
};
|
||||
|
||||
const setInProgressState = () => {
|
||||
const commentButton = selectCommentButton();
|
||||
const commentBox = selectCommentBox();
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
commentButton.innerText = 'Sending feedback';
|
||||
commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary');
|
||||
commentButton.style.opacity = 0.5;
|
||||
commentBox.style.color = MUTED;
|
||||
commentBox.style.pointerEvents = 'none';
|
||||
};
|
||||
|
||||
const commentErrors = error => {
|
||||
switch (error.status) {
|
||||
case 401:
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
return 'Unauthorized. You may have entered an incorrect authentication token.';
|
||||
case 404:
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
return 'Not found. You may have entered an incorrect merge request ID.';
|
||||
default:
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
return `Your comment could not be sent. Please try again. Error: ${error.message}`;
|
||||
}
|
||||
};
|
||||
|
||||
const postComment = ({
|
||||
platform,
|
||||
browser,
|
||||
userAgent,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
projectId,
|
||||
projectPath,
|
||||
mergeRequestId,
|
||||
mrUrl,
|
||||
token,
|
||||
}) => {
|
||||
// Clear any old errors
|
||||
clearNote(COMMENT_BOX);
|
||||
|
||||
setInProgressState();
|
||||
|
||||
const commentText = selectCommentBox().value.trim();
|
||||
// Get the href at the last moment to support SPAs
|
||||
const { href } = window.location;
|
||||
|
||||
if (!commentText) {
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
postError('Your comment appears to be empty.', COMMENT_BOX);
|
||||
resetCommentBox();
|
||||
resetCommentButton();
|
||||
return;
|
||||
}
|
||||
|
||||
const detailText = `
|
||||
\n
|
||||
<details>
|
||||
<summary>Metadata</summary>
|
||||
Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.
|
||||
<br /><br />
|
||||
<em>User agent: ${userAgent}</em>
|
||||
</details>
|
||||
`;
|
||||
|
||||
const url = `
|
||||
${mrUrl}/api/v4/projects/${projectId}/merge_requests/${mergeRequestId}/discussions`;
|
||||
|
||||
const body = `${commentText} ${detailText}`;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'PRIVATE-TOKEN': token,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ body }),
|
||||
})
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
return response.json();
|
||||
}
|
||||
|
||||
throw response;
|
||||
})
|
||||
.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 => {
|
||||
postError(commentErrors(err), COMMENT_BOX);
|
||||
resetCommentBox();
|
||||
resetCommentButton();
|
||||
});
|
||||
};
|
||||
|
||||
export default postComment;
|
|
@ -0,0 +1,20 @@
|
|||
import { selectCommentBox } from './utils';
|
||||
import { sessionStorage } from '../shared';
|
||||
|
||||
const getSavedComment = () => sessionStorage.getItem('comment') || '';
|
||||
|
||||
const saveComment = () => {
|
||||
const currentComment = selectCommentBox();
|
||||
|
||||
// This may be added to any view via top-level beforeunload listener
|
||||
// so let's skip if it does not apply
|
||||
if (currentComment && currentComment.value) {
|
||||
sessionStorage.setItem('comment', currentComment.value);
|
||||
}
|
||||
};
|
||||
|
||||
const clearSavedComment = () => {
|
||||
sessionStorage.removeItem('comment');
|
||||
};
|
||||
|
||||
export { getSavedComment, saveComment, clearSavedComment };
|
|
@ -0,0 +1,17 @@
|
|||
import { REMEMBER_ITEM } from '../shared';
|
||||
import { buttonClearStyles } from './utils';
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
const rememberBox = (rememberText = 'Remember me') => `
|
||||
<div class="gitlab-checkbox-wrapper">
|
||||
<input type="checkbox" id="${REMEMBER_ITEM}" name="${REMEMBER_ITEM}" value="remember">
|
||||
<label for="${REMEMBER_ITEM}" class="gitlab-checkbox-label">${rememberText}</label>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const submitButton = buttonId => `
|
||||
<div class="gitlab-button-wrapper">
|
||||
<button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${buttonId}"> Submit </button>
|
||||
</div>
|
||||
`;
|
||||
export { rememberBox, submitButton };
|
|
@ -1,33 +1,23 @@
|
|||
import { comment, postComment } from './comment';
|
||||
import {
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BUTTON,
|
||||
FORM_CONTAINER,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
REVIEW_CONTAINER,
|
||||
} from './constants';
|
||||
import { changeSelectedMr, comment, logoutUser, postComment, saveComment } from './comment';
|
||||
import { authorizeUser, login } from './login';
|
||||
import { addMr, mrForm } from './mr_id';
|
||||
import { note } from './note';
|
||||
import { selectContainer } from './utils';
|
||||
import { buttonAndForm, logoutUser, toggleForm } from './wrapper';
|
||||
import { collapseButton } from './wrapper_icons';
|
||||
import { selectContainer, selectForm } from './utils';
|
||||
import { buttonAndForm, toggleForm } from './wrapper';
|
||||
|
||||
export {
|
||||
addMr,
|
||||
authorizeUser,
|
||||
buttonAndForm,
|
||||
collapseButton,
|
||||
changeSelectedMr,
|
||||
comment,
|
||||
login,
|
||||
logoutUser,
|
||||
mrForm,
|
||||
note,
|
||||
postComment,
|
||||
saveComment,
|
||||
selectContainer,
|
||||
selectForm,
|
||||
toggleForm,
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BUTTON,
|
||||
FORM_CONTAINER,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
REVIEW_CONTAINER,
|
||||
};
|
||||
|
|
|
@ -1,35 +1,31 @@
|
|||
import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants';
|
||||
import { nextView } from '../store';
|
||||
import { localStorage, LOGIN, TOKEN_BOX } from '../shared';
|
||||
import { clearNote, postError } from './note';
|
||||
import { buttonClearStyles, selectRemember, selectToken } from './utils';
|
||||
import { addCommentForm } from './wrapper';
|
||||
import { rememberBox, submitButton } from './form_elements';
|
||||
import { selectRemember, selectToken } from './utils';
|
||||
import { addForm } from './wrapper';
|
||||
|
||||
const labelText = `
|
||||
Enter your <a class="gitlab-link" href="https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html">personal access token</a>
|
||||
`;
|
||||
|
||||
const login = `
|
||||
<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>
|
||||
<input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" aria-required="true" autocomplete="current-password">
|
||||
</div>
|
||||
<div class="gitlab-checkbox-wrapper">
|
||||
<input type="checkbox" id="${REMEMBER_TOKEN}" name="${REMEMBER_TOKEN}" value="remember">
|
||||
<label for="${REMEMBER_TOKEN}" class="gitlab-checkbox-label">Remember me</label>
|
||||
</div>
|
||||
<div class="gitlab-button-wrapper">
|
||||
<button class="gitlab-button-wide gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="${LOGIN}"> Submit </button>
|
||||
</div>
|
||||
<div>
|
||||
<label for="${TOKEN_BOX}" class="gitlab-label">${labelText}</label>
|
||||
<input class="gitlab-input" type="password" id="${TOKEN_BOX}" name="${TOKEN_BOX}" autocomplete="current-password" aria-required="true">
|
||||
</div>
|
||||
${rememberBox()}
|
||||
${submitButton(LOGIN)}
|
||||
`;
|
||||
|
||||
const storeToken = (token, state) => {
|
||||
const { localStorage } = window;
|
||||
const rememberMe = selectRemember().checked;
|
||||
|
||||
// All the browsers we support have localStorage, so let's silently fail
|
||||
// and go on with the rest of the functionality.
|
||||
try {
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
} finally {
|
||||
state.token = token;
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('token', token);
|
||||
}
|
||||
|
||||
state.token = token;
|
||||
};
|
||||
|
||||
const authorizeUser = state => {
|
||||
|
@ -45,7 +41,7 @@ const authorizeUser = state => {
|
|||
}
|
||||
|
||||
storeToken(token, state);
|
||||
addCommentForm();
|
||||
addForm(nextView(state, LOGIN));
|
||||
};
|
||||
|
||||
export { authorizeUser, login };
|
||||
export { authorizeUser, login, storeToken };
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
import { nextView } from '../store';
|
||||
import { MR_ID, MR_ID_BUTTON, localStorage } from '../shared';
|
||||
import { clearNote, postError } from './note';
|
||||
import { rememberBox, submitButton } from './form_elements';
|
||||
import { selectForm, selectMrBox, selectRemember } from './utils';
|
||||
import { addForm } from './wrapper';
|
||||
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
const mrLabel = `Enter your merge request ID`;
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
const mrRememberText = `Remember this number`;
|
||||
|
||||
const mrForm = `
|
||||
<div>
|
||||
<label for="${MR_ID}" class="gitlab-label">${mrLabel}</label>
|
||||
<input class="gitlab-input" type="text" pattern="[1-9][0-9]*" id="${MR_ID}" name="${MR_ID}" placeholder="e.g., 321" aria-required="true">
|
||||
</div>
|
||||
${rememberBox(mrRememberText)}
|
||||
${submitButton(MR_ID_BUTTON)}
|
||||
`;
|
||||
|
||||
const storeMR = (id, state) => {
|
||||
const rememberMe = selectRemember().checked;
|
||||
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('mergeRequestId', id);
|
||||
}
|
||||
|
||||
state.mergeRequestId = id;
|
||||
};
|
||||
|
||||
const getFormError = (mrNumber, form) => {
|
||||
if (!mrNumber) {
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
return 'Please enter your merge request ID number.';
|
||||
}
|
||||
|
||||
if (!form.checkValidity()) {
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
return 'Please remove any non-number values from the field.';
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const addMr = state => {
|
||||
// Clear any old errors
|
||||
clearNote(MR_ID);
|
||||
|
||||
const mrNumber = selectMrBox().value;
|
||||
const form = selectForm();
|
||||
const formError = getFormError(mrNumber, form);
|
||||
|
||||
if (formError) {
|
||||
postError(formError, MR_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
storeMR(mrNumber, state);
|
||||
addForm(nextView(state, MR_ID));
|
||||
};
|
||||
|
||||
export { addMr, mrForm, storeMR };
|
|
@ -1,4 +1,4 @@
|
|||
import { NOTE, NOTE_CONTAINER, RED } from './constants';
|
||||
import { NOTE, NOTE_CONTAINER, RED } from '../shared';
|
||||
import { selectById, selectNote, selectNoteContainer } from './utils';
|
||||
|
||||
const note = `
|
||||
|
|
|
@ -6,12 +6,13 @@ import {
|
|||
COMMENT_BUTTON,
|
||||
FORM,
|
||||
FORM_CONTAINER,
|
||||
MR_ID,
|
||||
NOTE,
|
||||
NOTE_CONTAINER,
|
||||
REMEMBER_TOKEN,
|
||||
REMEMBER_ITEM,
|
||||
REVIEW_CONTAINER,
|
||||
TOKEN_BOX,
|
||||
} from './constants';
|
||||
} from '../shared';
|
||||
|
||||
// this style must be applied inline in a handful of components
|
||||
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
|
||||
|
@ -27,9 +28,10 @@ const selectCommentButton = () => document.getElementById(COMMENT_BUTTON);
|
|||
const selectContainer = () => document.getElementById(REVIEW_CONTAINER);
|
||||
const selectForm = () => document.getElementById(FORM);
|
||||
const selectFormContainer = () => document.getElementById(FORM_CONTAINER);
|
||||
const selectMrBox = () => document.getElementById(MR_ID);
|
||||
const selectNote = () => document.getElementById(NOTE);
|
||||
const selectNoteContainer = () => document.getElementById(NOTE_CONTAINER);
|
||||
const selectRemember = () => document.getElementById(REMEMBER_TOKEN);
|
||||
const selectRemember = () => document.getElementById(REMEMBER_ITEM);
|
||||
const selectToken = () => document.getElementById(TOKEN_BOX);
|
||||
|
||||
export {
|
||||
|
@ -41,6 +43,7 @@ export {
|
|||
selectCommentButton,
|
||||
selectForm,
|
||||
selectFormContainer,
|
||||
selectMrBox,
|
||||
selectNote,
|
||||
selectNoteContainer,
|
||||
selectRemember,
|
||||
|
|
|
@ -1,55 +1,32 @@
|
|||
import { comment } from './comment';
|
||||
import { CLEAR, FORM, FORM_CONTAINER, WHITE } from './constants';
|
||||
import { login } from './login';
|
||||
import { clearNote } from './note';
|
||||
import { CLEAR, FORM, FORM_CONTAINER, WHITE } from '../shared';
|
||||
import {
|
||||
selectCollapseButton,
|
||||
selectForm,
|
||||
selectFormContainer,
|
||||
selectNoteContainer,
|
||||
} from './utils';
|
||||
import { commentIcon, compressIcon } from './wrapper_icons';
|
||||
import { collapseButton, commentIcon, compressIcon } from './wrapper_icons';
|
||||
|
||||
const form = content => `
|
||||
<form id="${FORM}">
|
||||
<form id="${FORM}" novalidate>
|
||||
${content}
|
||||
</form>
|
||||
`;
|
||||
|
||||
const buttonAndForm = ({ content, toggleButton }) => `
|
||||
const buttonAndForm = content => `
|
||||
<div id="${FORM_CONTAINER}" class="gitlab-form-open">
|
||||
${toggleButton}
|
||||
${collapseButton}
|
||||
${form(content)}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const addCommentForm = () => {
|
||||
const addForm = nextForm => {
|
||||
const formWrapper = selectForm();
|
||||
formWrapper.innerHTML = comment;
|
||||
formWrapper.innerHTML = nextForm;
|
||||
};
|
||||
|
||||
const addLoginForm = () => {
|
||||
const formWrapper = selectForm();
|
||||
formWrapper.innerHTML = login;
|
||||
};
|
||||
|
||||
function logoutUser() {
|
||||
const { localStorage } = window;
|
||||
|
||||
// All the browsers we support have localStorage, so let's silently fail
|
||||
// and go on with the rest of the functionality.
|
||||
try {
|
||||
localStorage.removeItem('token');
|
||||
} catch (err) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearNote();
|
||||
addLoginForm();
|
||||
}
|
||||
|
||||
function toggleForm() {
|
||||
const collapseButton = selectCollapseButton();
|
||||
const toggleButton = selectCollapseButton();
|
||||
const currentForm = selectForm();
|
||||
const formContainer = selectFormContainer();
|
||||
const noteContainer = selectNoteContainer();
|
||||
|
@ -84,19 +61,19 @@ function toggleForm() {
|
|||
},
|
||||
};
|
||||
|
||||
const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
|
||||
const nextState = toggleButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN;
|
||||
const currentVals = stateVals[nextState];
|
||||
|
||||
formContainer.classList.replace(...currentVals.containerClasses);
|
||||
formContainer.style.backgroundColor = currentVals.backgroundColor;
|
||||
formContainer.classList.toggle('gitlab-form-open');
|
||||
currentForm.style.display = currentVals.display;
|
||||
collapseButton.classList.replace(...currentVals.buttonClasses);
|
||||
collapseButton.innerHTML = currentVals.icon;
|
||||
toggleButton.classList.replace(...currentVals.buttonClasses);
|
||||
toggleButton.innerHTML = currentVals.icon;
|
||||
|
||||
if (noteContainer && noteContainer.innerText.length > 0) {
|
||||
noteContainer.style.display = currentVals.display;
|
||||
}
|
||||
}
|
||||
|
||||
export { addCommentForm, addLoginForm, buttonAndForm, logoutUser, toggleForm };
|
||||
export { addForm, buttonAndForm, toggleForm };
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import './styles/toolbar.css';
|
||||
|
||||
import { buttonAndForm, note, selectContainer, REVIEW_CONTAINER } from './components';
|
||||
import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store';
|
||||
import { buttonAndForm, note, selectForm, selectContainer } from './components';
|
||||
import { REVIEW_CONTAINER } from './shared';
|
||||
import { eventLookup, getInitialView, initializeGlobalListeners, initializeState } from './store';
|
||||
|
||||
/*
|
||||
|
||||
|
@ -20,7 +21,7 @@ import { debounce, eventLookup, getInitialView, initializeState, updateWindowSiz
|
|||
window.addEventListener('load', () => {
|
||||
initializeState(window, document);
|
||||
|
||||
const mainContent = buttonAndForm(getInitialView(window));
|
||||
const mainContent = buttonAndForm(getInitialView());
|
||||
const container = document.createElement('div');
|
||||
container.setAttribute('id', REVIEW_CONTAINER);
|
||||
container.insertAdjacentHTML('beforeend', note);
|
||||
|
@ -29,8 +30,22 @@ window.addEventListener('load', () => {
|
|||
document.body.insertBefore(container, document.body.firstChild);
|
||||
|
||||
selectContainer().addEventListener('click', event => {
|
||||
eventLookup(event)();
|
||||
eventLookup(event.target.id)();
|
||||
});
|
||||
|
||||
window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200));
|
||||
selectForm().addEventListener('submit', event => {
|
||||
// this is important to prevent the form from adding data
|
||||
// as URL params and inadvertently revealing secrets
|
||||
event.preventDefault();
|
||||
|
||||
const id =
|
||||
event.target.querySelector('.gitlab-button-wrapper') &&
|
||||
event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0] &&
|
||||
event.target.querySelector('.gitlab-button-wrapper').getElementsByTagName('button')[0].id;
|
||||
|
||||
// even if this is called with false, it's ok; it will get the default no-op
|
||||
eventLookup(id)();
|
||||
});
|
||||
|
||||
initializeGlobalListeners();
|
||||
});
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
// component selectors
|
||||
const CHANGE_MR_ID_BUTTON = 'gitlab-change-mr';
|
||||
const COLLAPSE_BUTTON = 'gitlab-collapse';
|
||||
const COMMENT_BOX = 'gitlab-comment';
|
||||
const COMMENT_BUTTON = 'gitlab-comment-button';
|
||||
const FORM = 'gitlab-form';
|
||||
const FORM_CONTAINER = 'gitlab-form-wrapper';
|
||||
const LOGIN = 'gitlab-login';
|
||||
const LOGIN = 'gitlab-login-button';
|
||||
const LOGOUT = 'gitlab-logout-button';
|
||||
const MR_ID = 'gitlab-submit-mr';
|
||||
const MR_ID_BUTTON = 'gitlab-submit-mr-button';
|
||||
const NOTE = 'gitlab-validation-note';
|
||||
const NOTE_CONTAINER = 'gitlab-note-wrapper';
|
||||
const REMEMBER_TOKEN = 'gitlab-remember_token';
|
||||
const REMEMBER_ITEM = 'gitlab-remember-item';
|
||||
const REVIEW_CONTAINER = 'gitlab-review-container';
|
||||
const TOKEN_BOX = 'gitlab-token';
|
||||
|
||||
|
@ -21,6 +24,7 @@ const RED = 'rgba(219, 59, 33, 1)';
|
|||
const WHITE = 'rgba(250, 250, 250, 1)';
|
||||
|
||||
export {
|
||||
CHANGE_MR_ID_BUTTON,
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BOX,
|
||||
COMMENT_BUTTON,
|
||||
|
@ -28,9 +32,11 @@ export {
|
|||
FORM_CONTAINER,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
MR_ID,
|
||||
MR_ID_BUTTON,
|
||||
NOTE,
|
||||
NOTE_CONTAINER,
|
||||
REMEMBER_TOKEN,
|
||||
REMEMBER_ITEM,
|
||||
REVIEW_CONTAINER,
|
||||
TOKEN_BOX,
|
||||
BLACK,
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
CHANGE_MR_ID_BUTTON,
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BOX,
|
||||
COMMENT_BUTTON,
|
||||
FORM,
|
||||
FORM_CONTAINER,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
MR_ID,
|
||||
MR_ID_BUTTON,
|
||||
NOTE,
|
||||
NOTE_CONTAINER,
|
||||
REMEMBER_ITEM,
|
||||
REVIEW_CONTAINER,
|
||||
TOKEN_BOX,
|
||||
BLACK,
|
||||
CLEAR,
|
||||
MUTED,
|
||||
RED,
|
||||
WHITE,
|
||||
} from './constants';
|
||||
|
||||
import { localStorage, sessionStorage } from './storage_utils';
|
||||
|
||||
export {
|
||||
localStorage,
|
||||
sessionStorage,
|
||||
CHANGE_MR_ID_BUTTON,
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BOX,
|
||||
COMMENT_BUTTON,
|
||||
FORM,
|
||||
FORM_CONTAINER,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
MR_ID,
|
||||
MR_ID_BUTTON,
|
||||
NOTE,
|
||||
NOTE_CONTAINER,
|
||||
REMEMBER_ITEM,
|
||||
REVIEW_CONTAINER,
|
||||
TOKEN_BOX,
|
||||
BLACK,
|
||||
CLEAR,
|
||||
MUTED,
|
||||
RED,
|
||||
WHITE,
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
import { setUsingGracefulStorageFlag } from '../store/state';
|
||||
|
||||
const TEST_KEY = 'gitlab-storage-test';
|
||||
|
||||
const createStorageStub = () => {
|
||||
const items = {};
|
||||
|
||||
return {
|
||||
getItem(key) {
|
||||
return items[key];
|
||||
},
|
||||
setItem(key, value) {
|
||||
items[key] = value;
|
||||
},
|
||||
removeItem(key) {
|
||||
delete items[key];
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const hasStorageSupport = storage => {
|
||||
// Support test taken from https://stackoverflow.com/a/11214467/1708147
|
||||
try {
|
||||
storage.setItem(TEST_KEY, TEST_KEY);
|
||||
storage.removeItem(TEST_KEY);
|
||||
setUsingGracefulStorageFlag(true);
|
||||
|
||||
return true;
|
||||
} catch (err) {
|
||||
setUsingGracefulStorageFlag(false);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const useGracefulStorage = storage =>
|
||||
// If a browser does not support local storage, let's return a graceful implementation.
|
||||
hasStorageSupport(storage) ? storage : createStorageStub();
|
||||
|
||||
const localStorage = useGracefulStorage(window.localStorage);
|
||||
const sessionStorage = useGracefulStorage(window.sessionStorage);
|
||||
|
||||
export { localStorage, sessionStorage };
|
|
@ -1,20 +1,37 @@
|
|||
import {
|
||||
addMr,
|
||||
authorizeUser,
|
||||
changeSelectedMr,
|
||||
logoutUser,
|
||||
postComment,
|
||||
saveComment,
|
||||
toggleForm,
|
||||
} from '../components';
|
||||
|
||||
import {
|
||||
CHANGE_MR_ID_BUTTON,
|
||||
COLLAPSE_BUTTON,
|
||||
COMMENT_BUTTON,
|
||||
LOGIN,
|
||||
LOGOUT,
|
||||
} from '../components';
|
||||
MR_ID_BUTTON,
|
||||
} from '../shared';
|
||||
|
||||
import { state } from './state';
|
||||
import debounce from './utils';
|
||||
|
||||
const noop = () => {};
|
||||
|
||||
const eventLookup = ({ target: { id } }) => {
|
||||
// State needs to be bound here to be acted on
|
||||
// because these are called by click events and
|
||||
// as such are called with only the `event` object
|
||||
const eventLookup = id => {
|
||||
switch (id) {
|
||||
case CHANGE_MR_ID_BUTTON:
|
||||
return () => {
|
||||
saveComment();
|
||||
changeSelectedMr(state);
|
||||
};
|
||||
case COLLAPSE_BUTTON:
|
||||
return toggleForm;
|
||||
case COMMENT_BUTTON:
|
||||
|
@ -22,7 +39,12 @@ const eventLookup = ({ target: { id } }) => {
|
|||
case LOGIN:
|
||||
return authorizeUser.bind(null, state);
|
||||
case LOGOUT:
|
||||
return logoutUser;
|
||||
return () => {
|
||||
saveComment();
|
||||
logoutUser(state);
|
||||
};
|
||||
case MR_ID_BUTTON:
|
||||
return addMr.bind(null, state);
|
||||
default:
|
||||
return noop;
|
||||
}
|
||||
|
@ -33,4 +55,19 @@ const updateWindowSize = wind => {
|
|||
state.innerHeight = wind.innerHeight;
|
||||
};
|
||||
|
||||
export { eventLookup, updateWindowSize };
|
||||
const initializeGlobalListeners = () => {
|
||||
window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200));
|
||||
window.addEventListener('beforeunload', event => {
|
||||
if (state.usingGracefulStorage) {
|
||||
// if there is no browser storage support, reloading will lose the comment; this way, the user will be warned
|
||||
// we assign the return value because it is required by Chrome see: https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload#Example,
|
||||
event.preventDefault();
|
||||
/* eslint-disable-next-line no-param-reassign */
|
||||
event.returnValue = '';
|
||||
}
|
||||
|
||||
saveComment();
|
||||
});
|
||||
};
|
||||
|
||||
export { eventLookup, initializeGlobalListeners };
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
import { eventLookup, updateWindowSize } from './events';
|
||||
import { getInitialView, initializeState } from './state';
|
||||
import debounce from './utils';
|
||||
import { eventLookup, initializeGlobalListeners } from './events';
|
||||
import { nextView, getInitialView, initializeState, setUsingGracefulStorageFlag } from './state';
|
||||
|
||||
export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize };
|
||||
export {
|
||||
eventLookup,
|
||||
getInitialView,
|
||||
initializeGlobalListeners,
|
||||
initializeState,
|
||||
nextView,
|
||||
setUsingGracefulStorageFlag,
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { comment, login, collapseButton } from '../components';
|
||||
import { comment, login, mrForm } from '../components';
|
||||
import { localStorage, COMMENT_BOX, LOGIN, MR_ID } from '../shared';
|
||||
|
||||
const state = {
|
||||
browser: '',
|
||||
href: '',
|
||||
usingGracefulStorage: '',
|
||||
innerWidth: '',
|
||||
innerHeight: '',
|
||||
mergeRequestId: '',
|
||||
|
@ -23,11 +24,31 @@ const getBrowserId = sUsrAg => {
|
|||
return aKeys[nIdx];
|
||||
};
|
||||
|
||||
const nextView = (appState, form = 'none') => {
|
||||
const formsList = {
|
||||
[COMMENT_BOX]: currentState => (currentState.token ? mrForm : login),
|
||||
[LOGIN]: currentState => (currentState.mergeRequestId ? comment(currentState) : mrForm),
|
||||
[MR_ID]: currentState => (currentState.token ? comment(currentState) : login),
|
||||
none: currentState => {
|
||||
if (!currentState.token) {
|
||||
return login;
|
||||
}
|
||||
|
||||
if (currentState.token && !currentState.mergeRequestId) {
|
||||
return mrForm;
|
||||
}
|
||||
|
||||
return comment(currentState);
|
||||
},
|
||||
};
|
||||
|
||||
return formsList[form](appState);
|
||||
};
|
||||
|
||||
const initializeState = (wind, doc) => {
|
||||
const {
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
location: { href },
|
||||
navigator: { platform, userAgent },
|
||||
} = wind;
|
||||
|
||||
|
@ -39,7 +60,6 @@ const initializeState = (wind, doc) => {
|
|||
// This mutates our default state object above. It's weird but it makes the linter happy.
|
||||
Object.assign(state, {
|
||||
browser,
|
||||
href,
|
||||
innerWidth,
|
||||
innerHeight,
|
||||
mergeRequestId,
|
||||
|
@ -49,30 +69,27 @@ const initializeState = (wind, doc) => {
|
|||
projectPath,
|
||||
userAgent,
|
||||
});
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
function getInitialView({ localStorage }) {
|
||||
const loginView = {
|
||||
content: login,
|
||||
toggleButton: collapseButton,
|
||||
};
|
||||
const getInitialView = () => {
|
||||
const token = localStorage.getItem('token');
|
||||
const mrId = localStorage.getItem('mergeRequestId');
|
||||
|
||||
const commentView = {
|
||||
content: comment,
|
||||
toggleButton: collapseButton,
|
||||
};
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (token) {
|
||||
state.token = token;
|
||||
return commentView;
|
||||
}
|
||||
return loginView;
|
||||
} catch (err) {
|
||||
return loginView;
|
||||
if (token) {
|
||||
state.token = token;
|
||||
}
|
||||
}
|
||||
|
||||
export { initializeState, getInitialView, state };
|
||||
if (mrId) {
|
||||
state.mergeRequestId = mrId;
|
||||
}
|
||||
|
||||
return nextView(state);
|
||||
};
|
||||
|
||||
const setUsingGracefulStorageFlag = flag => {
|
||||
state.usingGracefulStorage = !flag;
|
||||
};
|
||||
|
||||
export { initializeState, getInitialView, nextView, setUsingGracefulStorageFlag, state };
|
||||
|
|
|
@ -107,10 +107,14 @@
|
|||
}
|
||||
|
||||
.gitlab-button-wrapper {
|
||||
margin-top: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: flex-end;
|
||||
/*
|
||||
this makes sure the hit enter to submit picks the correct button
|
||||
on the comment view
|
||||
*/
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.gitlab-collapse {
|
||||
|
@ -155,6 +159,12 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.gitlab-link-button {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 0 .15rem;
|
||||
}
|
||||
|
||||
.gitlab-message {
|
||||
padding: .25rem 0;
|
||||
margin: 0;
|
||||
|
@ -165,7 +175,7 @@
|
|||
font-size: .7rem;
|
||||
line-height: 1rem;
|
||||
color: #666;
|
||||
margin-bottom: 0;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
|
||||
.gitlab-input {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Add MR form to Visual Review (EE) runtime configuration
|
||||
merge_request: 30481
|
||||
author:
|
||||
type: changed
|
Loading…
Reference in New Issue