From 7de82e0fefe096f87e667573a0b10d6730a1fddd Mon Sep 17 00:00:00 2001 From: Pavel Mineev Date: Wed, 1 Apr 2020 18:49:57 +0300 Subject: [PATCH] Fix clear localstorage after comment sending * add functions for saving json to localstorage and tests * remove comment form localstorage after posting --- frontend/app/common/local-storage.test.ts | 95 +++++++++++++++++++ frontend/app/common/local-storage.ts | 49 ++++++++++ .../comment-form/comment-form.test.tsx | 81 ++++++++++++++++ .../components/comment-form/comment-form.tsx | 67 ++++++------- frontend/app/components/comment/comment.tsx | 1 - 5 files changed, 260 insertions(+), 33 deletions(-) create mode 100644 frontend/app/common/local-storage.test.ts diff --git a/frontend/app/common/local-storage.test.ts b/frontend/app/common/local-storage.test.ts new file mode 100644 index 00000000..b3ee40d9 --- /dev/null +++ b/frontend/app/common/local-storage.test.ts @@ -0,0 +1,95 @@ +import { setJsonItem, getJsonItem, updateJsonItem } from './local-storage'; + +const LS_KEY = 'test'; + +describe('getJsonItem', () => { + afterAll(() => { + localStorage.clear(); + }); + it('should set json to empty localStorage', () => { + setJsonItem>(LS_KEY, {}); + expect(localStorage.getItem(LS_KEY)).toBe('{}'); + }); + + it('should update json in localStoeage', () => { + setJsonItem(LS_KEY, []); + expect(localStorage.getItem(LS_KEY)).toBe('[]'); + }); +}); + +describe('setJsonItem', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + }); + afterEach(() => { + localStorage.clear(); + }); + + it('should return null when localStorage is empty', () => { + expect(getJsonItem(LS_KEY)).toBe(null); + }); + + it('should return value of key', () => { + localStorage.setItem(LS_KEY, JSON.stringify({})); + expect(getJsonItem(LS_KEY)).toEqual({}); + + localStorage.setItem(LS_KEY, JSON.stringify([])); + expect(getJsonItem(LS_KEY)).toEqual([]); + + localStorage.setItem(LS_KEY, JSON.stringify(null)); + expect(getJsonItem(LS_KEY)).toBe(null); + + localStorage.setItem(LS_KEY, JSON.stringify(1)); + expect(getJsonItem(LS_KEY)).toBe(1); + + localStorage.setItem(LS_KEY, JSON.stringify(1)); + expect(getJsonItem(LS_KEY)).toBe(1); + }); + + it('should return `null` if value in localStorage is not JSON', () => { + localStorage.setItem(LS_KEY, '"{:"""'); + + expect(getJsonItem(LS_KEY)).toBe(null); + expect(consoleSpy).toHaveBeenCalled(); + + localStorage.setItem(LS_KEY, 'asdas'); + + expect(getJsonItem(LS_KEY)).toBe(null); + expect(consoleSpy).toHaveBeenCalled(); + }); +}); + +describe('updateJsonItem', () => { + afterEach(() => { + localStorage.clear(); + }); + + it('should set data to empty localStorage', () => { + updateJsonItem>(LS_KEY, {}); + + expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({})); + }); + + it('should update object in localStorage', () => { + localStorage.setItem(LS_KEY, JSON.stringify({ x: 1 })); + updateJsonItem(LS_KEY, { y: 1 }); + + expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify({ x: 1, y: 1 })); + }); + + it('should update array in localStorage', () => { + localStorage.setItem(LS_KEY, JSON.stringify([1, 2, 3])); + updateJsonItem(LS_KEY, [4, 5, 6]); + + expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify([1, 2, 3, 4, 5, 6])); + }); + + it('should update data in localStorage with merge', () => { + localStorage.setItem(LS_KEY, JSON.stringify([3, 4, 5])); + updateJsonItem(LS_KEY, data => [1, 2, ...data]); + + expect(localStorage.getItem(LS_KEY)).toBe(JSON.stringify([1, 2, 3, 4, 5])); + }); +}); diff --git a/frontend/app/common/local-storage.ts b/frontend/app/common/local-storage.ts index 571ed9d7..18e39b28 100644 --- a/frontend/app/common/local-storage.ts +++ b/frontend/app/common/local-storage.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { IS_STORAGE_AVAILABLE } from './constants'; const failMessage = 'remark42: localStorage access denied, check browser preferences'; @@ -20,3 +21,51 @@ export const removeItem = IS_STORAGE_AVAILABLE : () => { console.error(failMessage); // eslint-disable-line no-console }; + +export function getJsonItem(key: string): T | null { + try { + const json = getItem(key); + + if (json === null) { + return null; + } + + const data = JSON.parse(json); + + return data; + } catch (e) { + console.error(`remark42: error on read JSON from ${key} in localStorage`, e); // eslint-disable-line no-console + return null; + } +} + +export function setJsonItem(key: string, data: T) { + try { + setItem(key, JSON.stringify(data)); + } catch (e) { + console.error(`remark42: error on parse JSON from ${key} in localStorage`, e); // eslint-disable-line no-console + } +} + +export function updateJsonItem | any[]>(key: string, value: (data: T) => T): void; +export function updateJsonItem(key: string, value: T): void; +export function updateJsonItem>(key: string, value: T) { + const savedData = getJsonItem(key); + + if (Array.isArray(value)) { + setJsonItem(key, [...savedData, ...value]); + return; + } + + if (value !== null && typeof value === 'object') { + setJsonItem(key, { ...savedData, ...value }); + return; + } + + if (typeof value === 'function') { + setJsonItem(key, value(savedData)); + return; + } + + throw new Error(`remark42: error on update JSON for ${key} in localStorage`); +} diff --git a/frontend/app/components/comment-form/comment-form.test.tsx b/frontend/app/components/comment-form/comment-form.test.tsx index 7848f85c..77f3a1ad 100644 --- a/frontend/app/components/comment-form/comment-form.test.tsx +++ b/frontend/app/components/comment-form/comment-form.test.tsx @@ -4,9 +4,20 @@ import { shallow } from 'enzyme'; import { user } from '@app/testUtils/mocks/user'; import { StaticStore } from '@app/common/static_store'; +import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants'; +import * as localStorageModule from '@app/common/local-storage'; import { CommentForm, Props } from './comment-form'; import { SubscribeByEmail } from './__subscribe-by-email'; +import TextareaAutosize from './textarea-autosize'; + +function createEvent(type: string, value: T) { + const event = new Event(type); + + Object.defineProperty(event, 'target', { value }); + + return event; +} const DEFAULT_PROPS: Readonly> = { mode: 'main', @@ -50,4 +61,74 @@ describe('', () => { expect(wrapper.exists(SubscribeByEmail)).toEqual(false); }); + + describe('initial value of comment', () => { + afterEach(() => { + localStorage.clear(); + }); + it('should has empty value', () => { + localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 2: 'text' })); + + const props = { ...DEFAULT_PROPS, user, intl }; + const wrapper = shallow(); + + expect(wrapper.state('text')).toBe(''); + expect(wrapper.find(TextareaAutosize).prop('value')).toBe(''); + }); + + it('should get initial value from localStorage', () => { + const COMMENT_VALUE = 'text'; + + localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: COMMENT_VALUE })); + + const props = { ...DEFAULT_PROPS, user, intl }; + const wrapper = shallow(); + + expect(wrapper.state('text')).toBe(COMMENT_VALUE); + expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE); + }); + + it('should get initial value from props instead localStorage', () => { + const COMMENT_VALUE = 'text from props'; + + localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ 1: 'text from localStorage' })); + + const props = { ...DEFAULT_PROPS, user, intl, value: COMMENT_VALUE }; + const wrapper = shallow(); + + expect(wrapper.state('text')).toBe(COMMENT_VALUE); + expect(wrapper.find(TextareaAutosize).prop('value')).toBe(COMMENT_VALUE); + }); + }); + + describe('update value of comment in localStorage', () => { + afterEach(() => { + localStorage.clear(); + }); + it('should update value', () => { + const props = { ...DEFAULT_PROPS, user, intl }; + const wrapper = shallow(); + // @ts-ignore + const instance: CommentForm = wrapper.instance(); + + instance.onInput(createEvent('input', { value: '1' })); + expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"1"}'); + + instance.onInput(createEvent('input', { value: '11' })); + expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe('{"1":"11"}'); + }); + + it('should clear value after send', async () => { + localStorage.setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ '1': 'asd' })); + const updateJsonItemSpy = jest.spyOn(localStorageModule, 'updateJsonItem'); + const props = { ...DEFAULT_PROPS, user, intl }; + const wrapper = shallow(); + // @ts-ignore + const instance: CommentForm = wrapper.instance(); + + await instance.send(createEvent('send', { preventDefault: () => undefined })); + expect(updateJsonItemSpy).toHaveBeenCalled(); + expect(localStorage.getItem(LS_SAVED_COMMENT_VALUE)).toBe(JSON.stringify({})); + }); + }); }); diff --git a/frontend/app/components/comment-form/comment-form.tsx b/frontend/app/components/comment-form/comment-form.tsx index 4f99036b..21334c5e 100644 --- a/frontend/app/components/comment-form/comment-form.tsx +++ b/frontend/app/components/comment-form/comment-form.tsx @@ -11,7 +11,7 @@ import { sleep } from '@app/utils/sleep'; import { replaceSelection } from '@app/utils/replaceSelection'; import { Button } from '@app/components/button'; import Auth from '@app/components/auth'; -import { getItem, setItem } from '@app/common/local-storage'; +import { getJsonItem, updateJsonItem } from '@app/common/local-storage'; import { LS_SAVED_COMMENT_VALUE } from '@app/common/constants'; import { SubscribeByEmail } from './__subscribe-by-email'; @@ -42,7 +42,7 @@ export interface Props { intl: IntlShape; } -interface State { +export interface State { preview: string | null; isErrorShown: boolean; /** error message, if contains newlines, it will be splitted to multiple errors */ @@ -104,13 +104,16 @@ export class CommentForm extends Component { textareaId = textareaId + 1; this.textareaId = `textarea_${textareaId}`; - const savedCommentsJSON = getItem(LS_SAVED_COMMENT_VALUE); - let savedValue = ''; - try { - if (typeof savedCommentsJSON === 'string') { - savedValue = JSON.parse(savedCommentsJSON)[this.props.id] || ''; - } - } catch (e) {} + const savedComments = getJsonItem(LS_SAVED_COMMENT_VALUE); + let text = ''; + + if (savedComments !== null && savedComments[props.id]) { + text = savedComments[props.id]; + } + + if (props.value) { + text = props.value; + } this.state = { preview: null, @@ -119,13 +122,11 @@ export class CommentForm extends Component { errorLock: false, isDisabled: false, maxLength: StaticStore.config.max_comment_size, - text: props.value || savedValue, + text, buttonText: null, }; - this.send = this.send.bind(this); this.getPreview = this.getPreview.bind(this); - this.onInput = this.onInput.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onDragOver = this.onDragOver.bind(this); this.onDrop = this.onDrop.bind(this); @@ -169,12 +170,10 @@ export class CommentForm extends Component { } } - onInput(e: Event) { + onInput = (e: Event) => { const { value } = e.target as HTMLInputElement; - try { - setItem(LS_SAVED_COMMENT_VALUE, JSON.stringify({ [this.props.id]: value })); - } catch (e) {} + updateJsonItem(LS_SAVED_COMMENT_VALUE, { [this.props.id]: value }); if (this.state.errorLock) { this.setState({ @@ -183,13 +182,14 @@ export class CommentForm extends Component { }); return; } + this.setState({ isErrorShown: false, errorMessage: null, preview: null, text: value, }); - } + }; async onPaste(e: ClipboardEvent) { if (!(e.clipboardData && e.clipboardData.files.length > 0)) { @@ -200,32 +200,35 @@ export class CommentForm extends Component { await this.uploadImages(files); } - send(e: Event) { - const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text; - const props = this.props; + send = async (e: Event) => { + const { text } = this.state; if (e) e.preventDefault(); if (!text || !text.trim()) return; - if (text === this.props.value) { this.props.onCancel && this.props.onCancel(); this.setState({ preview: null, text: '' }); } this.setState({ isDisabled: true, isErrorShown: false, text }); + try { + await this.props.onSubmit(text, pageTitle || document.title); + updateJsonItem>(LS_SAVED_COMMENT_VALUE, data => { + delete data[this.props.id]; - props - .onSubmit(text, pageTitle || document.title) - .then(() => { - this.setState({ preview: null, text: '' }); - }) - .catch(e => { - const errorMessage = extractErrorMessageFromResponse(e, this.props.intl); - this.setState({ isErrorShown: true, errorMessage }); - }) - .finally(() => this.setState({ isDisabled: false })); - } + return data; + }); + this.setState({ preview: null, text: '' }); + } catch (e) { + this.setState({ + isErrorShown: true, + errorMessage: extractErrorMessageFromResponse(e, this.props.intl), + }); + } + + this.setState({ isDisabled: false }); + }; getPreview() { const text = this.textAreaRef.current ? this.textAreaRef.current.getValue() : this.state.text; diff --git a/frontend/app/components/comment/comment.tsx b/frontend/app/components/comment/comment.tsx index 36865c9a..b1528f7c 100644 --- a/frontend/app/components/comment/comment.tsx +++ b/frontend/app/components/comment/comment.tsx @@ -835,7 +835,6 @@ class Comment extends Component { intl={this.props.intl} user={props.user} theme={props.theme} - value="" mode="reply" mix="comment__input" onSubmit={(text, title) => this.addComment(text, title, o.id)}