Allow to configure endpoints for browser extension (#285)

* Allow to change endpoints in settings

* Allow to reset default settings

* Fix auth

* Release browser extension 2.0.0
This commit is contained in:
Sung Won Cho 2019-10-28 22:50:10 -07:00 committed by GitHub
commit 47d4a8f6fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 546 additions and 108 deletions

View file

@ -71,7 +71,13 @@ The following log documentes the history of the browser extensions project
### [Unreleased]
#### Fixed
N/A
### 2.0.0 - 2019-10-29
- Allow to customize API and web URLs (#285)
### 1.1.1 - 2019-10-02
- Fix failing requests (#263)

View file

@ -171,3 +171,9 @@ clean:
@rm -rf build
@rm -rf web/public
.PHONY: clean
clean-dep:
@rm -rf ./web/node_modules
@rm -rf ./jslib/node_modules
@rm -rf ./browser/node_modules
.PHONY: clean-dep

View file

@ -12,7 +12,7 @@
},
"author": "Monomax Software Pty Ltd",
"license": "GPL-3.0-or-later",
"version": "1.1.1",
"version": "2.0.0",
"dependencies": {
"classnames": "^2.2.5",
"lodash": "^4.17.15",

View file

@ -19,13 +19,15 @@
import React, { useState, useEffect } from 'react';
import classnames from 'classnames';
import services from '../utils/services';
import { resetSettings } from '../store/settings/actions';
import initServices from '../utils/services';
import { logout } from '../store/auth/actions';
import { AuthState } from '../store/auth/types';
import { useSelector, useDispatch } from '../store/hooks';
import Header from './Header';
import Home from './Home';
import Menu from './Menu';
import Success from './Success';
import Settings from './Settings';
import Composer from './Composer';
interface Props {}
@ -34,45 +36,58 @@ function renderRoutes(path: string, isLoggedIn: boolean) {
switch (path) {
case '/success':
return <Success />;
case '/':
case '/': {
if (isLoggedIn) {
return <Composer />;
}
return <Home />;
}
case '/settings': {
return <Settings />;
}
default:
return <div>Not found</div>;
}
}
// useCheckSessionValid ensures that the current session is valid
function useCheckSessionValid(auth: AuthState) {
const dispatch = useDispatch();
useEffect(() => {
// if session is expired, clear it
const now = Math.round(new Date().getTime() / 1000);
if (auth.sessionKey && auth.sessionKeyExpiry < now) {
dispatch(logout());
}
}, [dispatch, auth.sessionKey, auth.sessionKeyExpiry]);
}
const App: React.FunctionComponent<Props> = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [errMsg, setErrMsg] = useState('');
const dispatch = useDispatch();
const { path, settings } = useSelector(state => {
const { path, auth, settings } = useSelector(state => {
return {
path: state.location.path,
auth: state.auth,
settings: state.settings
};
});
useEffect(() => {
// if session is expired, clear it
const now = Math.round(new Date().getTime() / 1000);
if (settings.sessionKey && settings.sessionKeyExpiry < now) {
dispatch(resetSettings());
}
}, [dispatch]);
useCheckSessionValid(auth);
const isLoggedIn = Boolean(settings.sessionKey);
const isLoggedIn = Boolean(auth.sessionKey);
const toggleMenu = () => {
setIsMenuOpen(!isMenuOpen);
};
const handleLogout = async (done?: Function) => {
try {
await services.users.signout();
dispatch(resetSettings());
await initServices(settings.apiUrl).users.signout();
dispatch(logout());
if (done) {
done();

View file

@ -20,7 +20,7 @@ import React, { useState, useEffect, useRef } from 'react';
import classnames from 'classnames';
import { KEYCODE_ENTER } from 'jslib/helpers/keyboard';
import services from '../utils/services';
import initServices from '../utils/services';
import BookSelector from './BookSelector';
import Flash from './Flash';
import { useSelector, useDispatch } from '../store/hooks';
@ -88,16 +88,19 @@ const Composer: React.FunctionComponent<Props> = () => {
const [contentRef, setContentEl] = useState(null);
const [bookSelectorRef, setBookSelectorEl] = useState(null);
const { composer, settings } = useSelector(state => {
const { composer, settings, auth } = useSelector(state => {
return {
composer: state.composer,
settings: state.settings
settings: state.settings,
auth: state.auth
};
});
const handleSubmit = async e => {
e.preventDefault();
const services = initServices(settings.apiUrl);
setSubmitting(true);
try {
@ -109,7 +112,7 @@ const Composer: React.FunctionComponent<Props> = () => {
},
{
headers: {
Authorization: `Bearer ${settings.sessionKey}`
Authorization: `Bearer ${auth.sessionKey}`
}
}
);
@ -126,7 +129,7 @@ const Composer: React.FunctionComponent<Props> = () => {
},
{
headers: {
Authorization: `Bearer ${settings.sessionKey}`
Authorization: `Bearer ${auth.sessionKey}`
}
}
);
@ -176,7 +179,7 @@ const Composer: React.FunctionComponent<Props> = () => {
return (
<div className="composer">
<Flash when={errMsg !== ''} message={errMsg} />
<Flash kind="error" when={errMsg !== ''} message={errMsg} />
<form onSubmit={handleSubmit} className="form">
<BookSelector

View file

@ -18,14 +18,17 @@
import React from 'react';
type Kind = 'error' | 'info';
interface Props {
message: string;
when: boolean;
kind: Kind;
}
const Flash: React.FunctionComponent<Props> = ({ message, when }) => {
const Flash: React.FunctionComponent<Props> = ({ message, when, kind }) => {
if (when) {
return <div className="alert error">Error: {message}</div>;
return <div className={`alert alert-${kind}`}>{message}</div>;
}
return null;

View file

@ -22,9 +22,11 @@ import { findDOMNode } from 'react-dom';
import Link from './Link';
import config from '../utils/config';
import { login } from '../store/auth/actions';
import { updateSettings } from '../store/settings/actions';
import { useDispatch } from '../store/hooks';
import services from '../utils/services';
import Flash from '../components/Flash';
interface Props {}
@ -42,14 +44,7 @@ const Home: React.FunctionComponent<Props> = () => {
setLoggingIn(true);
try {
const signinResp = await services.users.signin({ email, password });
dispatch(
updateSettings({
sessionKey: signinResp.key,
sessionKeyExpiry: signinResp.expiresAt
})
);
await dispatch(login({ email, password }));
} catch (e) {
console.log('error while logging in', e);
@ -59,12 +54,12 @@ const Home: React.FunctionComponent<Props> = () => {
};
return (
<div className="home">
<h1 className="greet">Welcome to Dnote</h1>
<div className="home page">
<h1 className="heading">Welcome to Dnote</h1>
<p className="lead">A simple personal knowledge base</p>
{errMsg && <div className="alert error">{errMsg}</div>}
<Flash kind="error" when={errMsg !== ''} message={errMsg} />
<form id="login-form" onSubmit={handleLogin}>
<label htmlFor="email-input">Email</label>
@ -97,7 +92,7 @@ const Home: React.FunctionComponent<Props> = () => {
className="button button-first button-small login-btn"
disabled={loggingIn}
>
{loggingIn ? 'Signing in...' : 'Signin'}
{loggingIn ? 'Signing in...' : 'Sign in'}
</button>
</form>

View file

@ -28,6 +28,11 @@ export default ({ toggleMenu, loggedIn, onLogout }) => (
Home
</Link>
</li>
<li>
<Link to="/settings" onClick={toggleMenu} className="menu-link">
Settings
</Link>
</li>
{loggedIn && (
<li>

View file

@ -0,0 +1,163 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import React, { useState } from 'react';
import { connect } from 'react-redux';
import { findDOMNode } from 'react-dom';
import Link from './Link';
import Flash from './Flash';
import config from '../utils/config';
import { updateSettings, resetSettings } from '../store/settings/actions';
import { useDispatch, useSelector, useStore } from '../store/hooks';
import services from '../utils/services';
interface Props {}
// isValidURL checks if the given string is a valid URL
function isValidURL(url: string): boolean {
var a = document.createElement('a');
a.href = url;
return a.host && a.host != window.location.host;
}
// validateFormState validates the given form state. If any input is
// invalid, it throws an error.
function validateFormState({ apiUrl, webUrl }) {
if (!isValidURL(apiUrl)) {
throw new Error('Invalid URL for the API URL');
}
if (!isValidURL(webUrl)) {
throw new Error('Invalid URL for the web URL');
}
}
const Settings: React.FunctionComponent<Props> = () => {
const { settings } = useSelector(state => {
return {
settings: state.settings
};
});
const store = useStore();
const [apiUrl, setAPIUrl] = useState(settings.apiUrl);
const [webUrl, setWebUrl] = useState(settings.webUrl);
const [errMsg, setErrMsg] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const dispatch = useDispatch();
function handleRestore() {
dispatch(resetSettings());
setSuccessMsg('Restored the default settings');
const { settings } = store.getState();
setAPIUrl(settings.apiUrl);
setWebUrl(settings.webUrl);
}
function handleSubmit(e) {
e.preventDefault();
setSuccessMsg('');
setErrMsg('');
try {
validateFormState({ apiUrl, webUrl });
} catch (err) {
setErrMsg(err.message);
return;
}
dispatch(
updateSettings({
apiUrl,
webUrl
})
);
setSuccessMsg('Succesfully updated the settings.');
}
return (
<div>
<Flash kind="error" when={errMsg !== ''} message={errMsg} />
<Flash kind="info" when={successMsg !== ''} message={successMsg} />
<div className="settings page">
<h1 className="heading">Settings</h1>
<p className="lead">Customize your Dnote extension</p>
<form id="settings-form" onSubmit={handleSubmit}>
<div className="input-row">
<label htmlFor="api-url-input" className="label">
API URL
</label>
<input
type="api-url"
placeholder="https://api.getdnote.com"
className="input"
id="api-url-input"
value={apiUrl}
onChange={e => {
setAPIUrl(e.target.value);
}}
/>
</div>
<div className="input-row">
<label htmlFor="web-url-input" className="label">
Web URL
</label>
<input
type="web-url"
placeholder="https://app.getdnote.com"
className="input"
id="web-url-input"
value={webUrl}
onChange={e => {
setWebUrl(e.target.value);
}}
/>
</div>
<div className="actions">
<button
type="submit"
className="button button-first button-small button-stretch"
>
Save
</button>
<button
type="button"
onClick={handleRestore}
className="restore button-no-ui"
>
Restore default
</button>
</div>
</form>
</div>
</div>
);
};
export default Settings;

View file

@ -34,9 +34,10 @@ const Success: React.FunctionComponent = () => {
const [errorMsg, setErrorMsg] = useState('');
const dispatch = useDispatch();
const { location } = useSelector(state => {
const { location, settings } = useSelector(state => {
return {
location: state.location
location: state.location,
settings: state.settings
};
});
@ -50,7 +51,7 @@ const Success: React.FunctionComponent = () => {
} else if (e.keyCode === KEYCODE_ESC) {
window.close();
} else if (e.keyCode === KEYCODE_LOWERCASE_B) {
const url = `${config.webUrl}/notes/${noteUUID}`;
const url = `${settings.webUrl}/notes/${noteUUID}`;
ext.tabs
.create({ url })
@ -73,7 +74,7 @@ const Success: React.FunctionComponent = () => {
return (
<Fragment>
<Flash when={errorMsg !== ''} message={errorMsg} />
<Flash kind="error" when={errorMsg !== ''} message={errorMsg} />
<div className="success-page">
<div>

View file

@ -0,0 +1,46 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import { LOGIN, LOGOUT, LogoutAction, LoginAction } from './types';
import { ThunkAction } from '../types';
import initServices from '../../utils/services';
export function login({ email, password }): ThunkAction<void> {
return (dispatch, getState) => {
const { settings } = getState();
const { apiUrl } = settings;
return initServices(apiUrl)
.users.signin({ email, password })
.then(resp => {
dispatch({
type: LOGIN,
data: {
sessionKey: resp.key,
sessionKeyExpiry: resp.expiresAt
}
});
});
};
}
export function logout(): LogoutAction {
return {
type: LOGOUT
};
}

View file

@ -0,0 +1,46 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import { LOGIN, LOGOUT, AuthState, AuthActionType } from './types';
import config from '../../utils/config';
const initialState: AuthState = {
sessionKey: '',
sessionKeyExpiry: 0
};
export default function(
state = initialState,
action: AuthActionType
): AuthState {
switch (action.type) {
case LOGIN: {
const { sessionKey, sessionKeyExpiry } = action.data;
return {
...state,
sessionKey: sessionKey,
sessionKeyExpiry: sessionKeyExpiry
};
}
case LOGOUT:
return initialState;
default:
return state;
}
}

View file

@ -0,0 +1,39 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
export interface AuthState {
sessionKey: string;
sessionKeyExpiry: number;
}
export const LOGIN = 'auth/LOGIN';
export const LOGOUT = 'auth/LOGOUT';
export interface LoginAction {
type: typeof LOGIN;
data: {
sessionKey: string;
sessionKeyExpiry: number;
};
}
export interface LogoutAction {
type: typeof LOGOUT;
}
export type AuthActionType = LogoutAction | LoginAction;

View file

@ -16,7 +16,7 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import services from '../../utils/services';
import initServices from '../../utils/services';
import {
START_FETCHING,
@ -55,14 +55,15 @@ export function fetchBooks() {
return (dispatch, getState) => {
dispatch(startFetchingBooks());
const { settings } = getState();
const { settings, auth } = getState();
const services = initServices(settings.apiUrl);
services.books
.fetch(
{},
{
headers: {
Authorization: `Bearer ${settings.sessionKey}`
Authorization: `Bearer ${auth.sessionKey}`
}
}
)

View file

@ -24,18 +24,7 @@ import {
} from 'react-redux';
import { ThunkDispatch } from 'redux-thunk';
import { ComposerState } from './composer/types';
import { LocationState } from './location/types';
import { SettingsState } from './settings/types';
import { BooksState } from './books/types';
// AppState represents the application state
interface AppState {
composer: ComposerState;
location: LocationState;
settings: SettingsState;
books: BooksState;
}
import { AppState } from './types';
type ReduxDispatch = ThunkDispatch<AppState, any, Action>;

View file

@ -24,16 +24,39 @@ import location from './location/reducers';
import settings from './settings/reducers';
import books from './books/reducers';
import composer from './composer/reducers';
import auth from './auth/reducers';
import { AppState } from './types';
import config from '../utils/config';
const rootReducer = combineReducers({
auth,
location,
settings,
books,
composer
});
// configuruStore returns a new store that contains the appliation state
export default function configureStore(initialState) {
// initState returns a new state with any missing values populated
// if a state is given.
function initState(s: AppState | undefined): AppState {
if (s === undefined) {
return undefined;
}
const { settings } = s;
return {
...s,
settings: {
...settings,
apiUrl: settings.apiUrl || config.defaultApiEndpoint,
webUrl: settings.webUrl || config.defaultWebUrl
}
};
}
// configureStore returns a new store that contains the appliation state
export default function configureStore(state: AppState | undefined) {
const typedWindow = window as any;
const composeEnhancers =
@ -41,7 +64,7 @@ export default function configureStore(initialState) {
return createStore(
rootReducer,
initialState,
initState(state),
composeEnhancers(applyMiddleware(createLogger, thunkMiddleware))
);
}

View file

@ -17,6 +17,8 @@
*/
import { UPDATE, RESET, UpdateAction, ResetAction } from './types';
import { ThunkAction } from '../types';
import initServices from '../../utils/services';
export function updateSettings(settings): UpdateAction {
return {

View file

@ -17,10 +17,11 @@
*/
import { UPDATE, RESET, SettingsState, SettingsActionType } from './types';
import config from '../../utils/config';
const initialState: SettingsState = {
sessionKey: '',
sessionKeyExpiry: 0
apiUrl: config.defaultApiEndpoint,
webUrl: config.defaultWebUrl
};
export default function(

View file

@ -17,8 +17,8 @@
*/
export interface SettingsState {
sessionKey: string;
sessionKeyExpiry: number;
apiUrl: string;
webUrl: string;
}
export const UPDATE = 'settings/UPDATE';
@ -27,7 +27,7 @@ export const RESET = 'settings/RESET';
export interface UpdateAction {
type: typeof UPDATE;
data: {
settings: any;
settings: Partial<SettingsState>;
};
}

View file

@ -0,0 +1,43 @@
/* Copyright (C) 2019 Monomax Software Pty Ltd
*
* This file is part of Dnote.
*
* Dnote is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Dnote is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import { Action } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { AuthState } from './auth/types';
import { ComposerState } from './composer/types';
import { LocationState } from './location/types';
import { SettingsState } from './settings/types';
import { BooksState } from './books/types';
// AppState represents the application state
export interface AppState {
auth: AuthState;
composer: ComposerState;
location: LocationState;
settings: SettingsState;
books: BooksState;
}
// ThunkAction is a thunk action type
export type ThunkAction<T = void> = ThunkAction<
Promise<T>,
AppState,
void,
Action
>;

View file

@ -17,7 +17,7 @@
*/
export default {
webUrl: __WEB_URL__,
apiEndpoint: __API_ENDPOINT__,
defaultWebUrl: __WEB_URL__,
defaultApiEndpoint: __API_ENDPOINT__,
version: __VERSION__
};

View file

@ -16,12 +16,14 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import initServices from 'jslib/services';
import init from 'jslib/services';
import config from './config';
const services = initServices({
baseUrl: config.apiEndpoint,
pathPrefix: ''
});
const initServices = (baseUrl: string) => {
return init({
baseUrl: baseUrl,
pathPrefix: ''
});
};
export default services;
export default initServices;

View file

@ -16,9 +16,9 @@
* along with Dnote. If not, see <https://www.gnu.org/licenses/>.
*/
import ext from "./ext";
import ext from './ext';
const stateKey = "state";
const stateKey = 'state';
// filterState filters the given state to be suitable for reuse upon next app
// load
@ -27,13 +27,7 @@ function filterState(state) {
...state,
location: {
...state.location,
path: "/"
},
books: {
...state.books,
items: state.books.items.filter(item => {
return !item.isNew || item.selected;
})
path: '/'
}
};
}
@ -52,13 +46,13 @@ export function saveState(state) {
const serialized = JSON.stringify(filtered);
ext.storage.local.set({ [stateKey]: serialized }, () => {
console.log("synced state");
console.log('synced state');
});
}
// loadState loads and parses serialized state stored in ext.storage
export function loadState(done) {
ext.storage.local.get("state", items => {
ext.storage.local.get('state', items => {
const parsed = {
...items,
state: parseStorageItem(items.state)

View file

@ -18,6 +18,7 @@
/* container width: 345px */
/* global */
html {
font-size: 62.5%;
/* 1.0 rem = 10px */
@ -51,7 +52,7 @@ main.blur {
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
box-sizing: border-box;
font-size: 12px;
font-size: 16px;
}
.login-input {
margin-top: 4px;
@ -60,9 +61,16 @@ main.blur {
color: #2cae2c;
margin-bottom: 6px;
}
.alert.error {
.alert {
padding: 10px 9px;
font-size: 1.4rem;
}
.alert-info {
color: #0c5460;
background-color: #bee5eb;
border-color: #f5c6cb;
}
.alert-error {
color: #721c24;
background-color: #f8d7da;
border-color: #f5c6cb;
@ -106,6 +114,9 @@ kbd {
color: inherit;
text-decoration: none;
font-size: 16px;
/* override chrome default */
font: inherit !important;
}
.menu-link:hover {
background: #f7f7f7;
@ -139,16 +150,24 @@ kbd {
.header .menu-toggle {
height: 20px;
}
.home {
text-align: center;
padding: 16px 28px;
}
.home .greet {
.heading {
font-size: 2.2rem;
margin-top: 0;
margin-bottom: 0;
text-align: center;
}
.home .lead {
.lead {
color: #575757;
text-align: center;
}
.page {
padding: 16px 28px;
}
/* home */
.home {
text-align: center;
}
.home #login-form {
text-align: left;
@ -175,22 +194,30 @@ kbd {
.home .actions .signup:visited {
color: inherit;
}
.settings {
padding: 15px 8px;
/* settings */
.settings #settings-form {
margin-top: 12px;
}
.settings .input-row ~ .input-row {
margin-top: 12px;
}
.settings .label {
font-size: 1.4rem;
margin-bottom: 6px;
display: inline-block;
margin-bottom: 2px;
}
.settings .actions {
margin-top: 12px;
text-align: center;
}
.settings .hint {
font-size: 1.4rem;
color: #7e7e7e;
margin-top: 4px;
.settings .restore {
margin-top: 8px;
display: inline-block;
color: gray;
font-size: 1.3rem;
}
/* composer */
.composer .form {
display: flex;
flex-direction: column;
@ -271,6 +298,7 @@ kbd {
padding: 5px 8px;
}
/* success */
.success-page {
text-align: center;
padding: 21px 0;
@ -391,3 +419,13 @@ kbd {
.button-small {
padding: 5px 14px;
}
.button-stretch {
width: 100%;
justify-content: center;
}
.button-no-ui {
border: none;
background: none;
text-align: left;
cursor: pointer;
}

35
web/package-lock.json generated
View file

@ -12041,11 +12041,13 @@
},
"balanced-match": {
"version": "1.0.0",
"bundled": true
"bundled": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -12062,7 +12064,8 @@
},
"concat-map": {
"version": "0.0.1",
"bundled": true
"bundled": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
@ -12169,7 +12172,8 @@
},
"inherits": {
"version": "2.0.3",
"bundled": true
"bundled": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -12191,6 +12195,7 @@
"minimatch": {
"version": "3.0.4",
"bundled": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -13108,12 +13113,14 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -13128,17 +13135,20 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"core-util-is": {
"version": "1.0.2",
@ -13255,7 +13265,8 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"ini": {
"version": "1.3.5",
@ -13267,6 +13278,7 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -13281,6 +13293,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -13294,6 +13307,7 @@
"version": "2.3.5",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
@ -13392,7 +13406,8 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true
"dev": true,
"optional": true
},
"object-assign": {
"version": "4.1.1",
@ -13404,6 +13419,7 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -13525,6 +13541,7 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",

View file

@ -55,7 +55,7 @@ export const getNote = (
noteUUID: string,
params: GetNoteFacets
): ThunkAction<NoteData | void> => {
return dispatch => {
return (dispatch, getState) => {
dispatch(startFetchingNote());
return operations.notes