From 48bb19c532880961556b2abcb587b0d108ad0dcb Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Tue, 30 Aug 2022 00:14:43 +0100 Subject: [PATCH] [ENH] Add ability to bulk create sets --- functions/index.js | 2 +- src/AcceptDialog.js | 20 ++ src/App.js | 6 +- src/BulkCreateSets.js | 394 +++++++++++++++++++++++++++++++++ src/EditSet.js | 7 +- src/css/App.css | 2 +- src/css/ConfirmationDialog.css | 4 + src/css/Form.css | 34 +++ src/css/PopUp.css | 2 +- 9 files changed, 466 insertions(+), 5 deletions(-) create mode 100644 src/AcceptDialog.js create mode 100644 src/BulkCreateSets.js diff --git a/functions/index.js b/functions/index.js index 54d32f6..26d6ed9 100644 --- a/functions/index.js +++ b/functions/index.js @@ -430,7 +430,7 @@ exports.createProgressWithIncorrect = functions.https.onCall((data, context) => * @return {string} The original string with the unwanted characters removed. */ function cleanseVocabString(item, ignoreCaps=false, ignoreAccents=false) { - const chars = /[\p{P}\p{S} ]+/ug; + const chars = /[\p{P}\p{S}\n ]+/ug; let cleansed = item.replace(chars, ""); if (ignoreAccents) cleansed = cleansed.normalize('NFD').replace(/\p{Diacritic}/gu, ""); if (ignoreCaps) { diff --git a/src/AcceptDialog.js b/src/AcceptDialog.js new file mode 100644 index 0000000..57b202a --- /dev/null +++ b/src/AcceptDialog.js @@ -0,0 +1,20 @@ +import React from 'react'; +import Button from './Button'; + +export default function ConfirmationDialog(props) { + return ( + <> +
+
+

{props.message}

+
+ +
+
+ + ) +} diff --git a/src/App.js b/src/App.js index af94dc2..4078bb8 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,7 @@ import Settings from "./Settings"; import Progress from "./Progress"; import UserSets from "./UserSets"; import EditSet from "./EditSet"; +import BulkCreateSets from "./BulkCreateSets"; import Error404 from "./Error404"; import History from "./History"; import MistakesHistory from "./MistakesHistory"; @@ -284,10 +285,13 @@ class App extends React.Component { this.state.coloredEdges &&
} - + + + + diff --git a/src/BulkCreateSets.js b/src/BulkCreateSets.js new file mode 100644 index 0000000..dd821bd --- /dev/null +++ b/src/BulkCreateSets.js @@ -0,0 +1,394 @@ +import React, { Component } from 'react'; +import { withRouter, Prompt } from "react-router-dom"; +import { HomeRounded as HomeRoundedIcon, UndoRounded as TuneRoundedIcon } from "@material-ui/icons"; +import NavBar from "./NavBar"; +import Button from "./Button"; +import Footer from "./Footer"; +import LinkButton from "./LinkButton"; +import AcceptDialog from "./AcceptDialog"; +import Checkbox from '@material-ui/core/Checkbox'; + +const emptySetData = { + title: "", + public: false, + text: "", + vocabPairs: [], + vocabChanged: false, + incompletePairFound: false, +}; + +export default withRouter(class BulkCreateSets extends Component { + constructor(props) { + super(props); + this.state = { + user: props.user, + db: props.db, + loading: false, + canSave: false, + sets: [ + { + ...emptySetData + } + ], + navbarItems: [ + { + type: "link", + link: "/", + icon: , + hideTextMobile: true, + } + ], + termDefSeparator: "\\n", + pairSeparator: "\\n", + changesMade: false, + showErrorDialog: false, + }; + + let isMounted = true; + Object.defineProperty(this, "isMounted", { + get: () => isMounted, + set: (value) => isMounted = value, + }); + } + + setState = (state, callback = null) => { + if (this.isMounted) super.setState(state, callback); + } + + alertLeavingWithoutSaving = (e = null) => { + if (this.state.changesMade) { + var confirmationMessage = "Are you sure you want to leave? You will lose any unsaved changes."; + + (e || window.event).returnValue = confirmationMessage; //Gecko + IE + return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc. + } + return ""; + } + + async componentDidMount() { + window.addEventListener("beforeunload", this.alertLeavingWithoutSaving); + + document.title = "Bulk Create Sets | Parandum"; + this.props.logEvent("page_view"); + + this.firstSetNameInput.focus(); + this.props.page.load(); + } + + componentWillUnmount = () => { + window.removeEventListener('beforeunload', this.alertLeavingWithoutSaving); + this.isMounted = false; + this.props.page.unload(); + } + + stopLoading = () => { + this.setState({ + canSave: false, + loading: false, + }); + } + + cleanseVocabString = (item, otherPatterns=[]) => { + let newItem = item; + otherPatterns.map(pattern => newItem = newItem.replace(new RegExp(pattern, "g"), "")); + const chars = /[\p{P}\p{S}\n ]+/ug; + return newItem.replace(chars, ""); + } + + removeNewLines = (item) => item.replace(/[\n]+/ug, "") + + handleSetDataChange = () => { + const sets = [...this.state.sets]; + if (sets[this.state.sets.length - 1].text !== "" || sets[this.state.sets.length - 1].title !== "") { + sets.push({...emptySetData}); + this.setState({ + sets, + changesMade: true, + }); + } else if (sets[this.state.sets.length - 2].text === "" && sets[this.state.sets.length - 2].title === "") { + sets.pop(); + this.setState({ + sets, + changesMade: true, + }); + } + } + + checkIfCanSave = async () => { + let anySetIncomplete = this.state.termDefSeparator === "" || this.state.pairSeparator === ""; + let newSets; + if (!anySetIncomplete) { + let sets = [...this.state.sets]; + const pairSeparator = this.state.pairSeparator.replace("\\n","\n"); + const termDefSeparator = this.state.termDefSeparator.replace("\\n","\n"); + + const setsWithVocab = sets.slice(0,-1).map(set => { + let setIncomplete = this.cleanseVocabString(set.title) === "" || this.cleanseVocabString(set.text, [pairSeparator, termDefSeparator]) === ""; + if (setIncomplete) { + anySetIncomplete = true; + return { + ...set, + vocabChanged: false, + setIncomplete, + }; + } + if (set.vocabChanged) { + let vocabPairs = []; + if (pairSeparator === termDefSeparator) { + set.text.trim().split(pairSeparator).forEach((item, index, arr) => { + if (index % 2 === 0) { + let definition = "unknown"; + if (index === arr.length - 1 || this.cleanseVocabString(item, [pairSeparator, termDefSeparator]) === "" || this.cleanseVocabString(arr[index + 1], [pairSeparator, termDefSeparator]) === "") { + anySetIncomplete = setIncomplete = true; + } + else { + definition = arr[index + 1]; + } + vocabPairs.push({ + term: item, + definition, + sound: false, + }); + }; + }); + } else { + vocabPairs = set.text.trim().split(pairSeparator) + .map((pair) => { + let [first, ...rest] = pair.split(termDefSeparator); + if (rest.length <= 0 || this.cleanseVocabString(first, [pairSeparator, termDefSeparator]) === "" || this.cleanseVocabString(rest.join(termDefSeparator), [pairSeparator, termDefSeparator]) === "") { + rest = "unknown"; + anySetIncomplete = setIncomplete = true; + } else { + rest = rest.join(termDefSeparator); + } + return { + term: first, + definition: rest, + sound: false, + }; + }); + } + + if (vocabPairs.length < 1) { + anySetIncomplete = setIncomplete = true; + } + + return { + ...set, + vocabPairs, + vocabChanged: false, + setIncomplete, + }; + } + if (set.setIncomplete) { + anySetIncomplete = true; + } + return set; + }); + newSets = setsWithVocab.concat(sets[sets.length - 1]); + } else { + newSets = [...this.state.sets]; + } + + this.setState({ + sets: newSets, + canSave: !anySetIncomplete, + showErrorDialog: anySetIncomplete, + }, () => {if (!anySetIncomplete) this.saveSets()}); + } + + onTermDefSeparatorInputChange = (event) => { + this.setState({ + termDefSeparator: event.target.value, + }); + } + + onPairSeparatorInputChange = (event) => { + this.setState({ + pairSeparator: event.target.value, + }); + } + + onSetTitleInputChange = (event, setIndex) => { + let sets = [...this.state.sets]; + sets[setIndex].title = event.target.value; + this.setState({ + sets, + }, () => this.handleSetDataChange()); + } + + onPublicSetInputChange = (event, setIndex) => { + let sets = [...this.state.sets]; + sets[setIndex].public = event.target.checked; + this.setState({ + sets, + }); + } + + onVocabInputChange = (event, setIndex) => { + let sets = [...this.state.sets]; + sets[setIndex].text = event.target.value; + sets[setIndex].vocabChanged = true; + this.setState({ + sets, + }, () => this.handleSetDataChange()); + } + + saveSets = async () => { + if (this.state.canSave) { + this.setState({ + loading: true, + canSave: false, + }); + + const db = this.state.db; + const setCollectionRef = db.collection("sets"); + + let promises = []; + + this.state.sets.slice(0,-1).map(async (set) => { + let setDocRef = setCollectionRef.doc(); + setDocRef.set({ + title: set.title, + public: set.public, + owner: this.state.user.uid, + groups: [], + }).then(() => { + let vocabCollectionRef = setDocRef.collection("vocab"); + + let batches = [db.batch()]; + + set.vocabPairs.map((vocabPair, index) => { + if (index % 248 === 0) { + promises.push(batches[batches.length - 1].commit()); + batches.push(db.batch()); + } + + let vocabDocRef = vocabCollectionRef.doc() + return batches[batches.length - 1].set(vocabDocRef, { + term: this.removeNewLines(vocabPair.term), + definition: this.removeNewLines(vocabPair.definition), + sound: vocabPair.sound, + }); + }); + + if (!batches[batches.length - 1]._delegate._committed) promises.push(batches[batches.length - 1].commit().catch(() => null)); + }); + }); + + Promise.all(promises).then(() => { + this.stopLoading(); + this.props.history.push("/"); + }).catch((error) => { + console.log("Couldn't create sets: " + error); + this.stopLoading(); + }); + } + } + + closeErrorDialog = () => { + this.setState({ + showErrorDialog: false, + }) + } + + render() { + return ( +
+ + + + +
+
+

Bulk Create Sets

+ }>Normal +
+
+ + +
+ { + this.state.sets.map((data, setIndex) => +
+
+

+ this.onSetTitleInputChange(event, setIndex)} + placeholder="Set Title" + value={data.title} + className="set-title-input" + autoComplete="off" + ref={(inputEl) => {if (setIndex === 0) this.firstSetNameInput = inputEl}} + /> +

+
+ + + +
+ +