/////////////////////////////////////////////// /////////////////// STYLES //////////////////// /////////////////////////////////////////////// const buttonClearStyles = ` -webkit-appearance: none; `; const buttonBaseStyles = ` cursor: pointer; transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear; `; const buttonSuccessActiveStyles = ` background-color: #168f48; border-color: #12753a; color: #fff; `; const buttonSuccessHoverStyles = ` color: #fff; background-color: #137e3f; border-color: #127339; `; const buttonSuccessStyles = ` ${buttonBaseStyles} background-color: #1aaa55; border: 1px solid #168f48; color: #fff; `; const buttonSecondaryStyles = ` ${buttonBaseStyles} background: none #fff; margin: 0 .5rem; border: 1px solid #e3e3e3; `; const buttonSecondaryActiveStyles = ` color: #2e2e2e; background-color: #e1e1e1; border-color: #dadada; `; const buttonSecondaryHoverStyles = ` background-color: #f0f0f0; border-color: #e3e3e3; color: #2e2e2e; `; const buttonWideStyles = ` width: 100%; `; const buttonWrapperStyles = ` margin-top: 1rem; display: flex; align-items: baseline; justify-content: flex-end; `; const collapseStyles = ` ${buttonBaseStyles} width: 2.4rem; height: 2.2rem; margin-left: 1rem; padding: .5rem; `; const collapseClosedStyles = ` ${collapseStyles} align-self: center; `; const collapseOpenStyles = ` ${collapseStyles} `; const checkboxLabelStyles = ` padding: 0 .2rem; `; const checkboxWrapperStyles = ` display: flex; align-items: baseline; `; const formStyles = ` display: flex; flex-direction: column; width: 100% `; const labelStyles = ` font-weight: 600; display: inline-block; width: 100%; `; const linkStyles = ` color: #1b69b6; text-decoration: none; background-color: transparent; background-image: none; `; const messageStyles = ` padding: .25rem 0; margin: 0; line-height: 1.2rem; `; const metadataNoteStyles = ` font-size: .7rem; line-height: 1rem; color: #666; margin-bottom: 0; `; const inputStyles = ` width: 100%; border: 1px solid #dfdfdf; border-radius: 4px; padding: .1rem .2rem; min-height: 2rem; max-width: 17rem; `; const svgInnerStyles = ` pointer-events: none; `; const wrapperClosedStyles = ` max-width: 3.4rem; max-height: 3.4rem; `; const wrapperOpenStyles = ` max-width: 22rem; max-height: 22rem; `; const wrapperStyles = ` max-width: 22rem; max-height: 22rem; overflow: scroll; position: fixed; bottom: 1rem; right: 1rem; display: flex; flex-direction: row-reverse; padding: 1rem; background-color: #fff; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; font-size: .8rem; font-weight: 400; color: #2e2e2e; `; const gitlabStyles = ` #gitlab-collapse > * { ${svgInnerStyles} } #gitlab-form-wrapper { ${formStyles} } #gitlab-review-container { ${wrapperStyles} } .gitlab-open-wrapper { ${wrapperOpenStyles} } .gitlab-closed-wrapper { ${wrapperClosedStyles} } .gitlab-button-secondary { ${buttonSecondaryStyles} } .gitlab-button-secondary:hover { ${buttonSecondaryHoverStyles} } .gitlab-button-secondary:active { ${buttonSecondaryActiveStyles} } .gitlab-button-success:hover { ${buttonSuccessHoverStyles} } .gitlab-button-success:active { ${buttonSuccessActiveStyles} } .gitlab-button-success { ${buttonSuccessStyles} } .gitlab-button-wide { ${buttonWideStyles} } .gitlab-button-wrapper { ${buttonWrapperStyles} } .gitlab-collapse-closed { ${collapseClosedStyles} } .gitlab-collapse-open { ${collapseOpenStyles} } .gitlab-checkbox-label { ${checkboxLabelStyles} } .gitlab-checkbox-wrapper { ${checkboxWrapperStyles} } .gitlab-label { ${labelStyles} } .gitlab-link { ${linkStyles} } .gitlab-message { ${messageStyles} } .gitlab-metadata-note { ${metadataNoteStyles} } .gitlab-input { ${inputStyles} } `; function addStylesheet() { const styleEl = document.createElement('style'); styleEl.insertAdjacentHTML('beforeend', gitlabStyles); document.head.appendChild(styleEl); } /////////////////////////////////////////////// /////////////////// STATE //////////////////// /////////////////////////////////////////////// const data = {}; /////////////////////////////////////////////// ///////////////// COMPONENTS ////////////////// /////////////////////////////////////////////// const note = `

`; const comment = `
${note}

Additional metadata will be included: browser, OS, current page, user agent, and viewport dimensions.

`; const commentIcon = ` icn/comment ` const compressIcon = ` icn/compress `; const collapseButton = ` `; const form = (content) => `
${content}
`; const login = `
${note}
`; /////////////////////////////////////////////// //////////////// 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
Metadata Posted from ${href} | ${platform} | ${browser} | ${innerWidth} x ${innerHeight}.

User agent: ${userAgent}
`; 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); addStylesheet(); document.getElementById('gitlab-review-container').addEventListener('click', (event) => { eventLookup(event)(); }); });