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 = ;
+ } else {
+ 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 = ;
- } else {
- 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 <>
} onClick={() => { setVisible(true); }}>Export to string
@@ -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