Sfoglia il codice sorgente

Integrate the per-phase scripts modal into every flow

`WizardScripts` now manages scripts through the
per-target modal and a single `onScriptsChange` callback,
applied across the flows via `applyUserScriptsChange`.

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 1 settimana fa
parent
commit
e18332114f

+ 16 - 27
src/components/modules/TransferModule/DeploymentOptions/DeploymentOptions.tsx

@@ -32,7 +32,12 @@ import deploymentImage from "./images/deployment.svg";
 import deploymentFields from "./DeploymentFields";
 
 import type { Field } from "@src/@types/Field";
-import type { Instance, InstanceScript } from "@src/@types/Instance";
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
+import { applyUserScriptsChange } from "@src/utils/UserScriptUtils";
 const Wrapper = styled.div<any>`
   display: flex;
   flex-direction: column;
@@ -171,24 +176,14 @@ class DeploymentOptions extends React.Component<Props, State> {
     });
   }
 
-  handleCancelScript(global: string | null, instanceName: string | null) {
-    this.setState(prevState => ({
-      uploadedScripts: prevState.uploadedScripts.filter(s =>
-        global ? s.global !== global : s.instanceId !== instanceName,
-      ),
-    }));
-  }
-
-  handleScriptUpload(script: InstanceScript) {
-    this.setState(prevState => ({
-      uploadedScripts: [...prevState.uploadedScripts, script],
-    }));
-  }
-
-  handleScriptRemove(script: InstanceScript) {
-    this.setState(prevState => ({
-      removedScripts: [...prevState.removedScripts, script],
-    }));
+  handleScriptsChange(
+    target: UserScriptTarget,
+    scripts: InstanceScript[],
+    hadExisting: boolean,
+  ) {
+    this.setState(prevState =>
+      applyUserScriptsChange(prevState, target, scripts, hadExisting),
+    );
   }
 
   renderField(field: Field) {
@@ -270,14 +265,8 @@ class DeploymentOptions extends React.Component<Props, State> {
       <WizardScripts
         instances={this.props.instances}
         loadingInstances={this.props.loadingInstances}
-        onScriptUpload={s => {
-          this.handleScriptUpload(s);
-        }}
-        onScriptDataRemove={s => {
-          this.handleScriptRemove(s);
-        }}
-        onCancelScript={(g, i) => {
-          this.handleCancelScript(g, i);
+        onScriptsChange={(target, scripts, hadExisting) => {
+          this.handleScriptsChange(target, scripts, hadExisting);
         }}
         uploadedScripts={this.state.uploadedScripts}
         removedScripts={this.state.removedScripts}

+ 25 - 11
src/components/modules/TransferModule/DeploymentOptions/ReplicaDeploymentOptions.spec.tsx

@@ -34,16 +34,24 @@ jest.mock("@src/components/modules/WizardModule/WizardScripts", () => ({
       <div
         data-testid="ScriptsRemove"
         onClick={() => {
-          props.onScriptDataRemove(props.uploadedScripts[0]);
+          props.onScriptsChange(
+            { global: "windows", instanceId: null },
+            [],
+            true,
+          );
         }}
       />
       <div data-testid="ScriptsRemoved">
-        {props.removedScripts.map(s => s.scriptContent).join(", ")}
+        {props.removedScripts.map(s => s.global || s.instanceId).join(", ")}
       </div>
       <div
         data-testid="ScriptsCancel"
         onClick={() => {
-          props.onCancelScript("windows", null);
+          props.onScriptsChange(
+            { global: "windows", instanceId: null },
+            [],
+            false,
+          );
           props.scrollableRef &&
             props.scrollableRef(null as any as HTMLElement);
         }}
@@ -51,11 +59,18 @@ jest.mock("@src/components/modules/WizardModule/WizardScripts", () => ({
       <div
         data-testid="ScriptsUpload"
         onClick={() => {
-          props.onScriptUpload({
-            scriptContent: `script-content-${Math.random()}`,
-            fileName: `script-name.ps1`,
-            global: "windows",
-          });
+          props.onScriptsChange(
+            { global: "windows", instanceId: null },
+            [
+              {
+                scriptContent: `script-content-${Math.random()}`,
+                fileName: `script-name.ps1`,
+                global: "windows",
+                phase: "osmorphing_post_os_mount",
+              },
+            ],
+            false,
+          );
         }}
       />
     </div>
@@ -124,9 +139,8 @@ describe("ReplicaDeploymentOptions", () => {
     );
     expect(getByTestId("ScriptsRemoved").textContent).toBe("");
     fireEvent.click(getByTestId("ScriptsRemove"));
-    expect(getByTestId("ScriptsRemoved").textContent).toContain(
-      "script-content",
-    );
+    expect(getByTestId("ScriptsUploaded").textContent).toBe("");
+    expect(getByTestId("ScriptsRemoved").textContent).toContain("windows");
   });
 
   it("doesn't render minion pool mappings", () => {

+ 15 - 29
src/components/modules/TransferModule/TransferItemModal/TransferItemModal.tsx

@@ -46,7 +46,12 @@ import {
   StorageMap,
 } from "@src/@types/Endpoint";
 import type { Field } from "@src/@types/Field";
-import type { Instance, InstanceScript } from "@src/@types/Instance";
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
+import { applyUserScriptsChange } from "@src/utils/UserScriptUtils";
 import {
   Network,
   NetworkMap,
@@ -762,27 +767,14 @@ class TransferItemModal extends React.Component<Props, State> {
     });
   }
 
-  handleCancelScript(
-    global: "windows" | "linux" | null,
-    instanceName: string | null,
+  handleScriptsChange(
+    target: UserScriptTarget,
+    scripts: InstanceScript[],
+    hadExisting: boolean,
   ) {
-    this.setState(prevState => ({
-      uploadedScripts: prevState.uploadedScripts.filter(s =>
-        global ? s.global !== global : s.instanceId !== instanceName,
-      ),
-    }));
-  }
-
-  handleScriptUpload(script: InstanceScript) {
-    this.setState(prevState => ({
-      uploadedScripts: [...prevState.uploadedScripts, script],
-    }));
-  }
-
-  handleScriptDataRemove(script: InstanceScript) {
-    this.setState(prevState => ({
-      removedScripts: [...prevState.removedScripts, script],
-    }));
+    this.setState(prevState =>
+      applyUserScriptsChange(prevState, target, scripts, hadExisting),
+    );
   }
 
   handleStorageChange(mapping: StorageMap) {
@@ -963,14 +955,8 @@ class TransferItemModal extends React.Component<Props, State> {
       <WizardScripts
         instances={this.props.instancesDetails}
         loadingInstances={this.props.instancesDetailsLoading}
-        onScriptUpload={s => {
-          this.handleScriptUpload(s);
-        }}
-        onScriptDataRemove={s => {
-          this.handleScriptDataRemove(s);
-        }}
-        onCancelScript={(g, i) => {
-          this.handleCancelScript(g, i);
+        onScriptsChange={(target, scripts, hadExisting) => {
+          this.handleScriptsChange(target, scripts, hadExisting);
         }}
         uploadedScripts={this.state.uploadedScripts}
         removedScripts={this.state.removedScripts}

+ 9 - 8
src/components/modules/WizardModule/WizardPageContent/WizardPageContent.tsx

@@ -53,7 +53,11 @@ import configLoader from "@src/utils/Config";
 import transferItemIcon from "./images/transferItemIcon";
 
 import type { WizardData, WizardPage } from "@src/@types/WizardData";
-import type { Instance, InstanceScript } from "@src/@types/Instance";
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
 import type { Field } from "@src/@types/Field";
 import type { Schedule as ScheduleType } from "@src/@types/Schedule";
 
@@ -174,10 +178,9 @@ type Props = {
   onContentRef: (ref: any) => void;
   onReloadOptionsClick: () => void;
   onReloadNetworksClick: () => void;
-  onUserScriptUpload: (instanceScript: InstanceScript) => void;
-  onCancelUploadedScript: (
-    global: string | null,
-    instanceName: string | null,
+  onUserScriptsChange: (
+    target: UserScriptTarget,
+    scripts: InstanceScript[],
   ) => void;
   onTransferExecuteOptionsChange: (field: Field, value: any) => void;
 };
@@ -604,12 +607,10 @@ class WizardPageContent extends React.Component<Props, State> {
         body = (
           <WizardScripts
             instances={this.props.instanceStore.instancesDetails}
-            onScriptUpload={this.props.onUserScriptUpload}
-            onCancelScript={this.props.onCancelUploadedScript}
+            onScriptsChange={this.props.onUserScriptsChange}
             uploadedScripts={this.props.uploadedUserScripts}
             userScriptData={null}
             removedScripts={[]}
-            onScriptDataRemove={() => {}}
           />
         );
         break;

+ 34 - 5
src/components/modules/WizardModule/WizardScripts/WizardScripts.spec.tsx

@@ -14,7 +14,7 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import React from "react";
 
-import { render } from "@testing-library/react";
+import { fireEvent, render, screen } from "@testing-library/react";
 import { INSTANCE_MOCK } from "@tests/mocks/InstancesMock";
 
 import WizardScripts from "./";
@@ -28,14 +28,43 @@ describe("WizardScripts", () => {
       uploadedScripts: [],
       removedScripts: [],
       userScriptData: null,
-      onScriptUpload: jest.fn(),
-      onCancelScript: jest.fn(),
-      onScriptDataRemove: jest.fn(),
+      onScriptsChange: jest.fn(),
     };
   });
 
   it("renders without crashing", () => {
-    const { getByText } = render(<WizardScripts {...defaultProps} />);
+    const { getByText, getAllByText } = render(
+      <WizardScripts {...defaultProps} />,
+    );
     expect(getByText(INSTANCE_MOCK.name)).toBeTruthy();
+    expect(getAllByText("Choose Scripts").length).toBeGreaterThan(0);
+  });
+
+  it("offers 'Edit Scripts' for a configured target and 'Choose Scripts' otherwise", () => {
+    const { getByText, getAllByText } = render(
+      <WizardScripts
+        {...defaultProps}
+        userScriptData={{
+          global: {
+            linux: [
+              { phase: "osmorphing_pre_os_mount", payload: "echo pre" },
+              { phase: "replica_first_boot", payload: "echo boot" },
+            ],
+          },
+        }}
+      />,
+    );
+    expect(getByText("Edit Scripts")).toBeTruthy();
+    expect(getAllByText("Choose Scripts").length).toBe(2);
+  });
+
+  it("opens the per-phase modal showing all three phases", () => {
+    const { getAllByText } = render(<WizardScripts {...defaultProps} />);
+    fireEvent.click(getAllByText("Choose Scripts")[0]);
+    expect(screen.getByText("Windows Script File - User Scripts")).toBeTruthy();
+    expect(screen.getByText("OS morphing: before mount")).toBeTruthy();
+    expect(screen.getByText("OS morphing: after mount")).toBeTruthy();
+    expect(screen.getByText("VM first boot script")).toBeTruthy();
+    expect(screen.getAllByText("Choose File...").length).toBe(3);
   });
 });

+ 126 - 171
src/components/modules/WizardModule/WizardScripts/WizardScripts.tsx

@@ -14,20 +14,47 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import { observer } from "mobx-react";
 import React from "react";
-import styled, { css } from "styled-components";
+import styled from "styled-components";
 
-import { UserScriptData } from "@src/@types/MainItem";
+import { UserScriptData, UserScriptValue } from "@src/@types/MainItem";
 import { InstanceImage } from "@src/components/modules/WizardModule/WizardInstances";
 import { ThemePalette, ThemeProps } from "@src/components/Theme";
 import InfoIcon from "@src/components/ui/InfoIcon";
+import Modal from "@src/components/ui/Modal";
 import StatusIcon from "@src/components/ui/StatusComponents/StatusIcon";
-import { Close as InputClose } from "@src/components/ui/TextInput";
-import DomUtils from "@src/utils/DomUtils";
-import FileUtils from "@src/utils/FileUtils";
 
 import scriptItemImage from "./images/script-item.svg";
+import UserScriptsModal, { ScriptsByPhase } from "./UserScriptsModal";
+
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
+import {
+  DEFAULT_USER_SCRIPT_PHASE,
+  USER_SCRIPT_PHASES,
+  UserScriptPhase,
+} from "@src/@types/Instance";
+
+const parseScriptValueByPhase = (value: UserScriptValue): ScriptsByPhase => {
+  const map: ScriptsByPhase = {};
+  if (!value) {
+    return map;
+  }
+  if (typeof value === "string") {
+    map[DEFAULT_USER_SCRIPT_PHASE] = { content: value, fileName: null };
+    return map;
+  }
+  value.forEach(item => {
+    map[item.phase || DEFAULT_USER_SCRIPT_PHASE] = {
+      content: item.payload,
+      fileName: null,
+    };
+  });
+  return map;
+};
 
-import type { Instance, InstanceScript } from "@src/@types/Instance";
 const Wrapper = styled.div<any>`
   width: 100%;
   display: flex;
