diff --git a/packages/lib/data/ModPack.js b/packages/lib/data/ModPack.js index 7536f7b8..f574b524 100644 --- a/packages/lib/data/ModPack.js +++ b/packages/lib/data/ModPack.js @@ -112,6 +112,19 @@ class ModPack { */ isDeleted = false; + shallowClone() { + const clone = new this.constructor(); + clone.id = this.id; + clone.name = this.name; + clone.description = this.description; + clone.factorioVersion = this.factorioVersion; + clone.mods = this.mods; + clone.settings = this.settings; + clone.exportManifest = this.exportManifest; + clone.isDeleted = this.isDeleted; + return clone; + } + static jsonSchema = { type: "object", additionalProperties: false, diff --git a/packages/web_ui/src/components/ModPackViewPage.jsx b/packages/web_ui/src/components/ModPackViewPage.jsx index ed24d266..3a8a4b26 100644 --- a/packages/web_ui/src/components/ModPackViewPage.jsx +++ b/packages/web_ui/src/components/ModPackViewPage.jsx @@ -1,4 +1,4 @@ -import React, { Fragment, useEffect, useContext, useRef, useState } from "react"; +import React, { Fragment, memo, useCallback, useEffect, useContext, useRef, useState } from "react"; import { useHistory, useParams } from "react-router-dom"; import { Button, Card, Checkbox, Col, ConfigProvider, Descriptions, Form, Input, PageHeader, Pagination, @@ -187,7 +187,7 @@ function SearchModsTable(props) { onChange={modResultsTableChanged} dataSource={modResults} pagination={false} - rowKey={result => result.name} + rowKey="name" /> ; @@ -327,7 +327,7 @@ function ModsTable(props) { }} dataSource={mods} pagination={false} - rowKey={mod => mod.name} + rowKey="name" rowClassName={mod => { if (deletedMods.has(mod.name)) { return "deleted"; @@ -363,6 +363,45 @@ function groupToMap(array, fn) { return map; } +const SettingsTableField = memo((props) => { + const name = props.name; + const isBoolean = props.type === "bool-setting"; + const isNumber = ["int-setting", "double-setting"].includes(props.type); + const isSelect = Boolean(props.allowedValues); + let input; + if (isBoolean) { + input = ; + } else if (isSelect) { + const options = Object.values(props.allowedValues).map(value => ({ + value, + label: props.locale.get(`string-mod-setting.${name}-${value}`) || value, + })); + input = ; + } + + return + {input} + ; +}); + function SettingsTable(props) { const [modList] = useModList(); const modsInPack = new Set([...props.modPack.mods.values()].map(mod => `${mod.name}_${mod.version}`)); @@ -403,47 +442,17 @@ function SettingsTable(props) { {props.locale.get(`mod-name.${mod}`) || modTitles.get(mod) || mod} - {[...fieldsForMod].map(prototype => { - const name = prototype.name; - const isBoolean = prototype.type === "bool-setting"; - const isNumber = ["int-setting", "double-setting"].includes(prototype.type); - const isSelect = Boolean(prototype.allowed_values); - let input; - if (isBoolean) { - input = ; - } else if (isSelect) { - const options = Object.values(prototype.allowed_values).map(value => ({ - value, - label: props.locale.get(`string-mod-setting.${name}-${value}`) || value, - })); - input = ; - } - - return - {input} - ; - })} + {[...fieldsForMod].map( + prototype => + )} ); } @@ -499,7 +508,8 @@ function ExportButton(props) { setVisible(false); } - let exportString = props.modPack.toModPackString(); + let exportString; + if (visible) { exportString = props.modPack.toModPackString(); } return <> @@ -548,6 +558,43 @@ function useExportedAsset(modPack, asset) { return assetData; } +function applyModPackChanges(modPack, changes) { + let modifiedModPack = modPack.shallowClone(); + for (let change of changes) { + // Only modify fields that change to reduce re-renders + if (["mods.set", "mods.delete"].includes(change.type) && modifiedModPack.mods === modPack.mods) { + modifiedModPack.mods = new Map(modifiedModPack.mods); + } + if ( + ["settings.set", "settings.delete"].includes(change.type) + && modifiedModPack.settings[change.scope] === modPack.settings[change.scope] + ) { + if (modifiedModPack.settings === modPack.settings) { + modifiedModPack.settings = { ...modPack.settings }; + } + modifiedModPack.settings[change.scope] = new Map(modifiedModPack.settings[change.scope]); + } + + if (change.type === "name") { + modifiedModPack.name = change.value; + } else if (change.type === "description") { + modifiedModPack.description = change.value; + } else if (change.type === "factorioVersion") { + modifiedModPack.factorioVersion = change.value; + } else if (change.type === "mods.set") { + modifiedModPack.mods.set(change.name, change.value); + } else if (change.type === "mods.delete") { + modifiedModPack.mods.delete(change.name); + } else if (change.type === "settings.set") { + modifiedModPack.settings[change.scope].set(change.name, change.value); + } else if (change.type === "settings.delete") { + modifiedModPack.settings[change.scope].delete(change.name); + } else { + throw new Error(`Unknown change type ${change.type}`); + } + } +} + export default function ModPackViewPage() { let account = useAccount(); let history = useHistory(); @@ -560,12 +607,20 @@ export default function ModPackViewPage() { let prototypes = useExportedAsset(modPack, "settings"); let locale = useExportedAsset(modPack, "locale"); let [changes, setChanges] = useState([]); - let [modifiedModPack, setModifiedModPack] = useState(modPack); let syncTimeout = useRef(); + let modifiedModPack; + if (!(modPack instanceof libData.ModPack)) { + // Loading or invalid id + modifiedModPack = modPack; + + } else { + modifiedModPack = applyModPackChanges(modPack, changes); + } + useEffect(() => () => { clearTimeout(syncTimeout.current); }); - function pushChange(change) { + const pushChange = useCallback((change) => { setChanges(oldChanges => { const newChanges = [...oldChanges]; if ( @@ -580,20 +635,22 @@ export default function ModPackViewPage() { } return newChanges; }); - } + }, []); - function revertChange(change) { - const newChanges = [...changes]; - const index = newChanges.findLastIndex(reference => ( - change.type === reference.type && change.name === reference.name - )); - if (index === -1) { - logger.error(`Unable to revert ${change.type} ${change.name}: change not found`); - return; - } - newChanges.splice(index, 1); - setChanges(newChanges); - } + const revertChange = useCallback((change) => { + setChanges(oldChanges => { + const newChanges = [...oldChanges]; + const index = newChanges.findLastIndex(reference => ( + change.type === reference.type && change.name === reference.name + )); + if (index === -1) { + logger.error(`Unable to revert ${change.type} ${change.name}: change not found`); + return oldChanges; + } + newChanges.splice(index, 1); + return newChanges; + }); + }, []); function syncPack(fields) { for (let field of fields) { @@ -621,36 +678,6 @@ export default function ModPackViewPage() { } } - useEffect(() => { - // Loading or invalid id - if (!(modPack instanceof libData.ModPack)) { - setModifiedModPack(modPack); - return; - } - - const modified = libData.ModPack.fromJSON(JSON.parse(JSON.stringify(modPack))); - for (let change of changes) { - if (change.type === "name") { - modified.name = change.value; - } else if (change.type === "description") { - modified.description = change.value; - } else if (change.type === "factorioVersion") { - modified.factorioVersion = change.value; - } else if (change.type === "mods.set") { - modified.mods.set(change.name, change.value); - } else if (change.type === "mods.delete") { - modified.mods.delete(change.name); - } else if (change.type === "settings.set") { - modified.settings[change.scope].set(change.name, change.value); - } else if (change.type === "settings.delete") { - modified.settings[change.scope].delete(change.name); - } else { - throw new Error(`Unknown change type ${change.type}`); - } - } - setModifiedModPack(modified); - }, [modPack, changes]); - let nav = [{ name: "Mods", path: "/mods" }, { name: "Mod Packs" }, { name: modPack.name || modPackId }]; if (modifiedModPack.loading) { return