diff --git a/web/package.json b/web/package.json index f69b208e..774a8701 100644 --- a/web/package.json +++ b/web/package.json @@ -5,9 +5,9 @@ "repository": "https://github.com/dnote/dnote", "main": "index", "scripts": { - "test": "./node_modules/karma/bin/karma start ./karma.conf.js --single-run", - "test:watch": "./node_modules/karma/bin/karma start ./karma.conf.js --watch", - "lint": "./node_modules/.bin/eslint ./src" + "test": "karma start ./karma.conf.js --single-run", + "test:watch": "karma start ./karma.conf.js --watch", + "lint": "eslint ./src/**/*.ts ./src/**/*.tsx" }, "author": "Monomax Software Pty Ltd", "license": "AGPL-3.0-or-later", diff --git a/web/src/client.tsx b/web/src/client.tsx index c39736d6..990cafb5 100644 --- a/web/src/client.tsx +++ b/web/src/client.tsx @@ -27,6 +27,7 @@ import { Provider } from 'react-redux'; import { debounce } from 'jslib/helpers/perf'; import App from './components/App'; import configureStore from './store'; +import { markPersisted } from './store/editor'; import { loadState, saveState } from './libs/localStorage'; import './libs/restoreScroll'; @@ -40,7 +41,11 @@ store.subscribe( saveState({ editor: state.editor }); - }, 1000) + + if (!state.editor.persisted) { + store.dispatch(markPersisted()); + } + }, 50) ); function renderApp() { diff --git a/web/src/components/App/index.tsx b/web/src/components/App/index.tsx index 6725d668..4136391f 100644 --- a/web/src/components/App/index.tsx +++ b/web/src/components/App/index.tsx @@ -56,15 +56,21 @@ interface Props extends RouteComponentProps {} function useFetchData() { const dispatch = useDispatch(); - useEffect(() => { - dispatch(getCurrentUser()).then(u => { - if (!u) { - return null; - } + const { user } = useSelector(state => { + return { + user: state.auth.user + }; + }); - return dispatch(getBooks()); - }); + useEffect(() => { + dispatch(getCurrentUser()); }, [dispatch]); + + useEffect(() => { + if (user.isFetched) { + dispatch(getBooks()); + } + }, [dispatch, user.isFetched]); } function hasLocationChanged(loc1: Location, loc2: Location) { diff --git a/web/src/components/Common/Editor/BookSelector/index.scss b/web/src/components/Common/Editor/BookSelector/index.scss index e06eac19..49f73d32 100644 --- a/web/src/components/Common/Editor/BookSelector/index.scss +++ b/web/src/components/Common/Editor/BookSelector/index.scss @@ -66,10 +66,6 @@ } } -.trigger-hidden { - visibility: hidden; -} - .book-label { margin-left: rem(8px); font-weight: 600; diff --git a/web/src/components/Common/Editor/BookSelector/index.tsx b/web/src/components/Common/Editor/BookSelector/index.tsx index a670e63d..6f59d238 100644 --- a/web/src/components/Common/Editor/BookSelector/index.tsx +++ b/web/src/components/Common/Editor/BookSelector/index.tsx @@ -26,11 +26,12 @@ import BookIcon from '../../../Icons/Book'; import CaretIcon from '../../../Icons/Caret'; import SearchInput from '../../SearchInput'; import { useDispatch, useSelector } from '../../../../store'; -import { updateBook } from '../../../../store/editor'; +import { updateBook, EditorSession } from '../../../../store/editor'; import OptionItem from './OptionItem'; import styles from './index.scss'; interface Props { + editor: EditorSession; wrapperClassName?: string; triggerClassName?: string; isReady: boolean; @@ -42,6 +43,7 @@ interface Props { } const BookSelector: React.SFC = ({ + editor, wrapperClassName, triggerClassName, isReady, @@ -50,10 +52,9 @@ const BookSelector: React.SFC = ({ setIsOpen, triggerRef }) => { - const { books, editor } = useSelector(state => { + const { books } = useSelector(state => { return { - books: state.books, - editor: state.editor + books: state.books }; }); const dispatch = useDispatch(); @@ -76,7 +77,13 @@ const BookSelector: React.SFC = ({ } function handleSelect(option) { - dispatch(updateBook({ label: option.label, uuid: option.value })); + dispatch( + updateBook({ + sessionKey: editor.sessionKey, + label: option.label, + uuid: option.value + }) + ); onAfterChange(); } @@ -105,10 +112,7 @@ const BookSelector: React.SFC = ({ className={classnames( styles.trigger, triggerClassName, - triggerProps.triggerClassName, - { - [styles['trigger-hidden']]: !isReady - } + triggerProps.triggerClassName )} onClick={() => { setIsOpen(!isOpen); @@ -116,17 +120,18 @@ const BookSelector: React.SFC = ({ aria-haspopup="menu" aria-expanded={ariaExpanded} aria-controls="book-filter" - disabled={books.isFetching} + disabled={!isReady} > - {currentLabel || 'Choose a book'} + {isReady ? currentLabel || 'Choose a book' : 'Loading...'} void; - doFlushContent: (string) => void; onSubmit: () => void; textareaRef: React.MutableRefObject; inputTimerRef: React.MutableRefObject; @@ -37,15 +36,16 @@ interface Props { } const Textarea: React.SFC = ({ + sessionKey, content, onChange, - doFlushContent, onSubmit, textareaRef, inputTimerRef, disabled }) => { const [contentFocused, setContentFocused] = useState(false); + const dispatch = useDispatch(); return (
@@ -65,7 +65,7 @@ const Textarea: React.SFC = ({ // eslint-disable-next-line no-param-reassign inputTimerRef.current = null; - doFlushContent(value); + dispatch(flushContent(sessionKey, value)); }, 1000); }} onFocus={() => { @@ -93,17 +93,4 @@ const Textarea: React.SFC = ({ ); }; -function mapStateToProps(state: AppState) { - return { - editor: state.editor - }; -} - -const mapDispatchToProps = { - doFlushContent: flushContent -}; - -export default connect( - mapStateToProps, - mapDispatchToProps -)(Textarea); +export default Textarea; diff --git a/web/src/components/Common/Editor/index.tsx b/web/src/components/Common/Editor/index.tsx index 72a202f3..b36a4cdd 100644 --- a/web/src/components/Common/Editor/index.tsx +++ b/web/src/components/Common/Editor/index.tsx @@ -25,13 +25,30 @@ import { focusTextarea } from 'web/libs/dom'; import { getHomePath } from 'web/libs/paths'; import BooksSelector from './BookSelector'; import { useDispatch, useSelector } from '../../../store'; -import { flushContent, markDirty } from '../../../store/editor'; +import { + flushContent, + resetEditor, + EditorSession +} from '../../../store/editor'; import Textarea from './Textarea'; import Preview from './Preview'; import Button from '../Button'; import styles from './Editor.scss'; +function getContentCacheKey(editorSessionKey: string) { + return `editor.${editorSessionKey}.content`; +} + +function useEditorContent( + editor: EditorSession, + cacheKey: string +): [string, React.Dispatch] { + const cached = localStorage.getItem(cacheKey); + return useState(cached || editor.content); +} + interface Props { + editor: EditorSession; onSubmit: (param: { draftContent: string; draftBookUUID: string }) => void; isBusy: boolean; cancelPath?: Location; @@ -47,6 +64,7 @@ enum Mode { } const Editor: React.SFC = ({ + editor, onSubmit, isBusy, disabled, @@ -55,16 +73,16 @@ const Editor: React.SFC = ({ bookSelectorTriggerRef, cancelPath = getHomePath() }) => { - const { editor, books } = useSelector(state => { + const { books } = useSelector(state => { return { - editor: state.editor, books: state.books }; }); const dispatch = useDispatch(); const [bookSelectorOpen, setBookSelectorOpen] = useState(false); - const [content, setContent] = useState(editor.content); + const contentCacheKey = getContentCacheKey(editor.sessionKey); + const [content, setContent] = useEditorContent(editor, contentCacheKey); const [mode, setMode] = useState(Mode.write); const inputTimerRef = useRef(null); @@ -78,10 +96,11 @@ const Editor: React.SFC = ({ // eslint-disable-next-line no-param-reassign inputTimerRef.current = null; - dispatch(flushContent(content)); + dispatch(flushContent(editor.sessionKey, content)); } onSubmit({ draftContent: content, draftBookUUID: editor.bookUUID }); + localStorage.removeItem(contentCacheKey); } if (disabled) { @@ -100,13 +119,12 @@ const Editor: React.SFC = ({
{ - dispatch(markDirty()); - if (textareaRef.current) { focusTextarea(textareaRef.current); } @@ -148,10 +166,14 @@ const Editor: React.SFC = ({
{mode === Mode.write ? (