Commit 1d462d23 authored by Mike Greiling's avatar Mike Greiling

Merge branch '66402-use-visual-review-tools-npm-package' into 'master'

Resolve "Use Visual Review Tools NPM package"

See merge request gitlab-org/gitlab-ce!32159
parents 278c5942 c96adfcd
import { nextView } from '../store';
import { localStorage, COMMENT_BOX, LOGOUT, STORAGE_MR_ID, STORAGE_TOKEN } 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>
`;
};
// 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(STORAGE_TOKEN);
localStorage.removeItem(STORAGE_MR_ID);
state.token = '';
state.mergeRequestId = '';
clearNote();
addForm(nextView(state, COMMENT_BOX));
};
export { changeSelectedMr, comment, logoutUser, postComment, saveComment };
import { nextView } from '../store';
import { localStorage, CHANGE_MR_ID_BUTTON, COMMENT_BOX, STORAGE_MR_ID } 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(STORAGE_MR_ID);
state.mergeRequestId = '';
};
const changeSelectedMr = state => {
clearMrId(state);
clearNote();
addForm(nextView(state, COMMENT_BOX));
};
export { changeSelectedMr, selectedMrNote };
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;
import { selectCommentBox } from './utils';
import { sessionStorage, STORAGE_COMMENT } from '../shared';
const getSavedComment = () => sessionStorage.getItem(STORAGE_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(STORAGE_COMMENT, currentComment.value);
}
};
const clearSavedComment = () => {
sessionStorage.removeItem(STORAGE_COMMENT);
};
export { getSavedComment, saveComment, clearSavedComment };
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 };
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, selectForm } from './utils';
import { buttonAndForm, toggleForm } from './wrapper';
export {
addMr,
authorizeUser,
buttonAndForm,
changeSelectedMr,
comment,
login,
logoutUser,
mrForm,
note,
postComment,
saveComment,
selectContainer,
selectForm,
toggleForm,
};
import { nextView } from '../store';
import { localStorage, LOGIN, TOKEN_BOX, STORAGE_TOKEN } from '../shared';
import { clearNote, postError } from './note';
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">${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 rememberMe = selectRemember().checked;
if (rememberMe) {
localStorage.setItem(STORAGE_TOKEN, token);
}
state.token = token;
};
const authorizeUser = state => {
// Clear any old errors
clearNote(TOKEN_BOX);
const token = selectToken().value;
if (!token) {
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
postError('Please enter your token.', TOKEN_BOX);
return;
}
storeToken(token, state);
addForm(nextView(state, LOGIN));
};
export { authorizeUser, login, storeToken };
import { nextView } from '../store';
import { MR_ID, MR_ID_BUTTON, STORAGE_MR_ID, 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(STORAGE_MR_ID, 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 };
import { NOTE, NOTE_CONTAINER, RED } from '../shared';
import { selectById, selectNote, selectNoteContainer } from './utils';
const note = `
<div id="${NOTE_CONTAINER}" style="visibility: hidden;">
<p id="${NOTE}" class="gitlab-message"></p>
</div>
`;
const clearNote = inputId => {
const currentNote = selectNote();
const noteContainer = selectNoteContainer();
currentNote.innerText = '';
currentNote.style.color = '';
noteContainer.style.visibility = 'hidden';
if (inputId) {
const field = document.getElementById(inputId);
field.style.borderColor = '';
}
};
const postError = (message, inputId) => {
const currentNote = selectNote();
const noteContainer = selectNoteContainer();
const field = selectById(inputId);
field.style.borderColor = RED;
currentNote.style.color = RED;
currentNote.innerText = message;
noteContainer.style.visibility = 'visible';
setTimeout(clearNote.bind(null, inputId), 5000);
};
export { clearNote, note, postError };
/* global document */
import {
COLLAPSE_BUTTON,
COMMENT_BOX,
COMMENT_BUTTON,
FORM,
FORM_CONTAINER,
MR_ID,
NOTE,
NOTE_CONTAINER,
REMEMBER_ITEM,
REVIEW_CONTAINER,
TOKEN_BOX,
} from '../shared';
// this style must be applied inline in a handful of components
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
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 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_ITEM);
const selectToken = () => document.getElementById(TOKEN_BOX);
export {
buttonClearStyles,
selectById,
selectCollapseButton,
selectContainer,
selectCommentBox,
selectCommentButton,
selectForm,
selectFormContainer,
selectMrBox,
selectNote,
selectNoteContainer,
selectRemember,
selectToken,
};
import { CLEAR, FORM, FORM_CONTAINER, WHITE } from '../shared';
import {
selectCollapseButton,
selectForm,
selectFormContainer,
selectNoteContainer,
} from './utils';
import { collapseButton, commentIcon, compressIcon } from './wrapper_icons';
const form = content => `
<form id="${FORM}" novalidate>
${content}
</form>
`;
const buttonAndForm = content => `
<div id="${FORM_CONTAINER}" class="gitlab-form-open">
${collapseButton}
${form(content)}
</div>
`;
const addForm = nextForm => {
const formWrapper = selectForm();
formWrapper.innerHTML = nextForm;
};
function toggleForm() {
const toggleButton = selectCollapseButton();
const currentForm = selectForm();
const formContainer = selectFormContainer();
const noteContainer = selectNoteContainer();
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-wrapper-closed', 'gitlab-wrapper-open'];
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 = 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;
toggleButton.classList.replace(...currentVals.buttonClasses);
toggleButton.innerHTML = currentVals.icon;
if (noteContainer && noteContainer.innerText.length > 0) {
noteContainer.style.display = currentVals.display;
}
}
export { addForm, buttonAndForm, 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 { buttonAndForm, note, selectForm, selectContainer } from './components';
import { REVIEW_CONTAINER } from './shared';
import { eventLookup, getInitialView, initializeGlobalListeners, initializeState } 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 mainContent = buttonAndForm(getInitialView());
const container = document.createElement('div');
container.setAttribute('id', REVIEW_CONTAINER);
container.insertAdjacentHTML('beforeend', note);
container.insertAdjacentHTML('beforeend', mainContent);
document.body.insertBefore(container, document.body.firstChild);
selectContainer().addEventListener('click', event => {
eventLookup(event.target.id)();
});
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();
});
// 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-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_ITEM = 'gitlab-remember-item';
const REVIEW_CONTAINER = 'gitlab-review-container';
const TOKEN_BOX = 'gitlab-token';
// Storage keys
const STORAGE_PREFIX = '--gitlab'; // Using `--` to make the prefix more unique
const STORAGE_MR_ID = `${STORAGE_PREFIX}-merge-request-id`;
const STORAGE_TOKEN = `${STORAGE_PREFIX}-token`;
const STORAGE_COMMENT = `${STORAGE_PREFIX}-comment`;
// 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(250, 250, 250, 1)';
export {
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,
STORAGE_MR_ID,
STORAGE_TOKEN,
STORAGE_COMMENT,
BLACK,
CLEAR,
MUTED,
RED,
WHITE,
};
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,
STORAGE_MR_ID,
STORAGE_TOKEN,
STORAGE_COMMENT,
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,
STORAGE_MR_ID,
STORAGE_TOKEN,
STORAGE_COMMENT,
BLACK,
CLEAR,
MUTED,
RED,
WHITE,
};
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 };
import {
addMr,
authorizeUser,
changeSelectedMr,
logoutUser,
postComment,
saveComment,
toggleForm,
} from '../components';
import {
CHANGE_MR_ID_BUTTON,
COLLAPSE_BUTTON,
COMMENT_BUTTON,
LOGIN,
LOGOUT,
MR_ID_BUTTON,
} from '../shared';
import { state } from './state';
import debounce from './utils';
const noop = () => {};
// 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:
return postComment.bind(null, state);
case LOGIN:
return authorizeUser.bind(null, state);
case LOGOUT:
return () => {
saveComment();
logoutUser(state);
};
case MR_ID_BUTTON:
return addMr.bind(null, state);
default:
return noop;
}
};
const updateWindowSize = wind => {
state.innerWidth = wind.innerWidth;
state.innerHeight = wind.innerHeight;
};
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 };
import { eventLookup, initializeGlobalListeners } from './events';
import { nextView, getInitialView, initializeState, setUsingGracefulStorageFlag } from './state';
export {
eventLookup,
getInitialView,
initializeGlobalListeners,
initializeState,
nextView,
setUsingGracefulStorageFlag,
};
import { comment, login, mrForm } from '../components';
import { localStorage, COMMENT_BOX, LOGIN, MR_ID, STORAGE_MR_ID, STORAGE_TOKEN } from '../shared';
const state = {
browser: '',
usingGracefulStorage: '',
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 => {
/* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */
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 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,
navigator: { platform, userAgent },
} = wind;
const browser = getBrowserId(userAgent);
const scriptEl = doc.getElementById('review-app-toolbar-script');
const { projectId, mergeRequestId, mrUrl, projectPath } = scriptEl.dataset;
// This mutates our default state object above. It's weird but it makes the linter happy.
Object.assign(state, {
browser,
innerWidth,
innerHeight,
mergeRequestId,
mrUrl,
platform,
projectId,
projectPath,
userAgent,
});
return state;
};
const getInitialView = () => {
const token = localStorage.getItem(STORAGE_TOKEN);
const mrId = localStorage.getItem(STORAGE_MR_ID);
if (token) {
state.token = token;
}
if (mrId) {
state.mergeRequestId = mrId;
}
return nextView(state);
};
const setUsingGracefulStorageFlag = flag => {
state.usingGracefulStorage = !flag;
};
export { initializeState, getInitialView, nextView, setUsingGracefulStorageFlag, state };
const debounce = (fn, time) => {
let current;
const debounced = () => {
if (current) {
clearTimeout(current);
}
current = setTimeout(fn, time);
};
return debounced;
};
export default debounce;
/*
As a standalone script, the toolbar has its own css
*/
#gitlab-collapse > * {
pointer-events: none;
}
#gitlab-comment {
background-color: #fafafa;
}
#gitlab-form {
display: flex;
flex-direction: column;
width: 100%;
margin-bottom: 0;
}
#gitlab-note-wrapper {
display: flex;
flex-direction: column;
background-color: #fafafa;
border-radius: 4px;
margin-bottom: .5rem;
padding: 1rem;
}
#gitlab-form-wrapper {
overflow: auto;
display: flex;
flex-direction: row-reverse;
border-radius: 4px;
}
#gitlab-review-container {
max-width: 22rem;
max-height: 22rem;
overflow: auto;
display: flex;
flex-direction: column;
position: fixed;
bottom: 1rem;
right: 1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto 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;
z-index: 9999; /* toolbar should always be on top */
}
.gitlab-wrapper-open {
max-width: 22rem;
max-height: 22rem;
}
.gitlab-wrapper-closed {
max-width: 3.4rem;
max-height: 3.4rem;
}
.gitlab-button {
cursor: pointer;
transition: background-color 100ms linear, border-color 100ms linear, color 100ms linear, box-shadow 100ms linear;
}
.gitlab-button-secondary {
background: none #fafafa;
margin: 0 .5rem;
border: 1px solid #e3e3e3;
}
.gitlab-button-secondary:hover {
background-color: #f0f0f0;
border-color: #e3e3e3;
color: #2e2e2e;
}
.gitlab-button-secondary:active {
color: #2e2e2e;
background-color: #e1e1e1;
border-color: #dadada;
}
.gitlab-button-success:hover {
color: #fff;
background-color: #137e3f;
border-color: #127339;
}
.gitlab-button-success:active {
background-color: #168f48;
border-color: #12753a;
color: #fff;
}
.gitlab-button-success {
background-color: #1aaa55;
border: 1px solid #168f48;
color: #fff;
}
.gitlab-button-wide {
width: 100%;
}
.gitlab-button-wrapper {
margin-top: 0.5rem;
display: flex;
align-items: baseline;
/*
this makes sure the hit enter to submit picks the correct button
on the comment view
*/
flex-direction: row-reverse;
}
.gitlab-collapse {
width: 2.4rem;
height: 2.2rem;
margin-left: 1rem;
padding: .5rem;
}
.gitlab-collapse-closed {
align-self: center;
}
.gitlab-checkbox-label {
padding: 0 .2rem;
}
.gitlab-checkbox-wrapper {
display: flex;
align-items: baseline;
}
.gitlab-form-open {
padding: 1rem;
background-color: #fafafa;
}
.gitlab-label {
font-weight: 600;
display: inline-block;
width: 100%;
}
.gitlab-link {
color: #1b69b6;
text-decoration: none;
background-color: transparent;
background-image: none;
}
.gitlab-link:hover {
text-decoration: underline;
}
.gitlab-link-button {
border: none;
cursor: pointer;
padding: 0 .15rem;
}
.gitlab-message {
padding: .25rem 0;
margin: 0;
line-height: 1.2rem;
}
.gitlab-metadata-note {
font-size: .7rem;
line-height: 1rem;
color: #666;
margin-bottom: .5rem;
}
.gitlab-input {
width: 100%;
border: 1px solid #dfdfdf;
border-radius: 4px;
padding: .1rem .2rem;
min-height: 2rem;
max-width: 17rem;
}
---
title: Move visual review toolbar code to NPM
merge_request: 32159
author:
type: fixed
......@@ -298,6 +298,13 @@ module.exports = {
from: path.join(ROOT_PATH, 'node_modules/pdfjs-dist/cmaps/'),
to: path.join(ROOT_PATH, 'public/assets/webpack/cmaps/'),
},
{
from: path.join(
ROOT_PATH,
'node_modules/@gitlab/visual-review-tools/dist/visual_review_toolbar.js',
),
to: path.join(ROOT_PATH, 'public/assets/webpack'),
},
]),
// compression can require a lot of compute time and is disabled in CI
......
const path = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const ROOT_PATH = path.resolve(__dirname, '..');
const CACHE_PATH = process.env.WEBPACK_CACHE_PATH || path.join(ROOT_PATH, 'tmp/cache');
const NO_SOURCEMAPS = process.env.NO_SOURCEMAPS;
const IS_PRODUCTION = process.env.NODE_ENV === 'production';
const devtool = IS_PRODUCTION ? 'source-map' : 'cheap-module-eval-source-map';
const alias = {
vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
spec: path.join(ROOT_PATH, 'spec/javascripts'),
};
module.exports = {
mode: IS_PRODUCTION ? 'production' : 'development',
context: path.join(ROOT_PATH, 'app/assets/javascripts'),
name: 'visual_review_toolbar',
entry: './visual_review_toolbar',
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
filename: 'visual_review_toolbar.js',
library: 'VisualReviewToolbar',
libraryTarget: 'var',
},
resolve: {
alias,
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
cacheDirectory: path.join(CACHE_PATH, 'babel-loader'),
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
// compression can require a lot of compute time and is disabled in CI
new CompressionPlugin(),
].filter(Boolean),
devtool: NO_SOURCEMAPS ? false : devtool,
};
......@@ -10,15 +10,9 @@ namespace :gitlab do
rake:assets:precompile
webpack:compile
gitlab:assets:fix_urls
gitlab:assets:compile_vrt
].each(&Gitlab::TaskHelpers.method(:invoke_and_time_task))
end
desc 'GitLab | Assets | Compile visual review toolbar'
task :compile_vrt do
system 'yarn', 'webpack-vrt'
end
desc 'GitLab | Assets | Clean up old compiled frontend assets'
task clean: ['rake:assets:clean']
......
......@@ -26,8 +26,7 @@
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"test": "node scripts/frontend/test",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
"webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js",
"webpack-vrt": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.review_toolbar.js"
"webpack-prod": "NODE_OPTIONS=\"--max-old-space-size=3584\" NODE_ENV=production webpack --config config/webpack.config.js"
},
"dependencies": {
"@babel/core": "^7.5.5",
......@@ -40,6 +39,7 @@
"@gitlab/csslab": "^1.9.0",
"@gitlab/svgs": "^1.68.0",
"@gitlab/ui": "5.18.0",
"@gitlab/visual-review-tools": "^1.0.0",
"apollo-cache-inmemory": "^1.5.1",
"apollo-client": "^2.5.1",
"apollo-link": "^1.2.11",
......
......@@ -1021,6 +1021,11 @@
vue "^2.6.10"
vue-loader "^15.4.2"
"@gitlab/visual-review-tools@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.0.0.tgz#6012e0a19797c1f5dad34ccf4dacdaf38e400a73"
integrity sha512-xMvz9IwrXisQ1MH+Tj6lfbQcQSiQy88nTPuQV6OTLBGuV+vIQeVwXeIkQeTKuSpd0GqZvigPdRqxyQCa3blpIg==
"@gitlab/vue-toasted@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@gitlab/vue-toasted/-/vue-toasted-1.2.1.tgz#f407b5aa710863e5b7f021f4a1f66160331ab263"
......
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