diff --git a/apps/web/app/author/(components)/__tests__/ImportModal.xlsx.integration.test.tsx b/apps/web/app/author/(components)/__tests__/ImportModal.xlsx.integration.test.tsx
new file mode 100644
index 00000000..56ba274c
--- /dev/null
+++ b/apps/web/app/author/(components)/__tests__/ImportModal.xlsx.integration.test.tsx
@@ -0,0 +1,509 @@
+/**
+ * @jest-environment jsdom
+ *
+ * Integration tests for Excel import functionality
+ * Tests the complete flow from file selection to import
+ */
+
+import { generateTempQuestionId } from "@/lib/utils";
+import { ResponseType, QuestionType } from "@/config/types";
+
+jest.mock("@/lib/utils", () => ({
+ generateTempQuestionId: jest.fn(() => Math.random()),
+}));
+
+describe("ImportModal - Excel Import Integration", () => {
+ const parseExcelRows = (
+ rows: any[][],
+ options: { importChoiceFeedback: boolean } = { importChoiceFeedback: true },
+ ) => {
+ const questions: any[] = [];
+
+ for (let i = 1; i < rows.length; i++) {
+ const row = rows[i];
+
+ if (!row || row.length === 0) continue;
+
+ const questionText = (row[0] ?? "").toString().trim();
+ const correctAnswer = (row[1] ?? "").toString().trim();
+ const answer2 = (row[2] ?? "").toString().trim();
+ const answer3 = (row[3] ?? "").toString().trim();
+ const answer4 = (row[4] ?? "").toString().trim();
+ const answerLocation = (row[5] ?? "").toString().trim();
+ const additionalInfo = (row[6] ?? "").toString().trim();
+
+ if (!questionText) continue;
+
+ const question: any = {
+ id: generateTempQuestionId(),
+ alreadyInBackend: false,
+ assignmentId: 0,
+ index: questions.length + 1,
+ numRetries: 1,
+ type: "SINGLE_CORRECT" as QuestionType,
+ responseType: "OTHER" as ResponseType,
+ totalPoints: 1,
+ question: questionText,
+ scoring: { type: "CRITERIA_BASED", criteria: [] },
+ };
+
+ const choices: any[] = [];
+
+ if (correctAnswer) {
+ choices.push({
+ choice: correctAnswer,
+ isCorrect: true,
+ points: 1,
+ feedback:
+ options.importChoiceFeedback && additionalInfo
+ ? `You may find answer for this question at ${additionalInfo}`
+ : "",
+ });
+ }
+
+ [answer2, answer3, answer4].forEach((answer) => {
+ if (answer) {
+ choices.push({
+ choice: answer,
+ isCorrect: false,
+ points: 0,
+ feedback:
+ options.importChoiceFeedback && answerLocation
+ ? `You may find answer for this question at ${answerLocation}`
+ : "",
+ });
+ }
+ });
+
+ if (choices.length < 2) {
+ continue;
+ }
+
+ question.choices = choices;
+ questions.push(question);
+ }
+
+ return questions;
+ };
+
+ describe("Basic Parsing", () => {
+ it("should parse valid Excel rows into questions", () => {
+ const rows = [
+ [
+ "Question text",
+ "CORRECT ANSWER",
+ "Answer 2",
+ "Answer 3",
+ "Answer 4",
+ "Answer location",
+ "Additional Info",
+ ],
+ [
+ "What is 2+2?",
+ "4",
+ "3",
+ "5",
+ "22",
+ "Math basics, page 1",
+ "This is basic arithmetic",
+ ],
+ [
+ "What is the capital of France?",
+ "Paris",
+ "London",
+ "Berlin",
+ "Madrid",
+ "Geography book, chapter 3",
+ "Paris is the largest city in France",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(2);
+ expect(questions[0].question).toBe("What is 2+2?");
+ expect(questions[1].question).toBe("What is the capital of France?");
+ });
+
+ it("should skip rows without question text", () => {
+ const rows = [
+ ["Question text", "CORRECT ANSWER", "Answer 2", "Answer 3", "Answer 4"],
+ ["What is 2+2?", "4", "3", "5", "22"],
+ ["", "Answer", "Wrong1", "Wrong2", "Wrong3"],
+ ["Valid question?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(2);
+ expect(questions[0].question).toBe("What is 2+2?");
+ expect(questions[1].question).toBe("Valid question?");
+ });
+
+ it("should skip questions with less than 2 choices", () => {
+ const rows = [
+ ["Question text", "CORRECT ANSWER", "Answer 2", "Answer 3", "Answer 4"],
+ ["Question with only correct answer?", "Yes", "", "", ""],
+ ["Valid question?", "Yes", "No", "", ""],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(1);
+ expect(questions[0].question).toBe("Valid question?");
+ });
+
+ it("should handle rows with missing optional columns", () => {
+ const rows = [
+ ["Question text", "CORRECT ANSWER", "Answer 2"],
+ ["Simple question?", "Yes", "No"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(1);
+ expect(questions[0].choices).toHaveLength(2);
+ });
+ });
+
+ describe("Choice Feedback with importChoiceFeedback=true", () => {
+ it("should add Additional Info as feedback on correct answer", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ [
+ "Test question?",
+ "Correct answer",
+ "Wrong 1",
+ "Wrong 2",
+ "Wrong 3",
+ "Some location",
+ "This is additional info for correct answer",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows, { importChoiceFeedback: true });
+
+ expect(questions).toHaveLength(1);
+ const correctChoice = questions[0].choices.find((c: any) => c.isCorrect);
+
+ expect(correctChoice.feedback).toContain(
+ "This is additional info for correct answer",
+ );
+ });
+
+ it("should add Answer Location as feedback on incorrect answers", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ [
+ "Test question?",
+ "Correct answer",
+ "Wrong 1",
+ "Wrong 2",
+ "Wrong 3",
+ "Chapter 5, Page 42",
+ "Some info",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows, { importChoiceFeedback: true });
+
+ expect(questions).toHaveLength(1);
+ const incorrectChoices = questions[0].choices.filter(
+ (c: any) => !c.isCorrect,
+ );
+
+ incorrectChoices.forEach((choice: any) => {
+ expect(choice.feedback).toContain("Chapter 5, Page 42");
+ });
+ });
+
+ it("should handle empty feedback columns gracefully", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ [
+ "Test question?",
+ "Correct answer",
+ "Wrong 1",
+ "Wrong 2",
+ "Wrong 3",
+ "",
+ "",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows, { importChoiceFeedback: true });
+
+ expect(questions).toHaveLength(1);
+ questions[0].choices.forEach((choice: any) => {
+ expect(choice.feedback).toBe("");
+ });
+ });
+
+ it("should use feedback format with 'You may find answer' prefix", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ [
+ "Test?",
+ "Yes",
+ "No",
+ "Maybe",
+ "Not sure",
+ "Page 10",
+ "Additional info",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows, { importChoiceFeedback: true });
+
+ const correctChoice = questions[0].choices.find((c: any) => c.isCorrect);
+ const incorrectChoice = questions[0].choices.find(
+ (c: any) => !c.isCorrect,
+ );
+
+ expect(correctChoice.feedback).toBe(
+ "You may find answer for this question at Additional info",
+ );
+ expect(incorrectChoice.feedback).toBe(
+ "You may find answer for this question at Page 10",
+ );
+ });
+ });
+
+ describe("Choice Feedback with importChoiceFeedback=false", () => {
+ it("should not add any feedback when option is disabled", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ [
+ "Test question?",
+ "Correct answer",
+ "Wrong 1",
+ "Wrong 2",
+ "Wrong 3",
+ "Chapter 5, Page 42",
+ "This is additional info",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows, { importChoiceFeedback: false });
+
+ expect(questions).toHaveLength(1);
+ questions[0].choices.forEach((choice: any) => {
+ expect(choice.feedback).toBe("");
+ });
+ });
+ });
+
+ describe("Question Properties", () => {
+ it("should set question type to SINGLE_CORRECT", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].type).toBe("SINGLE_CORRECT");
+ });
+
+ it("should set totalPoints to 1", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].totalPoints).toBe(1);
+ });
+
+ it("should mark first choice as correct with points=1", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ const correctChoice = questions[0].choices[0];
+ expect(correctChoice.isCorrect).toBe(true);
+ expect(correctChoice.points).toBe(1);
+ expect(correctChoice.choice).toBe("Yes");
+ });
+
+ it("should mark other choices as incorrect with points=0", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ const incorrectChoices = questions[0].choices.slice(1);
+ incorrectChoices.forEach((choice: any) => {
+ expect(choice.isCorrect).toBe(false);
+ expect(choice.points).toBe(0);
+ });
+ });
+
+ it("should include all 4 choices when all are provided", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "Maybe", "Not sure"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].choices).toHaveLength(4);
+ expect(questions[0].choices.map((c: any) => c.choice)).toEqual([
+ "Yes",
+ "No",
+ "Maybe",
+ "Not sure",
+ ]);
+ });
+
+ it("should only include provided choices (can be less than 4)", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Test?", "Yes", "No", "", ""],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].choices).toHaveLength(2);
+ expect(questions[0].choices.map((c: any) => c.choice)).toEqual([
+ "Yes",
+ "No",
+ ]);
+ });
+ });
+
+ describe("Multiple Questions", () => {
+ it("should parse all valid questions from multiple rows", () => {
+ const rows = [
+ [
+ "Question",
+ "Correct",
+ "Wrong1",
+ "Wrong2",
+ "Wrong3",
+ "Location",
+ "Info",
+ ],
+ ["Q1?", "A1", "B1", "C1", "D1", "Loc1", "Info1"],
+ ["Q2?", "A2", "B2", "C2", "D2", "Loc2", "Info2"],
+ ["Q3?", "A3", "B3", "C3", "D3", "Loc3", "Info3"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(3);
+ expect(questions[0].question).toBe("Q1?");
+ expect(questions[1].question).toBe("Q2?");
+ expect(questions[2].question).toBe("Q3?");
+ });
+
+ it("should assign correct index to each question", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ ["Q1?", "A1", "B1", "C1", "D1"],
+ ["Q2?", "A2", "B2", "C2", "D2"],
+ ["Q3?", "A3", "B3", "C3", "D3"],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].index).toBe(1);
+ expect(questions[1].index).toBe(2);
+ expect(questions[2].index).toBe(3);
+ });
+ });
+
+ describe("Edge Cases", () => {
+ it("should handle empty rows array", () => {
+ const rows: any[][] = [];
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(0);
+ });
+
+ it("should handle only header row", () => {
+ const rows = [["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"]];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions).toHaveLength(0);
+ });
+
+ it("should trim whitespace from question text and answers", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ [" What is 2+2? ", " 4 ", " 3 ", " 5 ", " 22 "],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].question).toBe("What is 2+2?");
+ expect(questions[0].choices[0].choice).toBe("4");
+ expect(questions[0].choices[1].choice).toBe("3");
+ });
+
+ it("should handle special characters in question text", () => {
+ const rows = [
+ ["Question", "Correct", "Wrong1", "Wrong2", "Wrong3"],
+ [
+ "What's the difference between
and ?",
+ "Semantic meaning",
+ "Visual appearance",
+ "Browser support",
+ "No difference",
+ ],
+ ];
+
+ const questions = parseExcelRows(rows);
+
+ expect(questions[0].question).toContain("");
+ expect(questions[0].question).toContain("");
+ });
+ });
+});
diff --git a/apps/web/components/MarkDownEditor.tsx b/apps/web/components/MarkDownEditor.tsx
index 6c3264b9..1011d1eb 100644
--- a/apps/web/components/MarkDownEditor.tsx
+++ b/apps/web/components/MarkDownEditor.tsx
@@ -45,7 +45,6 @@ const MarkdownEditor: React.FC = ({
);
const [charCount, setCharCount] = useState(value?.length ?? 0);
- // Initialize Quill
useEffect(() => {
let isMounted = true;
const initializeQuill = async () => {
@@ -131,7 +130,6 @@ const MarkdownEditor: React.FC = ({
};
}, [quillInstance]);
- // Prevent paste (strongest method — works 100%)
useEffect(() => {
if (!quillInstance) return;
@@ -150,7 +148,6 @@ const MarkdownEditor: React.FC = ({
}
};
- // capture:true ensures we intercept BEFORE Quill
document.addEventListener("paste", handlePaste, true);
return () => {
@@ -158,7 +155,6 @@ const MarkdownEditor: React.FC = ({
};
}, [quillInstance, allowPaste]);
- // Prevent right-click if needed
useEffect(() => {
if (!quillInstance || allowRightClick) return;
@@ -174,7 +170,6 @@ const MarkdownEditor: React.FC = ({
return () => root.removeEventListener("contextmenu", handleContextMenu);
}, [quillInstance, allowRightClick]);
- // Sync value externally
useEffect(() => {
if (quillInstance) {
const currentHTML = quillInstance.root.innerHTML;
@@ -184,7 +179,6 @@ const MarkdownEditor: React.FC = ({
}
}, [quillInstance, value]);
- // Style injection
useEffect(() => {
const style = document.createElement("style");
style.innerHTML = `
@@ -228,7 +222,9 @@ const MarkdownEditor: React.FC = ({
`;
document.head.appendChild(style);
- return () => document.head.removeChild(style);
+ return () => {
+ document.head.removeChild(style);
+ };
}, []);
return (
diff --git a/apps/web/jest.setup.js b/apps/web/jest.setup.js
index 9643751d..ad1f9d43 100644
--- a/apps/web/jest.setup.js
+++ b/apps/web/jest.setup.js
@@ -1,11 +1,6 @@
-// Optional: configure or set up a testing framework before each test.
-// If you delete this file, remove `setupFilesAfterEnv` from `jest.config.js`
+import "@testing-library/jest-dom";
-// Setup for Node.js environment tests
-import '@testing-library/jest-dom';
-
-// Polyfill for ClipboardEvent (not available in jsdom)
-if (typeof global.ClipboardEvent === 'undefined') {
+if (typeof global.ClipboardEvent === "undefined") {
global.ClipboardEvent = class ClipboardEvent extends Event {
constructor(type, eventInitDict) {
super(type, eventInitDict);
@@ -14,11 +9,10 @@ if (typeof global.ClipboardEvent === 'undefined') {
};
}
-// Mock matchMedia (not available in jsdom)
-if (typeof window !== 'undefined') {
- Object.defineProperty(window, 'matchMedia', {
+if (typeof window !== "undefined") {
+ Object.defineProperty(window, "matchMedia", {
writable: true,
- value: jest.fn().mockImplementation(query => ({
+ value: jest.fn().mockImplementation((query) => ({
matches: false,
media: query,
onchange: null,
diff --git a/apps/web/package.json b/apps/web/package.json
index 6915b48c..ded144d6 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -85,7 +85,7 @@
"styled-components": "^6.1.14",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
- "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz",
+ "xlsx": "^0.18.5",
"zod": "^3.24.2",
"zustand": "^4.4.1"
},
diff --git a/yarn.lock b/yarn.lock
index c1f366d1..57926b53 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5746,6 +5746,11 @@ add@^2.0.6:
resolved "https://registry.yarnpkg.com/add/-/add-2.0.6.tgz#248f0a9f6e5a528ef2295dbeec30532130ae2235"
integrity sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==
+adler-32@~1.3.0:
+ version "1.3.1"
+ resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.3.1.tgz#1dbf0b36dda0012189a32b3679061932df1821e2"
+ integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
+
afinn-165-financialmarketnews@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/afinn-165-financialmarketnews/-/afinn-165-financialmarketnews-3.0.0.tgz#cf422577775bf94f9bc156f3f001a1f29338c3d8"
@@ -6628,6 +6633,14 @@ ccount@^2.0.0:
resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5"
integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
+cfb@~1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.2.2.tgz#94e687628c700e5155436dac05f74e08df23bc44"
+ integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
+ dependencies:
+ adler-32 "~1.3.0"
+ crc-32 "~1.2.0"
+
chalk@4.1.2, chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@@ -6900,6 +6913,11 @@ code-block-writer@^13.0.3:
resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-13.0.3.tgz#90f8a84763a5012da7af61319dd638655ae90b5b"
integrity sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==
+codepage@~1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/codepage/-/codepage-1.15.0.tgz#2e00519024b39424ec66eeb3ec07227e692618ab"
+ integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
+
collect-v8-coverage@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9"
@@ -7197,6 +7215,11 @@ cosmiconfig@^9.0.0:
js-yaml "^4.1.0"
parse-json "^5.2.0"
+crc-32@~1.2.0, crc-32@~1.2.1:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff"
+ integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
+
create-jest@^29.7.0:
version "29.7.0"
resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320"
@@ -8945,6 +8968,11 @@ forwarded@0.2.0:
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+frac@~1.1.2:
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b"
+ integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
+
fraction.js@^4.3.7:
version "4.3.7"
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
@@ -15250,6 +15278,13 @@ sprintf-js@~1.0.2:
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+ssf@~0.11.2:
+ version "0.11.2"
+ resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.11.2.tgz#0b99698b237548d088fc43cdf2b70c1a7512c06c"
+ integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
+ dependencies:
+ frac "~1.1.2"
+
sshpk@^1.14.1, sshpk@^1.7.0:
version "1.18.0"
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
@@ -17060,11 +17095,21 @@ winston@^3.9.0:
triple-beam "^1.3.0"
winston-transport "^4.9.0"
+wmf@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.yarnpkg.com/wmf/-/wmf-1.0.2.tgz#7d19d621071a08c2bdc6b7e688a9c435298cc2da"
+ integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
+
word-wrap@^1.2.5:
version "1.2.5"
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+word@~0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/word/-/word-0.3.0.tgz#8542157e4f8e849f4a363a288992d47612db9961"
+ integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
+
wordnet-db@^3.1.11:
version "3.1.14"
resolved "https://registry.yarnpkg.com/wordnet-db/-/wordnet-db-3.1.14.tgz#7ba1ec2cb5730393f0856efcc738a60085426199"
@@ -17129,14 +17174,23 @@ ws@~8.17.1:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b"
integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==
+xlsx@^0.18.5:
+ version "0.18.5"
+ resolved "https://registry.yarnpkg.com/xlsx/-/xlsx-0.18.5.tgz#16711b9113c848076b8a177022799ad356eba7d0"
+ integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
+ dependencies:
+ adler-32 "~1.3.0"
+ cfb "~1.2.1"
+ codepage "~1.15.0"
+ crc-32 "~1.2.1"
+ ssf "~0.11.2"
+ wmf "~1.0.1"
+ word "~0.3.0"
+
"xlsx@https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz":
version "0.20.2"
resolved "https://cdn.sheetjs.com/xlsx-0.20.2/xlsx-0.20.2.tgz#0f64eeed3f1a46e64724620c3553f2dbd3cd2d7d"
-"xlsx@https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz":
- version "0.20.3"
- resolved "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz#992725af0bb69fa9733590fcb8ab4a57e10b929b"
-
xml-name-validator@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673"