bare bones working with new auth

This commit is contained in:
lingdocs 2021-08-21 19:55:37 +04:00
parent 47a2482bd3
commit d344b381c6
22 changed files with 318 additions and 1874 deletions

View File

@ -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",

View File

@ -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 <div>
<button data-testid="mockSignInButton" onClick={props.firebaseAuth.signIn}>Sign In</button>
</div>;
});
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<BT.UpgradeUserResponse> => {
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(<BrowserRouter><App /></BrowserRouter>);
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(<BrowserRouter><App /></BrowserRouter>);
await waitFor(() => screen.getByText(/error loading/i));
});
test('renders dictionary loaded', async () => {
render(<BrowserRouter><App /></BrowserRouter>);
await waitFor(() => screen.getByText(/lingdocs pashto dictionary/i));
});
test('searches on type', async () => {
const history = createMemoryHistory();
render(<Router history={history}><App /></Router>);
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(<BrowserRouter><App /></BrowserRouter>);
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(<Router history={history}><App /></Router>);
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(<BrowserRouter><App /></BrowserRouter>);
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(<Router history={history}><App /></Router>);
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(<BrowserRouter><App /></BrowserRouter>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
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(<Router history={history}><App /></Router>);
// 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

View File

@ -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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
}
});
}
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<RouteComponentProps, State> {
});
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
// }
}
private unregisterAuthObserver() {
// will be filled in on mount
}
private wordlistSync: PouchDB.Replication.Sync<any> | undefined = undefined;
private reviewTastsSync: PouchDB.Replication.Sync<any> | undefined = undefined;
private async handleLoadUserInfo(): Promise<BT.CouchDbUser | undefined> {
private async handleLoadUser(): Promise<void> {
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<RouteComponentProps, State> {
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<RouteComponentProps, State> {
{this.state.options.searchType === "alphabetical" && <div className="mt-4 font-weight-light">
<div className="mb-3"><span className="fa fa-book mr-2" ></span> Alphabetical browsing mode</div>
</div>}
{this.state.options.level === "editor" && <div className="mt-4 font-weight-light">
{this.state.user?.level === "editor" && <div className="mt-4 font-weight-light">
<div className="mb-3">Editor priveleges active</div>
<Link to="/edit">
<button className="btn btn-secondary">New Entry</button>
@ -478,7 +410,7 @@ class App extends Component<RouteComponentProps, State> {
<About state={this.state} />
</Route>
<Route path="/settings">
<Options options={this.state.options} optionsDispatch={this.handleOptionsUpdate} />
<Options state={this.state} options={this.state.options} optionsDispatch={this.handleOptionsUpdate} />
</Route>
<Route path="/search">
<Results state={this.state} isolateEntry={this.handleIsolateEntry} />
@ -492,10 +424,7 @@ class App extends Component<RouteComponentProps, State> {
}
</Route>
<Route path="/account">
<Account level={this.state.options.level} loadUserInfo={this.handleLoadUserInfo} handleSignOut={(() => {
this.props.history.replace("/");
auth.signOut();
})} />
<Account user={this.state.user} />
</Route>
<Route path="/word">
<IsolatedEntry
@ -504,21 +433,21 @@ class App extends Component<RouteComponentProps, State> {
isolateEntry={this.handleIsolateEntry}
/>
</Route>
{wordlistEnabled(this.state) && <Route path="/wordlist">
{wordlistEnabled(this.state.user) && <Route path="/wordlist">
<Wordlist
state={this.state}
isolateEntry={this.handleIsolateEntry}
optionsDispatch={this.handleOptionsUpdate}
/>
</Route>}
{this.state.options.level === "editor" && <Route path="/edit">
{this.state.user?.level === "editor" && <Route path="/edit">
<EntryEditor
state={this.state}
dictionary={dictionary}
searchParams={new URLSearchParams(this.props.history.location.search)}
/>
</Route>}
{this.state.options.level === "editor" && <Route path="/review-tasks">
{this.state.user?.level === "editor" && <Route path="/review-tasks">
<ReviewTasks state={this.state} />
</Route>}
</>
@ -534,15 +463,15 @@ class App extends Component<RouteComponentProps, State> {
<div className="buttons-footer">
<BottomNavItem label="About" icon="info-circle" page="/about" />
<BottomNavItem label="Settings" icon="cog" page="/settings" />
<BottomNavItem label={auth.currentUser ? "Account" : "Sign In"} icon="user" page="/account" />
{wordlistEnabled(this.state) &&
<BottomNavItem label={this.state.user ? "Account" : "Sign In"} icon="user" page="/account" />
{wordlistEnabled(this.state.user) &&
<BottomNavItem
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
icon="list"
page="/wordlist"
/>
}
{this.state.options.level === "editor" &&
{this.state.user?.level === "editor" &&
<BottomNavItem
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
icon="edit"

View File

@ -1,56 +1,53 @@
import { auth } from "./firebase";
import * as BT from "./backend-types";
import * as FT from "./functions-types";
import * as AT from "./account-types";
const functionsBaseUrl = // process.env.NODE_ENV === "development"
// "http://127.0.0.1:5001/lingdocs/europe-west1/"
"https://europe-west1-lingdocs.cloudfunctions.net/";
// const functionsBaseUrl = // process.env.NODE_ENV === "development"
// // "http://127.0.0.1:5001/lingdocs/europe-west1/"
// "https://europe-west1-lingdocs.cloudfunctions.net/";
const accountBaseUrl = "https://account.lingdocs.com/api/";
export async function publishDictionary(): Promise<BT.PublishDictionaryResponse> {
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<AT.APIResponse> {
const response = await fetch(accountBaseUrl + url, {
method,
credentials: "include",
});
return await response.json() as AT.APIResponse;
}
export async function publishDictionary(): Promise<FT.PublishDictionaryResponse> {
return {
ok: true,
// @ts-ignore
info: {},
};
}
export async function upgradeAccount(password: string): Promise<FT.UpgradeUserResponse> {
return {
ok: false,
error: "incorrect password",
};
}
export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise<FT.SubmissionsResponse> {
// return await tokenFetch("submissions", "POST", submissions) as FT.SubmissionsResponse;
return {
ok: true,
message: "received",
submissions: [],
}
return res;
}
export async function upgradeAccount(password: string): Promise<BT.UpgradeUserResponse> {
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<BT.SubmissionsResponse> {
return await tokenFetch("submissions", "POST", submissions) as BT.SubmissionsResponse;
}
export async function loadUserInfo(): Promise<undefined | BT.CouchDbUser> {
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<any> {
if (!auth.currentUser) {
throw new Error("not signed in");
}
export async function getUser(): Promise<undefined | AT.LingdocsUser | "offline"> {
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";
}
}

View File

@ -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();

View File

@ -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",

View File

@ -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";
}

View File

@ -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,

View File

@ -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;
}
};

View File

@ -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" }))

View File

@ -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,

View File

@ -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<any>,
};
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<LocalDbType, LocalDb> = {
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<BT.Submission[]>;
export async function getAllDocsLocalDb(type: "submissions", limit?: number): Promise<FT.Submission[]>;
export async function getAllDocsLocalDb(type: "wordlist", limit?: number): Promise<WordlistWordDoc[]>;
export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise<BT.ReviewTask[]>
export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise<BT.Submission[] | WordlistWordDoc[] | BT.ReviewTask[]> {
export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise<FT.ReviewTask[]>
export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise<FT.Submission[] | WordlistWordDoc[] | FT.ReviewTask[]> {
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[];
}
}

View File

@ -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;
}

View File

@ -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 });

View File

@ -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<boolean>(false);
const [showingUpgradePrompt, setShowingUpgradePrompt] = useState<boolean>(false);
const [upgradePassword, setUpgradePassword] = useState<string>("");
const [upgradeError, setUpgradeError] = useState<string>("");
const [accountDeleted, setAccountDeleted] = useState<boolean>(false);
const [accountDeleteError, setAccountDeleteError] = useState<string>("");
const [emailVerification, setEmailVerification] = useState<"unverified" | "sent" | "verified">("verified");
const [waiting, setWaiting] = useState<boolean>(false);
const Account = ({ user }: { user: AT.LingdocsUser | undefined }) => {
// const [showingUpgradePrompt, setShowingUpgradePrompt] = useState<boolean>(false);
// const [upgradePassword, setUpgradePassword] = useState<string>("");
// const [upgradeError, setUpgradeError] = useState<string>("");
// const [waiting, setWaiting] = useState<boolean>(false);
const [publishingStatus, setPublishingStatus] = useState<undefined | "publishing" | any>(undefined);
const [showingPasswordChange, setShowingPasswordChange] = useState<boolean>(false);
const [password, setPassword] = useState<string>("");
const [passwordConfirmed, setPasswordConfirmed] = useState<string>("");
const [passwordError, setPasswordError] = useState<string>("");
const [showingUpdateEmail, setShowingUpdateEmail] = useState<boolean>(false);
const [updateEmailError, setUpdateEmailError] = useState<string>("");
const [newEmail, setNewEmail] = useState<string>("");
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 <div style={{ maxWidth: "30rem"}}>
<Helmet>
<link rel="canonical" href="https://dictionary.lingdocs.com/account" />
<title>Account Deleted - LingDocs Pashto Dictionary</title>
</Helmet>
<div className="alert alert-info my-4" role="alert">
<h4>Your account has been deleted 🙋</h4>
</div>
<Link to="/">
<button className="btn btn-outline-secondary">
<i className="fa fa-sign-out-alt"></i> Home
</button>
</Link>
</div>
}
if (!user) {
return <div className="text-center mt-3">
<Helmet>
@ -151,23 +66,8 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
<title>Sign In - LingDocs Pashto Dictionary</title>
</Helmet>
<h4 className="mb-4">Sign in to be able to suggest words/edits</h4>
<p style={{ margin: "0 auto", maxWidth: "500px"}}><strong>For people who previously signed in with Google.</strong> 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.</p>
<StyledFirebaseAuth uiConfig={authUiConfig}
// callbacks: {
// not using this now because of the doubling down on user email verification
// signInSuccessWithAuthResult: (res: any) => {
// const newUser = res.additionalUserInfo?.isNewUser;
// const emailVerified = res.user.emailVerified;
// if (newUser && !emailVerified) {
// res.user.sendEmailVerification();
// setEmailVerification("sent");
// }
// return false;
// }}
firebaseAuth={auth} />
</div>;
</div>
}
const defaultProviderId = user.providerData[0]?.providerId;
return (
<div style={{ marginBottom: "100px" }}>
<Helmet>
@ -176,7 +76,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
<title>Account - LingDocs Pashto Dictionary</title>
</Helmet>
<h2 className="mb-4">Account</h2>
{level === "editor" &&
{user.level === "editor" &&
<div className="mb-3">
<h4>Editor Tools</h4>
{publishingStatus !== "publishing" &&
@ -197,44 +97,34 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
</div>
}
<div style={{ maxWidth: "35rem" }}>
{user.photoURL && <div className="mb-4 mt-3" style={{ textAlign: "center" }}>
{/* {user.p && <div className="mb-4 mt-3" style={{ textAlign: "center" }}>
<img src={user.photoURL} data-testid="userAvatar" alt="avatar" style={{ borderRadius: "50%", width: "5rem", height: "5rem" }}/>
</div>}
</div>} */}
<div className="card mb-4">
<ul className="list-group list-group-flush">
<li className="list-group-item">Name: {user.displayName}</li>
<li className="list-group-item">Name: {user.name}</li>
<li className="list-group-item">
{user.email && <div className="d-flex justify-content-between align-items-center">
<div>
<div>Email: {user.email}
{emailVerification === "unverified" && <button type="button" onClick={handleVerifyEmail} className="ml-3 btn btn-sm btn-primary">
Verify Email
</button>}
</div>
{emailVerification === "unverified" && <div className="mt-2" style={{ color: "red" }}>
Please Verify Your Email Address
</div>}
{emailVerification === "sent" && <div className="mt-2">
📧 Check your email for the confirmation message
</div>}
<div>Email: {user.email}</div>
</div>
</div>}
</li>
<li className="list-group-item">Account Level: {capitalize(level)}</li>
<li className="list-group-item">Account Level: {capitalize(user.level)}</li>
</ul>
</div>
</div>
<button
{/* <button
type="button"
className="btn btn-secondary mr-3 mb-4"
onClick={handleSignOut}
data-testid="signoutButton"
>
<i className="fa fa-sign-out-alt"></i> Sign Out
</button>
</button> */}
<h4 className="mb-3">Account Admin</h4>
<div className="mb-4">
{level === "basic" && <button
{/* <div className="mb-4">
{user.level === "basic" && <button
type="button"
className="btn btn-outline-secondary mr-3 mb-3"
onClick={() => setShowingUpgradePrompt(true)}
@ -242,53 +132,8 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
>
<i className="fa fa-level-up-alt"></i> Upgrade Account
</button>}
<button
type="button"
className="btn btn-outline-secondary mr-3 mb-3"
onClick={() => setShowingPasswordChange(true)}
>
<i className="fa fa-lock"></i> {!hasPasswordProvider ? "Add" : "Change"} Password
</button>
<button
type="button"
className="btn btn-outline-secondary mr-3 mb-3"
onClick={() => setShowingUpdateEmail(true)}
>
<i className="fa fa-envelope"></i> Update Email
</button>
</div>
<hr className="mb-4" />
<button type="button" className="d-block my-3 btn btn-outline-danger" onClick={() => setShowingDeleteConfirmation(true)}>
<i className="fa fa-trash"></i> Delete Account
</button>
<Modal show={showingDeleteConfirmation} onHide={() => setShowingDeleteConfirmation(false)}>
<Modal.Header closeButton>
<Modal.Title>Delete Account?</Modal.Title>
</Modal.Header>
<Modal.Body>Are your sure you want to delete your account? This can't be undone.</Modal.Body>
{accountDeleteError && <div className="mt-3 alert alert-warning mx-3">
<p>
<strong>{accountDeleteError}</strong>
</p>
<button
type="button"
className="btn btn-secondary d-block my-3"
onClick={handleSignOut}
data-testid="signoutButton"
>
<i className="fa fa-sign-out-alt"></i> Sign Out
</button>
</div>}
<Modal.Footer>
<Button variant="secondary" onClick={() => setShowingDeleteConfirmation(false)}>
No, cancel
</Button>
<Button variant="danger" onClick={handleDelete}>
Yes, delete my account
</Button>
</Modal.Footer>
</Modal>
<Modal show={showingUpgradePrompt} onHide={closeUpgrade}>
</div> */}
{/* <Modal show={showingUpgradePrompt} onHide={closeUpgrade}>
<Modal.Header closeButton>
<Modal.Title>Upgrade Account</Modal.Title>
</Modal.Header>
@ -318,85 +163,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
Upgrade my account
</Button>
</Modal.Footer>
</Modal>
<Modal show={showingPasswordChange} onHide={closePasswordChange}>
<Modal.Header closeButton>
<Modal.Title>{hasPasswordProvider ? "Change" : "Add"} Password</Modal.Title>
</Modal.Header>
{!hasPasswordProvider && <Modal.Body>
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}.
</Modal.Body>}
<div className="form-group px-3">
<label htmlFor="newPassword">New Password:</label>
<input
type="password"
className="form-control mb-2"
id="newPassword"
value={password}
onChange={(e) => {
setPassword(e.target.value);
setPasswordError("");
}}
/>
<label htmlFor="confirmNewPassword">Confirm New Password:</label>
<input
type="password"
className="form-control"
id="confirmNewPassword"
value={passwordConfirmed}
onChange={(e) => {
setPasswordConfirmed(e.target.value);
setPasswordError("");
}}
/>
</div>
{passwordError && <div className="mt-3 alert alert-warning mx-3">
<p>
<strong>{passwordError}</strong>
</p>
</div>}
<Modal.Footer>
{waiting && <LoadingElipses />}
<Button variant="secondary" onClick={closePasswordChange}>
Cancel
</Button>
<Button variant="primary" onClick={handlePasswordChange}>
Change Password
</Button>
</Modal.Footer>
</Modal>
<Modal show={showingUpdateEmail} onHide={closeUpdateEmail}>
<Modal.Header closeButton>
<Modal.Title>Update Email</Modal.Title>
</Modal.Header>
<div className="form-group px-3 mt-3">
<label htmlFor="newEmail">New Email:</label>
<input
type="email"
className="form-control mb-2"
id="newEmail"
value={newEmail}
onChange={(e) => {
setNewEmail(e.target.value);
setUpdateEmailError("");
}}
/>
</div>
{updateEmailError && <div className="mt-3 alert alert-warning mx-3">
<p>
<strong>{updateEmailError}</strong>
</p>
</div>}
<Modal.Footer>
{waiting && <LoadingElipses />}
<Button variant="secondary" onClick={closeUpdateEmail}>
Cancel
</Button>
<Button variant="primary" onClick={handleUpdateEmail}>
Update Email
</Button>
</Modal.Footer>
</Modal>
</Modal> */}
</div>
);
};

View File

@ -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 }: {
</div>
))}
<div className="form-group">
<button type="submit" className="btn btn-primary mr-4" onClick={handleSubmit}>Submit</button>
<button type="button" className="btn btn-danger" onClick={handleDelete}>Delete Entry</button>
<button type="submit" className="ftn ftn-primary mr-4" onClick={handleSubmit}>Submit</button>
<button type="button" className="ftn ftn-danger" onClick={handleDelete}>Delete Entry</button>
{sTs && <div className="ml-3 form-group form-check-inline">
<input
id={"deleteSts"}

View File

@ -7,7 +7,6 @@
*/
import { useEffect, useState } from "react";
import { auth } from "../lib/firebase";
import {
ConjugationViewer,
InflectionsTable,
@ -28,9 +27,7 @@ import {
deleteWordFromWordlist,
hasAttachment,
} from "../lib/wordlist-database";
import {
wordlistEnabled,
} from "../lib/level-management";
import { wordlistEnabled } from "../lib/level-management";
import AudioPlayButton from "../components/AudioPlayButton";
import { Helmet } from "react-helmet";
import { Modal } from "react-bootstrap";
@ -52,12 +49,13 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
const wordlistWord = state.wordlist.find((w) => 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}
/>
</div>
{auth.currentUser &&
{state.user &&
<div className="col-4">
<div className="d-flex flex-row justify-content-end">
{state.options.level === "editor" &&
{state.user.level === "editor" &&
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
<div
className="clickable mr-3"
@ -128,7 +126,7 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
>
<i className="fa fa-pen"></i>
</div>
{wordlistEnabled(state) && <div
{wordlistEnabled(state.user) && <div
className="clickable"
data-testid={wordlistWord ? "fullStarButton" : "emptyStarButton"}
onClick={wordlistWord

View File

@ -129,9 +129,11 @@ const booleanOptions: {
function Options({
options,
state,
optionsDispatch,
}: {
options: Options,
state: State,
optionsDispatch: (action: OptionsAction) => void,
}) {
return <div style={{ maxWidth: "700px", marginBottom: "150px" }}>
@ -152,7 +154,7 @@ function Options({
<td><kbd>ctrl / </kbd> + <kbd>b</kbd></td>
<td>clear search</td>
</tr>
{wordlistEnabled(options.level) && <tr>
{wordlistEnabled(state.user) && <tr>
<td><kbd>ctrl / </kbd> + <kbd>\</kbd></td>
<td>show/hide wordlist</td>
</tr>}
@ -173,7 +175,7 @@ function Options({
handleChange={(p) => optionsDispatch({ type: "changeSearchBarPosition", payload: p as SearchBarPosition })}
/>
<div className="small mt-2">Bottom position doesn't work well with iPhones.</div>
{wordlistEnabled(options.level) && <>
{wordlistEnabled(state.user) && <>
<h4 className="mt-3">Show Number of Wordlist Words for Review</h4>
<ButtonSelect
small

View File

@ -7,7 +7,7 @@
*/
import { useEffect, useState } from "react";
import * as BT from "../lib/backend-types";
import * as FT from "../lib/functions-types";
import {
submissionBase,
addSubmission,
@ -15,7 +15,6 @@ import {
import { isPashtoScript } from "../lib/is-pashto";
import Entry from "../components/Entry";
import { Helmet } from "react-helmet";
import { auth } from "../lib/firebase";
import { allEntries } from "../lib/dictionary";
import {
standardizePashto,
@ -63,16 +62,17 @@ function Results({ state, isolateEntry }: {
}
function submitSuggestion(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
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 }: {
<Helmet>
<title>LingDocs Pashto Dictionary</title>
</Helmet>
{(auth.currentUser && (window.location.pathname !== "/word") && suggestionState === "none" && powerResults === undefined) && <button
{(state.user && (window.location.pathname !== "/word") && suggestionState === "none" && powerResults === undefined) && <button
type="button"
className={`btn btn-outline-secondary bg-white entry-suggestion-button${state.options.searchBarPosition === "bottom" ? " entry-suggestion-button-with-bottom-searchbar" : ""}`}
className={`ftn ftn-outline-secondary bg-white entry-suggestion-button${state.options.searchBarPosition === "bottom" ? " entry-suggestion-button-with-bottom-searchbar" : ""}`}
onClick={startSuggestion}
>
<i className="fas fa-plus" style={{ padding: "3px" }} />
</button>}
{(powerResults === undefined && suggestionState === "none" && window.location.pathname === "/search") && <button
type="button"
className={`btn btn-outline-secondary bg-white conjugation-search-button${state.options.searchBarPosition === "bottom" ? " conjugation-search-button-with-bottom-searchbar" : ""}`}
className={`ftn ftn-outline-secondary bg-white conjugation-search-button${state.options.searchBarPosition === "bottom" ? " conjugation-search-button-with-bottom-searchbar" : ""}`}
onClick={handlePowerSearch}
>
<i className={inflectionSearchIcon} style={{ padding: "3px" }} />
@ -142,7 +142,7 @@ function Results({ state, isolateEntry }: {
isolateEntry={isolateEntry}
/>
))}
{(auth.currentUser && (suggestionState === "editing")) && <div className="my-3">
{(state.user && (suggestionState === "editing")) && <div className="my-3">
<h5 className="mb-3">Suggest an entry for the dictionary:</h5>
<div className="form-group mt-4" style={{ maxWidth: "500px" }}>
<div className="row mb-2">
@ -199,7 +199,7 @@ function Results({ state, isolateEntry }: {
</div>
<button
type="button"
className="btn btn-secondary mr-3"
className="ftn ftn-secondary mr-3"
onClick={submitSuggestion}
data-testid="editWordSubmitButton"
>
@ -207,7 +207,7 @@ function Results({ state, isolateEntry }: {
</button>
<button
type="button"
className="btn btn-outline-secondary"
className="ftn ftn-outline-secondary"
onClick={cancelSuggestion}
data-testid="editWordCancelButton"
>

View File

@ -1,6 +1,6 @@
import Entry from "../components/Entry";
import { Link } from "react-router-dom";
import * as BT from "../lib/backend-types";
import * as FT from "../lib/functions-types";
import {
deleteFromLocalDb,
} from "../lib/pouch-dbs";
@ -9,7 +9,7 @@ import {
} from "@lingdocs/pashto-inflector";
import { Helmet } from "react-helmet";
function ReviewTask({ reviewTask, textOptions }: { reviewTask: BT.ReviewTask, textOptions: T.TextOptions }) {
function ReviewTask({ reviewTask, textOptions }: { reviewTask: FT.ReviewTask, textOptions: T.TextOptions }) {
function handleDelete() {
deleteFromLocalDb("reviewTasks", reviewTask._id);
}
@ -40,7 +40,7 @@ function ReviewTask({ reviewTask, textOptions }: { reviewTask: BT.ReviewTask, te
</div>}
<Entry textOptions={textOptions} entry={reviewTask.entry} />
<div className="mb-2">"{reviewTask.comment}"</div>
<div className="small">{reviewTask.user.displayName} - {reviewTask.user.email}</div>
<div className="small">{reviewTask.user.name} - {reviewTask.user.email}</div>
</div>
</div>
</Link>

View File

@ -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 = {

View File

@ -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"