From 66ff40adefb4f1ef6209071cf35632fb88914b8a Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 9 Apr 2024 03:29:56 +0400 Subject: [PATCH 1/6] chore: update operation result types --- src/shared/libs/operationResult/index.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/shared/libs/operationResult/index.ts b/src/shared/libs/operationResult/index.ts index cb4f7ad..b92900e 100644 --- a/src/shared/libs/operationResult/index.ts +++ b/src/shared/libs/operationResult/index.ts @@ -1,12 +1,24 @@ +import { errorStore } from '@entities/error' + export type TBaseError = { type: string; error: Error | null } -export type TResult = +export type TResult = | { data: T; error: null } | { data: null; error: E } export const Result = Object.freeze({ - Success: (data: T): TResult => ({ data, error: null }), - Error: (error: E): TResult => ({ - data: null, - error: error, + Success: ( + data: T, + ): TResult => ({ + data, + error: null, }), + Error: ( + error: E, + ): TResult => { + errorStore.addError(error) + return { + data: null, + error, + } + }, }) From 8a8eb6c3e03662e3b33cded726958a013e321a34 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 9 Apr 2024 03:32:40 +0400 Subject: [PATCH 2/6] refactor: remove react functionality from language store --- .../language/model/__tests__/store.test.ts | 16 +++------ src/entities/language/model/store.ts | 33 ++++++++++--------- .../SelectLanguages/model/__mock__/store.ts | 4 ++- .../model/__tests__/store.test.ts | 12 ++----- .../language/SelectLanguages/model/store.ts | 6 ++-- .../SelectLanguages/ui/SelectLanguages.tsx | 14 ++++++-- 6 files changed, 42 insertions(+), 43 deletions(-) diff --git a/src/entities/language/model/__tests__/store.test.ts b/src/entities/language/model/__tests__/store.test.ts index 79adf8f..417a263 100644 --- a/src/entities/language/model/__tests__/store.test.ts +++ b/src/entities/language/model/__tests__/store.test.ts @@ -2,8 +2,6 @@ import { allTasks, cleanStores, keepMount } from 'nanostores' import { afterEach, describe, expect, test, vi } from 'vitest' import { Result } from '@shared/libs/operationResult' -import { getErrorToastMock } from '@shared/ui/Toast/helpers/__mock__/getErrorToast' -import { addToastMock } from '@shared/ui/Toast/model/__mock__/store' import { waitFor } from '@tests/testUtils' @@ -31,7 +29,6 @@ describe('languages store', () => { await allTasks() expect($languages.get()).toEqual([language]) - expect(getErrorToastMock).toBeCalledTimes(0) }) test('should set empty array and show toast with error when LanguagesStorage returns error', async () => { @@ -43,7 +40,6 @@ describe('languages store', () => { await allTasks() expect($languages.get()).toEqual([]) - expect(getErrorToastMock).toBeCalledWith(error) }) }) @@ -55,11 +51,8 @@ describe('languages store', () => { await allTasks() waitFor(async () => { - select([]) - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() + const result = await select([]) + expect(result.error).toBeDefined() }) }) @@ -70,9 +63,8 @@ describe('languages store', () => { await allTasks() waitFor(async () => { - select([]) - - expect(getErrorToastMock).toBeCalledWith(error) + const result = await select([]) + expect(result.error).toBeDefined() }) }) }) diff --git a/src/entities/language/model/store.ts b/src/entities/language/model/store.ts index 7cf8f69..23550e7 100644 --- a/src/entities/language/model/store.ts +++ b/src/entities/language/model/store.ts @@ -4,7 +4,7 @@ import { ILanguage, TLanguageCode } from '@entities/language' import { getStorage } from '@entities/storage' import { browser } from '@shared/browser' -import { addToast, getErrorToast } from '@shared/ui/Toast' +import { Result, TResult } from '@shared/libs/operationResult' import { LANGUAGES } from './languages' @@ -17,27 +17,28 @@ export const $languages = atom(LANGUAGES) onMount($languages, () => { task(async () => { const getResult = await LanguageStorage.get() - if (getResult.data) { - $languages.set(getResult.data) - } else { - addToast(getErrorToast(getResult.error)) - } + getResult.data && $languages.set(getResult.data) }) + + const listener = LanguageStorage.onChanged.addListener((changes) => { + changes.data && $languages.set(changes.data.newValue) + }) + + return () => { + LanguageStorage.onChanged.removeListener(listener) + } }) -export const select = async (languageCodes: TLanguageCode[]) => { +export const select = async ( + languageCodes: TLanguageCode[], +): Promise => { const filteredLanguages = LANGUAGES.filter((language) => languageCodes.includes(language.value), ) const setResult = await LanguageStorage.set(filteredLanguages) - if (setResult.data) { - $languages.set(filteredLanguages) - addToast({ - type: 'success', - title: browser.i18n.getMessage('SELECTED_LANGUAGES_SAVED'), - }) - } else { - addToast(getErrorToast(setResult.error)) - } + + return setResult.data + ? Result.Success(browser.i18n.getMessage('SELECTED_LANGUAGES_SAVED')) + : setResult } diff --git a/src/features/language/SelectLanguages/model/__mock__/store.ts b/src/features/language/SelectLanguages/model/__mock__/store.ts index e33441e..3c48d12 100644 --- a/src/features/language/SelectLanguages/model/__mock__/store.ts +++ b/src/features/language/SelectLanguages/model/__mock__/store.ts @@ -1,5 +1,7 @@ import { MockedFunction, vi } from 'vitest' +import { Result } from '@shared/libs/operationResult' + import { checkIsSelected, reset, @@ -9,7 +11,7 @@ import { export const syncLocalStoreWithLanguageStoreMock: MockedFunction< typeof syncLocalStoreWithLanguageStore -> = vi.fn() +> = vi.fn(() => Promise.resolve(Result.Success('success'))) export const resetMock: MockedFunction = vi.fn() diff --git a/src/features/language/SelectLanguages/model/__tests__/store.test.ts b/src/features/language/SelectLanguages/model/__tests__/store.test.ts index 14a06c0..82af88b 100644 --- a/src/features/language/SelectLanguages/model/__tests__/store.test.ts +++ b/src/features/language/SelectLanguages/model/__tests__/store.test.ts @@ -3,9 +3,6 @@ import { afterEach, describe, expect, test } from 'vitest' import { languageStore } from '@entities/language' -import { getErrorToastMock } from '@shared/ui/Toast/helpers/__mock__/getErrorToast' -import { addToastMock } from '@shared/ui/Toast/model/__mock__/store' - import { waitFor } from '@tests/testUtils' import { @@ -28,7 +25,6 @@ describe('selectedLanguageCodes store', () => { keepMount(localStore.languageCodes) expect(localStore.languageCodes.get()).toEqual([]) - expect(getErrorToastMock).toBeCalledTimes(0) }) test('should set value from languages', async () => { @@ -38,7 +34,6 @@ describe('selectedLanguageCodes store', () => { await allTasks() expect(localStore.languageCodes.get()).toEqual(['en']) - expect(getErrorToastMock).toBeCalledTimes(0) }) }) @@ -76,11 +71,8 @@ describe('selectedLanguageCodes store', () => { await allTasks() waitFor(async () => { - syncLocalStoreWithLanguageStore() - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() + const result = await syncLocalStoreWithLanguageStore() + expect(result.data).toBeDefined() }) }) }) diff --git a/src/features/language/SelectLanguages/model/store.ts b/src/features/language/SelectLanguages/model/store.ts index 301d4ce..f83fbd6 100644 --- a/src/features/language/SelectLanguages/model/store.ts +++ b/src/features/language/SelectLanguages/model/store.ts @@ -2,6 +2,8 @@ import { atom } from 'nanostores' import { TLanguageCode, languageStore } from '@entities/language' +import { TResult } from '@shared/libs/operationResult' + let defaultValue: TLanguageCode[] = [] export const localStore = { @@ -38,6 +40,6 @@ export const reset = () => { localStore.languageCodes.set(defaultValue) } -export const syncLocalStoreWithLanguageStore = () => { - languageStore.select(localStore.languageCodes.get()) +export const syncLocalStoreWithLanguageStore = (): Promise => { + return languageStore.select(localStore.languageCodes.get()) } diff --git a/src/features/language/SelectLanguages/ui/SelectLanguages.tsx b/src/features/language/SelectLanguages/ui/SelectLanguages.tsx index b46a15c..bf582d8 100644 --- a/src/features/language/SelectLanguages/ui/SelectLanguages.tsx +++ b/src/features/language/SelectLanguages/ui/SelectLanguages.tsx @@ -5,6 +5,7 @@ import { ILanguage } from '@entities/language' import { browser } from '@shared/browser' import { Button } from '@shared/ui/Button' +import { addToast, getErrorToast } from '@shared/ui/Toast' import { checkIsSelected, @@ -20,9 +21,18 @@ interface Props { export const SelectLanguages = ({ languages }: Props) => { useStore(localStore.languageCodes) - const onSubmit: JSXInternal.SubmitEventHandler = (e) => { + + const onSubmit: JSXInternal.SubmitEventHandler = async ( + e, + ) => { e.preventDefault() - syncLocalStoreWithLanguageStore() + const result = await syncLocalStoreWithLanguageStore() + result.data && + addToast({ + type: 'success', + title: result.data, + }) + result.error && addToast(getErrorToast(result.error)) } return ( From a03b2212b0294fa8c2af3218a0dfac06b4a380c8 Mon Sep 17 00:00:00 2001 From: Alina Date: Tue, 9 Apr 2024 03:34:45 +0400 Subject: [PATCH 3/6] refactor: remove react functionality from note store --- .../note/model/__tests__/store.test.ts | 95 +++++--------- src/entities/note/model/store.ts | 123 +++++++++--------- src/features/note/DeleteNote/ui/index.tsx | 24 ++-- src/features/note/EditNote/ui/index.tsx | 17 ++- 4 files changed, 126 insertions(+), 133 deletions(-) diff --git a/src/entities/note/model/__tests__/store.test.ts b/src/entities/note/model/__tests__/store.test.ts index e1b0635..3478635 100644 --- a/src/entities/note/model/__tests__/store.test.ts +++ b/src/entities/note/model/__tests__/store.test.ts @@ -2,16 +2,22 @@ import { allTasks, cleanStores, keepMount } from 'nanostores' import { afterEach, describe, expect, test, vi } from 'vitest' import { Result } from '@shared/libs/operationResult' -import { getErrorToastMock } from '@shared/ui/Toast/helpers/__mock__/getErrorToast' -import { addToastMock } from '@shared/ui/Toast/model/__mock__/store' import { waitFor } from '@tests/testUtils' -import { $notes, NoteStorage, deleteNote, editNote } from '../store' +import { + $notes, + NoteStorage, + StorageValue, + defaultNotes, + deleteNote, + editNote, +} from '../store' describe('note store', () => { const languageCode = 'en' - const notes = { + const notes: StorageValue = { + ...defaultNotes, en: [ { id: '1', @@ -30,8 +36,6 @@ describe('note store', () => { transcription: 'text', }, ], - pt: [], - ko: [], } vi.spyOn(NoteStorage, 'get').mockResolvedValue(Result.Success(notes)) @@ -51,20 +55,14 @@ describe('note store', () => { describe('deleteNote', () => { const noteId = '1' - test('should set new notes to NoteStorage and show opration success information', async () => { + test('should set new notes note to NoteStorage and show operation success information', async () => { keepMount($notes) await allTasks() await waitFor(async () => { - await deleteNote(languageCode, noteId) - + const result = await deleteNote(languageCode, noteId) + expect(result.data).toBeDefined() expect($notes.get()[languageCode]).toEqual([]) - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() - - expect(getErrorToastMock).toBeCalledTimes(0) }) }) @@ -73,45 +71,33 @@ describe('note store', () => { await allTasks() await waitFor(async () => { - await deleteNote('jp', noteId) - + const result = await deleteNote('jp', noteId) + expect(result.data).toBeDefined() expect($notes.get()['jp']).toEqual([]) - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() - - expect(getErrorToastMock).toBeCalledTimes(0) }) }) - test('should show error if can not set to NoteStorage and keep note', async () => { + test('should return error if can not set to NoteStorage and keep note', async () => { vi.spyOn(NoteStorage, 'set').mockResolvedValueOnce(Result.Error(error)) keepMount($notes) await allTasks() await waitFor(async () => { - await deleteNote(languageCode, noteId) - + const result = await deleteNote(languageCode, noteId) + expect(result.error).toBeDefined() expect($notes.get()[languageCode]).toEqual(notes[languageCode]) - - expect(getErrorToastMock).toBeCalledWith(error) }) }) - test('should show error if note not found', async () => { + test('should return error if note not found', async () => { keepMount($notes) await allTasks() await waitFor(async () => { - await deleteNote(languageCode, 'NOT_EXISTING_ID') - + const result = await deleteNote(languageCode, 'NOT_EXISTING_ID') + expect(result.error).toBeDefined() expect($notes.get()[languageCode]).toEqual(notes[languageCode]) - - const call = getErrorToastMock.mock.calls[0] - expect(call[0].type).toBe('ERROR_CAN_NOT_FIND_NOTE_TO_DELETE') - expect(call[0].error).toBeDefined() }) }) }) @@ -130,15 +116,9 @@ describe('note store', () => { await allTasks() await waitFor(async () => { - await editNote(languageCode, newNote) - + const result = await editNote(languageCode, newNote) + expect(result.data).toBeDefined() expect($notes.get()[languageCode]).toEqual([newNote]) - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() - - expect(getErrorToastMock).toBeCalledTimes(0) }) }) @@ -147,30 +127,22 @@ describe('note store', () => { await allTasks() await waitFor(async () => { - await editNote('jp', newNote) - + const result = await editNote('jp', newNote) + expect(result.data).toBeDefined() expect($notes.get()['jp']).toEqual([newNote]) - - const call = addToastMock.mock.calls[0] - expect(call[0].type).toBe('success') - expect(call[0].title).toBeDefined() - - expect(getErrorToastMock).toBeCalledTimes(0) }) }) - test('should show error if can not set to NoteStorage and keep previous note', async () => { + test('should return error if can not set to NoteStorage and keep previous note', async () => { vi.spyOn(NoteStorage, 'set').mockResolvedValueOnce(Result.Error(error)) keepMount($notes) await allTasks() await waitFor(async () => { - await editNote(languageCode, newNote) - + const result = await editNote(languageCode, newNote) + expect(result.error).toBeDefined() expect($notes.get()[languageCode]).toEqual(notes[languageCode]) - - expect(getErrorToastMock).toBeCalledWith(error) }) }) @@ -179,13 +151,12 @@ describe('note store', () => { await allTasks() await waitFor(async () => { - await editNote(languageCode, { ...newNote, id: 'NOT_EXISTING_ID' }) - + const result = await editNote(languageCode, { + ...newNote, + id: 'NOT_EXISTING_ID', + }) + expect(result.error).toBeDefined() expect($notes.get()[languageCode]).toEqual(notes[languageCode]) - - const call = getErrorToastMock.mock.calls[0] - expect(call[0].type).toBe('ERROR_CAN_NOT_FIND_NOTE_TO_EDIT') - expect(call[0].error).toBeDefined() }) }) }) diff --git a/src/entities/note/model/store.ts b/src/entities/note/model/store.ts index 871607c..6f1ecdd 100644 --- a/src/entities/note/model/store.ts +++ b/src/entities/note/model/store.ts @@ -1,21 +1,22 @@ +import { nanoid } from 'nanoid' import { atom, onMount, task } from 'nanostores' import { TLanguageCode } from '@entities/language' import { getStorage } from '@entities/storage' import { browser } from '@shared/browser' -import { Result } from '@shared/libs/operationResult' -import { addToast, getErrorToast } from '@shared/ui/Toast' +import { Result, TResult } from '@shared/libs/operationResult' import { INote } from './types' export type StorageValue = Record -const defaultNotes: StorageValue = { +export const defaultNotes: StorageValue = { en: [], jp: [], pt: [], ko: [], + other: [], } export const NoteStorage = getStorage(`notes`, defaultNotes) @@ -25,87 +26,89 @@ export const $notes = atom(defaultNotes) onMount($notes, () => { task(async () => { const getResult = await NoteStorage.get() - if (getResult.data) { - $notes.set(getResult.data) - } else { - addToast(getErrorToast(getResult.error)) - } + getResult.data && $notes.set(getResult.data) }) + + const listener = NoteStorage.onChanged.addListener((changes) => { + changes.data && $notes.set(changes.data.newValue) + }) + + return () => { + NoteStorage.onChanged.removeListener(listener) + } }) export const deleteNote = async ( languageCode: TLanguageCode, noteId: string, -) => { +): Promise => { const notes = $notes.get() const note = notes[languageCode].find((note) => note.id === noteId) - if (note) { - const newNotes = notes[languageCode].filter((note) => note.id !== noteId) - - const setResult = await NoteStorage.set({ - ...notes, - [languageCode]: newNotes, - }) - if (setResult.data) { - $notes.set({ - ...notes, - [languageCode]: newNotes, - }) - addToast({ - type: 'success', - title: browser.i18n.getMessage('SUCCESS_DELETE'), - }) - } else { - addToast(getErrorToast(setResult.error)) - } - } else { - const resultError = Result.Error({ + if (!note) { + return Result.Error({ type: 'ERROR_CAN_NOT_FIND_NOTE_TO_DELETE', error: new Error( `Note with id ${noteId} not found, available notes: /n ${JSON.stringify(notes)}`, ), }) - if (resultError.error) { - addToast(getErrorToast(resultError.error)) - } } + const newNotes = notes[languageCode].filter((note) => note.id !== noteId) + + const setResult = await NoteStorage.set({ + ...notes, + [languageCode]: newNotes, + }) + + return setResult.data + ? Result.Success(browser.i18n.getMessage('SUCCESS_DELETE')) + : setResult } -export const editNote = async (languageCode: TLanguageCode, newNote: INote) => { +export const editNote = async ( + languageCode: TLanguageCode, + newNote: INote, +): Promise => { const notes = $notes.get() const note = notes[languageCode].find((note) => note.id === newNote.id) - if (note) { - const newNotes = $notes - .get() - [languageCode].map((note) => (note.id === newNote.id ? newNote : note)) - - const setResult = await NoteStorage.set({ - ...notes, - [languageCode]: newNotes, - }) - if (setResult.data) { - $notes.set({ - ...notes, - [languageCode]: newNotes, - }) - addToast({ - type: 'success', - title: browser.i18n.getMessage('SUCCESS_EDIT'), - }) - } else { - addToast(getErrorToast(setResult.error)) - } - } else { - const resultError = Result.Error({ + if (!note) { + return Result.Error({ type: 'ERROR_CAN_NOT_FIND_NOTE_TO_EDIT', error: new Error( `Note with id ${newNote.id} not found, available notes: /n ${JSON.stringify(notes)}`, ), }) - if (resultError.error) { - addToast(getErrorToast(resultError.error)) - } } + + const newNotes = $notes + .get() + [languageCode].map((note) => (note.id === newNote.id ? newNote : note)) + + const setResult = await NoteStorage.set({ + ...notes, + [languageCode]: newNotes, + }) + + return setResult.data + ? Result.Success(browser.i18n.getMessage('SUCCESS_EDIT')) + : setResult +} + +export const addNote = async ( + languageCode: TLanguageCode, + note: Omit, +): Promise => { + const notes = $notes.get() + const newNote = { id: nanoid(), ...note } + const newNotes = { + ...notes, + [languageCode]: [newNote, ...notes[languageCode]], + } + + const setResult = await NoteStorage.set(newNotes) + + return setResult.data + ? Result.Success(browser.i18n.getMessage('SUCCESS_ADD')) + : setResult } diff --git a/src/features/note/DeleteNote/ui/index.tsx b/src/features/note/DeleteNote/ui/index.tsx index a1bbf9c..9b7587b 100644 --- a/src/features/note/DeleteNote/ui/index.tsx +++ b/src/features/note/DeleteNote/ui/index.tsx @@ -3,6 +3,7 @@ import { noteStore } from '@entities/note' import { browser } from '@shared/browser' import { Button } from '@shared/ui/Button' +import { addToast, getErrorToast } from '@shared/ui/Toast' import { DeleteIcon } from '@shared/ui/icons/DeleteIcon' interface Props { @@ -11,11 +12,18 @@ interface Props { noteText: string } -export const DeleteNote = ({ lang, noteId, noteText }: Props) => ( -