mirror of
https://github.com/dnote/dnote
synced 2026-03-14 14:35:50 +01:00
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:
parent
bdb75fd16c
commit
47d4a8f6fc
26 changed files with 546 additions and 108 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
163
browser/src/scripts/components/Settings.tsx
Normal file
163
browser/src/scripts/components/Settings.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
46
browser/src/scripts/store/auth/actions.ts
Normal file
46
browser/src/scripts/store/auth/actions.ts
Normal 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
|
||||
};
|
||||
}
|
||||
46
browser/src/scripts/store/auth/reducers.ts
Normal file
46
browser/src/scripts/store/auth/reducers.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
39
browser/src/scripts/store/auth/types.ts
Normal file
39
browser/src/scripts/store/auth/types.ts
Normal 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;
|
||||
|
|
@ -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}`
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
43
browser/src/scripts/store/types.ts
Normal file
43
browser/src/scripts/store/types.ts
Normal 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
|
||||
>;
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
*/
|
||||
|
||||
export default {
|
||||
webUrl: __WEB_URL__,
|
||||
apiEndpoint: __API_ENDPOINT__,
|
||||
defaultWebUrl: __WEB_URL__,
|
||||
defaultApiEndpoint: __API_ENDPOINT__,
|
||||
version: __VERSION__
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
35
web/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ export const getNote = (
|
|||
noteUUID: string,
|
||||
params: GetNoteFacets
|
||||
): ThunkAction<NoteData | void> => {
|
||||
return dispatch => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch(startFetchingNote());
|
||||
|
||||
return operations.notes
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue