summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Slaughter <pslaughter@gitlab.com>2019-07-29 15:42:43 +0000
committerPaul Slaughter <pslaughter@gitlab.com>2019-07-29 15:42:43 +0000
commit6686cd35039a84b123799b468dbf1b7b2e5f2a6e (patch)
tree632def3f91b4281610d48176e772a5dc42501258
parentdbe3b9848b884ca9705259110edf58e801c89e5e (diff)
parent6bfdf529f5b6f604e1d157fa379a108d1b7f8586 (diff)
downloadgitlab-ce-6686cd35039a84b123799b468dbf1b7b2e5f2a6e.tar.gz
Merge branch '64190-add-mr-form' into 'master'
Resolve "Support runtime Visual Review configuration" See merge request gitlab-org/gitlab-ce!30481
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js179
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js31
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_post.js145
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment_storage.js20
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/form_elements.js17
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js28
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js46
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/mr_id.js63
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js2
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js9
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js47
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js25
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/constants.js (renamed from app/assets/javascripts/visual_review_toolbar/components/constants.js)12
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/index.js49
-rw-r--r--app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js42
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/events.js45
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/index.js14
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js67
-rw-r--r--app/assets/javascripts/visual_review_toolbar/styles/toolbar.css16
-rw-r--r--changelogs/unreleased/64190-add-mr-form.yml5
20 files changed, 591 insertions, 271 deletions
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js
index 04bfb5e9532..20effc1751d 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/comment.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js
@@ -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';
-
-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 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>
+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 = state => {
+ const savedComment = getSavedComment();
+
+ 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();
- });
+// 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 { comment, postComment };
+export { changeSelectedMr, comment, logoutUser, postComment, saveComment };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js b/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js
new file mode 100644
index 00000000000..f71ffbf4f20
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment_mr_note.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_post.js b/app/assets/javascripts/visual_review_toolbar/components/comment_post.js
new file mode 100644
index 00000000000..ee5f2b62425
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment_post.js
@@ -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;
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js b/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js
new file mode 100644
index 00000000000..32a9e7e2f05
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/comment_storage.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/form_elements.js b/app/assets/javascripts/visual_review_toolbar/components/form_elements.js
new file mode 100644
index 00000000000..608488a6fea
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/form_elements.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js
index 50b52d7d3a2..e88b3637ad8 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/index.js
@@ -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,
};
diff --git a/app/assets/javascripts/visual_review_toolbar/components/login.js b/app/assets/javascripts/visual_review_toolbar/components/login.js
index 0a71299f041..4a6976ef2fd 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/login.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/login.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/mr_id.js b/app/assets/javascripts/visual_review_toolbar/components/mr_id.js
new file mode 100644
index 00000000000..f51e9631dd2
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/components/mr_id.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js
index 0150f640aae..9cddcb710f2 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/note.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/note.js
@@ -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 = `
diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js
index 00f4460925d..4ec9bd4a32a 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/utils.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js
@@ -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,
diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
index f2eaf1d7916..fdf8ad7c41f 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
+++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js
index f94eb88835a..67b3fadd772 100644
--- a/app/assets/javascripts/visual_review_toolbar/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/index.js
@@ -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();
});
diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/shared/constants.js
index 07fcb179d15..a56ea378b14 100644
--- a/app/assets/javascripts/visual_review_toolbar/components/constants.js
+++ b/app/assets/javascripts/visual_review_toolbar/shared/constants.js
@@ -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,
diff --git a/app/assets/javascripts/visual_review_toolbar/shared/index.js b/app/assets/javascripts/visual_review_toolbar/shared/index.js
new file mode 100644
index 00000000000..751eae74dde
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/shared/index.js
@@ -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,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js b/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js
new file mode 100644
index 00000000000..00456d3536e
--- /dev/null
+++ b/app/assets/javascripts/visual_review_toolbar/shared/storage_utils.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js
index 93996be8473..c9095c77ef1 100644
--- a/app/assets/javascripts/visual_review_toolbar/store/events.js
+++ b/app/assets/javascripts/visual_review_toolbar/store/events.js
@@ -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 };
diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js
index 7143588c0bf..07c8dd6f1d2 100644
--- a/app/assets/javascripts/visual_review_toolbar/store/index.js
+++ b/app/assets/javascripts/visual_review_toolbar/store/index.js
@@ -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,
+};
diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js
index 22702d524b8..741a5c7d99c 100644
--- a/app/assets/javascripts/visual_review_toolbar/store/state.js
+++ b/app/assets/javascripts/visual_review_toolbar/store/state.js
@@ -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,
});
-};
-function getInitialView({ localStorage }) {
- const loginView = {
- content: login,
- toggleButton: collapseButton,
- };
+ return state;
+};
- const commentView = {
- content: comment,
- toggleButton: collapseButton,
- };
+const getInitialView = () => {
+ const token = localStorage.getItem('token');
+ const mrId = localStorage.getItem('mergeRequestId');
- try {
- const token = localStorage.getItem('token');
+ if (token) {
+ state.token = token;
+ }
- if (token) {
- state.token = token;
- return commentView;
- }
- return loginView;
- } catch (err) {
- return loginView;
+ if (mrId) {
+ state.mergeRequestId = mrId;
}
-}
-export { initializeState, getInitialView, state };
+ return nextView(state);
+};
+
+const setUsingGracefulStorageFlag = flag => {
+ state.usingGracefulStorage = !flag;
+};
+
+export { initializeState, getInitialView, nextView, setUsingGracefulStorageFlag, state };
diff --git a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
index 6a7b2f52549..e5732fd5d93 100644
--- a/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
+++ b/app/assets/javascripts/visual_review_toolbar/styles/toolbar.css
@@ -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 {
diff --git a/changelogs/unreleased/64190-add-mr-form.yml b/changelogs/unreleased/64190-add-mr-form.yml
new file mode 100644
index 00000000000..08340d01fd8
--- /dev/null
+++ b/changelogs/unreleased/64190-add-mr-form.yml
@@ -0,0 +1,5 @@
+---
+title: Add MR form to Visual Review (EE) runtime configuration
+merge_request: 30481
+author:
+type: changed