Commit 78082599 authored by Mike Greiling's avatar Mike Greiling

Merge branch '62440-vrt-make-mods' into 'master'

Review toolbar modularization

Closes #62440

See merge request gitlab-org/gitlab-ce!29433
parents 09238aba 13861f60
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 };
// 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,
};
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,
};
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 };
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 };
/* 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,
};
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 };
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 };
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));
});
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 };
import { eventLookup, updateWindowSize } from './events';
import { getInitialView, initializeState } from './state';
import debounce from './utils';
export { debounce, eventLookup, getInitialView, initializeState, updateWindowSize };
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 };
const debounce = (fn, time) => {
let current;
const debounced = () => {
if (current) {
clearTimeout(current);
}
current = setTimeout(fn, time);
};
return debounced;
};
export default debounce;
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment