bare bones working with new auth
This commit is contained in:
parent
47a2482bd3
commit
d344b381c6
|
@ -18,7 +18,6 @@
|
||||||
"classnames": "^2.2.6",
|
"classnames": "^2.2.6",
|
||||||
"cron": "^1.8.2",
|
"cron": "^1.8.2",
|
||||||
"dayjs": "^1.10.4",
|
"dayjs": "^1.10.4",
|
||||||
"firebase": "^8.3.0",
|
|
||||||
"lokijs": "^1.5.11",
|
"lokijs": "^1.5.11",
|
||||||
"mousetrap": "^1.6.5",
|
"mousetrap": "^1.6.5",
|
||||||
"nano": "^9.0.3",
|
"nano": "^9.0.3",
|
||||||
|
|
|
@ -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
|
|
|
@ -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 { Component } from "react";
|
||||||
import { defaultTextOptions } from "@lingdocs/pashto-inflector";
|
import { defaultTextOptions } from "@lingdocs/pashto-inflector";
|
||||||
import { withRouter, Route, RouteComponentProps, Link } from "react-router-dom";
|
import { withRouter, Route, RouteComponentProps, Link } from "react-router-dom";
|
||||||
|
@ -21,32 +24,31 @@ import ReviewTasks from "./screens/ReviewTasks";
|
||||||
import EntryEditor from "./screens/EntryEditor";
|
import EntryEditor from "./screens/EntryEditor";
|
||||||
import IsolatedEntry from "./screens/IsolatedEntry";
|
import IsolatedEntry from "./screens/IsolatedEntry";
|
||||||
import Wordlist from "./screens/Wordlist";
|
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 { dictionary, pageSize } from "./lib/dictionary";
|
||||||
import optionsReducer from "./lib/options-reducer";
|
import optionsReducer from "./lib/options-reducer";
|
||||||
import hitBottom from "./lib/hitBottom";
|
import hitBottom from "./lib/hitBottom";
|
||||||
import getWordId from "./lib/get-word-id";
|
import getWordId from "./lib/get-word-id";
|
||||||
import { auth } from "./lib/firebase";
|
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import {
|
||||||
sendSubmissions,
|
sendSubmissions,
|
||||||
} from "./lib/submissions";
|
} from "./lib/submissions";
|
||||||
import {
|
import {
|
||||||
loadUserInfo,
|
getUser,
|
||||||
} from "./lib/backend-calls";
|
} from "./lib/backend-calls";
|
||||||
import * as BT from "./lib/backend-types";
|
|
||||||
import {
|
import {
|
||||||
getWordlist,
|
getWordlist,
|
||||||
} from "./lib/wordlist-database";
|
} from "./lib/wordlist-database";
|
||||||
import {
|
import {
|
||||||
wordlistEnabled,
|
startLocalDbs,
|
||||||
} from "./lib/level-management";
|
stopLocalDbs,
|
||||||
import {
|
|
||||||
deInitializeLocalDb,
|
|
||||||
initializeLocalDb,
|
|
||||||
startLocalDbSync,
|
|
||||||
getLocalDbName,
|
|
||||||
getAllDocsLocalDb,
|
getAllDocsLocalDb,
|
||||||
} from "./lib/pouch-dbs";
|
} from "./lib/pouch-dbs";
|
||||||
import {
|
import {
|
||||||
|
@ -95,7 +97,6 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
theme: /* istanbul ignore next */ (window.matchMedia &&
|
theme: /* istanbul ignore next */ (window.matchMedia &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
window.matchMedia("(prefers-color-scheme: dark)").matches) ? "dark" : "light",
|
||||||
textOptions: defaultTextOptions,
|
textOptions: defaultTextOptions,
|
||||||
level: "basic",
|
|
||||||
wordlistMode: "browse",
|
wordlistMode: "browse",
|
||||||
wordlistReviewLanguage: "Pashto",
|
wordlistReviewLanguage: "Pashto",
|
||||||
wordlistReviewBadge: true,
|
wordlistReviewBadge: true,
|
||||||
|
@ -107,13 +108,14 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
results: [],
|
results: [],
|
||||||
wordlist: [],
|
wordlist: [],
|
||||||
reviewTasks: [],
|
reviewTasks: [],
|
||||||
|
user: readUser(),
|
||||||
};
|
};
|
||||||
this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this);
|
this.handleOptionsUpdate = this.handleOptionsUpdate.bind(this);
|
||||||
this.handleSearchValueChange = this.handleSearchValueChange.bind(this);
|
this.handleSearchValueChange = this.handleSearchValueChange.bind(this);
|
||||||
this.handleIsolateEntry = this.handleIsolateEntry.bind(this);
|
this.handleIsolateEntry = this.handleIsolateEntry.bind(this);
|
||||||
this.handleScroll = this.handleScroll.bind(this);
|
this.handleScroll = this.handleScroll.bind(this);
|
||||||
this.handleGoBack = this.handleGoBack.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.handleRefreshWordlist = this.handleRefreshWordlist.bind(this);
|
||||||
this.handleRefreshReviewTasks = this.handleRefreshReviewTasks.bind(this);
|
this.handleRefreshReviewTasks = this.handleRefreshReviewTasks.bind(this);
|
||||||
this.handleDictionaryUpdate = this.handleDictionaryUpdate.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)) {
|
if (!possibleLandingPages.includes(this.props.location.pathname)) {
|
||||||
this.props.history.replace("/");
|
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);
|
ReactGA.pageview(window.location.pathname + window.location.search);
|
||||||
}
|
}
|
||||||
dictionary.initialize().then((r) => {
|
dictionary.initialize().then((r) => {
|
||||||
|
@ -133,11 +135,8 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
dictionaryInfo: r.dictionaryInfo,
|
dictionaryInfo: r.dictionaryInfo,
|
||||||
});
|
});
|
||||||
// incase it took forever and timed out - might need to reinitialize the wordlist here ??
|
// incase it took forever and timed out - might need to reinitialize the wordlist here ??
|
||||||
if (wordlistEnabled(this.state)) {
|
if (this.state.user) {
|
||||||
initializeLocalDb("wordlist", this.handleRefreshWordlist, auth.currentUser ? auth.currentUser.uid : undefined);
|
startLocalDbs(this.state.user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks });
|
||||||
}
|
|
||||||
if (this.state.options.level === "editor") {
|
|
||||||
initializeLocalDb("reviewTasks", this.handleRefreshReviewTasks);
|
|
||||||
}
|
}
|
||||||
if (this.props.location.pathname === "/word") {
|
if (this.props.location.pathname === "/word") {
|
||||||
const wordId = getWordId(this.props.location.search);
|
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) => {
|
Mousetrap.bind(["ctrl+down", "ctrl+up", "command+down", "command+up"], (e) => {
|
||||||
if (e.repeat) return;
|
if (e.repeat) return;
|
||||||
this.handleOptionsUpdate({ type: "toggleLanguage" });
|
this.handleOptionsUpdate({ type: "toggleLanguage" });
|
||||||
|
@ -218,7 +191,7 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
});
|
});
|
||||||
Mousetrap.bind(["ctrl+\\", "command+\\"], (e) => {
|
Mousetrap.bind(["ctrl+\\", "command+\\"], (e) => {
|
||||||
if (e.repeat) return;
|
if (e.repeat) return;
|
||||||
if (this.state.options.level === "basic") return;
|
if (this.state.user?.level === "basic") return;
|
||||||
if (this.props.location.pathname !== "/wordlist") {
|
if (this.props.location.pathname !== "/wordlist") {
|
||||||
this.props.history.push("/wordlist");
|
this.props.history.push("/wordlist");
|
||||||
} else {
|
} else {
|
||||||
|
@ -229,14 +202,8 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
window.removeEventListener("scroll", this.handleScroll);
|
window.removeEventListener("scroll", this.handleScroll);
|
||||||
this.unregisterAuthObserver();
|
|
||||||
this.networkCronJob.stop();
|
this.networkCronJob.stop();
|
||||||
if (this.wordlistSync) {
|
stopLocalDbs();
|
||||||
this.wordlistSync.cancel();
|
|
||||||
}
|
|
||||||
if (this.reviewTastsSync) {
|
|
||||||
this.reviewTastsSync.cancel();
|
|
||||||
}
|
|
||||||
Mousetrap.unbind(["ctrl+down", "ctrl+up", "command+down", "command+up"]);
|
Mousetrap.unbind(["ctrl+down", "ctrl+up", "command+down", "command+up"]);
|
||||||
Mousetrap.unbind(["ctrl+b", "command+b"]);
|
Mousetrap.unbind(["ctrl+b", "command+b"]);
|
||||||
Mousetrap.unbind(["ctrl+\\", "command+\\"]);
|
Mousetrap.unbind(["ctrl+\\", "command+\\"]);
|
||||||
|
@ -244,7 +211,7 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
|
|
||||||
public componentDidUpdate(prevProps: RouteComponentProps) {
|
public componentDidUpdate(prevProps: RouteComponentProps) {
|
||||||
if (this.props.location.pathname !== prevProps.location.pathname) {
|
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);
|
ReactGA.pageview(window.location.pathname + window.location.search);
|
||||||
}
|
}
|
||||||
if (this.props.location.pathname === "/") {
|
if (this.props.location.pathname === "/") {
|
||||||
|
@ -256,12 +223,12 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
page: 1,
|
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("/");
|
this.props.history.replace("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (getWordId(this.props.location.search) !== getWordId(prevProps.location.search)) {
|
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);
|
ReactGA.pageview(window.location.pathname + window.location.search);
|
||||||
}
|
}
|
||||||
const wordId = getWordId(this.props.location.search);
|
const wordId = getWordId(this.props.location.search);
|
||||||
|
@ -277,54 +244,19 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
private unregisterAuthObserver() {
|
private async handleLoadUser(): Promise<void> {
|
||||||
// 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> {
|
|
||||||
try {
|
try {
|
||||||
const userInfo = await loadUserInfo();
|
const user = await getUser();
|
||||||
const differentUserInfoLevel = userInfo && (userInfo.level !== this.state.options.level);
|
if (user === "offline") return;
|
||||||
const needToDowngrade = (!userInfo && wordlistEnabled(this.state));
|
this.setState({ user });
|
||||||
if (differentUserInfoLevel || needToDowngrade) {
|
saveUser(user);
|
||||||
this.handleOptionsUpdate({
|
if (user) {
|
||||||
type: "changeUserLevel",
|
startLocalDbs(user, { wordlist: this.handleRefreshWordlist, reviewTasks: this.handleRefreshReviewTasks });
|
||||||
payload: userInfo ? userInfo.level : "basic",
|
} 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) {
|
} catch (err) {
|
||||||
console.error("error checking user level", 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 * * * *", () => {
|
private networkCronJob = new CronJob("1/5 * * * *", () => {
|
||||||
// TODO: check for new dictionary (in a seperate cron job - not dependant on the user being signed in)
|
// TODO: check for new dictionary (in a seperate cron job - not dependant on the user being signed in)
|
||||||
this.handleLoadUserInfo();
|
this.handleLoadUser();
|
||||||
sendSubmissions();
|
sendSubmissions();
|
||||||
this.handleDictionaryUpdate();
|
this.handleDictionaryUpdate();
|
||||||
});
|
});
|
||||||
|
@ -463,7 +395,7 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
{this.state.options.searchType === "alphabetical" && <div className="mt-4 font-weight-light">
|
{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 className="mb-3"><span className="fa fa-book mr-2" ></span> Alphabetical browsing mode</div>
|
||||||
</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>
|
<div className="mb-3">Editor priveleges active</div>
|
||||||
<Link to="/edit">
|
<Link to="/edit">
|
||||||
<button className="btn btn-secondary">New Entry</button>
|
<button className="btn btn-secondary">New Entry</button>
|
||||||
|
@ -478,7 +410,7 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
<About state={this.state} />
|
<About state={this.state} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/settings">
|
<Route path="/settings">
|
||||||
<Options options={this.state.options} optionsDispatch={this.handleOptionsUpdate} />
|
<Options state={this.state} options={this.state.options} optionsDispatch={this.handleOptionsUpdate} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/search">
|
<Route path="/search">
|
||||||
<Results state={this.state} isolateEntry={this.handleIsolateEntry} />
|
<Results state={this.state} isolateEntry={this.handleIsolateEntry} />
|
||||||
|
@ -492,10 +424,7 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
}
|
}
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/account">
|
<Route path="/account">
|
||||||
<Account level={this.state.options.level} loadUserInfo={this.handleLoadUserInfo} handleSignOut={(() => {
|
<Account user={this.state.user} />
|
||||||
this.props.history.replace("/");
|
|
||||||
auth.signOut();
|
|
||||||
})} />
|
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/word">
|
<Route path="/word">
|
||||||
<IsolatedEntry
|
<IsolatedEntry
|
||||||
|
@ -504,21 +433,21 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
isolateEntry={this.handleIsolateEntry}
|
isolateEntry={this.handleIsolateEntry}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
{wordlistEnabled(this.state) && <Route path="/wordlist">
|
{wordlistEnabled(this.state.user) && <Route path="/wordlist">
|
||||||
<Wordlist
|
<Wordlist
|
||||||
state={this.state}
|
state={this.state}
|
||||||
isolateEntry={this.handleIsolateEntry}
|
isolateEntry={this.handleIsolateEntry}
|
||||||
optionsDispatch={this.handleOptionsUpdate}
|
optionsDispatch={this.handleOptionsUpdate}
|
||||||
/>
|
/>
|
||||||
</Route>}
|
</Route>}
|
||||||
{this.state.options.level === "editor" && <Route path="/edit">
|
{this.state.user?.level === "editor" && <Route path="/edit">
|
||||||
<EntryEditor
|
<EntryEditor
|
||||||
state={this.state}
|
state={this.state}
|
||||||
dictionary={dictionary}
|
dictionary={dictionary}
|
||||||
searchParams={new URLSearchParams(this.props.history.location.search)}
|
searchParams={new URLSearchParams(this.props.history.location.search)}
|
||||||
/>
|
/>
|
||||||
</Route>}
|
</Route>}
|
||||||
{this.state.options.level === "editor" && <Route path="/review-tasks">
|
{this.state.user?.level === "editor" && <Route path="/review-tasks">
|
||||||
<ReviewTasks state={this.state} />
|
<ReviewTasks state={this.state} />
|
||||||
</Route>}
|
</Route>}
|
||||||
</>
|
</>
|
||||||
|
@ -534,15 +463,15 @@ class App extends Component<RouteComponentProps, State> {
|
||||||
<div className="buttons-footer">
|
<div className="buttons-footer">
|
||||||
<BottomNavItem label="About" icon="info-circle" page="/about" />
|
<BottomNavItem label="About" icon="info-circle" page="/about" />
|
||||||
<BottomNavItem label="Settings" icon="cog" page="/settings" />
|
<BottomNavItem label="Settings" icon="cog" page="/settings" />
|
||||||
<BottomNavItem label={auth.currentUser ? "Account" : "Sign In"} icon="user" page="/account" />
|
<BottomNavItem label={this.state.user ? "Account" : "Sign In"} icon="user" page="/account" />
|
||||||
{wordlistEnabled(this.state) &&
|
{wordlistEnabled(this.state.user) &&
|
||||||
<BottomNavItem
|
<BottomNavItem
|
||||||
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
|
label={`Wordlist ${this.state.options.wordlistReviewBadge ? textBadge(forReview(this.state.wordlist).length) : ""}`}
|
||||||
icon="list"
|
icon="list"
|
||||||
page="/wordlist"
|
page="/wordlist"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{this.state.options.level === "editor" &&
|
{this.state.user?.level === "editor" &&
|
||||||
<BottomNavItem
|
<BottomNavItem
|
||||||
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
|
label={`Tasks ${textBadge(this.state.reviewTasks.length)}`}
|
||||||
icon="edit"
|
icon="edit"
|
||||||
|
|
|
@ -1,56 +1,53 @@
|
||||||
import { auth } from "./firebase";
|
import * as FT from "./functions-types";
|
||||||
import * as BT from "./backend-types";
|
import * as AT from "./account-types";
|
||||||
|
|
||||||
const functionsBaseUrl = // process.env.NODE_ENV === "development"
|
// const functionsBaseUrl = // process.env.NODE_ENV === "development"
|
||||||
// "http://127.0.0.1:5001/lingdocs/europe-west1/"
|
// // "http://127.0.0.1:5001/lingdocs/europe-west1/"
|
||||||
"https://europe-west1-lingdocs.cloudfunctions.net/";
|
// "https://europe-west1-lingdocs.cloudfunctions.net/";
|
||||||
|
|
||||||
|
const accountBaseUrl = "https://account.lingdocs.com/api/";
|
||||||
|
|
||||||
export async function publishDictionary(): Promise<BT.PublishDictionaryResponse> {
|
async function accountApiFetch(url: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET"): Promise<AT.APIResponse> {
|
||||||
const res = await tokenFetch("publishDictionary");
|
const response = await fetch(accountBaseUrl + url, {
|
||||||
if (!res) {
|
|
||||||
throw new Error("Connection error/offline");
|
|
||||||
}
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const token = await auth.currentUser.getIdToken();
|
|
||||||
const response = await fetch(`${functionsBaseUrl}${subUrl}`, {
|
|
||||||
method,
|
method,
|
||||||
headers: {
|
credentials: "include",
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
...body ? {
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
} : {},
|
|
||||||
});
|
});
|
||||||
return await response.json();
|
return await response.json() as AT.APIResponse;
|
||||||
} catch (err) {
|
}
|
||||||
console.error(err);
|
|
||||||
throw err;
|
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: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getUser(): Promise<undefined | AT.LingdocsUser | "offline"> {
|
||||||
|
try {
|
||||||
|
const response = await accountApiFetch("user");
|
||||||
|
if ("user" in response) {
|
||||||
|
return response.user;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return "offline";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Types as T } from "@lingdocs/pashto-inflector";
|
import { Types as T } from "@lingdocs/pashto-inflector";
|
||||||
|
import * as AT from "./account-types";
|
||||||
|
|
||||||
export type PublishDictionaryResponse = {
|
export type PublishDictionaryResponse = {
|
||||||
ok: true,
|
ok: true,
|
||||||
|
@ -16,20 +17,18 @@ export type PublishDictionaryResponse = {
|
||||||
errors: T.DictionaryEntryError[],
|
errors: T.DictionaryEntryError[],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserInfo = {
|
|
||||||
uid: string,
|
|
||||||
email: string | null,
|
|
||||||
displayName: string | null,
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Submission = Edit | ReviewTask;
|
export type Submission = Edit | ReviewTask;
|
||||||
|
|
||||||
export type Edit = EntryEdit | NewEntry | EntryDeletion
|
export type Edit = EntryEdit | NewEntry | EntryDeletion
|
||||||
|
|
||||||
export type SubmissionBase = {
|
export type SubmissionBase = {
|
||||||
sTs: number,
|
|
||||||
user: UserInfo,
|
|
||||||
_id: string,
|
_id: string,
|
||||||
|
sTs: number,
|
||||||
|
user: {
|
||||||
|
userId: AT.UUID,
|
||||||
|
name: string,
|
||||||
|
email: string,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReviewTask = Issue | EditSuggestion | EntrySuggestion;
|
export type ReviewTask = Issue | EditSuggestion | EntrySuggestion;
|
||||||
|
@ -74,30 +73,6 @@ export type SubmissionsResponse = {
|
||||||
submissions: Submission[],
|
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 = {
|
export type UpgradeUserResponse = {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "incorrect password",
|
error: "incorrect password",
|
|
@ -1,14 +1,6 @@
|
||||||
/**
|
import type { LingdocsUser } from "./account-types";
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function wordlistEnabled(state: State | UserLevel): boolean {
|
export function wordlistEnabled(user: LingdocsUser | undefined): boolean {
|
||||||
const level = (typeof state === "string")
|
if (!user) return false;
|
||||||
? state
|
return user.level !== "basic";
|
||||||
: state.options.level;
|
|
||||||
return level !== "basic";
|
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { saveOptions, readOptions, optionsLocalStorageName } from "./options-storage";
|
import { saveOptions, readOptions, optionsLocalStorageName } from "./local-storage";
|
||||||
import {
|
import {
|
||||||
defaultTextOptions,
|
defaultTextOptions,
|
||||||
} from "@lingdocs/pashto-inflector";
|
} from "@lingdocs/pashto-inflector";
|
||||||
|
@ -16,7 +16,6 @@ const optionsStub: Options = {
|
||||||
searchType: "fuzzy",
|
searchType: "fuzzy",
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
textOptions: defaultTextOptions,
|
textOptions: defaultTextOptions,
|
||||||
level: "student",
|
|
||||||
wordlistMode: "browse",
|
wordlistMode: "browse",
|
||||||
wordlistReviewLanguage: "Pashto",
|
wordlistReviewLanguage: "Pashto",
|
||||||
wordlistReviewBadge: true,
|
wordlistReviewBadge: true,
|
|
@ -6,7 +6,10 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as AT from "./account-types";
|
||||||
|
|
||||||
export const optionsLocalStorageName = "options2";
|
export const optionsLocalStorageName = "options2";
|
||||||
|
export const userLocalStorageName = "user1";
|
||||||
|
|
||||||
export function saveOptions(options: Options): void {
|
export function saveOptions(options: Options): void {
|
||||||
localStorage.setItem(optionsLocalStorageName, JSON.stringify(options));
|
localStorage.setItem(optionsLocalStorageName, JSON.stringify(options));
|
||||||
|
@ -32,3 +35,25 @@ export const readOptions = (): Options | undefined => {
|
||||||
return undefined;
|
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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -6,7 +6,6 @@ const options: Options = {
|
||||||
language: "Pashto",
|
language: "Pashto",
|
||||||
searchType: "fuzzy",
|
searchType: "fuzzy",
|
||||||
theme: "light",
|
theme: "light",
|
||||||
level: "basic",
|
|
||||||
wordlistMode: "browse",
|
wordlistMode: "browse",
|
||||||
wordlistReviewLanguage: "Pashto",
|
wordlistReviewLanguage: "Pashto",
|
||||||
wordlistReviewBadge: true,
|
wordlistReviewBadge: true,
|
||||||
|
@ -33,11 +32,6 @@ test("options reducer should work", () => {
|
||||||
...options,
|
...options,
|
||||||
theme: "dark",
|
theme: "dark",
|
||||||
});
|
});
|
||||||
expect(optionsReducer(options, { type: "changeUserLevel", payload: "student" }))
|
|
||||||
.toEqual({
|
|
||||||
...options,
|
|
||||||
level: "student",
|
|
||||||
});
|
|
||||||
expect(optionsReducer(options, { type: "changeWordlistMode", payload: "review" }))
|
expect(optionsReducer(options, { type: "changeWordlistMode", payload: "review" }))
|
||||||
.toEqual({
|
.toEqual({
|
||||||
...options,
|
...options,
|
||||||
|
@ -66,12 +60,12 @@ test("options reducer should work", () => {
|
||||||
pTextSize: "largest",
|
pTextSize: "largest",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(optionsReducer(options, { type: "changeSpelling", payload: "Pakistani" }))
|
expect(optionsReducer(options, { type: "changeSpelling", payload: "Pakistani ی" }))
|
||||||
.toEqual({
|
.toEqual({
|
||||||
...options,
|
...options,
|
||||||
textOptions: {
|
textOptions: {
|
||||||
...defaultTextOptions,
|
...defaultTextOptions,
|
||||||
spelling: "Pakistani",
|
spelling: "Pakistani ی",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(optionsReducer(options, { type: "changePhonetics", payload: "ipa" }))
|
expect(optionsReducer(options, { type: "changePhonetics", payload: "ipa" }))
|
||||||
|
|
|
@ -23,12 +23,6 @@ function optionsReducer(options: Options, action: OptionsAction): Options {
|
||||||
searchBarPosition: action.payload,
|
searchBarPosition: action.payload,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (action.type === "changeUserLevel") {
|
|
||||||
return {
|
|
||||||
...options,
|
|
||||||
level: action.payload,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (action.type === "changeWordlistMode") {
|
if (action.type === "changeWordlistMode") {
|
||||||
return {
|
return {
|
||||||
...options,
|
...options,
|
||||||
|
|
|
@ -1,73 +1,110 @@
|
||||||
import PouchDB from "pouchdb";
|
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 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 DbInput = {
|
||||||
type: "wordlist",
|
type: "wordlist",
|
||||||
doc: WordlistWord,
|
doc: WordlistWord,
|
||||||
} | {
|
} | {
|
||||||
type: "submissions",
|
type: "submissions",
|
||||||
doc: BT.Submission,
|
doc: FT.Submission,
|
||||||
} | {
|
} | {
|
||||||
type: "reviewTasks",
|
type: "reviewTasks",
|
||||||
doc: BT.ReviewTask,
|
doc: FT.ReviewTask,
|
||||||
};
|
};
|
||||||
|
|
||||||
const dbs: Record<LocalDbType, LocalDb> = {
|
const dbs: DBS = {
|
||||||
/* for anyone logged in - for edits/suggestions submissions */
|
/* for anyone logged in - for edits/suggestions submissions */
|
||||||
submissions: null,
|
submissions: undefined,
|
||||||
/* for students and above - personal wordlist database */
|
/* for students and above - personal wordlist database */
|
||||||
wordlist: null,
|
wordlist: undefined,
|
||||||
/* for editors only - edits/suggestions (submissions) for review */
|
/* 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"
|
const name = type === "wordlist"
|
||||||
? `userdb-${uid? stringToHex(uid) : "guest"}`
|
? `userdb-${stringToHex(user.userId)}`
|
||||||
: type === "submissions"
|
: type === "submissions"
|
||||||
? "submissions"
|
? "submissions"
|
||||||
: "review-tasks";
|
: "review-tasks";
|
||||||
const db = dbs[type];
|
const db = dbs[type];
|
||||||
// only initialize the db if it doesn't exist or if it has a different name
|
// only initialize the db if it doesn't exist or if it has a different name
|
||||||
if ((!db) || (db.db?.name !== name)) {
|
if ((!db) || (db.db?.name !== name)) {
|
||||||
|
if (type === "submissions") {
|
||||||
dbs[type] = {
|
dbs[type] = {
|
||||||
db: new PouchDB(name),
|
|
||||||
refresh,
|
refresh,
|
||||||
|
db: new PouchDB(name),
|
||||||
};
|
};
|
||||||
refresh();
|
} else {
|
||||||
}
|
dbs[type]?.sync.cancel();
|
||||||
}
|
const db = new PouchDB(name);
|
||||||
|
const pass = "userDbPassword" in user ? user.userDbPassword : "";
|
||||||
export function getLocalDbName(type: LocalDbType) {
|
dbs[type] = {
|
||||||
return dbs[type]?.db.name;
|
db,
|
||||||
}
|
refresh,
|
||||||
|
sync: db.sync(
|
||||||
export function deInitializeLocalDb(type: LocalDbType) {
|
`https://${user.userId}:${pass}@couch.lingdocs.com/${name}`,
|
||||||
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 },
|
{ live: true, retry: true },
|
||||||
).on("change", (info) => {
|
).on("change", (info) => {
|
||||||
if (info.direction === "pull") {
|
if (info.direction === "pull") {
|
||||||
localDb.refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
}).on("error", (error) => {
|
}).on("error", (error) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
});
|
}),
|
||||||
return sync;
|
};
|
||||||
|
}
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addToLocalDb({ type, doc }: DbInput) {
|
export async function addToLocalDb({ type, doc }: DbInput) {
|
||||||
|
@ -99,10 +136,10 @@ export async function updateLocalDbDoc({ type, doc }: DbInput, id: string) {
|
||||||
return updated;
|
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: "wordlist", limit?: number): Promise<WordlistWordDoc[]>;
|
||||||
export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise<BT.ReviewTask[]>
|
export async function getAllDocsLocalDb(type: "reviewTasks", limit?: number): Promise<FT.ReviewTask[]>
|
||||||
export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise<BT.Submission[] | WordlistWordDoc[] | BT.ReviewTask[]> {
|
export async function getAllDocsLocalDb(type: LocalDbType, limit?: number): Promise<FT.Submission[] | WordlistWordDoc[] | FT.ReviewTask[]> {
|
||||||
const localDb = dbs[type];
|
const localDb = dbs[type];
|
||||||
if (!localDb) {
|
if (!localDb) {
|
||||||
throw new Error(`unable to get all docs from ${type} database - not initialized`);
|
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;
|
const docs = result.rows.map((row) => row.doc) as unknown;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "submissions":
|
case "submissions":
|
||||||
return docs as BT.Submission[];
|
return docs as FT.Submission[];
|
||||||
case "wordlist":
|
case "wordlist":
|
||||||
return docs as WordlistWordDoc[];
|
return docs as WordlistWordDoc[];
|
||||||
case "reviewTasks":
|
case "reviewTasks":
|
||||||
return docs as BT.ReviewTask[];
|
return docs as FT.ReviewTask[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ function fFuzzy(f: string): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue: string): { entry: T.DictionaryEntry, results: InflectionSearchResult[] }[] {
|
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 beg = fFuzzy(searchValue.slice(0, 2));
|
||||||
const preSearchFun = isPashtoScript(searchValue)
|
const preSearchFun = isPashtoScript(searchValue)
|
||||||
? (ps: T.PsString) => ps.p.slice(0, 2) === beg
|
? (ps: T.PsString) => ps.p.slice(0, 2) === beg
|
||||||
|
@ -37,7 +37,7 @@ export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue:
|
||||||
const searchFun = isPashtoScript(searchValue)
|
const searchFun = isPashtoScript(searchValue)
|
||||||
? (ps: T.PsString) => ps.p === searchValue
|
? (ps: T.PsString) => ps.p === searchValue
|
||||||
: (ps: T.PsString) => !!ps.f.match(fRegex);
|
: (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 results = allDocs.reduce((all: { entry: T.DictionaryEntry, results: InflectionSearchResult[] }[], entry) => {
|
||||||
const type = isNounAdjOrVerb(entry);
|
const type = isNounAdjOrVerb(entry);
|
||||||
if (entry.c && type === "verb") {
|
if (entry.c && type === "verb") {
|
||||||
|
@ -74,6 +74,6 @@ export function searchAllInflections(allDocs: T.DictionaryEntry[], searchValue:
|
||||||
}
|
}
|
||||||
return all;
|
return all;
|
||||||
}, []);
|
}, []);
|
||||||
console.timeEnd(timerLabel);
|
// console.timeEnd(timerLabel);
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
|
@ -1,28 +1,22 @@
|
||||||
import * as BT from "./backend-types";
|
import * as FT from "./functions-types";
|
||||||
import { auth } from "./firebase";
|
import * as AT from "./account-types";
|
||||||
import {
|
import {
|
||||||
postSubmissions,
|
postSubmissions,
|
||||||
} from "./backend-calls";
|
} from "./backend-calls";
|
||||||
import {
|
import {
|
||||||
initializeLocalDb,
|
|
||||||
addToLocalDb,
|
addToLocalDb,
|
||||||
getAllDocsLocalDb,
|
getAllDocsLocalDb,
|
||||||
deleteFromLocalDb,
|
deleteFromLocalDb,
|
||||||
} from "./pouch-dbs";
|
} from "./pouch-dbs";
|
||||||
|
|
||||||
initializeLocalDb("submissions", () => null);
|
export function submissionBase(user: AT.LingdocsUser): FT.SubmissionBase {
|
||||||
|
|
||||||
export function submissionBase(): BT.SubmissionBase {
|
|
||||||
if (!auth.currentUser) {
|
|
||||||
throw new Error("not signed in");
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
sTs: Date.now(),
|
sTs: Date.now(),
|
||||||
_id: new Date().toJSON(),
|
_id: new Date().toJSON(),
|
||||||
user: {
|
user: {
|
||||||
uid: auth.currentUser.uid,
|
name: user.name,
|
||||||
email: auth.currentUser.email,
|
email: user.email || "",
|
||||||
displayName: auth.currentUser.displayName,
|
userId: user.userId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -48,8 +42,8 @@ export async function sendSubmissions() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addSubmission(submission: BT.Submission, level: BT.UserLevel) {
|
export async function addSubmission(submission: FT.Submission, user: AT.LingdocsUser) {
|
||||||
if (level === "editor" && (submission.type === "issue" || submission.type === "entry suggestion" || submission.type === "edit suggestion")) {
|
if (user.level === "editor" && (submission.type === "issue" || submission.type === "entry suggestion" || submission.type === "edit suggestion")) {
|
||||||
await addToLocalDb({ type: "reviewTasks", doc: submission })
|
await addToLocalDb({ type: "reviewTasks", doc: submission })
|
||||||
} else {
|
} else {
|
||||||
await addToLocalDb({ type: "submissions", doc: submission });
|
await addToLocalDb({ type: "submissions", doc: submission });
|
||||||
|
|
|
@ -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 {
|
import {
|
||||||
upgradeAccount,
|
useState,
|
||||||
|
// useEffect,
|
||||||
|
} from "react";
|
||||||
|
// import { Modal, Button } from "react-bootstrap";
|
||||||
|
import {
|
||||||
|
// upgradeAccount,
|
||||||
publishDictionary,
|
publishDictionary,
|
||||||
} from "../lib/backend-calls";
|
} from "../lib/backend-calls";
|
||||||
import LoadingElipses from "../components/LoadingElipses";
|
// import LoadingElipses from "../components/LoadingElipses";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
|
import * as AT from "../lib/account-types";
|
||||||
|
|
||||||
const capitalize = (s: string): string => {
|
const capitalize = (s: string): string => {
|
||||||
// if (!s) return "";
|
// if (!s) return "";
|
||||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Account = ({ handleSignOut, level, loadUserInfo }: {
|
const Account = ({ user }: { user: AT.LingdocsUser | undefined }) => {
|
||||||
handleSignOut: () => void,
|
// const [showingUpgradePrompt, setShowingUpgradePrompt] = useState<boolean>(false);
|
||||||
loadUserInfo: () => void,
|
// const [upgradePassword, setUpgradePassword] = useState<string>("");
|
||||||
level: UserLevel,
|
// const [upgradeError, setUpgradeError] = useState<string>("");
|
||||||
}) => {
|
// const [waiting, setWaiting] = useState<boolean>(false);
|
||||||
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 [publishingStatus, setPublishingStatus] = useState<undefined | "publishing" | any>(undefined);
|
const [publishingStatus, setPublishingStatus] = useState<undefined | "publishing" | any>(undefined);
|
||||||
const [showingPasswordChange, setShowingPasswordChange] = useState<boolean>(false);
|
// useEffect(() => {
|
||||||
const [password, setPassword] = useState<string>("");
|
// setShowingUpgradePrompt(false);
|
||||||
const [passwordConfirmed, setPasswordConfirmed] = useState<string>("");
|
// setUpgradePassword("");
|
||||||
const [passwordError, setPasswordError] = useState<string>("");
|
// setUpgradeError("");
|
||||||
const [showingUpdateEmail, setShowingUpdateEmail] = useState<boolean>(false);
|
// setWaiting(false);
|
||||||
const [updateEmailError, setUpdateEmailError] = useState<string>("");
|
// }, []);
|
||||||
const [newEmail, setNewEmail] = useState<string>("");
|
// function closeUpgrade() {
|
||||||
const user = auth.currentUser;
|
// setShowingUpgradePrompt(false);
|
||||||
const hasPasswordProvider = user?.providerData?.some((d) => d?.providerId === "password");
|
// setUpgradePassword("");
|
||||||
useEffect(() => {
|
// setUpgradeError("");
|
||||||
setShowingDeleteConfirmation(false);
|
// }
|
||||||
setShowingUpgradePrompt(false);
|
// async function handleUpgrade() {
|
||||||
setUpgradePassword("");
|
// setUpgradeError("");
|
||||||
setUpgradeError("");
|
// setWaiting(true);
|
||||||
setWaiting(false);
|
// upgradeAccount(upgradePassword).then((res) => {
|
||||||
}, []);
|
// setWaiting(false);
|
||||||
useEffect(() => {
|
// if (res.ok) {
|
||||||
setEmailVerification((user && user.emailVerified) ? "verified" : "unverified");
|
// loadUserInfo();
|
||||||
}, [user]);
|
// closeUpgrade();
|
||||||
function handleDelete() {
|
// } else {
|
||||||
auth.currentUser?.delete().then(() => {
|
// setUpgradeError("Incorrect password");
|
||||||
setAccountDeleteError("");
|
// }
|
||||||
setShowingDeleteConfirmation(false);
|
// }).catch((err) => {
|
||||||
setAccountDeleted(true);
|
// setWaiting(false);
|
||||||
}).catch((err) => {
|
// setUpgradeError(err.message);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function handlePublish() {
|
function handlePublish() {
|
||||||
setPublishingStatus("publishing");
|
setPublishingStatus("publishing");
|
||||||
publishDictionary().then((response) => {
|
publishDictionary().then((response) => {
|
||||||
|
@ -93,56 +58,6 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
setPublishingStatus("Offline or connection error");
|
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) {
|
if (!user) {
|
||||||
return <div className="text-center mt-3">
|
return <div className="text-center mt-3">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -151,23 +66,8 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
<title>Sign In - LingDocs Pashto Dictionary</title>
|
<title>Sign In - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<h4 className="mb-4">Sign in to be able to suggest words/edits</h4>
|
<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>
|
</div>
|
||||||
<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>;
|
|
||||||
}
|
}
|
||||||
const defaultProviderId = user.providerData[0]?.providerId;
|
|
||||||
return (
|
return (
|
||||||
<div style={{ marginBottom: "100px" }}>
|
<div style={{ marginBottom: "100px" }}>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
|
@ -176,7 +76,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
<title>Account - LingDocs Pashto Dictionary</title>
|
<title>Account - LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<h2 className="mb-4">Account</h2>
|
<h2 className="mb-4">Account</h2>
|
||||||
{level === "editor" &&
|
{user.level === "editor" &&
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<h4>Editor Tools</h4>
|
<h4>Editor Tools</h4>
|
||||||
{publishingStatus !== "publishing" &&
|
{publishingStatus !== "publishing" &&
|
||||||
|
@ -197,44 +97,34 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<div style={{ maxWidth: "35rem" }}>
|
<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" }}/>
|
<img src={user.photoURL} data-testid="userAvatar" alt="avatar" style={{ borderRadius: "50%", width: "5rem", height: "5rem" }}/>
|
||||||
</div>}
|
</div>} */}
|
||||||
<div className="card mb-4">
|
<div className="card mb-4">
|
||||||
<ul className="list-group list-group-flush">
|
<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">
|
<li className="list-group-item">
|
||||||
{user.email && <div className="d-flex justify-content-between align-items-center">
|
{user.email && <div className="d-flex justify-content-between align-items-center">
|
||||||
<div>
|
<div>
|
||||||
<div>Email: {user.email}
|
<div>Email: {user.email}</div>
|
||||||
{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>
|
</div>
|
||||||
</div>}
|
</div>}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-group-item">Account Level: {capitalize(level)}</li>
|
<li className="list-group-item">Account Level: {capitalize(user.level)}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
{/* <button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary mr-3 mb-4"
|
className="btn btn-secondary mr-3 mb-4"
|
||||||
onClick={handleSignOut}
|
onClick={handleSignOut}
|
||||||
data-testid="signoutButton"
|
data-testid="signoutButton"
|
||||||
>
|
>
|
||||||
<i className="fa fa-sign-out-alt"></i> Sign Out
|
<i className="fa fa-sign-out-alt"></i> Sign Out
|
||||||
</button>
|
</button> */}
|
||||||
<h4 className="mb-3">Account Admin</h4>
|
<h4 className="mb-3">Account Admin</h4>
|
||||||
<div className="mb-4">
|
{/* <div className="mb-4">
|
||||||
{level === "basic" && <button
|
{user.level === "basic" && <button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-secondary mr-3 mb-3"
|
className="btn btn-outline-secondary mr-3 mb-3"
|
||||||
onClick={() => setShowingUpgradePrompt(true)}
|
onClick={() => setShowingUpgradePrompt(true)}
|
||||||
|
@ -242,53 +132,8 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
>
|
>
|
||||||
<i className="fa fa-level-up-alt"></i> Upgrade Account
|
<i className="fa fa-level-up-alt"></i> Upgrade Account
|
||||||
</button>}
|
</button>}
|
||||||
<button
|
</div> */}
|
||||||
type="button"
|
{/* <Modal show={showingUpgradePrompt} onHide={closeUpgrade}>
|
||||||
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}>
|
|
||||||
<Modal.Header closeButton>
|
<Modal.Header closeButton>
|
||||||
<Modal.Title>Upgrade Account</Modal.Title>
|
<Modal.Title>Upgrade Account</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
|
@ -318,85 +163,7 @@ const Account = ({ handleSignOut, level, loadUserInfo }: {
|
||||||
Upgrade my account
|
Upgrade my account
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</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>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
validateEntry,
|
validateEntry,
|
||||||
} from "@lingdocs/pashto-inflector";
|
} from "@lingdocs/pashto-inflector";
|
||||||
import Entry from "../components/Entry";
|
import Entry from "../components/Entry";
|
||||||
import * as BT from "../lib/backend-types";
|
import * as FT from "../lib/functions-types";
|
||||||
import {
|
import {
|
||||||
submissionBase,
|
submissionBase,
|
||||||
addSubmission,
|
addSubmission,
|
||||||
|
@ -136,18 +136,20 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function handleDelete() {
|
function handleDelete() {
|
||||||
const submission: BT.EntryDeletion = {
|
if (!state.user) return;
|
||||||
...submissionBase(),
|
const submission: FT.EntryDeletion = {
|
||||||
|
...submissionBase(state.user),
|
||||||
type: "entry deletion",
|
type: "entry deletion",
|
||||||
ts: entry.ts,
|
ts: entry.ts,
|
||||||
};
|
};
|
||||||
addSubmission(submission, state.options.level);
|
addSubmission(submission, state.user);
|
||||||
setDeleted(true);
|
setDeleted(true);
|
||||||
}
|
}
|
||||||
function handleSubmit(e: any) {
|
function handleSubmit(e: any) {
|
||||||
setErroneousFields([]);
|
setErroneousFields([]);
|
||||||
setErrors([]);
|
setErrors([]);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!state.user) return;
|
||||||
const result = validateEntry(entry);
|
const result = validateEntry(entry);
|
||||||
if ("errors" in result) {
|
if ("errors" in result) {
|
||||||
setErroneousFields(result.erroneousFields);
|
setErroneousFields(result.erroneousFields);
|
||||||
|
@ -155,12 +157,12 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: Check complement if checkComplement
|
// TODO: Check complement if checkComplement
|
||||||
const submission: BT.NewEntry | BT.EntryEdit = {
|
const submission: FT.NewEntry | FT.EntryEdit = {
|
||||||
...submissionBase(),
|
...submissionBase(state.user),
|
||||||
type: entry.ts === 1 ? "new entry" : "entry edit",
|
type: entry.ts === 1 ? "new entry" : "entry edit",
|
||||||
entry: { ...entry, ts: entry.ts === 1 ? Date.now() : entry.ts },
|
entry: { ...entry, ts: entry.ts === 1 ? Date.now() : entry.ts },
|
||||||
};
|
};
|
||||||
addSubmission(submission, state.options.level);
|
addSubmission(submission, state.user);
|
||||||
setSubmitted(true);
|
setSubmitted(true);
|
||||||
// TODO: Remove from suggestions
|
// TODO: Remove from suggestions
|
||||||
// if (willDeleteSuggestion && sTs) {
|
// if (willDeleteSuggestion && sTs) {
|
||||||
|
@ -306,8 +308,8 @@ function EntryEditor({ state, dictionary, searchParams }: {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<button type="submit" className="btn btn-primary mr-4" onClick={handleSubmit}>Submit</button>
|
<button type="submit" className="ftn ftn-primary mr-4" onClick={handleSubmit}>Submit</button>
|
||||||
<button type="button" className="btn btn-danger" onClick={handleDelete}>Delete Entry</button>
|
<button type="button" className="ftn ftn-danger" onClick={handleDelete}>Delete Entry</button>
|
||||||
{sTs && <div className="ml-3 form-group form-check-inline">
|
{sTs && <div className="ml-3 form-group form-check-inline">
|
||||||
<input
|
<input
|
||||||
id={"deleteSts"}
|
id={"deleteSts"}
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { auth } from "../lib/firebase";
|
|
||||||
import {
|
import {
|
||||||
ConjugationViewer,
|
ConjugationViewer,
|
||||||
InflectionsTable,
|
InflectionsTable,
|
||||||
|
@ -28,9 +27,7 @@ import {
|
||||||
deleteWordFromWordlist,
|
deleteWordFromWordlist,
|
||||||
hasAttachment,
|
hasAttachment,
|
||||||
} from "../lib/wordlist-database";
|
} from "../lib/wordlist-database";
|
||||||
import {
|
import { wordlistEnabled } from "../lib/level-management";
|
||||||
wordlistEnabled,
|
|
||||||
} from "../lib/level-management";
|
|
||||||
import AudioPlayButton from "../components/AudioPlayButton";
|
import AudioPlayButton from "../components/AudioPlayButton";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { Modal } from "react-bootstrap";
|
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);
|
const wordlistWord = state.wordlist.find((w) => w.entry.ts === state.isolatedEntry?.ts);
|
||||||
function submitEdit() {
|
function submitEdit() {
|
||||||
if (!state.isolatedEntry) return;
|
if (!state.isolatedEntry) return;
|
||||||
|
if (!state.user) return;
|
||||||
addSubmission({
|
addSubmission({
|
||||||
...submissionBase(),
|
...submissionBase(state.user),
|
||||||
type: "edit suggestion",
|
type: "edit suggestion",
|
||||||
entry: state.isolatedEntry,
|
entry: state.isolatedEntry,
|
||||||
comment,
|
comment,
|
||||||
}, state.options.level);
|
}, state.user);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
setComment("");
|
setComment("");
|
||||||
setEditSubmitted(true);
|
setEditSubmitted(true);
|
||||||
|
@ -108,10 +106,10 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
||||||
isolateEntry={isolateEntry}
|
isolateEntry={isolateEntry}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{auth.currentUser &&
|
{state.user &&
|
||||||
<div className="col-4">
|
<div className="col-4">
|
||||||
<div className="d-flex flex-row justify-content-end">
|
<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">
|
<Link to={`/edit?id=${entry.ts}`} className="plain-link">
|
||||||
<div
|
<div
|
||||||
className="clickable mr-3"
|
className="clickable mr-3"
|
||||||
|
@ -128,7 +126,7 @@ function IsolatedEntry({ state, dictionary, isolateEntry }: {
|
||||||
>
|
>
|
||||||
<i className="fa fa-pen"></i>
|
<i className="fa fa-pen"></i>
|
||||||
</div>
|
</div>
|
||||||
{wordlistEnabled(state) && <div
|
{wordlistEnabled(state.user) && <div
|
||||||
className="clickable"
|
className="clickable"
|
||||||
data-testid={wordlistWord ? "fullStarButton" : "emptyStarButton"}
|
data-testid={wordlistWord ? "fullStarButton" : "emptyStarButton"}
|
||||||
onClick={wordlistWord
|
onClick={wordlistWord
|
||||||
|
|
|
@ -129,9 +129,11 @@ const booleanOptions: {
|
||||||
|
|
||||||
function Options({
|
function Options({
|
||||||
options,
|
options,
|
||||||
|
state,
|
||||||
optionsDispatch,
|
optionsDispatch,
|
||||||
}: {
|
}: {
|
||||||
options: Options,
|
options: Options,
|
||||||
|
state: State,
|
||||||
optionsDispatch: (action: OptionsAction) => void,
|
optionsDispatch: (action: OptionsAction) => void,
|
||||||
}) {
|
}) {
|
||||||
return <div style={{ maxWidth: "700px", marginBottom: "150px" }}>
|
return <div style={{ maxWidth: "700px", marginBottom: "150px" }}>
|
||||||
|
@ -152,7 +154,7 @@ function Options({
|
||||||
<td><kbd>ctrl / ⌘</kbd> + <kbd>b</kbd></td>
|
<td><kbd>ctrl / ⌘</kbd> + <kbd>b</kbd></td>
|
||||||
<td>clear search</td>
|
<td>clear search</td>
|
||||||
</tr>
|
</tr>
|
||||||
{wordlistEnabled(options.level) && <tr>
|
{wordlistEnabled(state.user) && <tr>
|
||||||
<td><kbd>ctrl / ⌘</kbd> + <kbd>\</kbd></td>
|
<td><kbd>ctrl / ⌘</kbd> + <kbd>\</kbd></td>
|
||||||
<td>show/hide wordlist</td>
|
<td>show/hide wordlist</td>
|
||||||
</tr>}
|
</tr>}
|
||||||
|
@ -173,7 +175,7 @@ function Options({
|
||||||
handleChange={(p) => optionsDispatch({ type: "changeSearchBarPosition", payload: p as SearchBarPosition })}
|
handleChange={(p) => optionsDispatch({ type: "changeSearchBarPosition", payload: p as SearchBarPosition })}
|
||||||
/>
|
/>
|
||||||
<div className="small mt-2">Bottom position doesn't work well with iPhones.</div>
|
<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>
|
<h4 className="mt-3">Show Number of Wordlist Words for Review</h4>
|
||||||
<ButtonSelect
|
<ButtonSelect
|
||||||
small
|
small
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import * as BT from "../lib/backend-types";
|
import * as FT from "../lib/functions-types";
|
||||||
import {
|
import {
|
||||||
submissionBase,
|
submissionBase,
|
||||||
addSubmission,
|
addSubmission,
|
||||||
|
@ -15,7 +15,6 @@ import {
|
||||||
import { isPashtoScript } from "../lib/is-pashto";
|
import { isPashtoScript } from "../lib/is-pashto";
|
||||||
import Entry from "../components/Entry";
|
import Entry from "../components/Entry";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { auth } from "../lib/firebase";
|
|
||||||
import { allEntries } from "../lib/dictionary";
|
import { allEntries } from "../lib/dictionary";
|
||||||
import {
|
import {
|
||||||
standardizePashto,
|
standardizePashto,
|
||||||
|
@ -63,16 +62,17 @@ function Results({ state, isolateEntry }: {
|
||||||
}
|
}
|
||||||
function submitSuggestion(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
function submitSuggestion(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
if (!state.user) return;
|
||||||
const p = pashto;
|
const p = pashto;
|
||||||
const f = phonetics;
|
const f = phonetics;
|
||||||
const e = english;
|
const e = english;
|
||||||
const newEntry: BT.EntrySuggestion = {
|
const newEntry: FT.EntrySuggestion = {
|
||||||
...submissionBase(),
|
...submissionBase(state.user),
|
||||||
type: "entry suggestion",
|
type: "entry suggestion",
|
||||||
entry: { ts: 0, i: 0, p, f, g: "", e },
|
entry: { ts: 0, i: 0, p, f, g: "", e },
|
||||||
comment,
|
comment,
|
||||||
};
|
};
|
||||||
addSubmission(newEntry, state.options.level);
|
addSubmission(newEntry, state.user);
|
||||||
setSuggestionState("received");
|
setSuggestionState("received");
|
||||||
}
|
}
|
||||||
function handlePowerSearch() {
|
function handlePowerSearch() {
|
||||||
|
@ -91,16 +91,16 @@ function Results({ state, isolateEntry }: {
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>LingDocs Pashto Dictionary</title>
|
<title>LingDocs Pashto Dictionary</title>
|
||||||
</Helmet>
|
</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"
|
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}
|
onClick={startSuggestion}
|
||||||
>
|
>
|
||||||
<i className="fas fa-plus" style={{ padding: "3px" }} />
|
<i className="fas fa-plus" style={{ padding: "3px" }} />
|
||||||
</button>}
|
</button>}
|
||||||
{(powerResults === undefined && suggestionState === "none" && window.location.pathname === "/search") && <button
|
{(powerResults === undefined && suggestionState === "none" && window.location.pathname === "/search") && <button
|
||||||
type="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}
|
onClick={handlePowerSearch}
|
||||||
>
|
>
|
||||||
<i className={inflectionSearchIcon} style={{ padding: "3px" }} />
|
<i className={inflectionSearchIcon} style={{ padding: "3px" }} />
|
||||||
|
@ -142,7 +142,7 @@ function Results({ state, isolateEntry }: {
|
||||||
isolateEntry={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>
|
<h5 className="mb-3">Suggest an entry for the dictionary:</h5>
|
||||||
<div className="form-group mt-4" style={{ maxWidth: "500px" }}>
|
<div className="form-group mt-4" style={{ maxWidth: "500px" }}>
|
||||||
<div className="row mb-2">
|
<div className="row mb-2">
|
||||||
|
@ -199,7 +199,7 @@ function Results({ state, isolateEntry }: {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary mr-3"
|
className="ftn ftn-secondary mr-3"
|
||||||
onClick={submitSuggestion}
|
onClick={submitSuggestion}
|
||||||
data-testid="editWordSubmitButton"
|
data-testid="editWordSubmitButton"
|
||||||
>
|
>
|
||||||
|
@ -207,7 +207,7 @@ function Results({ state, isolateEntry }: {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-outline-secondary"
|
className="ftn ftn-outline-secondary"
|
||||||
onClick={cancelSuggestion}
|
onClick={cancelSuggestion}
|
||||||
data-testid="editWordCancelButton"
|
data-testid="editWordCancelButton"
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Entry from "../components/Entry";
|
import Entry from "../components/Entry";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import * as BT from "../lib/backend-types";
|
import * as FT from "../lib/functions-types";
|
||||||
import {
|
import {
|
||||||
deleteFromLocalDb,
|
deleteFromLocalDb,
|
||||||
} from "../lib/pouch-dbs";
|
} from "../lib/pouch-dbs";
|
||||||
|
@ -9,7 +9,7 @@ import {
|
||||||
} from "@lingdocs/pashto-inflector";
|
} from "@lingdocs/pashto-inflector";
|
||||||
import { Helmet } from "react-helmet";
|
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() {
|
function handleDelete() {
|
||||||
deleteFromLocalDb("reviewTasks", reviewTask._id);
|
deleteFromLocalDb("reviewTasks", reviewTask._id);
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@ function ReviewTask({ reviewTask, textOptions }: { reviewTask: BT.ReviewTask, te
|
||||||
</div>}
|
</div>}
|
||||||
<Entry textOptions={textOptions} entry={reviewTask.entry} />
|
<Entry textOptions={textOptions} entry={reviewTask.entry} />
|
||||||
<div className="mb-2">"{reviewTask.comment}"</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
|
@ -22,7 +22,6 @@ type Options = {
|
||||||
searchType: SearchType,
|
searchType: SearchType,
|
||||||
theme: Theme,
|
theme: Theme,
|
||||||
textOptions: import("@lingdocs/pashto-inflector").Types.TextOptions,
|
textOptions: import("@lingdocs/pashto-inflector").Types.TextOptions,
|
||||||
level: UserLevel,
|
|
||||||
wordlistMode: WordlistMode,
|
wordlistMode: WordlistMode,
|
||||||
wordlistReviewLanguage: Language,
|
wordlistReviewLanguage: Language,
|
||||||
wordlistReviewBadge: boolean,
|
wordlistReviewBadge: boolean,
|
||||||
|
@ -39,8 +38,9 @@ type State = {
|
||||||
isolatedEntry: import("@lingdocs/pashto-inflector").Types.DictionaryEntry | undefined,
|
isolatedEntry: import("@lingdocs/pashto-inflector").Types.DictionaryEntry | undefined,
|
||||||
results: import("@lingdocs/pashto-inflector").Types.DictionaryEntry[],
|
results: import("@lingdocs/pashto-inflector").Types.DictionaryEntry[],
|
||||||
wordlist: WordlistWord[],
|
wordlist: WordlistWord[],
|
||||||
reviewTasks: import("./lib/backend-types").ReviewTask[],
|
reviewTasks: import("./lib/functions-types").ReviewTask[],
|
||||||
dictionaryInfo: import("@lingdocs/pashto-inflector").Types.DictionaryInfo | undefined,
|
dictionaryInfo: import("@lingdocs/pashto-inflector").Types.DictionaryInfo | undefined,
|
||||||
|
user: undefined | import("./lib/account-types").LingdocsUser,
|
||||||
}
|
}
|
||||||
|
|
||||||
type OptionsAction = {
|
type OptionsAction = {
|
||||||
|
|
|
@ -1234,276 +1234,11 @@
|
||||||
minimatch "^3.0.4"
|
minimatch "^3.0.4"
|
||||||
strip-json-comments "^3.1.1"
|
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":
|
"@fortawesome/fontawesome-free@^5.15.2":
|
||||||
version "5.15.4"
|
version "5.15.4"
|
||||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-free/-/fontawesome-free-5.15.4.tgz#ecda5712b61ac852c760d8b3c79c96adca5554e5"
|
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==
|
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":
|
"@hapi/address@2.x.x":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/address/-/address-2.1.4.tgz#5d67ed43f3fd41a69d4b9ff7b56e7c0d1d0a81e5"
|
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"
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.3.tgz#8b68da1ebd7fc603999cf6ebee34a4899a14b88e"
|
||||||
integrity sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==
|
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":
|
"@restart/context@^2.1.4":
|
||||||
version "2.1.4"
|
version "2.1.4"
|
||||||
resolved "https://registry.yarnpkg.com/@restart/context/-/context-2.1.4.tgz#a99d87c299a34c28bd85bb489cb07bfd23149c02"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/lokijs/-/lokijs-1.5.5.tgz#bae743a9ae24d1a106b8291c31ab1e1ad28e00d2"
|
||||||
integrity sha512-TAvlc6vfYZnQVqPBVF3ITE33aSomqRLHOsZb5u1jQdmQxvj+LOLgbt8VaAJh85Tx7xXdWcsdeEO13i6TQBfV+w==
|
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":
|
"@types/mime@^1":
|
||||||
version "1.3.2"
|
version "1.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.2.tgz#93e25bf9ee75fe0fd80b594bc4feb0e862111b5a"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197"
|
||||||
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==
|
||||||
|
|
||||||
"@types/node@*", "@types/node@>=12.12.47", "@types/node@>=13.7.0":
|
"@types/node@*":
|
||||||
version "16.6.1"
|
version "16.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.1.tgz#aee62c7b966f55fc66c7b6dfa1d58db2a616da61"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.6.1.tgz#aee62c7b966f55fc66c7b6dfa1d58db2a616da61"
|
||||||
integrity sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==
|
integrity sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==
|
||||||
|
@ -4254,15 +3931,6 @@ cliui@^6.0.0:
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
wrap-ansi "^6.2.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:
|
clone-buffer@1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
|
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"
|
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.16.2.tgz#0ef4b79cabafb251ea86eb7d139b42bd98c533e8"
|
||||||
integrity sha512-oxKe64UH049mJqrKkynWp6Vu0Rlm/BTXO/bJZuN2mmR3RtOFNepLlSWDd1eo16PzHpQAoNG97rLU1V/YxesJjw==
|
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:
|
core-js@^2.4.0, core-js@^2.5.3:
|
||||||
version "2.6.12"
|
version "2.6.12"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec"
|
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"
|
domhandler "^4.2.0"
|
||||||
entities "^2.0.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:
|
domain-browser@^1.1.1:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
|
||||||
|
@ -6043,13 +5701,6 @@ fastq@^1.6.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
reusify "^1.0.4"
|
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:
|
faye-websocket@^0.11.3:
|
||||||
version "0.11.4"
|
version "0.11.4"
|
||||||
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
|
resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da"
|
||||||
|
@ -6186,27 +5837,6 @@ find-up@^3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
locate-path "^3.0.0"
|
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:
|
firebaseui@^4.7.1:
|
||||||
version "4.8.1"
|
version "4.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/firebaseui/-/firebaseui-4.8.1.tgz#29ccbc9dfd579c4453725f88e9cf81c8ea62c580"
|
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"
|
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||||
integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
|
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"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
|
||||||
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
|
||||||
|
@ -6934,11 +6564,6 @@ icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss "^7.0.14"
|
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:
|
identity-obj-proxy@3.0.0:
|
||||||
version "3.0.0"
|
version "3.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz#94d2bda96084453ef36fbc5aaec37e0f79f1fc14"
|
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"
|
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
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:
|
lodash.clonedeep@^4.5.0:
|
||||||
version "4.5.0"
|
version "4.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
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"
|
resolved "https://registry.yarnpkg.com/lokijs/-/lokijs-1.5.12.tgz#cb55b37009bdf09ee7952a6adddd555b893653a0"
|
||||||
integrity sha512-Q5ALD6JiS6xAUWCwX3taQmgwxyveCtIIuL08+ml0nHwT3k0S/GIFJN+Hd38b1qYIMaE5X++iqsqWVksz7SYW+Q==
|
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:
|
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"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
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"
|
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
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:
|
promise-polyfill@^8.1.3:
|
||||||
version "8.2.0"
|
version "8.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.2.0.tgz#367394726da7561457aba2133c9ceefbd6267da0"
|
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"
|
object-assign "^4.1.1"
|
||||||
react-is "^16.8.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:
|
protocol-buffers-schema@^3.3.1:
|
||||||
version "3.5.2"
|
version "3.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.5.2.tgz#38ad35ba768607a5ed2375f8db4c2ecc5ea293c8"
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.3, tslib@^2.1.0:
|
tslib@^2.0.3:
|
||||||
version "2.3.1"
|
version "2.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||||
|
@ -13405,11 +12996,6 @@ whatwg-encoding@^1.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite "0.4.24"
|
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:
|
whatwg-fetch@^3.4.1:
|
||||||
version "3.6.2"
|
version "3.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz#dced24f37f2624ed0281725d51d0e2e3fe677f8c"
|
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"
|
string-width "^4.1.0"
|
||||||
strip-ansi "^6.0.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:
|
wrappy@1:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
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"
|
resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.1.31.tgz#b76c9a1bd9f0a9737e5a72dc37231cf38375e2ff"
|
||||||
integrity sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==
|
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:
|
xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.0, xtend@~4.0.1:
|
||||||
version "4.0.2"
|
version "4.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
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"
|
resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
|
||||||
integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
|
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:
|
yallist@^3.0.2:
|
||||||
version "3.1.1"
|
version "3.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
|
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"
|
camelcase "^5.0.0"
|
||||||
decamelize "^1.2.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:
|
yargs@^13.3.2:
|
||||||
version "13.3.2"
|
version "13.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd"
|
||||||
|
@ -13810,19 +13372,6 @@ yargs@^15.4.1:
|
||||||
y18n "^4.0.0"
|
y18n "^4.0.0"
|
||||||
yargs-parser "^18.1.2"
|
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:
|
yocto-queue@^0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
|
|
Loading…
Reference in New Issue