summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
authorSarah Groff Hennigh-Palermo <sarah.groff.palermo@gmail.com>2019-06-14 00:20:14 +0000
committerMike Greiling <mike@pixelcog.com>2019-06-14 00:20:14 +0000
commit13861f60dff57958c8b004250607053fc9c102fd (patch)
tree03dd53b0da8785225ba63f2ab53b949899611110 /app/assets/javascripts
parent09238aba9a1d575fd0b7216b8f35dd052a36507d (diff)
downloadgitlab-ce-13861f60dff57958c8b004250607053fc9c102fd.tar.gz
Breakup first pass
It has a lot of files because that's the point
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/comment.js127
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/constants.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/index.js23
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/login.js51
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/note.js27
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/utils.js41
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper.js82
-rw-r--r--app/assets/javascripts/visual_review_toolbar/components/wrapper_icons.js15
-rw-r--r--app/assets/javascripts/visual_review_toolbar/index.js37
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/events.js36
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/index.js5
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/state.js76
-rw-r--r--app/assets/javascripts/visual_review_toolbar/store/utils.js15
13 files changed, 571 insertions, 1 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;