diff --git a/sakura/README.md b/sakura/README.md index 7bebf5d..04823b7 100644 --- a/sakura/README.md +++ b/sakura/README.md @@ -1,79 +1,70 @@ -# how to build a new version - -Cd into this directory +# Common development instructions ```sh -(cd ../loaderapp && ./publish.sh) - -``` - -# Getting Started with Create React App - -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). - -## Available Scripts - -In the project directory, you can run: - -### `yarn start` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in the browser. - -The page will reload if you make edits.\ -You will also see any lint errors in the console. - -### `yarn test` +# install dependencies +yarn install -Launches the test runner in the interactive watch mode.\ -See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +# start the development server +yarn start -### `yarn build` +# run unit tests +npx cypress run-ct -Builds the app for production to the `build` folder.\ -It correctly bundles React in production mode and optimizes the build for the best performance. +# run browser tests +npx cypress run -The build is minified and the filenames include the hashes.\ -Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `yarn eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). +# build a new version for deployment +(cd ../loaderapp && ./publish.sh) +``` -To learn React, check out the [React documentation](https://reactjs.org/). +# How to develop against Ankiconnect Android -### Code Splitting +These instructions are for developing against the Android version of Ankiconnect +called Ankiconnect Android. This will work for the following scenarios: -This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) +1. You have an android device such as your mobile phone or tablet. +2. You are using an existing version of Ankiconnect Android, or you are + developing a version with Android Studio and you can run it on your device. -### Analyzing the Bundle Size +## Prerequisites for development -This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) +These instructions are for Chrome on a Mac. If you are using a different browser +or operating system, you may need to find the equivalent steps. -### Making a Progressive Web App +- Install Ankiconnect Android on your device +- Connect your device to your computer and enable USB debugging in Android. +- Connect your Android device to the same network as your computer. -This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) +## Connecting to hare on your Android device -### Advanced Configuration +1. Find out the IP address of your computer. For example, if you are on a + Mac, you can run `ifconfig | grep inet` and look for the IP address that + starts with 192.168.0. or 10.0.0. +1. In the Ankiconnect Android app, go to the settings and change the cors setting + to `*`. This will allow access from any host. If you want to limit this, you + can also set it to `http://:4000`. The + settings will be taken into use immediately. +1. Start hare on your computer with `yarn start`. This will make it available to + the network so you can connect to it from your device. +1. On your device, open http://:4000 -This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) +## Connect your computer's chrome to your Android device's chrome -### Deployment +1. Connect your Android device to your computer with a USB cable. +2. Open Chrome on your computer and go to chrome://inspect/#devices . You will + be presented with a list of tabs on your device. Choose the tab that is running + hare. +3. Now you can use the developer tools on your computer to debug the code on your + device. Run the following code to test the connection: -This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) +```js +fetch("http://localhost:8765") + .then((r) => r.text()) + .then(console.log); +``` -### `yarn build` fails to minify +It will respond with "Ankiconnect Android is running.". If you get a CORS error, +double check that you have set the cors setting in the app to `*` or +`http://:4000`. -This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) +NOTE: on your device, you need to have the chrome tab active for this to work. diff --git a/sakura/cypress.config.ts b/sakura/cypress.config.ts new file mode 100644 index 0000000..371de55 --- /dev/null +++ b/sakura/cypress.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from "cypress"; + +export default defineConfig({ + env: { + coverage: false, + }, + + retries: { + runMode: 5, + openMode: 0, + }, + + video: false, + viewportHeight: 850, + viewportWidth: 400, + + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require("./cypress/plugins/index.js")(on, config); + }, + baseUrl: "http://localhost:4000", + excludeSpecPattern: ["**/*.md"], + }, + + component: { + setupNodeEvents(on, config) {}, + viewportWidth: 800, + viewportHeight: 800, + excludeSpecPattern: ["**/*.md"], + specPattern: "src/**/*.test.{js,ts,jsx,tsx}", + devServer: { + framework: "create-react-app", + bundler: "webpack", + }, + }, +}); diff --git a/sakura/cypress.json b/sakura/cypress.json deleted file mode 100644 index 8a65cd9..0000000 --- a/sakura/cypress.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "env": { - "coverage": false - }, - "baseUrl": "http://localhost:4000", - "ignoreTestFiles": ["**/*.md"], - "retries": { - "runMode": 5, - "openMode": 0 - }, - "video": false, - "viewportHeight": 850, - "viewportWidth": 400, - "nodeVersion": "system", - "component": { - "testFiles": "**/*.test.{js,ts,jsx,tsx}", - "componentFolder": "src", - "viewportWidth": 800, - "viewportHeight": 800 - } -} diff --git a/sakura/cypress/e2e/AnkiconnectMockApi.tsx b/sakura/cypress/e2e/AnkiconnectMockApi.tsx new file mode 100644 index 0000000..5e8464d --- /dev/null +++ b/sakura/cypress/e2e/AnkiconnectMockApi.tsx @@ -0,0 +1,41 @@ +export class AnkiconnectMockApi { + build(): void { + cy.intercept("GET", "http://127.0.0.1:8765/", { + body: "Ankiconnect is running.", + }).as("ankiconnect_is_running"); + + cy.intercept("POST", "http://127.0.0.1:8765/", (request) => { + if (request.body.version !== 6) { + throw new Error(`Unsupported version ${request.body.version}`); + } + + switch (request.body.action) { + case "deckNames": + request.alias = "deckNames"; + request.reply({ + body: { + result: ["Default"], + error: null, + }, + }); + break; + case "modelNames": + request.alias = "modelNames"; + request.reply({ + result: ["Japanese 2022 new accent note"], + error: null, + }); + break; + case "modelFieldNames": + request.alias = "modelFieldNames"; + request.reply({ + result: ["Expression", "Reading", "Meaning"], + error: null, + }); + break; + default: + throw new Error(`Unsupported action ${request.body.action}`); + } + }); + } +} diff --git a/sakura/cypress/integration/DictionarySpec.js b/sakura/cypress/e2e/Dictionary.cy.js similarity index 93% rename from sakura/cypress/integration/DictionarySpec.js rename to sakura/cypress/e2e/Dictionary.cy.js index 7754441..592bdba 100644 --- a/sakura/cypress/integration/DictionarySpec.js +++ b/sakura/cypress/e2e/Dictionary.cy.js @@ -1,6 +1,7 @@ /// import { DictionaryPage, SettingsPage } from "../support/pages"; +import { AnkiconnectMockApi } from "./AnkiconnectMockApi.tsx"; describe("dictionary view", () => { // for now these use the actual api so there is no mocking! @@ -154,11 +155,11 @@ describe("recursive searches", () => { cy.contains("Search").click(); // click some word that can be looked up recursively - cy.get("[data-word=山辺]").should("exist").click(); + cy.get(`[data-word="人"]`).first().should("exist").click(); cy.get(".modal-content").should("be.visible"); cy.url().should( "contain", - encodeURI("/dict/広辞苑/prefix/犬/0/recursive/大辞林/prefix/山辺/0") + encodeURI("/dict/広辞苑/prefix/犬/0/recursive/大辞林/prefix/人/0") ); // can close the modal @@ -210,23 +211,30 @@ describe("recursive searches", () => { }); it("uses the added yomichan dictionary as the default dict when available", () => { + // use AnkiConnect mock API to avoid needless connection errors in the cypress test log + const ankiconnectMockApi = new AnkiconnectMockApi(); + ankiconnectMockApi.build(); + // fallbacking to daijirin is tested in other tests already // const settings = new SettingsPage(); settings.visit(); settings.importYomichanDictionary("jmdict_english_truncated.zip"); + // wait for the dictionary to be imported + cy.get('[id="dictionary-JMdict (English)"]').should("exist"); + const dict = new DictionaryPage(); dict.visit(); dict.searchBox().type("あそこ"); // has to exist in the truncated dict dict.searchButton().click(); // click some word that can be looked up recursively - cy.get("[data-word=彼処]").should("exist").click(); + cy.get('[data-word="彼"]').first().should("exist").click(); cy.url().should( "contain", - encodeURI("/#/dict/広辞苑/prefix/あそこ/0/recursive/jmdict/prefix/彼処/0") + encodeURI("/#/dict/広辞苑/prefix/あそこ/0/recursive/jmdict/prefix/彼/0") ); cy.contains("over there"); diff --git a/sakura/cypress/integration/ExportSpec.js b/sakura/cypress/e2e/Export.cy.js similarity index 96% rename from sakura/cypress/integration/ExportSpec.js rename to sakura/cypress/e2e/Export.cy.js index fe1133a..cbefb27 100644 --- a/sakura/cypress/integration/ExportSpec.js +++ b/sakura/cypress/e2e/Export.cy.js @@ -67,6 +67,7 @@ describe("export view", () => { Audio: "audio", Focus: "word", Meaning2: "englishTranslation", + Snapshot: "(empty)", }); }); @@ -76,7 +77,7 @@ describe("export view", () => { // select an example sentence from the dictionary definition cy.contains("高価な―をとりそろえる") .children(".quote-actions") - .children(`button[aria-label="copy sentence"]`) + .find(`[aria-label="copy sentence"]`) .click(); cy.intercept("http://localhost:12345", "POST").as("create jap only"); @@ -96,6 +97,7 @@ describe("export view", () => { Audio: "", Focus: "品物", Meaning2: "", + Snapshot: "", }, tags: ["hare"], options: { @@ -108,13 +110,16 @@ describe("export view", () => { // next, select an audio sentence cy.contains("頼んでいた品物が今日届いた。") - .siblings(`[aria-label="copy sentence"]`) + .parent() + .find(`[aria-label="copy sentence"]`) .click(); cy.contains("The article which I have ordered arrived today.") - .siblings(`[aria-label="copy sentence"]`) + .parent() + .find(`[aria-label="copy sentence"]`) .click(); cy.get(`[aria-label="download audio"]`) - .children(`[aria-label="copy sentence"]`) + .parent() + .find(`[aria-label="copy sentence"]`) .first() .click(); diff --git a/sakura/cypress/integration/GrammarSpec.js b/sakura/cypress/e2e/Grammar.cy.js similarity index 68% rename from sakura/cypress/integration/GrammarSpec.js rename to sakura/cypress/e2e/Grammar.cy.js index 36310d8..40254f5 100644 --- a/sakura/cypress/integration/GrammarSpec.js +++ b/sakura/cypress/e2e/Grammar.cy.js @@ -1,10 +1,6 @@ describe("grammar view", () => { it("can search for grammar points", () => { - cy.visit("#/grammar", { - onBeforeLoad(win) { - cy.spy(win.console, "log").as("consoleLog"); - }, - }); + cy.visit("#/grammar"); cy.get("[aria-label=Search]").should("be.focused"); cy.get("[aria-label=Search]").type("という"); diff --git a/sakura/cypress/integration/SettingsSpec.js b/sakura/cypress/e2e/Settings.cy.js similarity index 56% rename from sakura/cypress/integration/SettingsSpec.js rename to sakura/cypress/e2e/Settings.cy.js index b005219..2ca79eb 100644 --- a/sakura/cypress/integration/SettingsSpec.js +++ b/sakura/cypress/e2e/Settings.cy.js @@ -1,6 +1,8 @@ /// -import { SettingsPage } from "../support/pages"; +import YomichanDatabase from "../../src/utils/yomichan/Types"; +import { SettingsPage } from "../support/pages.tsx"; +import { AnkiconnectMockApi } from "./AnkiconnectMockApi.tsx"; describe("settings view", () => { it("can display the settings view", () => { @@ -70,4 +72,49 @@ describe("importing a yomichan dictionary", () => { // must have reset to the first state cy.contains("Import Dictionary"); }); + + describe.only("ankiconnect settings", () => { + it("can set the ankiconnect url", () => { + const ankiconnect = new AnkiconnectMockApi(); + ankiconnect.build(); + + cy.visit("#/settings"); + cy.wait("@ankiconnect_is_running"); + cy.wait("@deckNames"); + cy.wait("@modelNames"); + + const page = new SettingsPage(); + + // the default value must be shown + page + .ankiConnectUrl() + .scrollIntoView() + .should("contain.value", "http://127.0.0.1:8765"); + + page.ankiConnectDeck().select("Default"); + page.ankiConnectModel().select("Japanese 2022 new accent note"); + page.ankiConnectFieldMapping("Expression").select("sentence"); + page.ankiConnectFieldMapping("Reading").select(""); + page + .ankiConnectFieldMapping("Meaning") + .select("englishTranslation") + .then(() => { + const db = new YomichanDatabase(); + db.getAnkiConnectSettings().then((settings) => { + expect(settings).to.deep.equal({ + data: { + address: "http://127.0.0.1:8765", + selectedDeckName: "Default", + selectedModelName: "Japanese 2022 new accent note", + fieldValueMapping: { + Expression: "sentence", + Reading: "(empty)", + Meaning: "englishTranslation", + }, + }, + }); + }); + }); + }); + }); }); diff --git a/sakura/cypress/integration/YomichanDictionarySpec.js b/sakura/cypress/e2e/YomichanDictionary.cy.js similarity index 96% rename from sakura/cypress/integration/YomichanDictionarySpec.js rename to sakura/cypress/e2e/YomichanDictionary.cy.js index 94a1885..661cd22 100644 --- a/sakura/cypress/integration/YomichanDictionarySpec.js +++ b/sakura/cypress/e2e/YomichanDictionary.cy.js @@ -1,7 +1,7 @@ /// -import YomichanDatabase from "../../src/utils/yomichan/yomichanDatabase"; -import { DictionaryPage, SettingsPage } from "../support/pages"; +import YomichanDatabase from "../../src/utils/yomichan/Types"; +import { DictionaryPage, SettingsPage } from "../support/pages.tsx"; describe("dictionary view with yomichan dictionaries", () => { beforeEach(() => { diff --git a/sakura/cypress/plugins/index.js b/sakura/cypress/plugins/index.js index d1895ef..3d13b28 100644 --- a/sakura/cypress/plugins/index.js +++ b/sakura/cypress/plugins/index.js @@ -12,7 +12,6 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) -const injectDevServer = require("@cypress/react/plugins/react-scripts"); /** * @type {Cypress.PluginConfig} */ @@ -20,6 +19,5 @@ const injectDevServer = require("@cypress/react/plugins/react-scripts"); module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config - injectDevServer(on, config); return config; }; diff --git a/sakura/cypress/support/commands.ts b/sakura/cypress/support/commands.ts new file mode 100644 index 0000000..698b01a --- /dev/null +++ b/sakura/cypress/support/commands.ts @@ -0,0 +1,37 @@ +/// +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } \ No newline at end of file diff --git a/sakura/cypress/support/component-index.html b/sakura/cypress/support/component-index.html new file mode 100644 index 0000000..ac6e79f --- /dev/null +++ b/sakura/cypress/support/component-index.html @@ -0,0 +1,12 @@ + + + + + + + Components App + + +
+ + \ No newline at end of file diff --git a/sakura/cypress/support/component.ts b/sakura/cypress/support/component.ts new file mode 100644 index 0000000..37f59ed --- /dev/null +++ b/sakura/cypress/support/component.ts @@ -0,0 +1,39 @@ +// *********************************************************** +// This example support/component.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands' + +// Alternatively you can use CommonJS syntax: +// require('./commands') + +import { mount } from 'cypress/react18' + +// Augment the Cypress namespace to include type definitions for +// your custom command. +// Alternatively, can be defined in cypress/support/component.d.ts +// with a at the top of your spec. +declare global { + namespace Cypress { + interface Chainable { + mount: typeof mount + } + } +} + +Cypress.Commands.add('mount', mount) + +// Example use: +// cy.mount() \ No newline at end of file diff --git a/sakura/cypress/support/index.js b/sakura/cypress/support/e2e.js similarity index 100% rename from sakura/cypress/support/index.js rename to sakura/cypress/support/e2e.js diff --git a/sakura/cypress/support/pages.js b/sakura/cypress/support/pages.tsx similarity index 52% rename from sakura/cypress/support/pages.js rename to sakura/cypress/support/pages.tsx index 8f1e7ca..5c42153 100644 --- a/sakura/cypress/support/pages.js +++ b/sakura/cypress/support/pages.tsx @@ -1,31 +1,31 @@ export class SettingsPage { - visit() { + visit(): void { cy.visit("#/settings"); } - selectFile(fileName) { - cy.get(`[aria-label="Select yomichan dictionary file"]`).attachFile( - fileName - ); + selectFile(fileName: string): void { + ( + cy.get(`[aria-label="Select yomichan dictionary file"]`) as any + ).attachFile(fileName); } - alias() { + alias(): Cypress.Chainable> { return cy.get(`[aria-label="Dictionary alias"]`); } - importButton() { + importButton(): Cypress.Chainable> { return cy.get(`button[aria-label="Import"]`); } - deleteButton() { + deleteButton(): Cypress.Chainable> { return cy.get(`button[aria-label="Delete dictionary"]`); } - confirmDeleteButton() { + confirmDeleteButton(): Cypress.Chainable> { return cy.get(`button[aria-label="Confirm deletion"]`); } - importYomichanDictionary(zipFileName) { + importYomichanDictionary(zipFileName: string) { cy.visit("#/settings"); cy.contains("Import Dictionary"); @@ -38,6 +38,22 @@ export class SettingsPage { // once the alias has been entered, the import button must be visible this.importButton().click(); } + + ankiConnectUrl(): Cypress.Chainable> { + return cy.get("#address"); + } + + ankiConnectDeck(): Cypress.Chainable> { + return cy.get("#deck"); + } + + ankiConnectModel(): Cypress.Chainable> { + return cy.get("#model"); + } + + ankiConnectFieldMapping(fieldName: string) { + return cy.get(`[data-field-name="${fieldName}"]`); + } } export class DictionaryPage { @@ -53,7 +69,7 @@ export class DictionaryPage { return cy.contains("Search"); } - resultButton(dictAlias) { + resultButton(dictAlias: string) { return cy.get("#dictionary-list").contains(dictAlias); } diff --git a/sakura/package.json b/sakura/package.json index 46ed017..b0c332c 100644 --- a/sakura/package.json +++ b/sakura/package.json @@ -3,47 +3,44 @@ "version": "0.54.0", "private": true, "dependencies": { + "@craco/craco": "^7.1.0", "@jsier/retrier": "^1.2.4", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^29.2.4", - "@types/node": "^18.11.17", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.10", + "@types/node": "^18.15.9", + "@types/react-dom": "^18.0.0", "axios": "^0.21.1", "axios-cache-adapter": "^2.7.3", "bootstrap": "^4.6.0", "bootstrap-icons": "^1.5.0", "comlink": "^4.3.1", "copy-to-clipboard": "^3.3.1", - "craco": "^0.0.3", "dexie": "^3.0.3", "ga-gtag": "^1.1.0", "jszip": "^3.7.0", "localforage": "^1.9.0", "parjs": "^0.12.7", "quick-score": "^0.0.12", - "react": "^17.0.2", + "react": "^18.0.0", "react-bootstrap": "^1.5.2", - "react-dom": "^17.0.2", + "react-dom": "^18.2.0", "react-router-dom": "^5.2.0", - "react-scripts": "5.0.0", + "react-scripts": "5.0.1", "tinygesture": "^1.1.4", "use-lodash-debounce": "^1.1.0" }, "devDependencies": { - "@cypress/react": "^5.8.0", - "@cypress/webpack-dev-server": "^1.3.0", "@types/lodash": "^4.14.191", "@types/react-router-dom": "^5.3.3", - "cypress": "8.1.0", + "cypress": "^12.0.0", "cypress-file-upload": "^5.0.8", "fake-indexeddb": "^3.1.3", "prettier": "^2.3.1", - "prettier-plugin-organize-imports": "^2.1.0", + "prettier-plugin-organize-imports": "^3.2.2", "react-json-view": "^1.21.3", - "typescript": "^4.9.4", + "typescript": "^5.0.0", "worker-loader": "^3.0.8" }, "scripts": { diff --git a/sakura/src/App.css b/sakura/src/App.css index fa5ffcd..bc586f7 100644 --- a/sakura/src/App.css +++ b/sakura/src/App.css @@ -273,9 +273,6 @@ superscript { background-color: black; } -#delete-confirmation { -} - #delete-confirmation .title { background-color: #212529; } diff --git a/sakura/src/App.js b/sakura/src/App.js index 914fef8..57dad43 100644 --- a/sakura/src/App.js +++ b/sakura/src/App.js @@ -1,6 +1,6 @@ import "bootstrap-icons/font/bootstrap-icons.css"; import "bootstrap/dist/css/bootstrap.min.css"; -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Container from "react-bootstrap/Container"; import { HashRouter as Router, diff --git a/sakura/src/index.js b/sakura/src/index.js index f488486..52dad0d 100644 --- a/sakura/src/index.js +++ b/sakura/src/index.js @@ -1,6 +1,6 @@ import { Retrier } from "@jsier/retrier"; import React from "react"; -import ReactDOM from "react-dom"; +import * as ReactDOMClient from "react-dom/client"; import App from "./App"; import "./index.css"; @@ -66,10 +66,10 @@ function prepareHostSite() { prepareHostSite().then(() => { const rootElement = document.getElementById("sakura-customizations"); log("starting the app"); - ReactDOM.render( + const root = ReactDOMClient.createRoot(rootElement); + root.render( - , - rootElement + ); }); diff --git a/sakura/src/utils/ClearableSearch.js b/sakura/src/utils/ClearableSearch.js index 03138c0..705e9ee 100644 --- a/sakura/src/utils/ClearableSearch.js +++ b/sakura/src/utils/ClearableSearch.js @@ -1,4 +1,3 @@ -import React from "react"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/sakura/src/utils/search.tsx b/sakura/src/utils/search.tsx index 680dd2f..fb0f369 100644 --- a/sakura/src/utils/search.tsx +++ b/sakura/src/utils/search.tsx @@ -1,15 +1,12 @@ import axios from "axios"; import { Dictionary } from "lodash"; import { default as groupBy } from "lodash/groupBy"; +import { getWordDefinitions, WordDefinition } from "../api"; +import YomichanDatabase from "./yomichan/Types"; import { - getWordDefinitions, - GetWordDefinitionsResponse, - WordDefinition, -} from "../api"; -import YomichanDatabase, { YomichanDictionary, YomichanTerm, -} from "./yomichan/yomichanDatabase"; +} from "./yomichan/YomichanDictionary"; export type AudioSentenceSearchResult = { jap: string; diff --git a/sakura/src/utils/testUtils.js b/sakura/src/utils/testUtils.js index 215bcef..230da80 100644 --- a/sakura/src/utils/testUtils.js +++ b/sakura/src/utils/testUtils.js @@ -1,8 +1,7 @@ -import { mount } from "@cypress/react"; import ReactJson from "react-json-view"; export function assertParses(parseResult, expected) { - mount().then(() => { + cy.mount().then(() => { if (parseResult.kind !== "OK") { console.log(parseResult.trace); } diff --git a/sakura/src/utils/testUtilsForBrowser.tsx b/sakura/src/utils/testUtilsForBrowser.tsx index 5cb548d..d875ac5 100644 --- a/sakura/src/utils/testUtilsForBrowser.tsx +++ b/sakura/src/utils/testUtilsForBrowser.tsx @@ -1,4 +1,4 @@ -import YomichanDatabase from "./yomichan/yomichanDatabase"; +import YomichanDatabase from "./yomichan/Types"; export function newDatabase() { indexedDB.deleteDatabase("hare-yomichan"); diff --git a/sakura/src/utils/yomichan/yomichanDatabase.tsx b/sakura/src/utils/yomichan/Types.tsx similarity index 74% rename from sakura/src/utils/yomichan/yomichanDatabase.tsx rename to sakura/src/utils/yomichan/Types.tsx index 3581873..820384c 100644 --- a/sakura/src/utils/yomichan/yomichanDatabase.tsx +++ b/sakura/src/utils/yomichan/Types.tsx @@ -1,87 +1,14 @@ import Dexie, { Transaction } from "dexie"; -import { Dictionary } from "lodash"; - -// This class is modeled after yomichan's "data file containing term -// information", -// https://github.com/FooSoft/yomichan/blob/master/ext/data/schemas/dictionary-term-bank-v3-schema.json -export interface YomichanTerm { - dictionaryName: string; - expression: string; - reading: string; - - // Tags - // - // "String of space-separated tags for the definition. An empty string is - // treated as no tags." - tags: string; - - // Rules - // - // "String of space-separated rule identifiers for the definition which is - // used to validate delinflection. Valid rule identifiers are: v1: ichidan - // verb; v5: godan verb; vs: suru verb; vk: kuru verb; adj-i: i-adjective. - // An empty string corresponds to words which aren't inflected, such as - // nouns." - rules: string; - - // Popularity - // "Score used to determine popularity. Negative values are more rare and - // positive values are more frequent. This score is also used to sort - // search results." - popularity: number; - definitions: string[]; -} - -export type AnkiFieldContentType = - | "sentence" - | "definition" - | "englishTranslation" - | "audio" - | "word"; - -export type FieldValues = Dictionary; - -export interface YomichanDictionary { - name: string; - alias: string; -} - -export interface DictionarySetting { - dictionaryName: string; - positionType: string; - position: number; -} - -export interface DictionaryAndDictionarySetting { - dictionary: YomichanDictionary; - setting?: DictionarySetting; -} - -/** All allowed keys for settings */ -export enum SettingKey { - ankiconnect_address = "ankiconnect_address", - ankiconnect_selectedDeckName = "ankiconnect_selectedDeckName", - ankiconnect_selectedModelName = "ankiconnect_selectedModelName", - ankiconnect_fieldValueMapping = "ankiconnect_fieldValueMapping", -} - -export interface Setting { - key: SettingKey; - value: string; -} - -export interface AnkiConnectSettingData { - address: string; - selectedDeckName: string; - selectedModelName: string; - fieldValueMapping: FieldValues; -} - -export enum YomichanDatabaseVersion { - version01 = 1, - version02 = 2, - latest = 3, -} +import { + AnkiConnectSettingData, + DictionaryAndDictionarySetting, + DictionarySetting, + Setting, + SettingKey, + YomichanDatabaseVersion, + YomichanDictionary, + YomichanTerm, +} from "./YomichanDictionary"; export default class YomichanDatabase extends Dexie { dictionaries!: Dexie.Table; @@ -149,7 +76,9 @@ export default class YomichanDatabase extends Dexie { } this.on("populate", async (tx) => { - addDefaultAnkiSettings(tx); + if (this.verno >= 3) { + addDefaultAnkiSettings(tx); + } }); // open the db to force migrations to happen now instead of lazily diff --git a/sakura/src/utils/yomichan/YomichanDictionary.tsx b/sakura/src/utils/yomichan/YomichanDictionary.tsx new file mode 100644 index 0000000..59c9b7f --- /dev/null +++ b/sakura/src/utils/yomichan/YomichanDictionary.tsx @@ -0,0 +1,85 @@ +import { Dictionary } from "lodash"; + +// This class is modeled after yomichan's "data file containing term +// information", +// https://github.com/FooSoft/yomichan/blob/master/ext/data/schemas/dictionary-term-bank-v3-schema.json + +export interface YomichanTerm { + dictionaryName: string; + expression: string; + reading: string; + + // Tags + // + // "String of space-separated tags for the definition. An empty string is + // treated as no tags." + tags: string; + + // Rules + // + // "String of space-separated rule identifiers for the definition which is + // used to validate delinflection. Valid rule identifiers are: v1: ichidan + // verb; v5: godan verb; vs: suru verb; vk: kuru verb; adj-i: i-adjective. + // An empty string corresponds to words which aren't inflected, such as + // nouns." + rules: string; + + // Popularity + // "Score used to determine popularity. Negative values are more rare and + // positive values are more frequent. This score is also used to sort + // search results." + popularity: number; + definitions: string[]; +} + +export type AnkiFieldContentType = + | "(empty)" + | "sentence" + | "definition" + | "englishTranslation" + | "audio" + | "word"; + +export type FieldValues = Dictionary; + +export interface YomichanDictionary { + name: string; + alias: string; +} + +export interface DictionarySetting { + dictionaryName: string; + positionType: string; + position: number; +} + +export interface DictionaryAndDictionarySetting { + dictionary: YomichanDictionary; + setting?: DictionarySetting; +} +/** All allowed keys for settings */ + +export enum SettingKey { + ankiconnect_address = "ankiconnect_address", + ankiconnect_selectedDeckName = "ankiconnect_selectedDeckName", + ankiconnect_selectedModelName = "ankiconnect_selectedModelName", + ankiconnect_fieldValueMapping = "ankiconnect_fieldValueMapping", +} + +export interface Setting { + key: SettingKey; + value: string; +} + +export interface AnkiConnectSettingData { + address: string; + selectedDeckName: string; + selectedModelName: string; + fieldValueMapping: FieldValues; +} + +export enum YomichanDatabaseVersion { + version01 = 1, + version02 = 2, + latest = 3, +} diff --git a/sakura/src/utils/yomichan/workers/databaseWorker.js b/sakura/src/utils/yomichan/workers/databaseWorker.js index 58d2381..b2080e4 100644 --- a/sakura/src/utils/yomichan/workers/databaseWorker.js +++ b/sakura/src/utils/yomichan/workers/databaseWorker.js @@ -5,7 +5,7 @@ import * as Comlink from "comlink"; import DatabaseWorker from "worker-loader?inline=no-fallback!./index"; /** A shared place to get a new worker instance. Will run in a web worker. - @returns {Promise} + @returns {Promise} */ export async function newDatabaseWorkerInstance() { const workerClass = Comlink.wrap(new DatabaseWorker()); diff --git a/sakura/src/utils/yomichan/workers/dictionaryImporter.js b/sakura/src/utils/yomichan/workers/dictionaryImporter.js index c36807f..202cb1f 100644 --- a/sakura/src/utils/yomichan/workers/dictionaryImporter.js +++ b/sakura/src/utils/yomichan/workers/dictionaryImporter.js @@ -1,5 +1,5 @@ import * as Comlink from "comlink"; -import YomichanDatabase from "../yomichanDatabase"; +import YomichanDatabase from "../Types"; // accepts an arraybuffer because it's a Transferable // https://developer.mozilla.org/en-US/docs/Web/API/Transferable diff --git a/sakura/src/utils/yomichan/workers/index.js b/sakura/src/utils/yomichan/workers/index.js index a2a625b..cd5fe4d 100644 --- a/sakura/src/utils/yomichan/workers/index.js +++ b/sakura/src/utils/yomichan/workers/index.js @@ -1,5 +1,5 @@ import * as Comlink from "comlink"; -import YomichanDatabase from "../yomichanDatabase"; +import YomichanDatabase from "../Types"; // This file as a wrapper and exists so that YomichanDatabase can be used with // Comlink from worker, non-worker and test contexts diff --git a/sakura/src/utils/yomichan/yomichanDatabase.test.tsx b/sakura/src/utils/yomichan/yomichanDatabase.test.tsx index c7c57bb..a7f9a88 100644 --- a/sakura/src/utils/yomichan/yomichanDatabase.test.tsx +++ b/sakura/src/utils/yomichan/yomichanDatabase.test.tsx @@ -1,10 +1,8 @@ /* eslint-disable jest/valid-expect, no-undef, jest/valid-expect-in-promise */ import { newDatabase } from "../testUtilsForBrowser"; -import YomichanDatabase, { - YomichanDatabaseVersion, - YomichanTerm, -} from "./yomichanDatabase"; +import YomichanDatabase from "./Types"; +import { YomichanDatabaseVersion, YomichanTerm } from "./YomichanDictionary"; describe("yomichan database", () => { it("can add terms", () => { diff --git a/sakura/src/views/dict/Definitions.js b/sakura/src/views/dict/Definitions.js index a608dc9..4e0d5c1 100644 --- a/sakura/src/views/dict/Definitions.js +++ b/sakura/src/views/dict/Definitions.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import Accordion from "react-bootstrap/Accordion"; import Alert from "react-bootstrap/Alert"; import Button from "react-bootstrap/Button"; diff --git a/sakura/src/views/dict/Dictionaries.js b/sakura/src/views/dict/Dictionaries.js index 059393c..d29a288 100644 --- a/sakura/src/views/dict/Dictionaries.js +++ b/sakura/src/views/dict/Dictionaries.js @@ -1,4 +1,3 @@ -import React from "react"; import Alert from "react-bootstrap/Alert"; import ButtonGroup from "react-bootstrap/ButtonGroup"; import Spinner from "react-bootstrap/Spinner"; diff --git a/sakura/src/views/dict/SearchBox.js b/sakura/src/views/dict/SearchBox.js index f687294..cbaa8e8 100644 --- a/sakura/src/views/dict/SearchBox.js +++ b/sakura/src/views/dict/SearchBox.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; diff --git a/sakura/src/views/dict/index.js b/sakura/src/views/dict/index.js index 141f80f..882c110 100644 --- a/sakura/src/views/dict/index.js +++ b/sakura/src/views/dict/index.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { generatePath, useHistory, @@ -22,6 +22,11 @@ const defaultYomiDict = (yomichanDictsAndSettings) => { (ds) => ds.setting.positionType === "before" ); before.sort((a, b) => a.setting.position - b.setting.position); + + // in case the user has no "before" dictionaries, we let the caller fallback + // to a default + if (before.length === 0) return null; + const { dictionary } = before[0]; return dictionary; }; diff --git a/sakura/src/views/export/DefinitionPreview.tsx b/sakura/src/views/export/DefinitionPreview.tsx new file mode 100644 index 0000000..4bed2af --- /dev/null +++ b/sakura/src/views/export/DefinitionPreview.tsx @@ -0,0 +1,104 @@ +import { RefCallback, useEffect, useRef } from "react"; +import * as ReactDOMClient from "react-dom/client"; +import { CopyQuoteButton, SelectSentenceForAnkiCardButton } from "./ExportView"; + +interface DefinitionPreviewProps { + headingHtml: string; + bodyHtml: string; + setDefinitionNode: React.Dispatch< + React.SetStateAction + >; + selectedWord: string | undefined; + selectedAnkiJapSentence: string | undefined; + setSelectedAnkiJapSentence: (sentence: string | undefined) => void; +} + +export const DefinitionPreview = ({ + headingHtml, + bodyHtml, + setDefinitionNode, + selectedWord, + selectedAnkiJapSentence, + setSelectedAnkiJapSentence, +}: DefinitionPreviewProps): JSX.Element => { + // Store the root nodes of the copy buttons so we can unmount them later. + // This is necessary because the buttons are added to the DOM after the + // definition is rendered, and we need to remove them when the definition + // is unmounted in order to avoid memory leaks. + type Sentence = string; + const sentenceButtonsRef = useRef>(); + + useEffect(() => { + return function cleanup() { + Object.entries(sentenceButtonsRef.current || {}).forEach( + ([sentence, buttonRootNode]) => { + // Use setTimeout to avoid a warning about synchronous unmounts. + // This may be a react bug. + // + // https://stackoverflow.com/questions/73459382/react-18-async-way-to-unmount-root + // https://github.com/facebook/react/issues/25675 + setTimeout(() => { + buttonRootNode.unmount(); + delete sentenceButtonsRef.current?.[sentence]; + }); + } + ); + }; + }, []); + + const onRefChange: RefCallback = (node) => { + // add extra buttons to the example sentences + setDefinitionNode(node || undefined); + + if (!node) return; + if (!sentenceButtonsRef.current) sentenceButtonsRef.current = {}; + + const quoteActions = Array.from(node.querySelectorAll(".quote-actions")); + quoteActions.forEach((e: Element) => { + const spanElement = e as HTMLSpanElement; + + const sentence = spanElement.dataset.quote; + if (!sentence) return; + + let root: ReactDOMClient.Root; + if (sentenceButtonsRef.current?.[sentence]) { + root = sentenceButtonsRef.current[sentence]; + } else { + // the root can be undefined in case the sentence is not in the ref, i.e. + // it's not been rendered before + root = ReactDOMClient.createRoot(spanElement); + } + + root.render( +
+ + setSelectedAnkiJapSentence(sentence)} + isSelected={sentence === selectedAnkiJapSentence} + /> +
+ ); + + if (sentenceButtonsRef.current) { + sentenceButtonsRef.current[sentence] = root; + } + }); + }; + + return ( +
+

+

+
+ ); +}; diff --git a/sakura/src/views/export/ExportView.tsx b/sakura/src/views/export/ExportView.tsx index 9171e91..b13fc6d 100644 --- a/sakura/src/views/export/ExportView.tsx +++ b/sakura/src/views/export/ExportView.tsx @@ -1,13 +1,5 @@ import copy from "copy-to-clipboard"; -import React, { - ReactElement, - ReactNode, - useEffect, - useRef, - useState, -} from "react"; -import { DOMElement } from "react"; -import { RefCallback } from "react"; +import { ReactNode, useEffect, useState } from "react"; import Alert from "react-bootstrap/Alert"; import Button from "react-bootstrap/Button"; import Col from "react-bootstrap/Col"; @@ -15,7 +7,7 @@ import Container from "react-bootstrap/Container"; import Form from "react-bootstrap/Form"; import Row from "react-bootstrap/Row"; import Spinner from "react-bootstrap/Spinner"; -import ReactDOM from "react-dom"; + import { useRouteMatch } from "react-router-dom"; import { pageView } from "../../telemetry"; import { addNote } from "../../utils/ankiconnect/ankiconnectApi"; @@ -27,14 +19,18 @@ import { WordDefinitionResult, } from "../../utils/search"; import * as wordParser from "../../utils/wordParser"; -import YomichanDatabase, { +import YomichanDatabase from "../../utils/yomichan/Types"; +import { AnkiConnectSettingData, + AnkiFieldContentType, YomichanDictionary, -} from "../../utils/yomichan/yomichanDatabase"; +} from "../../utils/yomichan/YomichanDictionary"; import ExportViewDefinitionTokenProcessor from "../dict/tokenProcessors/exportViewDefinitionTokenProcessor"; import ToPlainTextTokenProcessor from "../dict/tokenProcessors/toPlainTextTokenProcessor"; import { prettyText } from "../dict/utils"; import Navbar from "../navbar/Navbar"; +import { DefinitionPreview } from "./DefinitionPreview"; +import { ResultItemSection } from "./ResultItemSection"; type ErrorItemProps = { heading: string; error: any }; const ErrorItem = ({ heading, error }: ErrorItemProps) => { @@ -113,6 +109,7 @@ const SearchLinkWithIcon = ({ src={iconUrl} style={{ height: "16px", width: "16px" }} className="inline icon" + alt="search" > ); return ; @@ -122,7 +119,7 @@ type SelectSentenceForAnkiCardButtonProps = { onSelect: () => void; isSelected: boolean; }; -const SelectSentenceForAnkiCardButton = ({ +export const SelectSentenceForAnkiCardButton = ({ onSelect, isSelected, }: SelectSentenceForAnkiCardButtonProps) => { @@ -143,7 +140,10 @@ const SelectSentenceForAnkiCardButton = ({ }; type CopyQuoteButtonProps = { text: string; selectedWord?: string }; -const CopyQuoteButton = ({ text, selectedWord }: CopyQuoteButtonProps) => { +export const CopyQuoteButton = ({ + text, + selectedWord, +}: CopyQuoteButtonProps) => { const [wordWasCopied, setWordWasCopied] = useState(false); const copiableText = text @@ -205,56 +205,19 @@ const ExportView = ({ // the alternative spellings of the word const [wordOptions, setWordOptions] = useState([]); const [selectedWord, setSelectedWord] = useState(); - const buttonsRef = useRef(); - - const [selectedAnkiJapSentence, setSelectedAnkiJapSentence] = useState< - string - >(); - const [selectedAnkiEngSentence, setSelectedAnkiEngSentence] = useState< - string - >(); - const [ - selectedAnkiAudioSentenceUrl, - setSelectedAnkiAudioSentenceUrl, - ] = useState(); - const [ankiCardCreationError, setAnkiCardCreationError] = useState< - ReactNode - >(); + + const [selectedAnkiJapSentence, setSelectedAnkiJapSentence] = + useState(); + const [selectedAnkiEngSentence, setSelectedAnkiEngSentence] = + useState(); + const [selectedAnkiAudioSentenceUrl, setSelectedAnkiAudioSentenceUrl] = + useState(); + const [ankiCardCreationError, setAnkiCardCreationError] = + useState(); useEffect(() => { pageView("export", `/${dict}`); - - return function cleanup() { - buttonsRef.current?.forEach((b) => { - ReactDOM.unmountComponentAtNode(b); - }); - }; - }, []); - - const onRefChange: RefCallback = (node) => { - // add copy button to example sentences - setDefinitionNode(node || undefined); - - if (!node) return; - - const quoteActions = Array.from(node.querySelectorAll(".quote-actions")); - quoteActions.forEach((e: Element) => { - const q = e as HTMLSpanElement; - - const sentence = q.dataset.quote || ""; - ReactDOM.render( - <> - - setSelectedAnkiJapSentence(sentence)} - isSelected={sentence === selectedAnkiJapSentence} - /> - , - q - ); - }); - buttonsRef.current = quoteActions; - }; + }, [dict]); useEffect(() => { if (!dict || !search || !openeditem) { @@ -358,20 +321,14 @@ const ExportView = ({
-
-

-

-
+
{wordOptions?.length > 0 && ( @@ -412,63 +369,56 @@ const ExportView = ({ {audioSentences.map((sentenceRecord) => ( - - - {sentenceRecord.jap} - - - - - setSelectedAnkiJapSentence(sentenceRecord.jap) - } - isSelected={ - sentenceRecord.jap === selectedAnkiJapSentence - } + + - + {sentenceRecord.eng ? ( - <> - - {sentenceRecord.eng} - - - - - setSelectedAnkiEngSentence(sentenceRecord.eng) - } - isSelected={ - sentenceRecord.eng === selectedAnkiEngSentence - } - /> - + ) : null} - - - + + + - - - setSelectedAnkiAudioSentenceUrl( - sentenceRecord.audio_jap - ) - } - isSelected={ - sentenceRecord.audio_jap === - selectedAnkiAudioSentenceUrl - } - /> + + setSelectedAnkiAudioSentenceUrl( + sentenceRecord.audio_jap + ) + } + isSelected={ + sentenceRecord.audio_jap === + selectedAnkiAudioSentenceUrl + } + /> ))} @@ -507,7 +457,7 @@ const ExportView = ({ const baseUrl = ankiConnectSettings.address; let audioFields: string[] = []; - const options = { + const options: any = { deckName: ankiConnectSettings.selectedDeckName, modelName: ankiConnectSettings.selectedModelName, fields: Object.fromEntries( @@ -515,7 +465,8 @@ const ExportView = ({ ankiConnectSettings.fieldValueMapping ).map(([key, value]) => { let fieldContent = ""; - switch (value) { + const newValue = value as AnkiFieldContentType; + switch (newValue) { case "audio": { // will be added as a downloadable audio file instead audioFields.push(key); @@ -542,22 +493,34 @@ const ExportView = ({ fieldContent = selectedWord; break; } + case "(empty)": { + fieldContent = ""; + break; + } + default: { + // make sure that all cases are handled or it's a compile error + newValue satisfies never; + } } return [key, fieldContent]; }) ), - audio: selectedAnkiAudioSentenceUrl - ? { - url: selectedAnkiAudioSentenceUrl, - filename: - `hare_${selectedWord}_` + - new Date().toISOString(), - fields: audioFields, - } - : undefined, }; + if (selectedAnkiAudioSentenceUrl) { + // get the last "." separated part of the url + // which should be the file extension + const extension = + selectedAnkiAudioSentenceUrl.split(".").slice(-1)[0] || + "mp3"; + options.audio = { + url: selectedAnkiAudioSentenceUrl, + filename: `hare_${selectedWord}_${new Date().toISOString()}.${extension}`, + fields: audioFields, + }; + } + const response = await addNote(baseUrl, options); if (response.error) { setAnkiCardCreationError( diff --git a/sakura/src/views/export/ResultItemSection.tsx b/sakura/src/views/export/ResultItemSection.tsx new file mode 100644 index 0000000..21d4430 --- /dev/null +++ b/sakura/src/views/export/ResultItemSection.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { CopyQuoteButton, SelectSentenceForAnkiCardButton } from "./ExportView"; + +interface ResultItemSectionProps { + sentence: string; + selectedAnkiSentence?: string; + setSelectedAnkiSentence: React.Dispatch< + React.SetStateAction + >; +} +export const ResultItemSection = ({ + sentence, + selectedAnkiSentence, + setSelectedAnkiSentence, +}: ResultItemSectionProps) => { + return ( + <> + {sentence} +
+ + + setSelectedAnkiSentence(sentence)} + isSelected={sentence === selectedAnkiSentence} + /> +
+ + ); +}; diff --git a/sakura/src/views/grammar/GrammarView.js b/sakura/src/views/grammar/GrammarView.js index ec03929..693c54d 100644 --- a/sakura/src/views/grammar/GrammarView.js +++ b/sakura/src/views/grammar/GrammarView.js @@ -1,6 +1,6 @@ import axios from "axios"; import { QuickScore } from "quick-score"; -import React, { useEffect, useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Col from "react-bootstrap/Col"; import Form from "react-bootstrap/Form"; import InputGroup from "react-bootstrap/InputGroup"; @@ -25,7 +25,7 @@ const GrammarView = ({}) => { useEffect(() => { // preload the grammar search index setLoading(true); - return axios + axios .get("https://sp3ctum.github.io/hare/static/public/grammar-links.json") .then((response) => { const opts = response.data || []; diff --git a/sakura/src/views/help/AnkiConnectSettings.tsx b/sakura/src/views/help/AnkiConnectSettings.tsx index 127e3a4..d03b46b 100644 --- a/sakura/src/views/help/AnkiConnectSettings.tsx +++ b/sakura/src/views/help/AnkiConnectSettings.tsx @@ -1,54 +1,14 @@ /* eslint-disable import/no-webpack-loader-syntax */ -import React, { useEffect, useRef, useState } from "react"; -import Button from "react-bootstrap/Button"; +import { useEffect, useState } from "react"; import Col from "react-bootstrap/Col"; -import Container from "react-bootstrap/Container"; -import Form from "react-bootstrap/Form"; import Row from "react-bootstrap/Row"; -import { pageView } from "../../telemetry"; -import Navbar from "../navbar/Navbar"; -import ExistingDictionary from "./dictionaries/ExistingDictionary"; -import ImportDictionaryWizard from "./dictionaries/ImportDictionaryWizard"; import * as ankiconnectApi from "../../utils/ankiconnect/ankiconnectApi"; import { AnkiConnectSettingData, AnkiFieldContentType, -} from "../../utils/yomichan/yomichanDatabase"; - -type FieldNameSelectionProps = { - value: string; - onChanged: (newValue: AnkiFieldContentType) => void; -}; -const FieldNameSelection = ({ value, onChanged }: FieldNameSelectionProps) => { - return ( - - ); -}; +} from "../../utils/yomichan/YomichanDictionary"; +import { FieldNameSelection } from "./FieldNameSelection"; type AnkiConnectSettingsProps = { ankiConnectSettings: AnkiConnectSettingData; @@ -98,10 +58,11 @@ const AnkiConnectSettingsComponent = ({ const showModelFieldNames = async () => { if (!ankiConnectSettings.selectedModelName) return; - const getModelFieldNamesResponse = await ankiconnectApi.getModelFieldNames( - baseUrl, - ankiConnectSettings.selectedModelName - ); + const getModelFieldNamesResponse = + await ankiconnectApi.getModelFieldNames( + baseUrl, + ankiConnectSettings.selectedModelName + ); setAnkiConnectError(getModelFieldNamesResponse.error); if (getModelFieldNamesResponse.error) { return; @@ -135,7 +96,7 @@ const AnkiConnectSettingsComponent = ({ - +