diff options
14 files changed, 571 insertions, 378 deletions
diff --git a/app/assets/javascripts/visual_review_toolbar/components/comment.js b/app/assets/javascripts/visual_review_toolbar/components/comment.js new file mode 100644 index 00000000000..3ad99732c4c --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/comment.js @@ -0,0 +1,127 @@ +import { BLACK, COMMENT_BOX, MUTED, LOGOUT } from './constants'; +import { clearNote, note, postError } from './note'; +import { buttonClearStyles, selectCommentBox, selectCommentButton, selectNote } 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> + ${note} + <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}"> Logout </button> + <button class="gitlab-button gitlab-button-success" style="${buttonClearStyles}" type="button" id="gitlab-comment-button"> Send feedback </button> + </div> +`; + +const resetCommentBox = () => { + const commentBox = selectCommentBox(); + const commentButton = selectCommentButton(); + + commentButton.innerText = 'Send feedback'; + commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); + commentButton.style.opacity = 1; + + commentBox.style.pointerEvents = 'auto'; + commentBox.style.color = BLACK; +}; + +const resetCommentButton = () => { + const commentBox = selectCommentBox(); + const currentNote = selectNote(); + + commentBox.value = ''; + currentNote.innerText = ''; +}; + +const resetComment = () => { + resetCommentBox(); + resetCommentButton(); +}; + +const confirmAndClear = mergeRequestId => { + const commentButton = selectCommentButton(); + const currentNote = selectNote(); + + commentButton.innerText = 'Feedback sent'; + currentNote.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`; + setTimeout(resetComment, 2000); +}; + +const setInProgressState = () => { + const commentButton = selectCommentButton(); + const commentBox = selectCommentBox(); + + 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, + mergeRequestId, + mrUrl, + token, +}) => { + // Clear any old errors + clearNote(COMMENT_BOX); + + setInProgressState(); + + const commentText = selectCommentBox().value.trim(); + + if (!commentText) { + postError('Your comment appears to be empty.', COMMENT_BOX); + resetCommentBox(); + 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) { + confirmAndClear(mergeRequestId); + return; + } + + throw new Error(`${response.status}: ${response.statusText}`); + }) + .catch(err => { + postError( + `Your comment could not be sent. Please try again. Error: ${err.message}`, + COMMENT_BOX, + ); + resetCommentBox(); + }); +}; + +export { comment, postComment }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/constants.js b/app/assets/javascripts/visual_review_toolbar/components/constants.js new file mode 100644 index 00000000000..32ed1153515 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/constants.js @@ -0,0 +1,37 @@ +// component selectors +const COLLAPSE_BUTTON = 'gitlab-collapse'; +const COMMENT_BOX = 'gitlab-comment'; +const COMMENT_BUTTON = 'gitlab-comment-button'; +const FORM = 'gitlab-form-wrapper'; +const LOGIN = 'gitlab-login'; +const LOGOUT = 'gitlab-logout-button'; +const NOTE = 'gitlab-validation-note'; +const REMEMBER_TOKEN = 'gitlab-remember_token'; +const REVIEW_CONTAINER = 'gitlab-review-container'; +const TOKEN_BOX = 'gitlab-token'; + +// colors — these are applied programmatically +// rest of styles belong in ./styles +const BLACK = 'rgba(46, 46, 46, 1)'; +const CLEAR = 'rgba(255, 255, 255, 0)'; +const MUTED = 'rgba(223, 223, 223, 0.5)'; +const RED = 'rgba(219, 59, 33, 1)'; +const WHITE = 'rgba(255, 255, 255, 1)'; + +export { + COLLAPSE_BUTTON, + COMMENT_BOX, + COMMENT_BUTTON, + FORM, + LOGIN, + LOGOUT, + NOTE, + REMEMBER_TOKEN, + REVIEW_CONTAINER, + TOKEN_BOX, + BLACK, + CLEAR, + MUTED, + RED, + WHITE, +}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/index.js b/app/assets/javascripts/visual_review_toolbar/components/index.js new file mode 100644 index 00000000000..43581818152 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/index.js @@ -0,0 +1,23 @@ +import { comment, postComment } from './comment'; +import { COLLAPSE_BUTTON, COMMENT_BUTTON, LOGIN, LOGOUT, REVIEW_CONTAINER } from './constants'; +import { authorizeUser, login } from './login'; +import { selectContainer } from './utils'; +import { form, logoutUser, toggleForm } from './wrapper'; +import { collapseButton } from './wrapper_icons'; + +export { + authorizeUser, + collapseButton, + comment, + form, + login, + logoutUser, + postComment, + selectContainer, + toggleForm, + COLLAPSE_BUTTON, + COMMENT_BUTTON, + 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 new file mode 100644 index 00000000000..a2fb905aca7 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/login.js @@ -0,0 +1,51 @@ +import { LOGIN, REMEMBER_TOKEN, TOKEN_BOX } from './constants'; +import { clearNote, note, postError } from './note'; +import { buttonClearStyles, selectRemember, selectToken } from './utils'; +import { addCommentForm } from './wrapper'; + +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"> + ${note} + </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> +`; + +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; + } +}; + +const authorizeUser = state => { + // Clear any old errors + clearNote(TOKEN_BOX); + + const token = selectToken().value; + + if (!token) { + postError('Please enter your token.', TOKEN_BOX); + return; + } + + storeToken(token, state); + addCommentForm(); +}; + +export { authorizeUser, login }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/note.js b/app/assets/javascripts/visual_review_toolbar/components/note.js new file mode 100644 index 00000000000..dfebf58fd95 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/note.js @@ -0,0 +1,27 @@ +import { NOTE, RED } from './constants'; +import { selectById, selectNote } from './utils'; + +const note = ` + <p id=${NOTE} class='gitlab-message'></p> +`; + +const clearNote = inputId => { + const currentNote = selectNote(); + currentNote.innerText = ''; + currentNote.style.color = ''; + + if (inputId) { + const field = document.getElementById(inputId); + field.style.borderColor = ''; + } +}; + +const postError = (message, inputId) => { + const currentNote = selectNote(); + const field = selectById(inputId); + field.style.borderColor = RED; + currentNote.style.color = RED; + currentNote.innerText = message; +}; + +export { clearNote, note, postError }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/utils.js b/app/assets/javascripts/visual_review_toolbar/components/utils.js new file mode 100644 index 00000000000..5938441cd8c --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/utils.js @@ -0,0 +1,41 @@ +/* global document */ + +import { + COLLAPSE_BUTTON, + COMMENT_BOX, + COMMENT_BUTTON, + FORM, + NOTE, + REMEMBER_TOKEN, + REVIEW_CONTAINER, + TOKEN_BOX, +} from './constants'; + +// this style must be applied inline in a handful of components +const buttonClearStyles = ` + -webkit-appearance: none; +`; + +// selector functions to abstract out a little +const selectById = id => document.getElementById(id); +const selectCollapseButton = () => document.getElementById(COLLAPSE_BUTTON); +const selectCommentBox = () => document.getElementById(COMMENT_BOX); +const selectCommentButton = () => document.getElementById(COMMENT_BUTTON); +const selectContainer = () => document.getElementById(REVIEW_CONTAINER); +const selectForm = () => document.getElementById(FORM); +const selectNote = () => document.getElementById(NOTE); +const selectRemember = () => document.getElementById(REMEMBER_TOKEN); +const selectToken = () => document.getElementById(TOKEN_BOX); + +export { + buttonClearStyles, + selectById, + selectCollapseButton, + selectContainer, + selectCommentBox, + selectCommentButton, + selectForm, + selectNote, + selectRemember, + selectToken, +}; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js new file mode 100644 index 00000000000..233b7ec496c --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper.js @@ -0,0 +1,82 @@ +import { comment } from './comment'; +import { CLEAR, FORM, WHITE } from './constants'; +import { login } from './login'; +import { selectCollapseButton, selectContainer, selectForm } from './utils'; +import { commentIcon, compressIcon } from './wrapper_icons'; + +const form = content => ` + <form id=${FORM}> + ${content} + </form> +`; + +const addCommentForm = () => { + const formWrapper = selectForm(); + formWrapper.innerHTML = comment; +}; + +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; + } + + addLoginForm(); +} + +function toggleForm() { + const container = selectContainer(); + const collapseButton = selectCollapseButton(); + const currentForm = selectForm(); + const OPEN = 'open'; + const CLOSED = 'closed'; + + /* + You may wonder why we spread the arrays before we reverse them. + In the immortal words of MDN, + Careful: reverse is destructive. It also changes the original array + */ + + const openButtonClasses = ['gitlab-collapse-closed', 'gitlab-collapse-open']; + const closedButtonClasses = [...openButtonClasses].reverse(); + const openContainerClasses = ['gitlab-closed-wrapper', 'gitlab-open-wrapper']; + const closedContainerClasses = [...openContainerClasses].reverse(); + + const stateVals = { + [OPEN]: { + buttonClasses: openButtonClasses, + containerClasses: openContainerClasses, + icon: compressIcon, + display: 'flex', + backgroundColor: WHITE, + }, + [CLOSED]: { + buttonClasses: closedButtonClasses, + containerClasses: closedContainerClasses, + icon: commentIcon, + display: 'none', + backgroundColor: CLEAR, + }, + }; + + const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; + const currentVals = stateVals[nextState]; + + container.classList.replace(...currentVals.containerClasses); + container.style.backgroundColor = currentVals.backgroundColor; + currentForm.style.display = currentVals.display; + collapseButton.classList.replace(...currentVals.buttonClasses); + collapseButton.innerHTML = currentVals.icon; +} + +export { addCommentForm, addLoginForm, form, logoutUser, toggleForm }; diff --git a/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js new file mode 100644 index 00000000000..b686fd4f5c2 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js @@ -0,0 +1,15 @@ +import { buttonClearStyles } from './utils'; + +const commentIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg> +`; + +const compressIcon = ` + <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg> +`; + +const collapseButton = ` + <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button> +`; + +export { commentIcon, compressIcon, collapseButton }; diff --git a/app/assets/javascripts/visual_review_toolbar/index.js b/app/assets/javascripts/visual_review_toolbar/index.js index 91d0382feac..941d77e25b4 100644 --- a/app/assets/javascripts/visual_review_toolbar/index.js +++ b/app/assets/javascripts/visual_review_toolbar/index.js @@ -1,2 +1,37 @@ import './styles/toolbar.css'; -import 'vendor/visual_review_toolbar'; + +import { form, selectContainer, REVIEW_CONTAINER } from './components'; +import { debounce, eventLookup, getInitialView, initializeState, updateWindowSize } from './store'; + +/* + + Welcome to the visual review toolbar files. A few useful notes: + + - These files build a static script that is served from our webpack + assets folder. (https://gitlab.com/assets/webpack/visual_review_toolbar.js) + + - To compile this file, run `yarn webpack-vrt`. + + - Vue is not used in these files because we do not want to ask users to + install another library at this time. It's all pure vanilla javascript. + +*/ + +window.addEventListener('load', () => { + initializeState(window, document); + + const { content, toggleButton } = getInitialView(window); + const container = document.createElement('div'); + + container.setAttribute('id', REVIEW_CONTAINER); + container.insertAdjacentHTML('beforeend', toggleButton); + container.insertAdjacentHTML('beforeend', form(content)); + + document.body.insertBefore(container, document.body.firstChild); + + selectContainer().addEventListener('click', event => { + eventLookup(event)(); + }); + + window.addEventListener('resize', debounce(updateWindowSize.bind(null, window), 200)); +}); diff --git a/app/assets/javascripts/visual_review_toolbar/store/events.js b/app/assets/javascripts/visual_review_toolbar/store/events.js new file mode 100644 index 00000000000..93996be8473 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/events.js @@ -0,0 +1,36 @@ +import { + authorizeUser, + logoutUser, + postComment, + toggleForm, + COLLAPSE_BUTTON, + COMMENT_BUTTON, + LOGIN, + LOGOUT, +} from '../components'; + +import { state } from './state'; + +const noop = () => {}; + +const eventLookup = ({ target: { id } }) => { + switch (id) { + case COLLAPSE_BUTTON: + return toggleForm; + case COMMENT_BUTTON: + return postComment.bind(null, state); + case LOGIN: + return authorizeUser.bind(null, state); + case LOGOUT: + return logoutUser; + default: + return noop; + } +}; + +const updateWindowSize = wind => { + state.innerWidth = wind.innerWidth; + state.innerHeight = wind.innerHeight; +}; + +export { eventLookup, updateWindowSize }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/index.js b/app/assets/javascripts/visual_review_toolbar/store/index.js new file mode 100644 index 00000000000..7143588c0bf --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/index.js @@ -0,0 +1,5 @@ +import { eventLookup, updateWindowSize } from './events'; +import { getInitialView, initializeState } from './state'; +import debounce from './utils'; + +export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/state.js b/app/assets/javascripts/visual_review_toolbar/store/state.js new file mode 100644 index 00000000000..b4e239a9586 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/state.js @@ -0,0 +1,76 @@ +import { comment, login, collapseButton } from '../components'; + +const state = { + browser: '', + href: '', + innerWidth: '', + innerHeight: '', + mergeRequestId: '', + mrUrl: '', + platform: '', + projectId: '', + userAgent: '', + token: '', +}; + +// adapted from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator#Example_2_Browser_detect_and_return_an_index +const getBrowserId = sUsrAg => { + const aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera']; + let nIdx = aKeys.length - 1; + + for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx -= 1); + return aKeys[nIdx]; +}; + +const initializeState = (wind, doc) => { + const { + innerWidth, + innerHeight, + location: { href }, + navigator: { platform, userAgent }, + } = wind; + + const browser = getBrowserId(userAgent); + + const scriptEl = doc.getElementById('review-app-toolbar-script'); + const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset; + + // This mutates our default state object above. It's weird but it makes the linter happy. + Object.assign(state, { + browser, + href, + innerWidth, + innerHeight, + mergeRequestId, + mrUrl, + platform, + projectId, + userAgent, + }); +}; + +function getInitialView({ localStorage }) { + const loginView = { + content: login, + toggleButton: collapseButton, + }; + + 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; + } +} + +export { initializeState, getInitialView, state }; diff --git a/app/assets/javascripts/visual_review_toolbar/store/utils.js b/app/assets/javascripts/visual_review_toolbar/store/utils.js new file mode 100644 index 00000000000..5cf145351b3 --- /dev/null +++ b/app/assets/javascripts/visual_review_toolbar/store/utils.js @@ -0,0 +1,15 @@ +const debounce = (fn, time) => { + let current; + + const debounced = () => { + if (current) { + clearTimeout(current); + } + + current = setTimeout(fn, time); + }; + + return debounced; +}; + +export default debounce; diff --git a/vendor/assets/javascripts/visual_review_toolbar.js b/vendor/assets/javascripts/visual_review_toolbar.js deleted file mode 100644 index 12a3a4c9672..00000000000 --- a/vendor/assets/javascripts/visual_review_toolbar.js +++ /dev/null @@ -1,377 +0,0 @@ -/////////////////////////////////////////////// -/////////////////// STYLES //////////////////// -/////////////////////////////////////////////// - -// this style must be applied inline -const buttonClearStyles = ` - -webkit-appearance: none; -`; - -/////////////////////////////////////////////// -/////////////////// STATE //////////////////// -/////////////////////////////////////////////// -const data = {}; - -/////////////////////////////////////////////// -///////////////// COMPONENTS ////////////////// -/////////////////////////////////////////////// -const note = ` - <p id='gitlab-validation-note' class='gitlab-message'></p> -`; - -const comment = ` - <div> - <textarea id='gitlab-comment' name='gitlab-comment' rows='3' placeholder='Enter your feedback or idea' class='gitlab-input'></textarea> - ${note} - <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='gitlab-logout-button'> Logout </button> - <button class='gitlab-button gitlab-button-success' style='${buttonClearStyles}' type='button' id='gitlab-comment-button'> Send feedback </button> - </div> -`; - -const commentIcon = ` - <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/comment</title><path d="M4 11.132l1.446-.964A1 1 0 0 1 6 10h5a1 1 0 0 0 1-1V5a1 1 0 0 0-1-1H5a1 1 0 0 0-1 1v6.132zM6.303 12l-2.748 1.832A1 1 0 0 1 2 13V5a3 3 0 0 1 3-3h6a3 3 0 0 1 3 3v4a3 3 0 0 1-3 3H6.303z" id="gitlab-comment-icon"/></svg> -`; - -const compressIcon = ` - <svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>icn/compress</title><path d="M5.27 12.182l-1.562 1.561a1 1 0 0 1-1.414 0h-.001a1 1 0 0 1 0-1.415l1.56-1.56L2.44 9.353a.5.5 0 0 1 .353-.854H7.09a.5.5 0 0 1 .5.5v4.294a.5.5 0 0 1-.853.353l-1.467-1.465zm6.911-6.914l1.464 1.464a.5.5 0 0 1-.353.854H8.999a.5.5 0 0 1-.5-.5V2.793a.5.5 0 0 1 .854-.354l1.414 1.415 1.56-1.561a1 1 0 1 1 1.415 1.414l-1.561 1.56z" id="gitlab-compress-icon"/></svg> -`; - -const collapseButton = ` - <button id='gitlab-collapse' style='${buttonClearStyles}' class='gitlab-button gitlab-button-secondary gitlab-collapse gitlab-collapse-open'>${compressIcon}</button> -`; - -const form = content => ` - <div id='gitlab-form-wrapper'> - ${content} - </div> -`; - -const login = ` - <div> - <label for='gitlab-token' 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='gitlab-token' name='gitlab-token'> - ${note} - </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='gitlab-login'> Submit </button> - </div> -`; - -/////////////////////////////////////////////// -//////////////// INTERACTIONS ///////////////// -/////////////////////////////////////////////// - -// from https://developer.mozilla.org/en-US/docs/Web/API/Window/navigator -function getBrowserId(sUsrAg) { - var aKeys = ['MSIE', 'Edge', 'Firefox', 'Safari', 'Chrome', 'Opera'], - nIdx = aKeys.length - 1; - - for (nIdx; nIdx > -1 && sUsrAg.indexOf(aKeys[nIdx]) === -1; nIdx--); - return aKeys[nIdx]; -} - -function addCommentForm() { - const formWrapper = document.getElementById('gitlab-form-wrapper'); - formWrapper.innerHTML = comment; -} - -function addLoginForm() { - const formWrapper = document.getElementById('gitlab-form-wrapper'); - formWrapper.innerHTML = login; -} - -function authorizeUser() { - // Clear any old errors - clearNote('gitlab-token'); - - const token = document.getElementById('gitlab-token').value; - const rememberMe = document.getElementById('remember_token').checked; - - if (!token) { - postError('Please enter your token.', 'gitlab-token'); - return; - } - - if (rememberMe) { - storeToken(token); - } - - authSuccess(token); - return; -} - -function authSuccess(token) { - data.token = token; - addCommentForm(); -} - -function clearNote(inputId) { - const note = document.getElementById('gitlab-validation-note'); - note.innerText = ''; - note.style.color = ''; - - if (inputId) { - const field = document.getElementById(inputId); - field.style.borderColor = ''; - } -} - -function confirmAndClear(mergeRequestId) { - const commentButton = document.getElementById('gitlab-comment-button'); - const note = document.getElementById('gitlab-validation-note'); - - commentButton.innerText = 'Feedback sent'; - note.innerText = `Your comment was successfully posted to merge request #${mergeRequestId}`; - - setTimeout(resetCommentButton, 1000); -} - -function getInitialState() { - const { localStorage } = window; - - try { - let token = localStorage.getItem('token'); - - if (token) { - data.token = token; - return comment; - } - - return login; - } catch (err) { - return login; - } -} - -function getProjectDetails() { - const { - innerWidth, - innerHeight, - location: { href }, - navigator: { platform, userAgent }, - } = window; - const browser = getBrowserId(userAgent); - - const scriptEl = document.getElementById('review-app-toolbar-script'); - const { projectId, mergeRequestId, mrUrl } = scriptEl.dataset; - - return { - href, - platform, - browser, - userAgent, - innerWidth, - innerHeight, - projectId, - mergeRequestId, - mrUrl, - }; -} - -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; - } - - addLoginForm(); -} - -function postComment({ - href, - platform, - browser, - userAgent, - innerWidth, - innerHeight, - projectId, - mergeRequestId, - mrUrl, -}) { - // Clear any old errors - clearNote('gitlab-comment'); - - setInProgressState(); - - const commentText = document.getElementById('gitlab-comment').value.trim(); - - if (!commentText) { - postError('Your comment appears to be empty.', 'gitlab-comment'); - resetCommentBox(); - 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': data.token, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ body }), - }) - .then(response => { - if (response.ok) { - confirmAndClear(mergeRequestId); - return; - } - - throw new Error(`${response.status}: ${response.statusText}`); - }) - .catch(err => { - postError( - `The feedback was not sent successfully. Please try again. Error: ${err.message}`, - 'gitlab-comment', - ); - resetCommentBox(); - }); -} - -function postError(message, inputId) { - const note = document.getElementById('gitlab-validation-note'); - const field = document.getElementById(inputId); - field.style.borderColor = '#db3b21'; - note.style.color = '#db3b21'; - note.innerText = message; -} - -function resetCommentBox() { - const commentBox = document.getElementById('gitlab-comment'); - const commentButton = document.getElementById('gitlab-comment-button'); - - commentButton.innerText = 'Send feedback'; - commentButton.classList.replace('gitlab-button-secondary', 'gitlab-button-success'); - commentButton.style.opacity = 1; - - commentBox.style.pointerEvents = 'auto'; - commentBox.style.color = 'rgba(0, 0, 0, 1)'; -} - -function resetCommentButton() { - const commentBox = document.getElementById('gitlab-comment'); - const note = document.getElementById('gitlab-validation-note'); - - commentBox.value = ''; - note.innerText = ''; - resetCommentBox(); -} - -function setInProgressState() { - const commentButton = document.getElementById('gitlab-comment-button'); - const commentBox = document.getElementById('gitlab-comment'); - - commentButton.innerText = 'Sending feedback'; - commentButton.classList.replace('gitlab-button-success', 'gitlab-button-secondary'); - commentButton.style.opacity = 0.5; - commentBox.style.color = 'rgba(223, 223, 223, 0.5)'; - commentBox.style.pointerEvents = 'none'; -} - -function storeToken(token) { - 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.setItem('token', token); - } catch (err) { - return; - } -} - -function toggleForm() { - const container = document.getElementById('gitlab-review-container'); - const collapseButton = document.getElementById('gitlab-collapse'); - const form = document.getElementById('gitlab-form-wrapper'); - const OPEN = 'open'; - const CLOSED = 'closed'; - - const stateVals = { - [OPEN]: { - buttonClasses: ['gitlab-collapse-closed', 'gitlab-collapse-open'], - containerClasses: ['gitlab-closed-wrapper', 'gitlab-open-wrapper'], - icon: compressIcon, - display: 'flex', - backgroundColor: 'rgba(255, 255, 255, 1)', - }, - [CLOSED]: { - buttonClasses: ['gitlab-collapse-open', 'gitlab-collapse-closed'], - containerClasses: ['gitlab-open-wrapper', 'gitlab-closed-wrapper'], - icon: commentIcon, - display: 'none', - backgroundColor: 'rgba(255, 255, 255, 0)', - }, - }; - - const nextState = collapseButton.classList.contains('gitlab-collapse-open') ? CLOSED : OPEN; - - container.classList.replace(...stateVals[nextState].containerClasses); - container.style.backgroundColor = stateVals[nextState].backgroundColor; - form.style.display = stateVals[nextState].display; - collapseButton.classList.replace(...stateVals[nextState].buttonClasses); - collapseButton.innerHTML = stateVals[nextState].icon; -} - -/////////////////////////////////////////////// -///////////////// INJECTION ////////////////// -/////////////////////////////////////////////// - -function noop() {} - -const eventLookup = ({ target: { id } }) => { - switch (id) { - case 'gitlab-collapse': - return toggleForm; - case 'gitlab-comment-button': - const projectDetails = getProjectDetails(); - return postComment.bind(null, projectDetails); - case 'gitlab-login': - return authorizeUser; - case 'gitlab-logout-button': - return logoutUser; - default: - return noop; - } -}; - -window.addEventListener('load', () => { - const content = getInitialState(); - const container = document.createElement('div'); - - container.setAttribute('id', 'gitlab-review-container'); - container.insertAdjacentHTML('beforeend', collapseButton); - container.insertAdjacentHTML('beforeend', form(content)); - - document.body.insertBefore(container, document.body.firstChild); - - document.getElementById('gitlab-review-container').addEventListener('click', event => { - eventLookup(event)(); - }); - -}); |