diff --git a/CHANGELOG.md b/CHANGELOG.md index c873ee04..30e3a24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Makefile b/Makefile index b4d32b79..d32d03ef 100644 --- a/Makefile +++ b/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 diff --git a/browser/package.json b/browser/package.json index 2d4e4b45..3a578e07 100644 --- a/browser/package.json +++ b/browser/package.json @@ -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", diff --git a/browser/src/scripts/components/App.tsx b/browser/src/scripts/components/App.tsx index a97ac662..47fec645 100644 --- a/browser/src/scripts/components/App.tsx +++ b/browser/src/scripts/components/App.tsx @@ -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 ; - case '/': + case '/': { if (isLoggedIn) { return ; } return ; + } + case '/settings': { + return ; + } default: return
Not found
; } } +// 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 = () => { 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(); diff --git a/browser/src/scripts/components/Composer.tsx b/browser/src/scripts/components/Composer.tsx index 7d961020..47fba925 100644 --- a/browser/src/scripts/components/Composer.tsx +++ b/browser/src/scripts/components/Composer.tsx @@ -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 = () => { 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 = () => { }, { headers: { - Authorization: `Bearer ${settings.sessionKey}` + Authorization: `Bearer ${auth.sessionKey}` } } ); @@ -126,7 +129,7 @@ const Composer: React.FunctionComponent = () => { }, { headers: { - Authorization: `Bearer ${settings.sessionKey}` + Authorization: `Bearer ${auth.sessionKey}` } } ); @@ -176,7 +179,7 @@ const Composer: React.FunctionComponent = () => { return (
- +
= ({ message, when }) => { +const Flash: React.FunctionComponent = ({ message, when, kind }) => { if (when) { - return
Error: {message}
; + return
{message}
; } return null; diff --git a/browser/src/scripts/components/Home.tsx b/browser/src/scripts/components/Home.tsx index 7d6d164a..09b7a169 100644 --- a/browser/src/scripts/components/Home.tsx +++ b/browser/src/scripts/components/Home.tsx @@ -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 = () => { 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 = () => { }; return ( -
-

Welcome to Dnote

+
+

Welcome to Dnote

A simple personal knowledge base

- {errMsg &&
{errMsg}
} + @@ -97,7 +92,7 @@ const Home: React.FunctionComponent = () => { className="button button-first button-small login-btn" disabled={loggingIn} > - {loggingIn ? 'Signing in...' : 'Signin'} + {loggingIn ? 'Signing in...' : 'Sign in'} diff --git a/browser/src/scripts/components/Menu.tsx b/browser/src/scripts/components/Menu.tsx index 1e9ef04c..01915e01 100644 --- a/browser/src/scripts/components/Menu.tsx +++ b/browser/src/scripts/components/Menu.tsx @@ -28,6 +28,11 @@ export default ({ toggleMenu, loggedIn, onLogout }) => ( Home +
  • + + Settings + +
  • {loggedIn && (
  • diff --git a/browser/src/scripts/components/Settings.tsx b/browser/src/scripts/components/Settings.tsx new file mode 100644 index 00000000..430218fb --- /dev/null +++ b/browser/src/scripts/components/Settings.tsx @@ -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 . + */ + +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 = () => { + 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 ( +
    + + + +
    +

    Settings

    + +

    Customize your Dnote extension

    + +
    +
    + + + { + setAPIUrl(e.target.value); + }} + /> +
    + +
    + + + { + setWebUrl(e.target.value); + }} + /> +
    + +
    + + + +
    +
    +
    +
    + ); +}; + +export default Settings; diff --git a/browser/src/scripts/components/Success.tsx b/browser/src/scripts/components/Success.tsx index 390efd13..61d38baf 100644 --- a/browser/src/scripts/components/Success.tsx +++ b/browser/src/scripts/components/Success.tsx @@ -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 ( - +
    diff --git a/browser/src/scripts/store/auth/actions.ts b/browser/src/scripts/store/auth/actions.ts new file mode 100644 index 00000000..af9ce6ee --- /dev/null +++ b/browser/src/scripts/store/auth/actions.ts @@ -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 . + */ + +import { LOGIN, LOGOUT, LogoutAction, LoginAction } from './types'; +import { ThunkAction } from '../types'; +import initServices from '../../utils/services'; + +export function login({ email, password }): ThunkAction { + 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 + }; +} diff --git a/browser/src/scripts/store/auth/reducers.ts b/browser/src/scripts/store/auth/reducers.ts new file mode 100644 index 00000000..fdbe465f --- /dev/null +++ b/browser/src/scripts/store/auth/reducers.ts @@ -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 . + */ + +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; + } +} diff --git a/browser/src/scripts/store/auth/types.ts b/browser/src/scripts/store/auth/types.ts new file mode 100644 index 00000000..e2ebb724 --- /dev/null +++ b/browser/src/scripts/store/auth/types.ts @@ -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 . + */ + +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; diff --git a/browser/src/scripts/store/books/actions.ts b/browser/src/scripts/store/books/actions.ts index 6dc7469f..c2a6a93c 100644 --- a/browser/src/scripts/store/books/actions.ts +++ b/browser/src/scripts/store/books/actions.ts @@ -16,7 +16,7 @@ * along with Dnote. If not, see . */ -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}` } } ) diff --git a/browser/src/scripts/store/hooks.ts b/browser/src/scripts/store/hooks.ts index 29dad71d..cb0bf85e 100644 --- a/browser/src/scripts/store/hooks.ts +++ b/browser/src/scripts/store/hooks.ts @@ -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; diff --git a/browser/src/scripts/store/index.ts b/browser/src/scripts/store/index.ts index fa2dbef7..203d603d 100644 --- a/browser/src/scripts/store/index.ts +++ b/browser/src/scripts/store/index.ts @@ -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)) ); } diff --git a/browser/src/scripts/store/settings/actions.ts b/browser/src/scripts/store/settings/actions.ts index 347abf46..1ebb4400 100644 --- a/browser/src/scripts/store/settings/actions.ts +++ b/browser/src/scripts/store/settings/actions.ts @@ -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 { diff --git a/browser/src/scripts/store/settings/reducers.ts b/browser/src/scripts/store/settings/reducers.ts index 3ce61c06..1cfef15c 100644 --- a/browser/src/scripts/store/settings/reducers.ts +++ b/browser/src/scripts/store/settings/reducers.ts @@ -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( diff --git a/browser/src/scripts/store/settings/types.ts b/browser/src/scripts/store/settings/types.ts index 4b37e403..9849787d 100644 --- a/browser/src/scripts/store/settings/types.ts +++ b/browser/src/scripts/store/settings/types.ts @@ -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; }; } diff --git a/browser/src/scripts/store/types.ts b/browser/src/scripts/store/types.ts new file mode 100644 index 00000000..49a315d6 --- /dev/null +++ b/browser/src/scripts/store/types.ts @@ -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 . + */ + +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 = ThunkAction< + Promise, + AppState, + void, + Action +>; diff --git a/browser/src/scripts/utils/config.ts b/browser/src/scripts/utils/config.ts index 9c2636a0..9b0ce665 100644 --- a/browser/src/scripts/utils/config.ts +++ b/browser/src/scripts/utils/config.ts @@ -17,7 +17,7 @@ */ export default { - webUrl: __WEB_URL__, - apiEndpoint: __API_ENDPOINT__, + defaultWebUrl: __WEB_URL__, + defaultApiEndpoint: __API_ENDPOINT__, version: __VERSION__ }; diff --git a/browser/src/scripts/utils/services.ts b/browser/src/scripts/utils/services.ts index 8b3815ce..d9fee01e 100644 --- a/browser/src/scripts/utils/services.ts +++ b/browser/src/scripts/utils/services.ts @@ -16,12 +16,14 @@ * along with Dnote. If not, see . */ -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; diff --git a/browser/src/scripts/utils/storage.ts b/browser/src/scripts/utils/storage.ts index 5f632344..0de6e291 100644 --- a/browser/src/scripts/utils/storage.ts +++ b/browser/src/scripts/utils/storage.ts @@ -16,9 +16,9 @@ * along with Dnote. If not, see . */ -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) diff --git a/browser/src/styles/popup.css b/browser/src/styles/popup.css index 113d0ffe..b288f2ad 100644 --- a/browser/src/styles/popup.css +++ b/browser/src/styles/popup.css @@ -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; +} diff --git a/web/package-lock.json b/web/package-lock.json index fe3f2555..40ae4ba9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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", diff --git a/web/src/store/note/actions.ts b/web/src/store/note/actions.ts index bf77683b..834f12ba 100644 --- a/web/src/store/note/actions.ts +++ b/web/src/store/note/actions.ts @@ -55,7 +55,7 @@ export const getNote = ( noteUUID: string, params: GetNoteFacets ): ThunkAction => { - return dispatch => { + return (dispatch, getState) => { dispatch(startFetchingNote()); return operations.notes