@@ -108,45 +135,7 @@ const LinkButton = styled.div<any>`
     text-decoration: underline;
   }
 `;
-const UploadedScript = styled.div<any>`
-  display: flex;
-  position: relative;
-`;
-const UploadedScriptFileName = styled.div<any>`
-  max-width: 124px;
-  text-overflow: ellipsis;
-  overflow: hidden;
-  margin-right: 32px;
-  white-space: nowrap;
-`;
-const InputCloseStyled = styled(InputClose)`
-  top: 0px;
-`;
-const FakeFileInput = styled.input`
-  position: absolute;
-  opacity: 0;
-  top: -99999px;
-`;
-const ScriptDataActions = styled.div`
-  display: flex;
-  margin-left: -8px;
-  margin-top: 8px;
-  > div {
-    margin-left: 8px;
-  }
-`;
-const ScriptDataAction = styled.div<{ red?: boolean; disabled?: boolean }>`
-  color: ${props => (props.red ? ThemePalette.alert : ThemePalette.primary)};
-  cursor: pointer;
-  ${props =>
-    props.disabled
-      ? css`
-          opacity: 0.6;
-          cursor: default;
-        `
-      : ""}
-  font-size: 12px;
-`;
+type ModalTarget = UserScriptTarget & { title: string };
 
 type Props = {
   instances: Instance[];
@@ -156,44 +145,76 @@ type Props = {
   loadingInstances?: boolean;
   userScriptData: UserScriptData | null | undefined;
   style?: React.CSSProperties;
-  onScriptUpload: (instanceScript: InstanceScript) => void;
-  onCancelScript: (
-    global: "windows" | "linux" | null,
-    instanceName: string | null,
-  ) => void;
   onScrollableRef?: (ref: HTMLElement) => void;
   scrollableRef?: (r: HTMLElement) => void;
-  onScriptDataRemove: (script: InstanceScript) => void;
+  onScriptsChange: (
+    target: UserScriptTarget,
+    scripts: InstanceScript[],
+    hadExisting: boolean,
+  ) => void;
 };
-type FileInputRefs = {
-  [prop: string]: {
-    inputRef: HTMLInputElement;
-  };
+type State = {
+  modalTarget: ModalTarget | null;
 };
 @observer
-class WizardScripts extends React.Component<Props> {
-  fileInputRefs: FileInputRefs = {};
+class WizardScripts extends React.Component<Props, State> {
+  state: State = {
+    modalTarget: null,
+  };
 
-  async handleFileUpload(
-    files: FileList | null,
-    global: "windows" | "linux" | null,
-    instanceId: string | null,
-  ) {
-    if (!files || !files.length) {
-      return;
+  matchesTarget(script: InstanceScript, target: UserScriptTarget): boolean {
+    return target.global
+      ? script.global === target.global
+      : script.instanceId === target.instanceId;
+  }
+
+  getBaseValue(target: UserScriptTarget): UserScriptValue {
+    if (target.global) {
+      return this.props.userScriptData?.global?.[target.global] ?? null;
+    }
+    if (target.instanceId) {
+      return this.props.userScriptData?.instances?.[target.instanceId] ?? null;
+    }
+    return null;
+  }
+
+  getScriptsByPhase(target: UserScriptTarget): ScriptsByPhase {
+    const uploaded = this.props.uploadedScripts.filter(s =>
+      this.matchesTarget(s, target),
+    );
+    const isRemoved = this.props.removedScripts.some(s =>
+      this.matchesTarget(s, target),
+    );
+    if (uploaded.length || isRemoved) {
+      const map: ScriptsByPhase = {};
+      uploaded.forEach(s => {
+        map[s.phase || DEFAULT_USER_SCRIPT_PHASE] = {
+          content: s.scriptContent,
+          fileName: s.fileName,
+        };
+      });
+      return map;
     }
-    const fileName = files[0].name;
-    const scriptContent = await FileUtils.readTextFromFirstFile(files);
-    this.props.onScriptUpload({
-      instanceId,
-      global,
-      fileName,
-      scriptContent: scriptContent || "",
-    });
+    return parseScriptValueByPhase(this.getBaseValue(target));
+  }
+
+  getConfiguredPhases(target: UserScriptTarget): UserScriptPhase[] {
+    const map = this.getScriptsByPhase(target);
+    return USER_SCRIPT_PHASES.filter(phase => map[phase]?.content);
+  }
+
+  computeHadExisting(target: UserScriptTarget): boolean {
+    const baseMap = parseScriptValueByPhase(this.getBaseValue(target));
+    return USER_SCRIPT_PHASES.some(phase => baseMap[phase]?.content);
   }
 
-  handleScriptDataDownload(scriptData: string, fileName: string) {
-    DomUtils.download(scriptData, fileName);
+  handleModalSave(target: ModalTarget, scripts: InstanceScript[]) {
+    this.props.onScriptsChange(
+      { global: target.global, instanceId: target.instanceId },
+      scripts,
+      this.computeHadExisting(target),
+    );
+    this.setState({ modalTarget: null });
   }
 
   renderScriptItem(opts: {
@@ -203,24 +224,11 @@ class WizardScripts extends React.Component<Props> {
     subtitle?: string;
   }) {
     const { global, instanceId, title, subtitle } = opts;
-    const uploadedScript = this.props.uploadedScripts.find(s =>
-      s.instanceId
-        ? s.instanceId === instanceId
-        : s.global
-          ? s.global === global
-          : false,
-    );
-    let scriptData: string | null | undefined = null;
-    if (global) {
-      scriptData = this.props.userScriptData?.global?.[global];
-    } else if (instanceId) {
-      scriptData = this.props.userScriptData?.instances?.[instanceId];
-    }
-    const isRemoved = Boolean(
-      this.props.removedScripts.find(s =>
-        global ? s.global === global : s.instanceId === instanceId,
-      ),
-    );
+    const target: UserScriptTarget = {
+      global: global ?? null,
+      instanceId: instanceId ?? null,
+    };
+    const isConfigured = this.getConfiguredPhases(target).length > 0;
 
     return (
       <Script key={title}>
@@ -231,86 +239,15 @@ class WizardScripts extends React.Component<Props> {
             {subtitle ? (
               <NameLabelSubtitle>{subtitle}</NameLabelSubtitle>
             ) : null}
-            {scriptData ? (
-              <ScriptDataActions>
-                <ScriptDataAction
-                  title="Downloads the currently uploaded script"
-                  onClick={() => {
-                    this.handleScriptDataDownload(
-                      scriptData as string,
-                      title.toLowerCase().replaceAll(" ", "_"),
-                    );
-                  }}
-                >
-                  Download
-                </ScriptDataAction>
-                <ScriptDataAction
-                  title={
-                    isRemoved
-                      ? "The currently uploaded script will be removed"
-                      : "Removes the currently uploaded script"
-                  }
-                  red
-                  disabled={isRemoved}
-                  onClick={() => {
-                    if (isRemoved) {
-                      return;
-                    }
-                    this.props.onScriptDataRemove({
-                      global,
-                      instanceId,
-                      scriptContent: null,
-                      fileName: null,
-                    });
-                  }}
-                >
-                  {isRemoved ? "To be removed" : "Remove"}
-                </ScriptDataAction>
-              </ScriptDataActions>
-            ) : null}
           </NameLabel>
         </Name>
-        {uploadedScript ? (
-          <UploadedScript>
-            <UploadedScriptFileName title={uploadedScript.fileName}>
-              {uploadedScript.fileName}
-            </UploadedScriptFileName>
-            <InputCloseStyled
-              show
-              onClick={() => {
-                this.props.onCancelScript(global || null, instanceId || null);
-                const ref = this.fileInputRefs[title];
-                if (ref) {
-                  ref.inputRef.value = "";
-                }
-              }}
-            />
-          </UploadedScript>
-        ) : (
-          <LinkButton
-            onClick={() => {
-              const ref = this.fileInputRefs[title];
-              if (ref) {
-                ref.inputRef.click();
-              }
-            }}
-          >
-            Choose File...
-          </LinkButton>
-        )}
-        <FakeFileInput
-          type="file"
-          ref={(r: HTMLInputElement) => {
-            this.fileInputRefs[title] = { inputRef: r };
-          }}
-          onChange={e => {
-            this.handleFileUpload(
-              e.target.files,
-              global || null,
-              instanceId || null,
-            );
+        <LinkButton
+          onClick={() => {
+            this.setState({ modalTarget: { ...target, title } });
           }}
-        />
+        >
+          {isConfigured ? "Edit Scripts" : "Choose Scripts"}
+        </LinkButton>
       </Script>
     );
   }
@@ -323,7 +260,7 @@ class WizardScripts extends React.Component<Props> {
             Global Scripts
             <InfoIconStyled
               layout={this.props.layout}
-              text="Specify user scripts that will run during OS morphing for a particular OS type"
+              text="Specify user scripts that will run during OS morphing for a particular OS type. You can attach one script per phase."
             />
           </Heading>
           <Scripts>
@@ -351,7 +288,7 @@ class WizardScripts extends React.Component<Props> {
           {!this.props.loadingInstances ? (
             <InfoIconStyled
               layout={this.props.layout}
-              text="Specify user scripts that will run during OS morphing for a particular instance. These override the uploaded global scripts."
+              text="Specify user scripts that will run during OS morphing for a particular instance. You can attach one script per phase. These override the uploaded global scripts."
             />
           ) : null}
           {this.props.loadingInstances ? (
@@ -383,6 +320,7 @@ class WizardScripts extends React.Component<Props> {
   }
 
   render() {
+    const { modalTarget } = this.state;
     return (
       <Wrapper
         style={this.props.style}
@@ -394,6 +332,23 @@ class WizardScripts extends React.Component<Props> {
       >
         {this.renderScriptGroup("global")}
         {this.renderScriptGroup("instance")}
+        <Modal
+          isOpen={Boolean(modalTarget)}
+          title={modalTarget ? `${modalTarget.title} - User Scripts` : ""}
+          contentWidth={576}
+          onRequestClose={() => this.setState({ modalTarget: null })}
+        >
+          {modalTarget ? (
+            <UserScriptsModal
+              title={modalTarget.title}
+              global={modalTarget.global}
+              instanceId={modalTarget.instanceId}
+              scriptsByPhase={this.getScriptsByPhase(modalTarget)}
+              onRequestClose={() => this.setState({ modalTarget: null })}
+              onSave={scripts => this.handleModalSave(modalTarget, scripts)}
+            />
+          ) : null}
+        </Modal>
       </Wrapper>
     );
   }

+ 9 - 15
src/components/smart/WizardPage/WizardPage.tsx

@@ -52,7 +52,11 @@ import type {
   Endpoint as EndpointType,
   StorageMap,
 } from "@src/@types/Endpoint";
-import type { Instance, InstanceScript } from "@src/@types/Instance";
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
 import type { Field } from "@src/@types/Field";
 import type { Schedule } from "@src/@types/Schedule";
 import type { WizardPage as WizardPageType } from "@src/@types/WizardData";
@@ -834,15 +838,8 @@ class WizardPage extends React.Component<Props, State> {
     transferStore.execute(transfer.id, executeNowOptions);
   }
 
-  handleCancelUploadedScript(
-    global: string | null,
-    instanceName: string | null,
-  ) {
-    wizardStore.cancelUploadedScript(global, instanceName);
-  }
-
-  handleUserScriptUpload(instanceScript: InstanceScript) {
-    wizardStore.uploadUserScript(instanceScript);
+  handleUserScriptsChange(target: UserScriptTarget, scripts: InstanceScript[]) {
+    wizardStore.setUserScripts(target, scripts);
   }
 
   render() {
@@ -944,11 +941,8 @@ class WizardPage extends React.Component<Props, State> {
                 this.loadNetworks(false);
               }}
               uploadedUserScripts={wizardStore.uploadedUserScripts}
-              onCancelUploadedScript={(g, i) => {
-                this.handleCancelUploadedScript(g, i);
-              }}
-              onUserScriptUpload={s => {
-                this.handleUserScriptUpload(s);
+              onUserScriptsChange={(target, scripts) => {
+                this.handleUserScriptsChange(target, scripts);
               }}
               onTransferExecuteOptionsChange={(field, value) => {
                 this.handleTransferExecuteOptionsChange(field, value);

+ 14 - 12
src/stores/WizardStore.ts

@@ -15,7 +15,11 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import { observable, action, runInAction } from "mobx";
 
 import type { WizardData, WizardPage } from "@src/@types/WizardData";
-import type { Instance, InstanceScript } from "@src/@types/Instance";
+import type {
+  Instance,
+  InstanceScript,
+  UserScriptTarget,
+} from "@src/@types/Instance";
 import type { Field } from "@src/@types/Field";
 import type { NetworkMap } from "@src/@types/Network";
 import type { StorageMap } from "@src/@types/Endpoint";
@@ -421,17 +425,15 @@ class WizardStore {
     }
   }
 
-  @action cancelUploadedScript(
-    global: string | null,
-    instanceName: string | null,
-  ) {
-    this.uploadedUserScripts = this.uploadedUserScripts.filter(s =>
-      global ? s.global !== global : s.instanceId !== instanceName,
-    );
-  }
-
-  @action uploadUserScript(instanceScript: InstanceScript) {
-    this.uploadedUserScripts = [...this.uploadedUserScripts, instanceScript];
+  @action setUserScripts(target: UserScriptTarget, scripts: InstanceScript[]) {
+    const matches = (s: InstanceScript) =>
+      target.global
+        ? s.global === target.global
+        : s.instanceId === target.instanceId;
+    this.uploadedUserScripts = [
+      ...this.uploadedUserScripts.filter(s => !matches(s)),
+      ...scripts,
+    ];
   }
 
   @action clearUploadedUserScripts() {