Add text import export for toolbox modules
This commit is contained in:
parent
a9b5ca423a
commit
117b7b79e1
27 changed files with 1430 additions and 181 deletions
|
|
@ -58,6 +58,9 @@ export function validateSiteContent(site) {
|
|||
"library.toolsLabel",
|
||||
"library.categoryLabel",
|
||||
"library.featureHeading",
|
||||
"library.advancedHeading",
|
||||
"library.copyExampleLabel",
|
||||
"library.copyExampleSuccess",
|
||||
"library.controlLegendHeading",
|
||||
"library.categories.notesTracking",
|
||||
"library.categories.references",
|
||||
|
|
@ -488,11 +491,9 @@ export function validateSiteContent(site) {
|
|||
});
|
||||
}
|
||||
});
|
||||
if (docs.importFormat) {
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.title");
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.text");
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.example");
|
||||
}
|
||||
assert.equal(typeof docs.importFormat, "object", `library.toolDocs.${toolType}.importFormat must be an object`);
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.text");
|
||||
assertNonEmptyString({ importFormat: docs.importFormat }, "importFormat.example");
|
||||
});
|
||||
|
||||
const aboutLimits = valueAt(site, "about.limits");
|
||||
|
|
|
|||
|
|
@ -116,6 +116,8 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
const imageAnnotationModule = await readFile("website/src/features/toolboxes/modules/ImageAnnotationModule.jsx", "utf8");
|
||||
const linksModule = await readFile("website/src/features/toolboxes/modules/LinksModule.jsx", "utf8");
|
||||
const textImportModal = await readFile("website/src/features/toolboxes/modules/TextImportModal.jsx", "utf8");
|
||||
const textExchangeActions = await readFile("website/src/features/toolboxes/modules/TextExchangeActions.jsx", "utf8");
|
||||
const textExchangeHook = await readFile("website/src/features/toolboxes/modules/useTextExchange.js", "utf8");
|
||||
const countersModule = await readFile("website/src/features/toolboxes/modules/CountersModule.jsx", "utf8");
|
||||
const textImport = await readFile("website/src/features/toolboxes/modules/textImport.js", "utf8");
|
||||
const imageViewer = await readFile("website/src/components/ImageViewer.jsx", "utf8");
|
||||
|
|
@ -133,7 +135,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(moduleRegistry, /CombosModule/);
|
||||
assert.match(moduleRegistry, /calculator:/);
|
||||
assert.match(moduleRegistry, /table:/);
|
||||
assert.match(moduleRegistry, /table: \{ label: "Tableau", icon: "table", Component: TableModule, editable: false, scrollable: true \}/);
|
||||
assert.match(moduleRegistry, /table: \{ label: "Tableau", icon: "table", Component: TableModule, editable: true, scrollable: true \}/);
|
||||
assert.match(moduleRegistry, /timer:/);
|
||||
assert.match(moduleRegistry, /taskPlanner:/);
|
||||
assert.match(moduleRegistry, /equipmentPlanner:/);
|
||||
|
|
@ -224,6 +226,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(tableModule, /ArrowRight/);
|
||||
assert.match(tableModule, /function HeaderLabelInput/);
|
||||
assert.match(tableModule, /useInlineEdit/);
|
||||
assert.match(tableModule, /TextExchangeActions/);
|
||||
assert.match(tableModule, /event\.target\.select/);
|
||||
assert.match(tableFormulaEngine, /export function evaluateTableCell/);
|
||||
assert.doesNotMatch(tableFormulaEngine, /Function\(/);
|
||||
|
|
@ -235,6 +238,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(timerModule, /useInlineEdit/);
|
||||
assert.match(timerModule, /useDraftForm/);
|
||||
assert.match(timerModule, /normalizeTimerData/);
|
||||
assert.match(timerModule, /TextExchangeActions/);
|
||||
assert.match(taskPlannerModule, /export function TaskPlannerModule/);
|
||||
assert.match(taskPlannerModule, /normalizeTaskPlannerData/);
|
||||
assert.match(taskPlannerModule, /applyDueResets/);
|
||||
|
|
@ -247,6 +251,7 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(taskPlannerModule, /useInlineEdit/);
|
||||
assert.match(taskPlannerModule, /useDraftForm/);
|
||||
assert.match(taskPlannerModule, /commitOnEnter: false/);
|
||||
assert.match(taskPlannerModule, /TextExchangeActions/);
|
||||
assert.match(taskPlannerModule, /reorderFeatures/);
|
||||
assert.doesNotMatch(taskPlannerModule, /canMoveItem:/);
|
||||
assert.doesNotMatch(taskPlannerModule, /canMoveGroup:/);
|
||||
|
|
@ -257,18 +262,14 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(equipmentPlannerModule, /socketLinks/);
|
||||
assert.match(equipmentPlannerModule, /useGroupedReorder/);
|
||||
assert.match(equipmentPlannerModule, /reorderFeatures/);
|
||||
assert.match(equipmentPlannerModule, /TextExchangeActions/);
|
||||
assert.match(checklistModule, /export function ChecklistModule/);
|
||||
assert.match(checklistModule, /TextImportModal/);
|
||||
assert.match(checklistModule, /Icon name="import"/);
|
||||
assert.match(checklistModule, /TextExchangeActions/);
|
||||
assert.match(checklistModule, /editing &&/);
|
||||
assert.match(checklistModule, /ChecklistItem/);
|
||||
assert.match(checklistModule, /ChecklistSection/);
|
||||
assert.match(checklistModule, /parseChecklistImport/);
|
||||
assert.match(checklistModule, /parseColonImportLines/);
|
||||
assert.match(checklistModule, /startsWith\("#"\)/);
|
||||
assert.match(checklistModule, /chevron-down/);
|
||||
assert.match(checklistModule, /chevron-up/);
|
||||
assert.match(checklistModule, /Number\.parseInt/);
|
||||
assert.match(checklistModule, /checklist-qty-current/);
|
||||
assert.match(checklistModule, /checklist-delete-button danger/);
|
||||
assert.match(checklistModule, /useGroupedReorder/);
|
||||
|
|
@ -282,12 +283,21 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(checklistModule, /moveChecklistItemToCategory/);
|
||||
assert.match(textImportModal, /createPortal/);
|
||||
assert.match(textImportModal, /lockBodyScroll/);
|
||||
assert.match(textImportModal, /has-error/);
|
||||
assert.match(textExchangeActions, /export function TextExchangeActions/);
|
||||
assert.match(textExchangeActions, /Icon name="import"/);
|
||||
assert.match(textExchangeActions, /Icon name="export"/);
|
||||
assert.match(textExchangeActions, /useTextExchange/);
|
||||
assert.match(textExchangeHook, /export function useTextExchange/);
|
||||
assert.match(textExchangeHook, /importModuleText/);
|
||||
assert.match(textExchangeHook, /exportModuleText/);
|
||||
assert.match(imagesModule, /export function ImagesModule/);
|
||||
assert.match(imagesModule, /useGroupedReorder/);
|
||||
assert.match(imagesModule, /orientation: "horizontal"/);
|
||||
assert.match(imagesModule, /editing &&/);
|
||||
assert.match(imagesModule, /clipboardData/);
|
||||
assert.match(imagesModule, /createImageAnnotationModule/);
|
||||
assert.match(imagesModule, /TextExchangeActions/);
|
||||
assert.match(imagesModule, /image-annotate-button/);
|
||||
assert.match(imagesModule, /dataTransfer\.files/);
|
||||
assert.match(imageAnnotationModule, /export function ImageAnnotationModule/);
|
||||
|
|
@ -297,14 +307,12 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(imageAnnotationModule, /context\.setImage/);
|
||||
assert.match(imageAnnotationModule, /createMarkerId/);
|
||||
assert.match(imageAnnotationModule, /<Icon name="zoom"/);
|
||||
assert.match(imageAnnotationModule, /TextExchangeActions/);
|
||||
assert.match(linksModule, /export function LinksModule/);
|
||||
assert.match(linksModule, /TextImportModal/);
|
||||
assert.match(linksModule, /Icon name="import"/);
|
||||
assert.match(linksModule, /TextExchangeActions/);
|
||||
assert.match(linksModule, /editing &&/);
|
||||
assert.match(linksModule, /normalizeUrl/);
|
||||
assert.match(linksModule, /copyText/);
|
||||
assert.match(linksModule, /parseColonImportLines/);
|
||||
assert.match(linksModule, /context\.normalizeUrl/);
|
||||
assert.match(linksModule, /useGroupedReorder/);
|
||||
assert.match(linksModule, /useDraftForm/);
|
||||
assert.match(linksModule, /linkDraft\.getFieldProps/);
|
||||
|
|
@ -317,11 +325,15 @@ test("toolbox module registry and modules expose expected behavior", async () =>
|
|||
assert.match(countersModule, /counterDraft\.getFieldProps/);
|
||||
assert.match(countersModule, /orientation: "horizontal"/);
|
||||
assert.match(countersModule, /counter-drag-handle/);
|
||||
assert.match(countersModule, /TextExchangeActions/);
|
||||
assert.match(timerModule, /useGroupedReorder/);
|
||||
assert.match(timerModule, /reorderEnabled=\{!data\.sortResults\}/);
|
||||
assert.match(timerModule, /parentId=\{moduleId\}/);
|
||||
assert.match(timerModule, /timer-drag-handle/);
|
||||
assert.match(textImport, /export function parseColonImportLines/);
|
||||
assert.match(textImport, /export function exportModuleText/);
|
||||
assert.match(textImport, /export function importModuleText/);
|
||||
assert.match(textImport, /export function hasTextExchangeContent/);
|
||||
assert.match(textImport, /line\.indexOf\(":\"\)/);
|
||||
assert.match(imageViewer, /image-viewer-media/);
|
||||
assert.match(imageViewer, /image-viewer-image-frame/);
|
||||
|
|
|
|||
|
|
@ -2,11 +2,29 @@
|
|||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { evaluateTableCell } from "../website/src/features/toolboxes/modules/tableFormulaEngine.js";
|
||||
import { parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { exportModuleText, importModuleText, parseColonImportLines } from "../website/src/features/toolboxes/modules/textImport.js";
|
||||
import { getTimePatternRecurrenceMs, getTimePatternTargetMs } from "../website/src/features/toolboxes/modules/timerUtils.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeChecklistData, normalizeCombosData, normalizeEquipmentPlannerData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { compactModuleDataForStorage, createToolboxExportPayload, normalizeCalculatorData, normalizeChecklistData, normalizeCombosData, normalizeCountersData, normalizeEquipmentPlannerData, normalizeImageAnnotationData, normalizeLinksData, normalizeNotepadData, normalizeTableData, normalizeTaskPlannerData, normalizeTimerData, normalizeUrl, summarizeEquipmentPlannerData } from "../website/src/features/toolboxes/storage/toolboxStorage.js";
|
||||
import { applyGroupedReorderOperation, completeGroupOrder, getBoundaryItemId, getGroupedEntries, moveGroupOrder, moveGroupOrderToEnd, moveGroupOrderToStart, moveItem, moveItemGroup } from "../website/src/hooks/useGroupedReorder.js";
|
||||
|
||||
function createTextImportContext() {
|
||||
let index = 0;
|
||||
return {
|
||||
uid: (prefix) => `${prefix}-${index += 1}`,
|
||||
normalizeChecklistData,
|
||||
normalizeLinksData,
|
||||
normalizeCountersData,
|
||||
normalizeCombosData,
|
||||
normalizeCalculatorData,
|
||||
normalizeTableData,
|
||||
normalizeTimerData,
|
||||
normalizeTaskPlannerData,
|
||||
normalizeEquipmentPlannerData,
|
||||
normalizeImageAnnotationData,
|
||||
normalizeUrl
|
||||
};
|
||||
}
|
||||
|
||||
test("colon text import keeps urls intact after the first separator", () => {
|
||||
assert.deepEqual(parseColonImportLines("Build:https://example.com/build?class=rogue\nPotion:10"), [
|
||||
{ label: "Build", value: "https://example.com/build?class=rogue" },
|
||||
|
|
@ -20,6 +38,140 @@ test("colon text import accepts labels without value", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test("text exchange round trips simple toolbox modules and resets checklist progress", () => {
|
||||
const context = createTextImportContext();
|
||||
const checklist = normalizeChecklistData({
|
||||
sections: [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Potions",
|
||||
items: [
|
||||
{ id: "i1", label: "Potion", qtyTarget: 10, qtyCurrent: 7 },
|
||||
{ id: "i2", label: "Mega potion", qtyTarget: 5, qtyCurrent: 5 }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
const checklistImport = importModuleText("checklist", exportModuleText("checklist", checklist), context);
|
||||
assert.equal(checklistImport.ok, true);
|
||||
assert.deepEqual(checklistImport.data.sections[0].items.map((item) => [item.label, item.qtyTarget, item.qtyCurrent]), [
|
||||
["Potion", 10, 0],
|
||||
["Mega potion", 5, 0]
|
||||
]);
|
||||
|
||||
const links = normalizeLinksData({ links: [{ id: "l1", title: "Build", url: "https://example.com/build" }] });
|
||||
const linksImport = importModuleText("links", exportModuleText("links", links), context);
|
||||
assert.equal(linksImport.ok, true);
|
||||
assert.equal(linksImport.data.links[0].url, "https://example.com/build");
|
||||
|
||||
const counters = normalizeCountersData({ counters: [{ id: "c1", label: "Win", value: 12 }, { id: "c2", label: "Loss", value: -2 }] });
|
||||
const countersImport = importModuleText("counters", exportModuleText("counters", counters), context);
|
||||
assert.equal(countersImport.ok, true);
|
||||
assert.deepEqual(countersImport.data.counters.map((counter) => [counter.label, counter.value]), [["Win", 12], ["Loss", -2]]);
|
||||
});
|
||||
|
||||
test("text exchange imports table TSV and calculator hierarchy", () => {
|
||||
const context = createTextImportContext();
|
||||
const tableImport = importModuleText("table", "@size: 2x2\n@columns: Item\tTotal\n@rows: One\tTwo\n@tsv\nPotion\t=A1\nMega\t5", context);
|
||||
assert.equal(tableImport.ok, true);
|
||||
assert.deepEqual(normalizeTableData(tableImport.data), {
|
||||
rows: 2,
|
||||
columns: 2,
|
||||
cells: { A1: "Potion", B1: "=A1", A2: "Mega", B2: "5" },
|
||||
rowLabels: { 0: "One", 1: "Two" },
|
||||
columnLabels: { 0: "Item", 1: "Total" }
|
||||
});
|
||||
const pureTsvImport = importModuleText("table", "A\tB\n1\t2", context);
|
||||
assert.equal(pureTsvImport.ok, true);
|
||||
assert.equal(pureTsvImport.data.rows, 2);
|
||||
assert.equal(pureTsvImport.data.columns, 2);
|
||||
|
||||
const calculator = normalizeCalculatorData({
|
||||
scrollResults: true,
|
||||
entries: [
|
||||
{ id: "a", label: "Ore", value: 100 },
|
||||
{ id: "b", parentId: "a", label: "Shard", value: 500 }
|
||||
]
|
||||
});
|
||||
const calculatorImport = importModuleText("calculator", exportModuleText("calculator", calculator), context);
|
||||
assert.equal(calculatorImport.ok, true);
|
||||
assert.equal(calculatorImport.data.scrollResults, true);
|
||||
assert.equal(calculatorImport.data.entries[1].parentId, calculatorImport.data.entries[0].id);
|
||||
});
|
||||
|
||||
test("text exchange round trips combos, tasks, equipment, images and timers", () => {
|
||||
const context = createTextImportContext();
|
||||
const combos = normalizeCombosData({
|
||||
device: "xbox",
|
||||
combos: [{ id: "o1", name: "Anti air", category: "Neutral", device: "xbox", inputs: [[{ kind: "direction", value: "down" }], [{ kind: "button", value: "a", holdMs: 2000 }]] }]
|
||||
});
|
||||
const combosImport = importModuleText("combos", exportModuleText("combos", combos), context);
|
||||
assert.equal(combosImport.ok, true);
|
||||
assert.equal(combosImport.data.combos[0].inputs[1][0].holdMs, 2000);
|
||||
|
||||
const tasks = normalizeTaskPlannerData({
|
||||
resetTime: "06:00",
|
||||
tasks: [
|
||||
{ id: "a", title: "Root", type: "daily", category: "Raid", checked: true, checkedAt: 99 },
|
||||
{ id: "b", title: "Child", type: "unique", checked: true, checkedAt: 99 }
|
||||
],
|
||||
relations: [{ id: "r", fromTaskId: "b", toTaskId: "a", prerequisite: true }]
|
||||
});
|
||||
const tasksImport = importModuleText("taskPlanner", exportModuleText("taskPlanner", tasks), context);
|
||||
assert.equal(tasksImport.ok, true);
|
||||
assert.deepEqual(tasksImport.data.tasks.map((task) => [task.title, task.checked, task.checkedAt]), [["Root", false, 0], ["Child", false, 0]]);
|
||||
assert.equal(tasksImport.data.relations[0].prerequisite, true);
|
||||
|
||||
const equipment = normalizeEquipmentPlannerData({
|
||||
types: [{ id: "t", title: "Weapons", icon: "sword" }],
|
||||
equipments: [{
|
||||
id: "e",
|
||||
typeId: "t",
|
||||
name: "Blade",
|
||||
icon: "sword",
|
||||
characteristics: [{ id: "v", category: "Stats", icon: "sword", name: "Attack", value: 12 }],
|
||||
socketItems: [
|
||||
{ id: "j", name: "Jewel", shape: "jewel", color: "yellow", bonuses: [{ id: "b", category: "Skill", name: "Focus", value: 1 }] },
|
||||
{ id: "k", name: "Materia", shape: "ball", color: "blue", bonuses: [] }
|
||||
],
|
||||
socketLinks: [{ id: "u", fromSocketItemId: "j", toSocketItemId: "k" }],
|
||||
materials: [{ id: "m", name: "Ore", qty: 4 }]
|
||||
}]
|
||||
});
|
||||
const equipmentImport = importModuleText("equipmentPlanner", exportModuleText("equipmentPlanner", equipment), context);
|
||||
assert.equal(equipmentImport.ok, true);
|
||||
assert.equal(equipmentImport.data.equipments[0].socketItems[0].bonuses[0].name, "Focus");
|
||||
assert.equal(equipmentImport.data.equipments[0].socketLinks.length, 1);
|
||||
|
||||
const imageDataUrl = "data:image/png;base64,QUJD";
|
||||
const imagesImport = importModuleText("images", `# Map\n${imageDataUrl}`, context);
|
||||
assert.equal(imagesImport.ok, true);
|
||||
assert.equal(imagesImport.data.images[0].dataUrl, imageDataUrl);
|
||||
const annotationImport = importModuleText("imageAnnotation", `${imageDataUrl}\n@marker: 42.5,68,Entry\n@drawings: {"strokes":[]}`, context);
|
||||
assert.equal(annotationImport.ok, true);
|
||||
assert.equal(annotationImport.data.markers[0].label, "Entry");
|
||||
|
||||
const timer = normalizeTimerData({ countdowns: [{ id: "z", label: "Boss", type: "duration", durationMs: 300000, targetAt: Date.now() + 300000 }] });
|
||||
const timerImport = importModuleText("timer", exportModuleText("timer", timer), context);
|
||||
assert.equal(timerImport.ok, true);
|
||||
assert.equal(timerImport.data.countdowns[0].durationMs, 300000);
|
||||
assert.ok(timerImport.data.countdowns[0].targetAt > Date.now());
|
||||
});
|
||||
|
||||
test("text exchange rejects invalid imports without data", () => {
|
||||
const context = createTextImportContext();
|
||||
const invalidLink = importModuleText("links", "Not a url", context);
|
||||
const invalidImage = importModuleText("images", "data:text/plain;base64,QUJD", context);
|
||||
const invalidMarker = importModuleText("imageAnnotation", "data:image/png;base64,QUJD\n@marker: 120,0,Bad", context);
|
||||
assert.equal(invalidLink.ok, false);
|
||||
assert.match(invalidLink.error, /^Ligne 1:/);
|
||||
assert.equal(invalidImage.ok, false);
|
||||
assert.match(invalidImage.error, /^Ligne 1:/);
|
||||
assert.equal(invalidMarker.ok, false);
|
||||
assert.match(invalidMarker.error, /^Ligne 2:/);
|
||||
assert.equal(importModuleText("table", "\n", context).ok, false);
|
||||
});
|
||||
|
||||
test("time pattern recurrence uses configured frequency instead of next remaining delay", () => {
|
||||
const now = new Date(2026, 0, 1, 13, 23, 45, 0).getTime();
|
||||
const target = getTimePatternTargetMs("X:24:X", now);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue