From d344b381c6dd563f0b42e109940e5558436661fc Mon Sep 17 00:00:00 2001 From: lingdocs <71590811+lingdocs@users.noreply.github.com> Date: Sat, 21 Aug 2021 19:55:37 +0400 Subject: [PATCH] bare bones working with new auth --- website/package.json | 1 - website/src/App.test.tsx | 779 ------------------ website/src/App.tsx | 155 +--- website/src/lib/backend-calls.ts | 91 +- website/src/lib/firebase.ts | 30 - .../{backend-types.ts => functions-types.ts} | 39 +- website/src/lib/level-management.ts | 18 +- ...-storage.test.ts => local-storage.test.ts} | 3 +- .../{options-storage.ts => local-storage.ts} | 25 + website/src/lib/options-reducer.test.ts | 10 +- website/src/lib/options-reducer.ts | 6 - website/src/lib/pouch-dbs.ts | 135 +-- website/src/lib/search-all-inflections.ts | 6 +- website/src/lib/submissions.ts | 22 +- website/src/screens/Account.tsx | 341 ++------ website/src/screens/EntryEditor.tsx | 20 +- website/src/screens/IsolatedEntry.tsx | 16 +- website/src/screens/Options.tsx | 6 +- website/src/screens/Results.tsx | 22 +- website/src/screens/ReviewTasks.tsx | 6 +- website/src/types.d.ts | 4 +- website/yarn.lock | 457 +--------- 22 files changed, 318 insertions(+), 1874 deletions(-) delete mode 100644 website/src/App.test.tsx delete mode 100644 website/src/lib/firebase.ts rename website/src/lib/{backend-types.ts => functions-types.ts} (73%) rename website/src/lib/{options-storage.test.ts => local-storage.test.ts} (96%) rename website/src/lib/{options-storage.ts => local-storage.ts} (57%) diff --git a/website/package.json b/website/package.json index d623877..1b29bdd 100644 --- a/website/package.json +++ b/website/package.json @@ -18,7 +18,6 @@ "classnames": "^2.2.6", "cron": "^1.8.2", "dayjs": "^1.10.4", - "firebase": "^8.3.0", "lokijs": "^1.5.11", "mousetrap": "^1.6.5", "nano": "^9.0.3", diff --git a/website/src/App.test.tsx b/website/src/App.test.tsx deleted file mode 100644 index 3a3ed66..0000000 --- a/website/src/App.test.tsx +++ /dev/null @@ -1,779 +0,0 @@ -/** - * Copyright (c) 2021 lingdocs.com - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ - -// TODO: IndexedDB mocking not working for couchdb - it defaults to disk storage -// tslint:disable-next-line -// require("fake-indexeddb/auto"); -// // tslint:disable-next-line -// const FDBFactory = require("fake-indexeddb/lib/FDBFactory"); - -import { render, screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react'; -import { Types as T } from "@lingdocs/pashto-inflector"; -import { Router, BrowserRouter } from "react-router-dom"; -import App from './App'; -import { dictionary } from "./lib/dictionary"; -import { - mockResults, -} from "./lib/dictionary-mock-fillers"; -import userEvent from '@testing-library/user-event'; -import { createMemoryHistory } from 'history'; -import { - loadUserInfo, - upgradeAccount, - publishDictionary, -} from "./lib/backend-calls"; -import { - addSubmission, sendSubmissions, -} from "./lib/submissions"; -import * as BT from "./lib/backend-types"; -jest.mock("./lib/submissions"); -jest.mock("./lib/backend-calls"); -jest.mock("./lib/pouch-dbs"); -jest.mock("./lib/wordlist-database"); -jest.mock("react-ga"); - -const mockUserInfo = { - displayName: "Bob Billywinkle", - email: "bob@example.com", - providerData: [], -}; - -const mockCouchDbStudent: BT.CouchDbUser = { - _id: "123", - type: "user", - name: "123", - email: mockUserInfo.email, - providerData: [], - displayName: mockUserInfo.displayName, - roles: [], - level: "student", - userdbPassword: "12345", -} - -const mockCouchDbEditor: BT.CouchDbUser = { - ...mockCouchDbStudent, - level: "editor", -} - -jest.mock("./lib/firebase", (): any => { - class mockAuth { - constructor() { - this.signIn = this.signIn.bind(this); - this.onAuthStateChanged = this.onAuthStateChanged.bind(this); - this.unsubscribeAll = this.unsubscribeAll.bind(this); - } - private mockUser = { - displayName: "Bob Billywinkle", - email: "bob@example.com", - providerData: [], - delete: () => { - this.currentUser = null; - return Promise.resolve(); - }, - }; - private observers: ((user: any) => void)[] = []; - public currentUser: any = null; - onAuthStateChanged (callback: () => void) { - this.observers.push(callback); - callback(); - return () => { this.unsubscribeAll() }; - } - unsubscribeAll () { - this.observers = []; - } - signOut () { - this.currentUser = null; - this.observers.forEach((item) => { - item.call(undefined, this.mockUser); - }); - return null; - } - signIn () { - this.currentUser = this.mockUser; - this.observers.forEach((item) => { - item.call(undefined, this.mockUser); - }); - return null; - } - } - return { - auth: new mockAuth(), - }; -}); - - - -jest.mock('react-firebaseui/StyledFirebaseAuth', () => function (props: any) { - return
- -
; -}); - -const allMockEntries: T.DictionaryEntry[] = Object.keys(mockResults).reduce((all: T.DictionaryEntry[], key: string) => ( - // @ts-ignore - [...all, ...mockResults[key]] -), []); - -const fakeDictInfo: T.DictionaryInfo = { - title: "not found", - license: "not found", - release: 0, - numberOfEntries: 0, - url: "not found", - infoUrl: "not found", -}; - -const fakeDictionary: DictionaryAPI = { - initialize: () => Promise.resolve({ - response: "loaded from saved", - dictionaryInfo: fakeDictInfo, - }), - update: () => Promise.resolve({ - response: "no need for update", - dictionaryInfo: fakeDictInfo, - }), - search: function(state: State): T.DictionaryEntry[] { - if (state.options.searchType === "alphabetical") { - return state.searchValue === "ا" ? mockResults.alphabeticalA : []; - } - if (state.options.language === "Pashto") { - return state.searchValue === "کور" - ? mockResults.pashtoKor - : []; - } - if (state.options.language === "English") { - return state.searchValue === "tired" - ? mockResults.englishTired as T.DictionaryEntry[] - : []; - } - return []; - }, - getNewWordsThisMonth: function(): T.DictionaryEntry[] { - return []; - }, - findOneByTs: function(ts: number): T.DictionaryEntry | undefined { - return allMockEntries.find((entry) => entry.ts === ts); - }, - findRelatedEntries: function(entry: T.DictionaryEntry): T.DictionaryEntry[] { - // TODO: Better mock - return allMockEntries.filter((e) => e.e.includes("house")); - }, - exactPashtoSearch: function(search: string ): T.DictionaryEntry[] { - return []; - }, -}; - -const dictionaryPublishResponse = "dictionary published"; - -beforeEach(() => { - jest.clearAllMocks(); - jest.spyOn(dictionary, "initialize").mockImplementation(() => Promise.resolve("loaded from saved")); - jest.spyOn(dictionary, "search").mockImplementation(fakeDictionary.search); - jest.spyOn(dictionary, "findOneByTs").mockImplementation(fakeDictionary.findOneByTs); - jest.spyOn(dictionary, "findRelatedEntries").mockImplementation(fakeDictionary.findRelatedEntries); - jest.spyOn(dictionary, "exactPashtoSearch").mockImplementation(fakeDictionary.exactPashtoSearch); - loadUserInfo.mockResolvedValue(undefined); - // fetchSuggestions.mockResolvedValue({ ok: true, suggestions: [] }); - upgradeAccount.mockImplementation(async (password: string): Promise => { - if (password === "correct password") { - return { ok: true, message: "user upgraded to student" }; - } - return { - ok: false, - error: "incorrect password", - }; - }); - publishDictionary.mockResolvedValue(dictionaryPublishResponse); - localStorage.clear(); - // indexedDB = new FDBFactory(); -}); - -// TODO: feed it a fake mini dictionary through JSON - to get more realistic testing -// don't mock the dictionary object - -test('renders loading', async () => { - jest.spyOn(dictionary, "initialize").mockImplementation(() => Promise.resolve("loaded from saved")); - render(); - const text = screen.getByText(/loading/i); - expect(text).toBeInTheDocument(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); -}); - -test('renders error loading', async () => { - jest.spyOn(dictionary, "initialize").mockImplementation(() => Promise.reject()); - - render(); - await waitFor(() => screen.getByText(/error loading/i)); -}); - -test('renders dictionary loaded', async () => { - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); -}); - -test('searches on type', async () => { - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - // Search Pashto - let searchInput = screen.getByPlaceholderText(/search pashto/i); - userEvent.type(searchInput, "کور"); - mockResults.pashtoKor.slice(0, 10).forEach((result) => { - expect(screen.getAllByText(result.e)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.p)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.f)[0]).toBeInTheDocument(); - }); - expect(history.location.pathname).toBe("/search"); - // Clear - userEvent.type(searchInput, "{backspace}{backspace}{backspace}"); - mockResults.pashtoKor.slice(0, 10).forEach((result) => { - expect(screen.queryByText(result.e)).toBeNull(); - expect(screen.queryByText(result.p)).toBeNull(); - expect(screen.queryByText(result.f)).toBeNull(); - }); - expect(history.location.pathname).toBe("/"); - // Switch To English - const languageToggle = screen.getByTestId("languageToggle"); - userEvent.click(languageToggle); - expect(screen.queryByPlaceholderText(/search pashto/i)).toBeNull(); - searchInput = screen.getByPlaceholderText(/search english/i); - userEvent.type(searchInput, "tired"); - mockResults.englishTired.slice(0, 10).forEach((result) => { - expect(screen.getAllByText(result.e)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.p)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.f)[0]).toBeInTheDocument(); - }); - expect(history.location.pathname).toBe("/search"); - // Clear - const clearButton = screen.getByTestId("clearButton"); - userEvent.click(clearButton); - mockResults.englishTired.slice(0, 10).forEach((result) => { - expect(screen.queryByText(result.e)).toBeNull(); - expect(screen.queryByText(result.p)).toBeNull(); - expect(screen.queryByText(result.f)).toBeNull(); - }); - // Search again - userEvent.type(searchInput, "tired"); - mockResults.englishTired.slice(0, 10).forEach((result) => { - expect(screen.getAllByText(result.e)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.p)[0]).toBeInTheDocument(); - expect(screen.getAllByText(result.f)[0]).toBeInTheDocument(); - }); - // Go back - history.goBack(); - expect(history.location.pathname).toBe("/"); -}); - -test('does alphabetical browse search', async () => { - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - expect(screen.queryByText(/alphabetical browsing mode/i)).toBeNull(); - const searchTypeButton = screen.getByTestId("searchTypeToggle"); - userEvent.click(searchTypeButton); - expect(screen.queryByText(/alphabetical browsing mode/i)).toBeInTheDocument(); - const searchInput = screen.getByPlaceholderText(/browse/i); - userEvent.type(searchInput, "ا"); - mockResults.alphabeticalA.forEach((entry) => { - expect(screen.queryAllByText(entry.e)).toBeTruthy; - }); - userEvent.type(searchInput, "{backspace}"); - userEvent.type(searchInput, "ززززز"); - expect(screen.queryByText(/no results found/i)).toBeInTheDocument(); - expect(screen.queryByText(/You are using alphabetical browsing mode/i)).toBeInTheDocument(); -}); - -test('isolates word on click', async () => { - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - let searchInput = screen.getByPlaceholderText(/search pashto/i); - userEvent.type(searchInput, "کور"); - expect(history.location.pathname).toBe("/search"); - const firstResult = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResult); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - expect(history.location.pathname).toBe("/word"); - const params = new URLSearchParams(history.location.search); - const wordId = params.get("id"); - expect(wordId && parseInt(wordId)).toBe(mockResults.pashtoKor[0].ts); - // should leave word when going back - history.goBack(); - expect(history.location.pathname).toBe("/search"); - // go back to word when going forward - history.goForward(); - expect(history.location.pathname).toBe("/word"); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - // leave word when clearing - const clearButton = screen.getByTestId("clearButton"); - userEvent.click(clearButton); - expect(history.location.pathname).toBe("/") - expect(screen.queryByText(/related words/i)).toBeNull(); - userEvent.type(searchInput, "کور"); - expect(history.location.pathname).toBe("/search"); - const firstResultb = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResultb); - expect(history.location.pathname).toBe("/word"); - // leave word when searching - const input = screen.getByTestId("searchInput"); - userEvent.type(input, "سړی"); - expect(history.location.pathname).toBe("/search"); - expect(screen.queryByText(/related words/i)).toBeNull(); - expect(screen.queryByText(/no results found/i)).toBeTruthy(); - const clearButton1 = screen.getByTestId("clearButton"); - userEvent.click(clearButton1); - expect(history.location.pathname).toBe("/"); - // search click on a word again - userEvent.type(searchInput, "کور"); - expect(history.location.pathname).toBe("/search"); - const firstResultc = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResultc); - expect(history.location.pathname).toBe("/word") - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - expect(history.location.search).toBe(`?id=${mockResults.pashtoKor[0].ts}`); - const relatedEntry = mockResults.pashtoKor.filter((entry) => entry.e.includes("house"))[1] as T.DictionaryEntry; - const otherResult = screen.getByText(relatedEntry.p); - userEvent.click(otherResult); - expect(history.location.pathname).toBe(`/word`); - expect(history.location.search).toBe(`?id=${relatedEntry.ts}`); - // search for a word that uses a complement - userEvent.click(clearButton1); - const languageToggle = screen.getByTestId("languageToggle"); - userEvent.click(languageToggle); - userEvent.type(searchInput, "tired"); - const resultWComplement = mockResults.englishTired.find((entry) => entry.c.includes(" comp.") && entry.l) as T.DictionaryEntry; - userEvent.click(screen.getByText(resultWComplement.e)); - expect(history.location.pathname).toBe(`/word`); - expect(history.location.search).toBe(`?id=${resultWComplement.ts}`); - expect(screen.queryByText(resultWComplement.e)).toBeInTheDocument(); -}); - -test('shows about page', async () => { - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - const aboutButton = screen.getByText(/about/i); - userEvent.click(aboutButton); - expect(screen.queryByText(/inspiration and sources/i)).toBeInTheDocument(); - const homeButton = screen.getByText(/home/i); - userEvent.click(homeButton); - expect(screen.queryByText(/inspiration and sources/i)).toBeNull(); -}); - -test('starts on about page when starting from /about', async () => { - const history = createMemoryHistory(); - history.push("/about"); - render(); - await waitFor(() => screen.getAllByText(/about/i)); - expect(screen.queryByText(/inspiration and sources/i)).toBeInTheDocument(); - const homeButton = screen.getByText(/home/i); - userEvent.click(homeButton); - expect(screen.queryByText(/inspiration and sources/i)).toBeNull(); -}); - -test('shows settings page / settings page works', async () => { - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - const settingsButton = screen.getAllByText(/settings/i)[0]; - userEvent.click(settingsButton); - expect(screen.queryByText(/diacritics/i)).toBeInTheDocument(); - const homeButton = screen.getByText(/home/i); - userEvent.click(homeButton); - expect(screen.queryByText(/diacritics/i)).toBeNull(); - // play with settings - const settingsButton1 = screen.getAllByText(/settings/i)[0]; - userEvent.click(settingsButton1); - const darkButton = screen.getByText(/dark/i); - userEvent.click(darkButton); - const lightButton = screen.getByText(/light/i); - userEvent.click(lightButton); -}); - -test('starts on settings page when starting from /settings', async () => { - const history = createMemoryHistory(); - history.push("/settings"); - render(); - await waitFor(() => screen.getAllByText(/settings/i)); - expect(screen.queryByText(/diacritics/i)).toBeInTheDocument(); - const homeButton = screen.getByText(/home/i); - userEvent.click(homeButton); - expect(screen.queryByText(/diacritics/i)).toBeNull(); -}); - -test('persists settings', async () => { - const history = createMemoryHistory(); - history.push("/settings"); - const { unmount, rerender } = render(); - await waitFor(() => screen.getAllByText(/settings/i)); - const darkButton = screen.getByText(/dark/i); - const lightButton = screen.getByText(/light/i); - expect(darkButton.className.toString().includes("active")).toBe(false); - expect(lightButton.className.toString().includes("active")).toBe(true); - userEvent.click(darkButton); - expect(darkButton.className.toString().includes("active")).toBe(true); - expect(lightButton.className.toString().includes("active")).toBe(false); - const afghanSp = screen.getByText(/afghan/i); - const pakSp = screen.getByText(/pakistani ی/i); - expect(afghanSp.className.toString().includes("active")).toBe(true); - expect(pakSp.className.toString().includes("active")).toBe(false); - userEvent.click(pakSp); - expect(afghanSp.className.toString().includes("active")).toBe(false); - expect(pakSp.className.toString().includes("active")).toBe(true); - unmount(); - rerender(); - await waitFor(() => screen.getAllByText(/settings/i)); - const afghanSp1 = screen.getByText(/afghan/i); - const pakSp1 = screen.getByText(/pakistani ی/i); - const darkButton1 = screen.getByText(/dark/i); - const lightButton1 = screen.getByText(/light/i); - expect(darkButton1.className.toString().includes("active")).toBe(true); - expect(lightButton1.className.toString().includes("active")).toBe(false); - expect(afghanSp1.className.toString().includes("active")).toBe(false); - expect(pakSp1.className.toString().includes("active")).toBe(true); -}); - -test('starts on home page when starting on invalid page', async () => { - const history = createMemoryHistory(); - history.push("/search"); - render(); - await waitFor(() => screen.getAllByText(/lingdocs pashto dictionary/i)); - expect(history.location.pathname).toBe("/"); -}); - -test('starts on home page when starting on an unauthorized page', async () => { - const history = createMemoryHistory(); - history.push("/edits"); - render(); - await waitFor(() => screen.getAllByText(/lingdocs pashto dictionary/i)); - expect(history.location.pathname).toBe("/"); -}); - -test('starts on isolated word when starting from /word?id=_____', async () => { - const history = createMemoryHistory(); - const entry = mockResults.pashtoKor[0]; - history.push(`/word?id=${entry.ts}`); - render(); - await waitFor(() => screen.getAllByText(/related words/i)); - expect(screen.queryAllByText(entry.p)).toBeTruthy(); -}); - -test('says word not found if starting on /word?id=_____ with an unfound id', async () => { - const history = createMemoryHistory(); - const entry = mockResults.pashtoKor[0]; - history.push(`/word?id=${entry.ts + 20000}`); - render(); - await waitFor(() => screen.getAllByText(/word not found/i)); -}); - -test('goes to home page if starts with /word but without an id param', async () => { - const history = createMemoryHistory(); - const entry = mockResults.pashtoKor[0]; - history.push(`/word?badparam=${entry.ts}`); - render(); - await waitFor(() => screen.getAllByText(/lingdocs pashto dictionary/i)); - expect(history.location.pathname).toBe("/"); -}); - -test('sign in and out of account works', async () => { - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - userEvent.click(screen.getByText(/sign in/i)); - expect(screen.queryByText(/sign in to be able to/i)).toBeInTheDocument(); - userEvent.click(screen.getByTestId("mockSignInButton")); - expect(screen.queryByText(new RegExp(mockUserInfo.email))).toBeInTheDocument(); - expect(screen.queryByText(new RegExp(mockUserInfo.displayName))).toBeInTheDocument(); - userEvent.click(screen.getByText(/home/i)); - // now to get back to the account page there should be an account button, not a sign-in button - expect(screen.queryByText(/sign in/i)).toBeNull(); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByTestId("signoutButton")); - expect(history.location.pathname).toBe("/"); - expect(screen.getByText(/sign in/i)).toBeInTheDocument(); - // sign back in and delete account - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - userEvent.click(screen.getByText(/delete account/i)); - expect(screen.queryByText(/yes, delete my account/i)).toBeInTheDocument(); - userEvent.click(screen.getByText(/no, cancel/i)); - await waitForElementToBeRemoved(() => screen.queryByText(/yes, delete my account/i)); - userEvent.click(screen.getByText(/delete account/i)); - userEvent.click(screen.getByText(/yes, delete my account/i)); - await waitFor(() => screen.queryByText(/Your account has been deleted/i)); - expect(history.location.pathname).toBe("/account"); - userEvent.click(screen.getAllByText(/home/i)[0]); - expect(history.location.pathname).toBe("/"); -}); - -test('word edit suggestion works', async () => { - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - // first try without signing in - expect(screen.getByText(/sign in/i)).toBeInTheDocument(); - let searchInput = screen.getByPlaceholderText(/search pashto/i); - userEvent.type(searchInput, "کور"); - expect(history.location.pathname).toBe("/search"); - let firstResult = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResult); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - // the edit button should not be there - expect(screen.queryByTestId(/editEntryButton/i)).toBeNull(); - // nor should the finalEdit button - expect(screen.queryByTestId(/finalEditEntryButton/i)).toBeNull(); - // sign in to be able to suggest an edit - history.goBack(); - history.goBack(); - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - expect(sendSubmissions).toHaveBeenCalledTimes(1); - userEvent.click(screen.getByText(/home/i)); - userEvent.type(searchInput, "کور"); - firstResult = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResult); - // the final edit button should not be there - expect(screen.queryByTestId(/finalEditEntryButton/i)).toBeNull(); - userEvent.click(screen.getByTestId(/editEntryButton/i)); - userEvent.type(screen.getByLabelText(/Suggest correction\/edit:/i), "my suggestion"); - userEvent.click(screen.getByText(/cancel/i)); - expect(screen.queryByLabelText(/Suggest correction\/edit:/i)).toBeNull(); - userEvent.click(screen.getByTestId(/editEntryButton/i)); - userEvent.type(screen.getByLabelText(/Suggest correction\/edit:/i), "my comment"); - userEvent.click(screen.getByText(/submit/i)); - expect(screen.queryByText(/Thank you for your help!/i)).toBeInTheDocument(); - expect(addSubmission).toHaveBeenCalledTimes(1); - expect(addSubmission).toHaveBeenCalledWith(expect.objectContaining({ - entry: mockResults.pashtoKor[0], - comment: "my comment", - }), "basic"); - history.goBack(); - history.goBack(); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByText(/sign out/i)); -}); - -test('upgrade account works', async () => { - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - userEvent.click(screen.getByText(/sign in/i)); - expect(screen.queryByText(/sign in to be able to/i)).toBeInTheDocument(); - userEvent.click(screen.getByTestId("mockSignInButton")); - expect(screen.queryByText(new RegExp(mockUserInfo.email))).toBeInTheDocument(); - expect(screen.queryByText(new RegExp(mockUserInfo.displayName))).toBeInTheDocument(); - expect(screen.queryByText(/level: basic/i)).toBeInTheDocument(); - userEvent.click(screen.getByText(/upgrade account/i)); - userEvent.type(screen.getByLabelText(/upgrade password:/i), "something wrong"); - userEvent.click(screen.getByText(/upgrade my account/i)); - await waitFor(() => screen.queryByText(/incorrect password/i)); - userEvent.click(screen.getByText(/cancel/i)); - await waitFor(() => screen.getByText(/upgrade account/i)); - userEvent.click(screen.getByText(/upgrade account/i)); - userEvent.type(screen.getByLabelText(/upgrade password:/i), "correct password"); - loadUserInfo.mockResolvedValue(mockCouchDbStudent); - userEvent.click(screen.getByText(/upgrade my account/i)); - await waitForElementToBeRemoved(() => screen.getAllByText(/upgrade account/i)); - userEvent.click(screen.getByText(/sign out/i)); -}); - -test('editor priveledges show up and allow you to make a final edit of an entry', async () => { - loadUserInfo.mockResolvedValue(mockCouchDbEditor); - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/account level: editor/i)); - expect(sendSubmissions).toHaveBeenCalledTimes(1); - userEvent.click(screen.getByText(/home/i)); - expect(screen.getByText(/editor priveleges active/i)).toBeInTheDocument() - let searchInput = screen.getByPlaceholderText(/search pashto/i); - userEvent.type(searchInput, "کور"); - expect(history.location.pathname).toBe("/search"); - let firstResult = screen.getByText(mockResults.pashtoKor[0].e); - userEvent.click(firstResult); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - // the edit button should be there - expect(screen.getByTestId("editEntryButton")).toBeInTheDocument(); - // the final edit button should also be there - expect(screen.getByTestId("finalEditEntryButton")).toBeInTheDocument(); - userEvent.click(screen.getByTestId("finalEditEntryButton")); - userEvent.type(screen.getByLabelText(/english/i), " adding more in english"); - userEvent.click(screen.getByLabelText(/no inflection/i)); - userEvent.click(screen.getByText(/submit/i)); - expect(screen.getByText(/edit submitted\/saved/i)).toBeInTheDocument(); - expect(addSubmission).toHaveBeenCalledTimes(1); - expect(addSubmission).toHaveBeenCalledWith(expect.objectContaining({ - type: "entry edit", - entry: { - ...mockResults.pashtoKor[0], - e: mockResults.pashtoKor[0].e + " adding more in english", - noInf: true, - }, - }), "editor"); - userEvent.click(screen.getByTestId(/navItemHome/i)); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByText(/sign out/i)); -}); - -test('editor should be able to publish the dictionary', async () => { - loadUserInfo.mockResolvedValue(undefined); - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/account level: basic/i)); - // publish dictionary option should not be available to non editor - expect(screen.queryByText(/publish dictionary/i)).toBeNull(); - userEvent.click(screen.getByText(/sign out/i)); - userEvent.click(screen.getByText(/sign in/i)); - loadUserInfo.mockResolvedValue(mockCouchDbStudent); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/account level: student/i)); - // publish dictionary option should not be available to non editor - expect(screen.queryByText(/publish dictionary/i)).toBeNull(); - userEvent.click(screen.getByText(/sign out/i)); - userEvent.click(screen.getByText(/sign in/i)); - loadUserInfo.mockResolvedValue(mockCouchDbEditor); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/account level: editor/i)); - // publish dictionary options should only be available to editor - userEvent.click(screen.getByText(/publish dictionary/i)); - expect(screen.getByText(/processing\.\.\./i)).toBeInTheDocument(); - await waitFor(() => screen.getByText(JSON.stringify(dictionaryPublishResponse, null, "\\t"))); - userEvent.click(screen.getByText(/sign out/i)); -}); - -test('wordlist should be hidden from basic users and available for upgraded users', async () => { - loadUserInfo.mockResolvedValue(undefined); - const history = createMemoryHistory(); - render(); - await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); - // doesn't exist on basic accounts signed in or not - expect(screen.queryByText(/wordlist/i)).toBeNull(); - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.queryByText(mockUserInfo.displayName)); - userEvent.click(screen.getByText(/home/i)); - expect(screen.queryByText(/wordlist/i)).toBeNull(); - userEvent.type(screen.getByPlaceholderText(/search pashto/i), "کور"); - expect(history.location.pathname).toBe("/search"); - userEvent.click(screen.getByText(mockResults.pashtoKor[0].e)); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - // shouldn't be able to see the add to wordlist star - expect(screen.queryByTestId("emptyStarButton")).toBeNull(); - expect(screen.queryByTestId("fullStarButton")).toBeNull(); - history.goBack(); - history.goBack(); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByText(/sign out/i)); - loadUserInfo.mockResolvedValue(mockCouchDbStudent); - // does exist for student account - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/level: student/i)); - userEvent.click(screen.getByText(/home/i)); - expect(screen.getByText(/wordlist/i)).toBeInTheDocument(); - userEvent.type(screen.getByPlaceholderText(/search pashto/i), "کور"); - expect(history.location.pathname).toBe("/search"); - userEvent.click(screen.getByText(mockResults.pashtoKor[0].e)); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - // should be able to see the word list star - expect(screen.queryByTestId("emptyStarButton")).toBeInTheDocument(); - history.goBack(); - history.goBack(); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByText(/sign out/i)); - loadUserInfo.mockResolvedValue(mockCouchDbEditor); - // also exists for editor account - userEvent.click(screen.getByText(/sign in/i)); - userEvent.click(screen.getByTestId("mockSignInButton")); - await waitFor(() => screen.getByText(/level: editor/i)); - userEvent.click(screen.getByText(/home/i)); - expect(screen.getByText(/wordlist/i)).toBeInTheDocument(); - userEvent.type(screen.getByPlaceholderText(/search pashto/i), "کور"); - expect(history.location.pathname).toBe("/search"); - userEvent.click(screen.getByText(mockResults.pashtoKor[0].e)); - expect(screen.getByText(/related words/i)).toBeInTheDocument(); - expect(screen.getByTestId("emptyStarButton")).toBeInTheDocument(); - history.goBack(); - history.goBack(); - userEvent.click(screen.getByText(/account/i)); - userEvent.click(screen.getByText(/sign out/i)); -}); - -// test('wordlist adding and removing should work', async () => { -// const wordNotes = "my test notes"; -// const noteAddition = " and some more"; -// const wordToAdd = mockResults.pashtoKor[0]; -// loadUserInfo.mockResolvedValue(mockCouchDbStudent); -// const history = createMemoryHistory(); -// render(); -// await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i)); -// userEvent.click(screen.getByText(/sign in/i)); -// userEvent.click(screen.getByTestId("mockSignInButton")); -// await waitFor(() => screen.getByText(/level: student/i)); -// userEvent.click(screen.getByText(/home/i)); -// expect(screen.getByText(/wordlist/i)).toBeInTheDocument(); -// userEvent.type(screen.getByPlaceholderText(/search pashto/i), "کور"); -// expect(history.location.pathname).toBe("/search"); -// userEvent.click(screen.getByText(wordToAdd.e)); -// // should be able to see the word list star -// expect(screen.getByTestId("emptyStarButton")).toBeInTheDocument(); -// userEvent.click(screen.getByTestId("emptyStarButton")); -// await waitFor(() => screen.getByTestId("fullStarButton")); -// userEvent.type(screen.getByTestId("wordlistWordContextForm"), wordNotes); -// userEvent.click(screen.getByText(/save context/i)); -// userEvent.click(screen.getByTestId("backButton")); -// userEvent.click(screen.getByTestId("backButton")); -// // should have one word in wordlist for review -// userEvent.click(screen.getByText("Wordlist (1)")); -// // should appear on screen with notes -// userEvent.click(screen.getByText(/browse/i)); -// expect(screen.getByText(wordNotes)).toBeInTheDocument(); -// // notes should be editable -// userEvent.click(screen.getByText(wordToAdd.e)); -// userEvent.type(screen.getByText(wordNotes), noteAddition); -// userEvent.click(screen.getByText(/save context/i)); -// await waitFor(() => screen.getByText(/context saved/i)); -// userEvent.click(screen.getByText(wordToAdd.e)); -// expect(screen.queryByText(/context saved/)).toBeNull(); -// expect(screen.getByText(wordNotes + noteAddition)).toBeInTheDocument(); -// // should be able to delete from the browsing screen -// userEvent.click(screen.getByText(wordToAdd.e)); -// userEvent.click(screen.getByText(/delete/i)); -// await waitForElementToBeRemoved(() => screen.getByText(wordToAdd.e)); -// userEvent.click(screen.getByText(/home/i)); -// // now try adding and deleting a word from the isolated word screen -// userEvent.type(screen.getByPlaceholderText(/search pashto/i), "کور"); -// expect(history.location.pathname).toBe("/search"); -// userEvent.click(screen.getByText(wordToAdd.e)); -// expect(screen.getByTestId("emptyStarButton")).toBeInTheDocument(); -// userEvent.click(screen.getByTestId("emptyStarButton")); -// await waitFor(() => screen.getByTestId("fullStarButton")); -// userEvent.click(screen.getByTestId("backButton")); -// userEvent.click(screen.getByTestId("backButton")); -// userEvent.click(screen.getByText(/wordlist.*/i)); -// userEvent.click(screen.getByText(/browse/i)); -// // go back to isolated word screen from the dictionary entry button -// userEvent.click(screen.getByText(wordToAdd.e)); -// userEvent.click(screen.getByText(/dictionary entry/i)); -// expect(screen.getByText(/related words/i)).toBeInTheDocument(); -// expect(history.location.pathname).toBe("/word"); -// // delete the word from the wordlist from the isolated word screen -// userEvent.click(screen.getByTestId("fullStarButton")); -// userEvent.click(screen.getByText(/cancel/i)); -// userEvent.click(screen.getByTestId("fullStarButton")); -// userEvent.click(screen.getByTestId("confirmDeleteFromWordlist")); -// await waitFor(() => screen.getByTestId("emptyStarButton")); -// userEvent.click(screen.getByTestId("backButton")); -// expect(screen.queryByText(/wordlist is empty/i)).toBeInTheDocument(); -// }); - -// TODO: REMOVE waitFor(() => screen.//queryByText// ) - -// TODO: Test review diff --git a/website/src/App.tsx b/website/src/App.tsx index 62036db..ee4643e 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -6,6 +6,9 @@ * */ +// TODO: Put the DB sync on the localDb object, and then have it cancel()'ed and removed as part of the deinitialization +// sync on initialization and cancel sync on de-initialization + import { Component } from "react"; import { defaultTextOptions } from "@lingdocs/pashto-inflector"; import { withRouter, Route, RouteComponentProps, Link } from "react-router-dom"; @@ -21,32 +24,31 @@ import ReviewTasks from "./screens/ReviewTasks"; import EntryEditor from "./screens/EntryEditor"; import IsolatedEntry from "./screens/IsolatedEntry"; import Wordlist from "./screens/Wordlist"; -import { saveOptions, readOptions } from "./lib/options-storage"; +import { wordlistEnabled } from "./lib/level-management"; +import { + saveOptions, + readOptions, + saveUser, + readUser, +} from "./lib/local-storage"; import { dictionary, pageSize } from "./lib/dictionary"; import optionsReducer from "./lib/options-reducer"; import hitBottom from "./lib/hitBottom"; import getWordId from "./lib/get-word-id"; -import { auth } from "./lib/firebase"; import { CronJob } from "cron"; import Mousetrap from "mousetrap"; import { sendSubmissions, } from "./lib/submissions"; import { - loadUserInfo, + getUser, } from "./lib/backend-calls"; -import * as BT from "./lib/backend-types"; import { getWordlist, } from "./lib/wordlist-database"; import { - wordlistEnabled, -} from "./lib/level-management"; -import { - deInitializeLocalDb, - initializeLocalDb, - startLocalDbSync, - getLocalDbName, + startLocalDbs, + stopLocalDbs, getAllDocsLocalDb, } from "./lib/pouch-dbs"; import { @@ -95,7 +97,6 @@ class App extends Component { theme: /* istanbul ignore next */ (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light", textOptions: defaultTextOptions, - level: "basic", wordlistMode: "browse", wordlistReviewLanguage: "Pashto", wordlistReviewBadge: true, @@ -107,13 +108,14 @@ class App extends Component { results: [], wordlist: [], reviewTasks: [], + user: readUser(), }; this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this); this.handleSearchValueChange = this.handleSearchValueChange.bind(this); this.handleIsolateEntry = this.handleIsolateEntry.bind(this); this.handleScroll = this.handleScroll.bind(this); this.handleGoBack = this.handleGoBack.bind(this); - this.handleLoadUserInfo = this.handleLoadUserInfo.bind(this); + this.handleLoadUser = this.handleLoadUser.bind(this); this.handleRefreshWordlist = this.handleRefreshWordlist.bind(this); this.handleRefreshReviewTasks = this.handleRefreshReviewTasks.bind(this); this.handleDictionaryUpdate = this.handleDictionaryUpdate.bind(this); @@ -124,7 +126,7 @@ class App extends Component { if (!possibleLandingPages.includes(this.props.location.pathname)) { this.props.history.replace("/"); } - if (prod && (this.state.options.level !== "editor")) { + if (prod && (!(this.state.user?.level === "editor"))) { ReactGA.pageview(window.location.pathname + window.location.search); } dictionary.initialize().then((r) => { @@ -133,11 +135,8 @@ class App extends Component { dictionaryInfo: r.dictionaryInfo, }); // incase it took forever and timed out - might need to reinitialize the wordlist here ?? - if (wordlistEnabled(this.state)) { - initializeLocalDb("wordlist", this.handleRefreshWordlist, auth.currentUser ? auth.currentUser.uid : undefined); - } - if (this.state.options.level === "editor") { - initializeLocalDb("reviewTasks", this.handleRefreshReviewTasks); + if (this.state.user) { + startLocalDbs(this.state.user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks }); } if (this.props.location.pathname === "/word") { const wordId = getWordId(this.props.location.search); @@ -182,32 +181,6 @@ class App extends Component { } }); } - this.unregisterAuthObserver = auth.onAuthStateChanged((user) => { - if (user) { - if (wordlistEnabled(this.state)) { - initializeLocalDb("wordlist", this.handleRefreshWordlist, user.uid); - } - sendSubmissions(); - this.handleLoadUserInfo().catch(console.error); - this.networkCronJob.stop(); - this.networkCronJob.start(); - } else { - // signed out - this.networkCronJob.stop(); - if (this.wordlistSync) { - this.wordlistSync.cancel(); - this.wordlistSync = undefined; - } - if (this.reviewTastsSync) { - this.reviewTastsSync.cancel(); - this.reviewTastsSync = undefined; - } - deInitializeLocalDb("wordlist"); - deInitializeLocalDb("reviewTasks"); - this.handleOptionsUpdate({ type: "changeUserLevel", payload: "basic" }); - } - this.forceUpdate(); - }); Mousetrap.bind(["ctrl+down", "ctrl+up", "command+down", "command+up"], (e) => { if (e.repeat) return; this.handleOptionsUpdate({ type: "toggleLanguage" }); @@ -218,7 +191,7 @@ class App extends Component { }); Mousetrap.bind(["ctrl+\\", "command+\\"], (e) => { if (e.repeat) return; - if (this.state.options.level === "basic") return; + if (this.state.user?.level === "basic") return; if (this.props.location.pathname !== "/wordlist") { this.props.history.push("/wordlist"); } else { @@ -229,14 +202,8 @@ class App extends Component { public componentWillUnmount() { window.removeEventListener("scroll", this.handleScroll); - this.unregisterAuthObserver(); this.networkCronJob.stop(); - if (this.wordlistSync) { - this.wordlistSync.cancel(); - } - if (this.reviewTastsSync) { - this.reviewTastsSync.cancel(); - } + stopLocalDbs(); Mousetrap.unbind(["ctrl+down", "ctrl+up", "command+down", "command+up"]); Mousetrap.unbind(["ctrl+b", "command+b"]); Mousetrap.unbind(["ctrl+\\", "command+\\"]); @@ -244,7 +211,7 @@ class App extends Component { public componentDidUpdate(prevProps: RouteComponentProps) { if (this.props.location.pathname !== prevProps.location.pathname) { - if (prod && (this.state.options.level !== "editor")) { + if (prod && (!(this.state.user?.level === "editor"))) { ReactGA.pageview(window.location.pathname + window.location.search); } if (this.props.location.pathname === "/") { @@ -256,12 +223,12 @@ class App extends Component { page: 1, }); } - if (editorOnlyPages.includes(this.props.location.pathname) && this.state.options.level !== "editor") { + if (editorOnlyPages.includes(this.props.location.pathname) && !(this.state.user?.level === "editor")) { this.props.history.replace("/"); } } if (getWordId(this.props.location.search) !== getWordId(prevProps.location.search)) { - if (prod && (this.state.options.level !== "editor")) { + if (prod && ((this.state.user?.level !== "editor"))) { ReactGA.pageview(window.location.pathname + window.location.search); } const wordId = getWordId(this.props.location.search); @@ -277,54 +244,19 @@ class App extends Component { // } } - private unregisterAuthObserver() { - // will be filled in on mount - } - - private wordlistSync: PouchDB.Replication.Sync | undefined = undefined; - private reviewTastsSync: PouchDB.Replication.Sync | undefined = undefined; - - private async handleLoadUserInfo(): Promise { + private async handleLoadUser(): Promise { try { - const userInfo = await loadUserInfo(); - const differentUserInfoLevel = userInfo && (userInfo.level !== this.state.options.level); - const needToDowngrade = (!userInfo && wordlistEnabled(this.state)); - if (differentUserInfoLevel || needToDowngrade) { - this.handleOptionsUpdate({ - type: "changeUserLevel", - payload: userInfo ? userInfo.level : "basic", - }); + const user = await getUser(); + if (user === "offline") return; + this.setState({ user }); + saveUser(user); + if (user) { + startLocalDbs(user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks }); + } else { + stopLocalDbs(); } - if (!userInfo) return undefined; - // only sync wordlist for upgraded accounts - if (userInfo && wordlistEnabled(userInfo.level)) { - // TODO: GO OVER THIS HORRENDOUS BLOCK - if (userInfo.level === "editor") { - initializeLocalDb("reviewTasks", this.handleRefreshReviewTasks); - if (!this.reviewTastsSync) { - this.reviewTastsSync = startLocalDbSync("reviewTasks", { name: userInfo.name, password: userInfo.userdbPassword }); - } - } - const wordlistName = getLocalDbName("wordlist") ?? ""; - const usersWordlistInitialized = wordlistName.includes(userInfo.name); - if (this.wordlistSync && usersWordlistInitialized) { - // sync already started for the correct db, don't start it again - return userInfo; - } - if (this.wordlistSync) { - this.wordlistSync.cancel(); - this.wordlistSync = undefined; - } - if (!usersWordlistInitialized) { - initializeLocalDb("wordlist", this.handleRefreshWordlist, userInfo.name); - } - this.wordlistSync = startLocalDbSync("wordlist", { name: userInfo.name, password: userInfo.userdbPassword }); - } - return userInfo; } catch (err) { console.error("error checking user level", err); - // don't downgrade the level if it's editor/studend and offline (can't check user info) - return undefined; } } @@ -403,7 +335,7 @@ class App extends Component { private networkCronJob = new CronJob("1/5 * * * *", () => { // TODO: check for new dictionary (in a seperate cron job - not dependant on the user being signed in) - this.handleLoadUserInfo(); + this.handleLoadUser(); sendSubmissions(); this.handleDictionaryUpdate(); }); @@ -463,7 +395,7 @@ class App extends Component { {this.state.options.searchType === "alphabetical" &&
Alphabetical browsing mode
} - {this.state.options.level === "editor" &&
+ {this.state.user?.level === "editor" &&
Editor priveleges active
@@ -478,7 +410,7 @@ class App extends Component { - + @@ -492,10 +424,7 @@ class App extends Component { } - { - this.props.history.replace("/"); - auth.signOut(); - })} /> + { isolateEntry={this.handleIsolateEntry} /> - {wordlistEnabled(this.state) && + {wordlistEnabled(this.state.user) && } - {this.state.options.level === "editor" && + {this.state.user?.level === "editor" && } - {this.state.options.level === "editor" && + {this.state.user?.level === "editor" && } @@ -534,15 +463,15 @@ class App extends Component {
- - {wordlistEnabled(this.state) && + + {wordlistEnabled(this.state.user) && } - {this.state.options.level === "editor" && + {this.state.user?.level === "editor" && { - const res = await tokenFetch("publishDictionary"); - if (!res) { - throw new Error("Connection error/offline"); +async function accountApiFetch(url: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET"): Promise { + const response = await fetch(accountBaseUrl + url, { + method, + credentials: "include", + }); + return await response.json() as AT.APIResponse; +} + +export async function publishDictionary(): Promise { + return { + ok: true, + // @ts-ignore + info: {}, + }; +} + +export async function upgradeAccount(password: string): Promise { + return { + ok: false, + error: "incorrect password", + }; +} + +export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise { + // return await tokenFetch("submissions", "POST", submissions) as FT.SubmissionsResponse; + return { + ok: true, + message: "received", + submissions: [], } - return res; } -export async function upgradeAccount(password: string): Promise { - const res = await tokenFetch("upgradeUser", "POST", { password }); - if (!res) { - throw new Error("Connection error/offline"); - } - return res; -} - -export async function postSubmissions(submissions: BT.SubmissionsRequest): Promise { - return await tokenFetch("submissions", "POST", submissions) as BT.SubmissionsResponse; -} - -export async function loadUserInfo(): Promise { - const res = await tokenFetch("getUserInfo", "GET") as BT.GetUserInfoResponse; - return "user" in res ? res.user : undefined; -} - -// TODO: HARD TYPING OF THIS WITH THE subUrl and return values etc? -async function tokenFetch(subUrl: string, method?: "GET" | "POST", body?: any): Promise { - if (!auth.currentUser) { - throw new Error("not signed in"); - } +export async function getUser(): Promise { try { - const token = await auth.currentUser.getIdToken(); - const response = await fetch(`${functionsBaseUrl}${subUrl}`, { - method, - headers: { - "Content-Type": "application/json", - "Authorization": `Bearer ${token}`, - }, - ...body ? { - body: JSON.stringify(body), - } : {}, - }); - return await response.json(); - } catch (err) { - console.error(err); - throw err; + const response = await accountApiFetch("user"); + if ("user" in response) { + return response.user; + } + return undefined; + } catch (e) { + console.error(e); + return "offline"; } } diff --git a/website/src/lib/firebase.ts b/website/src/lib/firebase.ts deleted file mode 100644 index 27bf643..0000000 --- a/website/src/lib/firebase.ts +++ /dev/null @@ -1,30 +0,0 @@ -import firebase from "firebase/app"; -import "firebase/auth"; - -// Configure Firebase. -const config = { - apiKey: "AIzaSyDZrG2BpQi0MGktEKXL6mIWeAYEn_gFacw", - authDomain: "lingdocs.firebaseapp.com", - projectId: "lingdocs", -}; - -firebase.initializeApp(config); - -export const authUiConfig = { - // Popup signin flow rather than redirect flow. - signInFlow: "popup", - signInOptions: [ - firebase.auth.EmailAuthProvider.PROVIDER_ID, - firebase.auth.GithubAuthProvider.PROVIDER_ID, - // twitter auth is set up, but not using because it doesn't provide an email - // firebase.auth.TwitterAuthProvider.PROVIDER_ID, - // firebase.auth.GoogleAuthProvider.PROVIDER_ID, - ], - callbacks: { - // Avoid redirects after sign-in. - signInSuccessWithAuthResult: () => false, - }, -}; - -export const auth = firebase.auth(); - diff --git a/website/src/lib/backend-types.ts b/website/src/lib/functions-types.ts similarity index 73% rename from website/src/lib/backend-types.ts rename to website/src/lib/functions-types.ts index f5d4f34..7f02789 100644 --- a/website/src/lib/backend-types.ts +++ b/website/src/lib/functions-types.ts @@ -7,6 +7,7 @@ */ import { Types as T } from "@lingdocs/pashto-inflector"; +import * as AT from "./account-types"; export type PublishDictionaryResponse = { ok: true, @@ -16,20 +17,18 @@ export type PublishDictionaryResponse = { errors: T.DictionaryEntryError[], }; -export type UserInfo = { - uid: string, - email: string | null, - displayName: string | null, -} - export type Submission = Edit | ReviewTask; export type Edit = EntryEdit | NewEntry | EntryDeletion export type SubmissionBase = { - sTs: number, - user: UserInfo, _id: string, + sTs: number, + user: { + userId: AT.UUID, + name: string, + email: string, + }, } export type ReviewTask = Issue | EditSuggestion | EntrySuggestion; @@ -74,30 +73,6 @@ export type SubmissionsResponse = { submissions: Submission[], }; -export type UserLevel = "basic" | "student" | "editor"; - -export type CouchDbUser = { - _id: string, - type: "user", - _rev?: string, - name: string, - email: string, - providerData: any, - displayName: string, - roles: [], - password?: string, - level: UserLevel, - userdbPassword: string, -} - -export type GetUserInfoResponse = { - ok: true, - message: "no couchdb user found", -} | { - ok: true, - user: CouchDbUser, -} - export type UpgradeUserResponse = { ok: false, error: "incorrect password", diff --git a/website/src/lib/level-management.ts b/website/src/lib/level-management.ts index da5f632..990a602 100644 --- a/website/src/lib/level-management.ts +++ b/website/src/lib/level-management.ts @@ -1,14 +1,6 @@ -/** - * Copyright (c) 2021 lingdocs.com - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - */ +import type { LingdocsUser } from "./account-types"; -export function wordlistEnabled(state: State | UserLevel): boolean { - const level = (typeof state === "string") - ? state - : state.options.level; - return level !== "basic"; -} +export function wordlistEnabled(user: LingdocsUser | undefined): boolean { + if (!user) return false; + return user.level !== "basic"; +} \ No newline at end of file diff --git a/website/src/lib/options-storage.test.ts b/website/src/lib/local-storage.test.ts similarity index 96% rename from website/src/lib/options-storage.test.ts rename to website/src/lib/local-storage.test.ts index daf04de..f44fe0b 100644 --- a/website/src/lib/options-storage.test.ts +++ b/website/src/lib/local-storage.test.ts @@ -6,7 +6,7 @@ * */ -import { saveOptions, readOptions, optionsLocalStorageName } from "./options-storage"; +import { saveOptions, readOptions, optionsLocalStorageName } from "./local-storage"; import { defaultTextOptions, } from "@lingdocs/pashto-inflector"; @@ -16,7 +16,6 @@ const optionsStub: Options = { searchType: "fuzzy", theme: "dark", textOptions: defaultTextOptions, - level: "student", wordlistMode: "browse", wordlistReviewLanguage: "Pashto", wordlistReviewBadge: true, diff --git a/website/src/lib/options-storage.ts b/website/src/lib/local-storage.ts similarity index 57% rename from website/src/lib/options-storage.ts rename to website/src/lib/local-storage.ts index 4983dab..8d4182f 100644 --- a/website/src/lib/options-storage.ts +++ b/website/src/lib/local-storage.ts @@ -6,7 +6,10 @@ * */ +import * as AT from "./account-types"; + export const optionsLocalStorageName = "options2"; +export const userLocalStorageName = "user1"; export function saveOptions(options: Options): void { localStorage.setItem(optionsLocalStorageName, JSON.stringify(options)); @@ -31,4 +34,26 @@ export const readOptions = (): Options | undefined => { console.error("error parsing saved state JSON", e); return undefined; } +}; + +export function saveUser(user: AT.LingdocsUser | undefined): void { + if (user) { + localStorage.setItem(userLocalStorageName, JSON.stringify(user)); + } else { + localStorage.removeItem(userLocalStorageName); + } +}; + +export const readUser = (): AT.LingdocsUser | undefined => { + const userRaw = localStorage.getItem(userLocalStorageName); + if (!userRaw) { + return undefined; + } + try { + const user = JSON.parse(userRaw) as AT.LingdocsUser; + return user; + } catch (e) { + console.error("error parsing saved user JSON", e); + return undefined; + } }; \ No newline at end of file diff --git a/website/src/lib/options-reducer.test.ts b/website/src/lib/options-reducer.test.ts index c35b5bb..3018fe8 100644 --- a/website/src/lib/options-reducer.test.ts +++ b/website/src/lib/options-reducer.test.ts @@ -6,7 +6,6 @@ const options: Options = { language: "Pashto", searchType: "fuzzy", theme: "light", - level: "basic", wordlistMode: "browse", wordlistReviewLanguage: "Pashto", wordlistReviewBadge: true, @@ -33,11 +32,6 @@ test("options reducer should work", () => { ...options, theme: "dark", }); - expect(optionsReducer(options, { type: "changeUserLevel", payload: "student" })) - .toEqual({ - ...options, - level: "student", - }); expect(optionsReducer(options, { type: "changeWordlistMode", payload: "review" })) .toEqual({ ...options, @@ -66,12 +60,12 @@ test("options reducer should work", () => { pTextSize: "largest", }, }); - expect(optionsReducer(options, { type: "changeSpelling", payload: "Pakistani" })) + expect(optionsReducer(options, { type: "changeSpelling", payload: "Pakistani ی" })) .toEqual({ ...options, textOptions: { ...defaultTextOptions, - spelling: "Pakistani", + spelling: "Pakistani ی", }, }); expect(optionsReducer(options, { type: "changePhonetics", payload: "ipa" })) diff --git a/website/src/lib/options-reducer.ts b/website/src/lib/options-reducer.ts index 61f4e61..faa709d 100644 --- a/website/src/lib/options-reducer.ts +++ b/website/src/lib/options-reducer.ts @@ -23,12 +23,6 @@ function optionsReducer(options: Options, action: OptionsAction): Options { searchBarPosition: action.payload, }; } - if (action.type === "changeUserLevel") { - return { - ...options, - level: action.payload, - }; - } if (action.type === "changeWordlistMode") { return { ...options, diff --git a/website/src/lib/pouch-dbs.ts b/website/src/lib/pouch-dbs.ts index 3de1438..311d6d0 100644 --- a/website/src/lib/pouch-dbs.ts +++ b/website/src/lib/pouch-dbs.ts @@ -1,75 +1,112 @@ import PouchDB from "pouchdb"; -import * as BT from "./backend-types"; +import * as AT from "./account-types"; +import * as FT from "./functions-types"; type LocalDbType = "submissions" | "wordlist" | "reviewTasks"; -type LocalDb = null | { refresh: () => void, db: PouchDB.Database }; + +const localDbTypes: LocalDbType[] = ["submissions", "wordlist", "reviewTasks"]; + +type UnsyncedLocalDb = { + refresh: () => void, + db: PouchDB.Database, +}; + +type SyncedLocalDb = UnsyncedLocalDb & { + sync: PouchDB.Replication.Sync, +}; + +type DBS = { + submissions: undefined | UnsyncedLocalDb, + wordlist: undefined | SyncedLocalDb, + reviewTasks: undefined | SyncedLocalDb, +}; + type DbInput = { type: "wordlist", doc: WordlistWord, } | { type: "submissions", - doc: BT.Submission, + doc: FT.Submission, } | { type: "reviewTasks", - doc: BT.ReviewTask, + doc: FT.ReviewTask, }; -const dbs: Record = { +const dbs: DBS = { /* for anyone logged in - for edits/suggestions submissions */ - submissions: null, + submissions: undefined, /* for students and above - personal wordlist database */ - wordlist: null, + wordlist: undefined, /* for editors only - edits/suggestions (submissions) for review */ - reviewTasks: null, + reviewTasks: undefined, }; -export function initializeLocalDb(type: LocalDbType, refresh: () => void, uid?: string | undefined) { +export function startLocalDbs(user: AT.LingdocsUser, refreshFns: { wordlist: () => void, reviewTasks: () => void }) { + if (user.level === "basic") { + initializeLocalDb("submissions", () => null, user); + } + if (user.level === "student") { + initializeLocalDb("submissions", () => null, user); + initializeLocalDb("wordlist", refreshFns.wordlist, user); + } + if (user.level === "editor") { + deInitializeLocalDb("submissions"); + initializeLocalDb("reviewTasks", refreshFns.reviewTasks, user); + initializeLocalDb("wordlist", refreshFns.wordlist, user); + } +} + +function deInitializeLocalDb(type: LocalDbType) { + const db = dbs[type]; + if (db && "sync" in db) { + db.sync.cancel(); + } + dbs[type] = undefined; +} + +export function stopLocalDbs() { + localDbTypes.forEach((type) => { + deInitializeLocalDb(type); + }); +} + +function initializeLocalDb(type: LocalDbType, refresh: () => void, user: AT.LingdocsUser) { const name = type === "wordlist" - ? `userdb-${uid? stringToHex(uid) : "guest"}` + ? `userdb-${stringToHex(user.userId)}` : type === "submissions" ? "submissions" : "review-tasks"; const db = dbs[type]; // only initialize the db if it doesn't exist or if it has a different name if ((!db) || (db.db?.name !== name)) { - dbs[type] = { - db: new PouchDB(name), - refresh, - }; + if (type === "submissions") { + dbs[type] = { + refresh, + db: new PouchDB(name), + }; + } else { + dbs[type]?.sync.cancel(); + const db = new PouchDB(name); + const pass = "userDbPassword" in user ? user.userDbPassword : ""; + dbs[type] = { + db, + refresh, + sync: db.sync( + `https://${user.userId}:${pass}@couch.lingdocs.com/${name}`, + { live: true, retry: true }, + ).on("change", (info) => { + if (info.direction === "pull") { + refresh(); + } + }).on("error", (error) => { + console.error(error); + }), + }; + } refresh(); } } -export function getLocalDbName(type: LocalDbType) { - return dbs[type]?.db.name; -} - -export function deInitializeLocalDb(type: LocalDbType) { - dbs[type] = null; -} - -export function startLocalDbSync( - type: "wordlist" | "reviewTasks", - auth: { name: string, password: string }, -) { - const localDb = dbs[type]; - if (!localDb) { - console.error(`unable to start sync because ${type} database is not initialized`); - return; - } - const sync = localDb.db.sync( - `https://${auth.name}:${auth.password}@couchdb.lingdocs.com/${localDb.db.name}`, - { live: true, retry: true }, - ).on("change", (info) => { - if (info.direction === "pull") { - localDb.refresh(); - } - }).on("error", (error) => { - console.error(error); - }); - return sync; -} - export async function addToLocalDb({ type, doc }: DbInput) { const localDb = dbs[type]; if (!localDb) { @@ -99,10 +136,10 @@ export async function updateLocalDbDoc({ type, doc }: DbInput, id: string) { return updated; } -export async function getAllDocsLocalDb(type: "submissions", limit?: number): Promise; +export async function getAllDocsLocalDb(type: "submissions", limit?: number): Promise; export async function getAllDocsLocalDb(type: "wordlist", limit?: number): Promise; -export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise -export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise { +export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise +export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise { const localDb = dbs[type]; if (!localDb) { throw new Error(`unable to get all docs from ${type} database - not initialized`); @@ -116,11 +153,11 @@ export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Prom const docs = result.rows.map((row) => row.doc) as unknown; switch (type) { case "submissions": - return docs as BT.Submission[]; + return docs as FT.Submission[]; case "wordlist": return docs as WordlistWordDoc[]; case "reviewTasks": - return docs as BT.ReviewTask[]; + return docs as FT.ReviewTask[]; } } diff --git a/website/src/lib/search-all-inflections.ts b/website/src/lib/search-all-inflections.ts index 91db554..e39face 100644 --- a/website/src/lib/search-all-inflections.ts +++ b/website/src/lib/search-all-inflections.ts @@ -28,7 +28,7 @@ function fFuzzy(f: string): string { } export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue: string): { entry: T.DictionaryEntry, results: InflectionSearchResult[] }[] { - const timerLabel = "Search inflections"; + // const timerLabel = "Search inflections"; const beg = fFuzzy(searchValue.slice(0, 2)); const preSearchFun = isPashtoScript(searchValue) ? (ps: T.PsString) => ps.p.slice(0, 2) === beg @@ -37,7 +37,7 @@ export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue: const searchFun = isPashtoScript(searchValue) ? (ps: T.PsString) => ps.p === searchValue : (ps: T.PsString) => !!ps.f.match(fRegex); - console.time(timerLabel); + // console.time(timerLabel); const results = allDocs.reduce((all: { entry: T.DictionaryEntry, results: InflectionSearchResult[] }[], entry) => { const type = isNounAdjOrVerb(entry); if (entry.c && type === "verb") { @@ -74,6 +74,6 @@ export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue: } return all; }, []); - console.timeEnd(timerLabel); + // console.timeEnd(timerLabel); return results; } \ No newline at end of file diff --git a/website/src/lib/submissions.ts b/website/src/lib/submissions.ts index 3bcd086..8b1eb86 100644 --- a/website/src/lib/submissions.ts +++ b/website/src/lib/submissions.ts @@ -1,28 +1,22 @@ -import * as BT from "./backend-types"; -import { auth } from "./firebase"; +import * as FT from "./functions-types"; +import * as AT from "./account-types"; import { postSubmissions, } from "./backend-calls"; import { - initializeLocalDb, addToLocalDb, getAllDocsLocalDb, deleteFromLocalDb, } from "./pouch-dbs"; -initializeLocalDb("submissions", () => null); - -export function submissionBase(): BT.SubmissionBase { - if (!auth.currentUser) { - throw new Error("not signed in"); - } +export function submissionBase(user: AT.LingdocsUser): FT.SubmissionBase { return { sTs: Date.now(), _id: new Date().toJSON(), user: { - uid: auth.currentUser.uid, - email: auth.currentUser.email, - displayName: auth.currentUser.displayName, + name: user.name, + email: user.email || "", + userId: user.userId, }, }; } @@ -48,8 +42,8 @@ export async function sendSubmissions() { } } -export async function addSubmission(submission: BT.Submission, level: BT.UserLevel) { - if (level === "editor" && (submission.type === "issue" || submission.type === "entry suggestion" || submission.type === "edit suggestion")) { +export async function addSubmission(submission: FT.Submission, user: AT.LingdocsUser) { + if (user.level === "editor" && (submission.type === "issue" || submission.type === "entry suggestion" || submission.type === "edit suggestion")) { await addToLocalDb({ type: "reviewTasks", doc: submission }) } else { await addToLocalDb({ type: "submissions", doc: submission }); diff --git a/website/src/screens/Account.tsx b/website/src/screens/Account.tsx index 2e4fa71..d817b74 100644 --- a/website/src/screens/Account.tsx +++ b/website/src/screens/Account.tsx @@ -1,89 +1,54 @@ -import { useState, useEffect } from "react"; -import { Modal, Button } from "react-bootstrap"; -import { Link } from "react-router-dom"; -import { auth, authUiConfig } from "../lib/firebase"; -import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth"; import { - upgradeAccount, + useState, + // useEffect, +} from "react"; +// import { Modal, Button } from "react-bootstrap"; +import { + // upgradeAccount, publishDictionary, } from "../lib/backend-calls"; -import LoadingElipses from "../components/LoadingElipses"; +// import LoadingElipses from "../components/LoadingElipses"; import { Helmet } from "react-helmet"; +import * as AT from "../lib/account-types"; const capitalize = (s: string): string => { // if (!s) return ""; return s.charAt(0).toUpperCase() + s.slice(1); } -const Account = ({ handleSignOut, level, loadUserInfo }: { - handleSignOut: () => void, - loadUserInfo: () => void, - level: UserLevel, -}) => { - const [showingDeleteConfirmation, setShowingDeleteConfirmation] = useState(false); - const [showingUpgradePrompt, setShowingUpgradePrompt] = useState(false); - const [upgradePassword, setUpgradePassword] = useState(""); - const [upgradeError, setUpgradeError] = useState(""); - const [accountDeleted, setAccountDeleted] = useState(false); - const [accountDeleteError, setAccountDeleteError] = useState(""); - const [emailVerification, setEmailVerification] = useState<"unverified" | "sent" | "verified">("verified"); - const [waiting, setWaiting] = useState(false); +const Account = ({ user }: { user: AT.LingdocsUser | undefined }) => { + // const [showingUpgradePrompt, setShowingUpgradePrompt] = useState(false); + // const [upgradePassword, setUpgradePassword] = useState(""); + // const [upgradeError, setUpgradeError] = useState(""); + // const [waiting, setWaiting] = useState(false); const [publishingStatus, setPublishingStatus] = useState(undefined); - const [showingPasswordChange, setShowingPasswordChange] = useState(false); - const [password, setPassword] = useState(""); - const [passwordConfirmed, setPasswordConfirmed] = useState(""); - const [passwordError, setPasswordError] = useState(""); - const [showingUpdateEmail, setShowingUpdateEmail] = useState(false); - const [updateEmailError, setUpdateEmailError] = useState(""); - const [newEmail, setNewEmail] = useState(""); - const user = auth.currentUser; - const hasPasswordProvider = user?.providerData?.some((d) => d?.providerId === "password"); - useEffect(() => { - setShowingDeleteConfirmation(false); - setShowingUpgradePrompt(false); - setUpgradePassword(""); - setUpgradeError(""); - setWaiting(false); - }, []); - useEffect(() => { - setEmailVerification((user && user.emailVerified) ? "verified" : "unverified"); - }, [user]); - function handleDelete() { - auth.currentUser?.delete().then(() => { - setAccountDeleteError(""); - setShowingDeleteConfirmation(false); - setAccountDeleted(true); - }).catch((err) => { - console.error(err); - setAccountDeleteError(err.message); - }); - } - function closeUpgrade() { - setShowingUpgradePrompt(false); - setUpgradePassword(""); - setUpgradeError(""); - } - function closeUpdateEmail() { - setShowingUpdateEmail(false); - setNewEmail(""); - setUpdateEmailError(""); - } - async function handleUpgrade() { - setUpgradeError(""); - setWaiting(true); - upgradeAccount(upgradePassword).then((res) => { - setWaiting(false); - if (res.ok) { - loadUserInfo(); - closeUpgrade(); - } else { - setUpgradeError("Incorrect password"); - } - }).catch((err) => { - setWaiting(false); - setUpgradeError(err.message); - }); - } + // useEffect(() => { + // setShowingUpgradePrompt(false); + // setUpgradePassword(""); + // setUpgradeError(""); + // setWaiting(false); + // }, []); + // function closeUpgrade() { + // setShowingUpgradePrompt(false); + // setUpgradePassword(""); + // setUpgradeError(""); + // } + // async function handleUpgrade() { + // setUpgradeError(""); + // setWaiting(true); + // upgradeAccount(upgradePassword).then((res) => { + // setWaiting(false); + // if (res.ok) { + // loadUserInfo(); + // closeUpgrade(); + // } else { + // setUpgradeError("Incorrect password"); + // } + // }).catch((err) => { + // setWaiting(false); + // setUpgradeError(err.message); + // }); + // } function handlePublish() { setPublishingStatus("publishing"); publishDictionary().then((response) => { @@ -93,56 +58,6 @@ const Account = ({ handleSignOut, level, loadUserInfo }: { setPublishingStatus("Offline or connection error"); }); } - function handleVerifyEmail() { - if (!user) return; - user.sendEmailVerification(); - setEmailVerification("sent"); - } - function handleUpdateEmail() { - if (!user) return; - user.updateEmail(newEmail).then(() => { - setShowingUpdateEmail(false); - }).catch((err) => { - setUpdateEmailError(err.message); - }); - } - function closePasswordChange() { - setShowingPasswordChange(false); - setPassword(""); - setPasswordConfirmed(""); - } - function handlePasswordChange() { - if (!user) return; - if (password === "") { - setPasswordError("Please enter a password"); - return; - } - if (password !== passwordConfirmed) { - setPasswordError("Your passwords do not match"); - return; - } - user.updatePassword(password).then(() => { - closePasswordChange(); - }).catch((err) => { - setPasswordError(err.message); - }); - } - if (accountDeleted) { - return
- - - Account Deleted - LingDocs Pashto Dictionary - -
-

Your account has been deleted 🙋‍♂️

-
- - - -
- } if (!user) { return
@@ -151,23 +66,8 @@ const Account = ({ handleSignOut, level, loadUserInfo }: { Sign In - LingDocs Pashto Dictionary

Sign in to be able to suggest words/edits

-

For people who previously signed in with Google. Sorry, there is a problem now and you can't get to your previous account! 😬 Don't worry, all your info is safe and it will be restored in the near future. Stay tuned.

- { - // const newUser = res.additionalUserInfo?.isNewUser; - // const emailVerified = res.user.emailVerified; - // if (newUser && !emailVerified) { - // res.user.sendEmailVerification(); - // setEmailVerification("sent"); - // } - // return false; - // }} - firebaseAuth={auth} /> -
; +
} - const defaultProviderId = user.providerData[0]?.providerId; return (
@@ -176,7 +76,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: { Account - LingDocs Pashto Dictionary

Account

- {level === "editor" && + {user.level === "editor" &&

Editor Tools

{publishingStatus !== "publishing" && @@ -197,44 +97,34 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
}
- {user.photoURL &&
+ {/* {user.p &&
avatar -
} +
} */}
    -
  • Name: {user.displayName}
  • +
  • Name: {user.name}
  • {user.email &&
    -
    Email: {user.email} - {emailVerification === "unverified" && } -
    - {emailVerification === "unverified" &&
    - Please Verify Your Email Address -
    } - {emailVerification === "sent" &&
    - 📧 Check your email for the confirmation message -
    } +
    Email: {user.email}
    }
  • -
  • Account Level: {capitalize(level)}
  • +
  • Account Level: {capitalize(user.level)}
- + */}

Account Admin

-
- {level === "basic" && } - - -
-
- - setShowingDeleteConfirmation(false)}> - - Delete Account? - - Are your sure you want to delete your account? This can't be undone. - {accountDeleteError &&
-

- {accountDeleteError} -

- -
} - - - - -
- +
*/} + {/* Upgrade Account @@ -318,85 +163,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: { Upgrade my account - - - - {hasPasswordProvider ? "Change" : "Add"} Password - - {!hasPasswordProvider && - You can create a password here if you would like to sign in with your email and password, instead of just signing in with {defaultProviderId}. - } -
- - { - setPassword(e.target.value); - setPasswordError(""); - }} - /> - - { - setPasswordConfirmed(e.target.value); - setPasswordError(""); - }} - /> -
- {passwordError &&
-

- {passwordError} -

-
} - - {waiting && } - - - -
- - - Update Email - -
- - { - setNewEmail(e.target.value); - setUpdateEmailError(""); - }} - /> -
- {updateEmailError &&
-

- {updateEmailError} -

-
} - - {waiting && } - - - -
+ */}
); }; diff --git a/website/src/screens/EntryEditor.tsx b/website/src/screens/EntryEditor.tsx index 619dec3..6dafe6f 100644 --- a/website/src/screens/EntryEditor.tsx +++ b/website/src/screens/EntryEditor.tsx @@ -18,7 +18,7 @@ import { validateEntry, } from "@lingdocs/pashto-inflector"; import Entry from "../components/Entry"; -import * as BT from "../lib/backend-types"; +import * as FT from "../lib/functions-types"; import { submissionBase, addSubmission, @@ -136,18 +136,20 @@ function EntryEditor({ state, dictionary, searchParams }: { } } function handleDelete() { - const submission: BT.EntryDeletion = { - ...submissionBase(), + if (!state.user) return; + const submission: FT.EntryDeletion = { + ...submissionBase(state.user), type: "entry deletion", ts: entry.ts, }; - addSubmission(submission, state.options.level); + addSubmission(submission, state.user); setDeleted(true); } function handleSubmit(e: any) { setErroneousFields([]); setErrors([]); e.preventDefault(); + if (!state.user) return; const result = validateEntry(entry); if ("errors" in result) { setErroneousFields(result.erroneousFields); @@ -155,12 +157,12 @@ function EntryEditor({ state, dictionary, searchParams }: { return; } // TODO: Check complement if checkComplement - const submission: BT.NewEntry | BT.EntryEdit = { - ...submissionBase(), + const submission: FT.NewEntry | FT.EntryEdit = { + ...submissionBase(state.user), type: entry.ts === 1 ? "new entry" : "entry edit", entry: { ...entry, ts: entry.ts === 1 ? Date.now() : entry.ts }, }; - addSubmission(submission, state.options.level); + addSubmission(submission, state.user); setSubmitted(true); // TODO: Remove from suggestions // if (willDeleteSuggestion && sTs) { @@ -306,8 +308,8 @@ function EntryEditor({ state, dictionary, searchParams }: {
))}
- - + + {sTs &&
w.entry.ts === state.isolatedEntry?.ts); function submitEdit() { if (!state.isolatedEntry) return; + if (!state.user) return; addSubmission({ - ...submissionBase(), + ...submissionBase(state.user), type: "edit suggestion", entry: state.isolatedEntry, comment, - }, state.options.level); + }, state.user); setEditing(false); setComment(""); setEditSubmitted(true); @@ -108,10 +106,10 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: { isolateEntry={isolateEntry} />
- {auth.currentUser && + {state.user &&
- {state.options.level === "editor" && + {state.user.level === "editor" &&
- {wordlistEnabled(state) &&
void, }) { return
@@ -152,7 +154,7 @@ function Options({ ctrl / ⌘ + b clear search - {wordlistEnabled(options.level) && + {wordlistEnabled(state.user) && ctrl / ⌘ + \ show/hide wordlist } @@ -173,7 +175,7 @@ function Options({ handleChange={(p) => optionsDispatch({ type: "changeSearchBarPosition", payload: p as SearchBarPosition })} />
Bottom position doesn't work well with iPhones.
- {wordlistEnabled(options.level) && <> + {wordlistEnabled(state.user) && <>

Show Number of Wordlist Words for Review

) { event.preventDefault(); + if (!state.user) return; const p = pashto; const f = phonetics; const e = english; - const newEntry: BT.EntrySuggestion = { - ...submissionBase(), + const newEntry: FT.EntrySuggestion = { + ...submissionBase(state.user), type: "entry suggestion", entry: { ts: 0, i: 0, p, f, g: "", e }, comment, }; - addSubmission(newEntry, state.options.level); + addSubmission(newEntry, state.user); setSuggestionState("received"); } function handlePowerSearch() { @@ -91,16 +91,16 @@ function Results({ state, isolateEntry }: { LingDocs Pashto Dictionary - {(auth.currentUser && (window.location.pathname !== "/word") && suggestionState === "none" && powerResults === undefined) && } {(powerResults === undefined && suggestionState === "none" && window.location.pathname === "/search") &&
}
"{reviewTask.comment}"
-
{reviewTask.user.displayName} - {reviewTask.user.email}
+
{reviewTask.user.name} - {reviewTask.user.email}
diff --git a/website/src/types.d.ts b/website/src/types.d.ts index fa9fe2f..7061929 100644 --- a/website/src/types.d.ts +++ b/website/src/types.d.ts @@ -22,7 +22,6 @@ type Options = { searchType: SearchType, theme: Theme, textOptions: import("@lingdocs/pashto-inflector").Types.TextOptions, - level: UserLevel, wordlistMode: WordlistMode, wordlistReviewLanguage: Language, wordlistReviewBadge: boolean, @@ -39,8 +38,9 @@ type State = { isolatedEntry: import("@lingdocs/pashto-inflector").Types.DictionaryEntry | undefined, results: import("@lingdocs/pashto-inflector").Types.DictionaryEntry[], wordlist: WordlistWord[], - reviewTasks: import("./lib/backend-types").ReviewTask[], + reviewTasks: import("./lib/functions-types").ReviewTask[], dictionaryInfo: import("@lingdocs/pashto-inflector").Types.DictionaryInfo | undefined, + user: undefined | import("./lib/account-types").LingdocsUser, } type OptionsAction = { diff --git a/website/yarn.lock b/website/yarn.lock index 22d7ff4..15781a4 100644 --- a/website/yarn.lock +++ b/website/yarn.lock @@ -1234,276 +1234,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@firebase/analytics-types@0.6.0": - version "0.6.0" - resolved "https://registry.yarnpkg.com/@firebase/analytics-types/-/analytics-types-0.6.0.tgz#164116ebe8d3b338272acc7f9904cac38556d6cd" - integrity sha512-kbMawY0WRPyL/lbknBkme4CNLl+Gw+E9G4OpNeXAauqoQiNkBgpIvZYy7BRT4sNGhZbxdxXxXbruqUwDzLmvTw== - -"@firebase/analytics@0.6.17": - version "0.6.17" - resolved "https://registry.yarnpkg.com/@firebase/analytics/-/analytics-0.6.17.tgz#61df8155f474e7eb1cc180dd7ba70c6f0100f102" - integrity sha512-Iiip24vQw7p+dBxoGWP2WvVqV2tOdLPjWw6OP6a+8vgss9PRsjKE2AAskruqveMUO4Ox5uPW65wdgeJxoFoMvQ== - dependencies: - "@firebase/analytics-types" "0.6.0" - "@firebase/component" "0.5.5" - "@firebase/installations" "0.4.31" - "@firebase/logger" "0.2.6" - "@firebase/util" "1.2.0" - tslib "^2.1.0" - -"@firebase/app-check-interop-types@0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@firebase/app-check-interop-types/-/app-check-interop-types-0.1.0.tgz#83afd9d41f99166c2bdb2d824e5032e9edd8fe53" - integrity sha512-uZfn9s4uuRsaX5Lwx+gFP3B6YsyOKUE+Rqa6z9ojT4VSRAsZFko9FRn6OxQUA1z5t5d08fY4pf+/+Dkd5wbdbA== - -"@firebase/app-check-types@0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@firebase/app-check-types/-/app-check-types-0.3.1.tgz#1084723debad3ad9e7997d3b356165d275c25fcc" - integrity sha512-KJ+BqJbdNsx4QT/JIT1yDj5p6D+QN97iJs3GuHnORrqL+DU3RWc9nSYQsrY6Tv9jVWcOkMENXAgDT484vzsm2w== - -"@firebase/app-check@0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@firebase/app-check/-/app-check-0.3.1.tgz#78210513455ea6da437cb5fcf18239db9cd1e7de" - integrity sha512-5OWXzhXdtrmOqn2aN44FyNTwjlZJrip/3WC7UlMeUBPi3apuUgChHTolZ1oITszGdw52lP3r5SOCDtRsNtbkJg== - dependencies: - "@firebase/app-check-interop-types" "0.1.0" - "@firebase/app-check-types" "0.3.1" - "@firebase/component" "0.5.5" - "@firebase/logger" "0.2.6" - "@firebase/util" "1.2.0" - tslib "^2.1.0" - -"@firebase/app-types@0.6.3": - version "0.6.3" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.6.3.tgz#3f10514786aad846d74cd63cb693556309918f4b" - integrity sha512-/M13DPPati7FQHEQ9Minjk1HGLm/4K4gs9bR4rzLCWJg64yGtVC0zNg9gDpkw9yc2cvol/mNFxqTtd4geGrwdw== - -"@firebase/app@0.6.29": - version "0.6.29" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.6.29.tgz#e2f88274b39917ab766f9fe73da48c353eaed557" - integrity sha512-duCzk9/BSVVsb5Y9b0rnvGSuD5zQA/JghiQsccRl+lA4xiUYjFudTU4cVFftkw+0zzeYBHn4KiVxchsva1O9dA== - dependencies: - "@firebase/app-types" "0.6.3" - "@firebase/component" "0.5.5" - "@firebase/logger" "0.2.6" - "@firebase/util" "1.2.0" - dom-storage "2.1.0" - tslib "^2.1.0" - xmlhttprequest "1.8.0" - -"@firebase/auth-interop-types@0.1.6": - version "0.1.6" - resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.6.tgz#5ce13fc1c527ad36f1bb1322c4492680a6cf4964" - integrity sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g== - -"@firebase/auth-types@0.10.3": - version "0.10.3" - resolved "https://registry.yarnpkg.com/@firebase/auth-types/-/auth-types-0.10.3.tgz#2be7dd93959c8f5304c63e09e98718e103464d8c" - integrity sha512-zExrThRqyqGUbXOFrH/sowuh2rRtfKHp9SBVY2vOqKWdCX1Ztn682n9WLtlUDsiYVIbBcwautYWk2HyCGFv0OA== - -"@firebase/auth@0.16.8": - version "0.16.8" - resolved "https://registry.yarnpkg.com/@firebase/auth/-/auth-0.16.8.tgz#4edd44673d3711e94cfa1e6b03883214ae1f2255" - integrity sha512-mR0UXG4LirWIfOiCWxVmvz1o23BuKGxeItQ2cCUgXLTjNtWJXdcky/356iTUsd7ZV5A78s2NHeN5tIDDG6H4rg== - dependencies: - "@firebase/auth-types" "0.10.3" - -"@firebase/component@0.5.5": - version "0.5.5" - resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.5.5.tgz#849ccf7cbf0398a43058f274ffcd43620ae9521f" - integrity sha512-L41SdS/4a164jx2iGfakJgaBUPPBI3DI+RrUlmh3oHSUljTeCwfj/Nhcv3S7e2lyXsGFJtAyepfPUx4IQ05crw== - dependencies: - "@firebase/util" "1.2.0" - tslib "^2.1.0" - -"@firebase/database-types@0.7.3": - version "0.7.3" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.7.3.tgz#819f16dd4c767c864b460004458620f265a3f735" - integrity sha512-dSOJmhKQ0nL8O4EQMRNGpSExWCXeHtH57gGg0BfNAdWcKhC8/4Y+qfKLfWXzyHvrSecpLmO0SmAi/iK2D5fp5A== - dependencies: - "@firebase/app-types" "0.6.3" - -"@firebase/database@0.10.9": - version "0.10.9" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.10.9.tgz#79f7b03cbe8a127dddfb7ea7748a3e923990f046" - integrity sha512-Jxi9SiE4cNOftO9YKlG71ccyWFw4kSM9AG/xYu6vWXUGBr39Uw1TvYougANOcU21Q0TP4J08VPGnOnpXk/FGbQ== - dependencies: - "@firebase/auth-interop-types" "0.1.6" - "@firebase/component" "0.5.5" - "@firebase/database-types" "0.7.3" - "@firebase/logger" "0.2.6" - "@firebase/util" "1.2.0" - faye-websocket "0.11.3" - tslib "^2.1.0" - -"@firebase/firestore-types@2.3.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@firebase/firestore-types/-/firestore-types-2.3.0.tgz#baf5c9470ba8be96bf0d76b83b413f03104cf565" - integrity sha512-QTW7NP7nDL0pgT/X53lyj+mIMh4nRQBBTBlRNQBt7eSyeqBf3ag3bxdQhCg358+5KbjYTC2/O6QtX9DlJZmh1A== - -"@firebase/firestore@2.3.10": - version "2.3.10" - resolved "https://registry.yarnpkg.com/@firebase/firestore/-/firestore-2.3.10.tgz#76d5137e5c37d33ccf3c5d77a9261c73493494b2" - integrity sha512-O+XpaZVhDIBK2fMwBUBR2BuhaXF6zTmz+afAuXAx18DK+2rFfLefbALZLaUYw0Aabe9pryy0c7OenzRbHA8n4Q== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/firestore-types" "2.3.0" - "@firebase/logger" "0.2.6" - "@firebase/util" "1.2.0" - "@firebase/webchannel-wrapper" "0.5.1" - "@grpc/grpc-js" "^1.3.2" - "@grpc/proto-loader" "^0.6.0" - node-fetch "2.6.1" - tslib "^2.1.0" - -"@firebase/functions-types@0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@firebase/functions-types/-/functions-types-0.4.0.tgz#0b789f4fe9a9c0b987606c4da10139345b40f6b9" - integrity sha512-3KElyO3887HNxtxNF1ytGFrNmqD+hheqjwmT3sI09FaDCuaxGbOnsXAXH2eQ049XRXw9YQpHMgYws/aUNgXVyQ== - -"@firebase/functions@0.6.14": - version "0.6.14" - resolved "https://registry.yarnpkg.com/@firebase/functions/-/functions-0.6.14.tgz#f6b452a53dc15299595bd079dd6ed4afb59e1a8c" - integrity sha512-Gthru/wHPQqkn651MenVM+qKVFFqIyFcNT3qfJUacibqrKlvDtYtaCMjFGAkChuGnYzNVnXJIaNrIHkEIII4Hg== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/functions-types" "0.4.0" - "@firebase/messaging-types" "0.5.0" - node-fetch "2.6.1" - tslib "^2.1.0" - -"@firebase/installations-types@0.3.4": - version "0.3.4" - resolved "https://registry.yarnpkg.com/@firebase/installations-types/-/installations-types-0.3.4.tgz#589a941d713f4f64bf9f4feb7f463505bab1afa2" - integrity sha512-RfePJFovmdIXb6rYwtngyxuEcWnOrzdZd9m7xAW0gRxDIjBT20n3BOhjpmgRWXo/DAxRmS7bRjWAyTHY9cqN7Q== - -"@firebase/installations@0.4.31": - version "0.4.31" - resolved "https://registry.yarnpkg.com/@firebase/installations/-/installations-0.4.31.tgz#dbde30c0542fb4343b075f0574d4e0d0f4b49aa7" - integrity sha512-qWolhAgMHvD3avsNCl+K8+untzoDDFQIRR8At8kyWMKKosy0vttdWTWzjvDoZbyKU6r0RNlxDUWAgV88Q8EudQ== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/installations-types" "0.3.4" - "@firebase/util" "1.2.0" - idb "3.0.2" - tslib "^2.1.0" - -"@firebase/logger@0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.2.6.tgz#3aa2ca4fe10327cabf7808bd3994e88db26d7989" - integrity sha512-KIxcUvW/cRGWlzK9Vd2KB864HlUnCfdTH0taHE0sXW5Xl7+W68suaeau1oKNEqmc3l45azkd4NzXTCWZRZdXrw== - -"@firebase/messaging-types@0.5.0": - version "0.5.0" - resolved "https://registry.yarnpkg.com/@firebase/messaging-types/-/messaging-types-0.5.0.tgz#c5d0ef309ced1758fda93ef3ac70a786de2e73c4" - integrity sha512-QaaBswrU6umJYb/ZYvjR5JDSslCGOH6D9P136PhabFAHLTR4TWjsaACvbBXuvwrfCXu10DtcjMxqfhdNIB1Xfg== - -"@firebase/messaging@0.7.15": - version "0.7.15" - resolved "https://registry.yarnpkg.com/@firebase/messaging/-/messaging-0.7.15.tgz#d3b9a053331238480860c71385819babda2076f3" - integrity sha512-81t6iJtqMBJF5LHTjDhlHUpbPZOV6dKhW0TueAoON4omc0SaDXgf4nnk6JkvZRfdcuOaP8848Cv53tvZPFFAYQ== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/installations" "0.4.31" - "@firebase/messaging-types" "0.5.0" - "@firebase/util" "1.2.0" - idb "3.0.2" - tslib "^2.1.0" - -"@firebase/performance-types@0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@firebase/performance-types/-/performance-types-0.0.13.tgz#58ce5453f57e34b18186f74ef11550dfc558ede6" - integrity sha512-6fZfIGjQpwo9S5OzMpPyqgYAUZcFzZxHFqOyNtorDIgNXq33nlldTL/vtaUZA8iT9TT5cJlCrF/jthKU7X21EA== - -"@firebase/performance@0.4.17": - version "0.4.17" - resolved "https://registry.yarnpkg.com/@firebase/performance/-/performance-0.4.17.tgz#b160a4352f682c1039b49ec9d24d6c473a31b3c3" - integrity sha512-uhDs9rhdMrGraYHcd3CTRkGtcNap4hp6rAHTwJNIX56Z3RzQ1VW2ea9vvesl7EjFtEIPU0jfdrS32wV+qer5DQ== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/installations" "0.4.31" - "@firebase/logger" "0.2.6" - "@firebase/performance-types" "0.0.13" - "@firebase/util" "1.2.0" - tslib "^2.1.0" - -"@firebase/polyfill@0.3.36": - version "0.3.36" - resolved "https://registry.yarnpkg.com/@firebase/polyfill/-/polyfill-0.3.36.tgz#c057cce6748170f36966b555749472b25efdb145" - integrity sha512-zMM9oSJgY6cT2jx3Ce9LYqb0eIpDE52meIzd/oe/y70F+v9u1LDqk5kUF5mf16zovGBWMNFmgzlsh6Wj0OsFtg== - dependencies: - core-js "3.6.5" - promise-polyfill "8.1.3" - whatwg-fetch "2.0.4" - -"@firebase/remote-config-types@0.1.9": - version "0.1.9" - resolved "https://registry.yarnpkg.com/@firebase/remote-config-types/-/remote-config-types-0.1.9.tgz#fe6bbe4d08f3b6e92fce30e4b7a9f4d6a96d6965" - integrity sha512-G96qnF3RYGbZsTRut7NBX0sxyczxt1uyCgXQuH/eAfUCngxjEGcZQnBdy6mvSdqdJh5mC31rWPO4v9/s7HwtzA== - -"@firebase/remote-config@0.1.42": - version "0.1.42" - resolved "https://registry.yarnpkg.com/@firebase/remote-config/-/remote-config-0.1.42.tgz#84573ac2f1ee49cb9d4327a25c5625f2e274695d" - integrity sha512-hWwtAZmYLB274bxjV2cdMYhyBCUUqbYErihGx3rMyab76D+VbIxOuKJb2z0DS67jQG+SA3pr9/MtWsTPHV/l9g== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/installations" "0.4.31" - "@firebase/logger" "0.2.6" - "@firebase/remote-config-types" "0.1.9" - "@firebase/util" "1.2.0" - tslib "^2.1.0" - -"@firebase/storage-types@0.4.1": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@firebase/storage-types/-/storage-types-0.4.1.tgz#da6582ae217e3db485c90075dc71100ca5064cc6" - integrity sha512-IM4cRzAnQ6QZoaxVZ5MatBzqXVcp47hOlE28jd9xXw1M9V7gfjhmW0PALGFQx58tPVmuUwIKyoEbHZjV4qRJwQ== - -"@firebase/storage@0.6.2": - version "0.6.2" - resolved "https://registry.yarnpkg.com/@firebase/storage/-/storage-0.6.2.tgz#0355c903a9aa5a5958c87e8cae37f5f1b0e1d58e" - integrity sha512-qbYx+KOxnD5andWAtVPgMHFa/62jdLCgPGdfIRQIEgBoYibk3KigRYVdnfKRLcCO3Vip63BRUvyeqfiUHNNu2g== - dependencies: - "@firebase/component" "0.5.5" - "@firebase/storage-types" "0.4.1" - "@firebase/util" "1.2.0" - node-fetch "2.6.1" - tslib "^2.1.0" - -"@firebase/util@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-1.2.0.tgz#4d4e419bf8c9bc1bc51308d1953dc2e4353c0770" - integrity sha512-8W9TTGImXr9cu+oyjBJ7yjoEd/IVAv0pBZA4c1uIuKrpGZi2ee38m+8xlZOBRmsAaOU/tR9DXz1WF/oeM6Fb7Q== - dependencies: - tslib "^2.1.0" - -"@firebase/webchannel-wrapper@0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.5.1.tgz#a64d1af3c62e3bb89576ec58af880980a562bf4e" - integrity sha512-dZMzN0uAjwJXWYYAcnxIwXqRTZw3o14hGe7O6uhwjD1ZQWPVYA5lASgnNskEBra0knVBsOXB4KXg+HnlKewN/A== - "@fortawesome/fontawesome-free@^5.15.2": version "5.15.4" resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5" integrity sha512-eYm8vijH/hpzr/6/1CJ/V/Eb1xQFW2nnUKArb3z+yUWv7HTwj6M7SP957oMjfZjAHU6qpoNc2wQvIxBLWYa/Jg== -"@grpc/grpc-js@^1.3.2": - version "1.3.7" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-1.3.7.tgz#58b687aff93b743aafde237fd2ee9a3259d7f2d8" - integrity sha512-CKQVuwuSPh40tgOkR7c0ZisxYRiN05PcKPW72mQL5y++qd7CwBRoaJZvU5xfXnCJDFBmS3qZGQ71Frx6Ofo2XA== - dependencies: - "@types/node" ">=12.12.47" - -"@grpc/proto-loader@^0.6.0": - version "0.6.4" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.6.4.tgz#5438c0d771e92274e77e631babdc14456441cbdc" - integrity sha512-7xvDvW/vJEcmLUltCUGOgWRPM8Oofv0eCFSVMuKqaqWJaXSzmB+m9hiyqe34QofAl4WAzIKUZZlinIF9FOHyTQ== - dependencies: - "@types/long" "^4.0.1" - lodash.camelcase "^4.3.0" - long "^4.0.0" - protobufjs "^6.10.0" - yargs "^16.1.1" - "@hapi/address@2.x.x": version "2.1.4" resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5" @@ -1803,59 +1538,6 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e" integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ== -"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" - integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= - -"@protobufjs/base64@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" - integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== - -"@protobufjs/codegen@^2.0.4": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" - integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== - -"@protobufjs/eventemitter@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" - integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= - -"@protobufjs/fetch@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" - integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= - dependencies: - "@protobufjs/aspromise" "^1.1.1" - "@protobufjs/inquire" "^1.1.0" - -"@protobufjs/float@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" - integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= - -"@protobufjs/inquire@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" - integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= - -"@protobufjs/path@^1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" - integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= - -"@protobufjs/pool@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" - integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= - -"@protobufjs/utf8@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" - integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= - "@restart/context@^2.1.4": version "2.1.4" resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02" @@ -2250,11 +1932,6 @@ resolved "https://registry.yarnpkg.com/@types/lokijs/-/lokijs-1.5.5.tgz#bae743a9ae24d1a106b8291c31ab1e1ad28e00d2" integrity sha512-TAvlc6vfYZnQVqPBVF3ITE33aSomqRLHOsZb5u1jQdmQxvj+LOLgbt8VaAJh85Tx7xXdWcsdeEO13i6TQBfV+w== -"@types/long@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" - integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== - "@types/mime@^1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a" @@ -2275,7 +1952,7 @@ resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== -"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0": +"@types/node@*": version "16.6.1" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.1.tgz#aee62c7b966f55fc66c7b6dfa1d58db2a616da61" integrity sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw== @@ -4254,15 +3931,6 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" -cliui@^7.0.2: - version "7.0.4" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" - integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.0" - wrap-ansi "^7.0.0" - clone-buffer@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" @@ -4520,11 +4188,6 @@ core-js-pure@^3.16.0: resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8" integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw== -core-js@3.6.5: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== - core-js@^2.4.0, core-js@^2.5.3: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -5220,11 +4883,6 @@ dom-serializer@^1.0.1: domhandler "^4.2.0" entities "^2.0.0" -dom-storage@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39" - integrity sha512-g6RpyWXzl0RR6OTElHKBl7nwnK87GUyZMYC7JWsB/IA73vpqK2K6LT39x4VepLxlSsWBFrPVLnsSR5Jyty0+2Q== - domain-browser@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" @@ -6043,13 +5701,6 @@ fastq@^1.6.0: dependencies: reusify "^1.0.4" -faye-websocket@0.11.3: - version "0.11.3" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" - integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== - dependencies: - websocket-driver ">=0.5.1" - faye-websocket@^0.11.3: version "0.11.4" resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" @@ -6186,27 +5837,6 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -firebase@^8.3.0: - version "8.9.1" - resolved "https://registry.yarnpkg.com/firebase/-/firebase-8.9.1.tgz#9b3bc8e69830cd8bcabd3d296068e9d9ff481f35" - integrity sha512-4aKRynB0LSWneYTPwWlAUbcJbgSS11lZRIo9MLNQh1uCo9BxRIYq/r3CCDJOx59tI2nwyv4RXf7hdkgSTF5FYw== - dependencies: - "@firebase/analytics" "0.6.17" - "@firebase/app" "0.6.29" - "@firebase/app-check" "0.3.1" - "@firebase/app-types" "0.6.3" - "@firebase/auth" "0.16.8" - "@firebase/database" "0.10.9" - "@firebase/firestore" "2.3.10" - "@firebase/functions" "0.6.14" - "@firebase/installations" "0.4.31" - "@firebase/messaging" "0.7.15" - "@firebase/performance" "0.4.17" - "@firebase/polyfill" "0.3.36" - "@firebase/remote-config" "0.1.42" - "@firebase/storage" "0.6.2" - "@firebase/util" "1.2.0" - firebaseui@^4.7.1: version "4.8.1" resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-4.8.1.tgz#29ccbc9dfd579c4453725f88e9cf81c8ea62c580" @@ -6411,7 +6041,7 @@ gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== -get-caller-file@^2.0.1, get-caller-file@^2.0.5: +get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== @@ -6934,11 +6564,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -idb@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/idb/-/idb-3.0.2.tgz#c8e9122d5ddd40f13b60ae665e4862f8b13fa384" - integrity sha512-+FLa/0sTXqyux0o6C+i2lOR0VoS60LU/jzUo5xjfY6+7sEEgy4Gz1O7yFBXvjd7N0NyIGWIRg8DcQSLEG+VSPw== - identity-obj-proxy@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14" @@ -8424,11 +8049,6 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.camelcase@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" - integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= - lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" @@ -8489,11 +8109,6 @@ lokijs@^1.5.11: resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0" integrity sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q== -long@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== - loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" @@ -10602,11 +10217,6 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-polyfill@8.1.3: - version "8.1.3" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" - integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== - promise-polyfill@^8.1.3: version "8.2.0" resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0" @@ -10652,25 +10262,6 @@ prop-types@^15.6.2, prop-types@^15.7.2: object-assign "^4.1.1" react-is "^16.8.1" -protobufjs@^6.10.0: - version "6.11.2" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.2.tgz#de39fabd4ed32beaa08e9bb1e30d08544c1edf8b" - integrity sha512-4BQJoPooKJl2G9j3XftkIXjoC9C0Av2NOrWmbLWT1vH32GcSUHjM0Arra6UfTsVyfMAuFzaLucXn1sadxJydAw== - dependencies: - "@protobufjs/aspromise" "^1.1.2" - "@protobufjs/base64" "^1.1.2" - "@protobufjs/codegen" "^2.0.4" - "@protobufjs/eventemitter" "^1.1.0" - "@protobufjs/fetch" "^1.1.0" - "@protobufjs/float" "^1.0.2" - "@protobufjs/inquire" "^1.1.0" - "@protobufjs/path" "^1.1.2" - "@protobufjs/pool" "^1.1.0" - "@protobufjs/utf8" "^1.1.0" - "@types/long" "^4.0.1" - "@types/node" ">=13.7.0" - long "^4.0.0" - protocol-buffers-schema@^3.3.1: version "3.5.2" resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz#38ad35ba768607a5ed2375f8db4c2ecc5ea293c8" @@ -12797,7 +12388,7 @@ tslib@^1.8.1: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.1.0: +tslib@^2.0.3: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -13405,11 +12996,6 @@ whatwg-encoding@^1.0.5: dependencies: iconv-lite "0.4.24" -whatwg-fetch@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz#dde6a5df315f9d39991aa17621853d720b85566f" - integrity sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng== - whatwg-fetch@^3.4.1: version "3.6.2" resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c" @@ -13656,15 +13242,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -13714,11 +13291,6 @@ xmldom@0.1.x: resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff" integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ== -xmlhttprequest@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" - integrity sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw= - xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" @@ -13736,11 +13308,6 @@ y18n@^4.0.0: resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf" integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ== -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" @@ -13772,11 +13339,6 @@ yargs-parser@^18.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.9" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" - integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== - yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" @@ -13810,19 +13372,6 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" -yargs@^16.1.1: - version "16.2.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" - integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== - dependencies: - cliui "^7.0.2" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.0" - y18n "^5.0.5" - yargs-parser "^20.2.2" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"