Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visual Experiments - Add EditDOMMutations modal #1394

Merged
merged 7 commits into from Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
130 changes: 130 additions & 0 deletions packages/front-end/components/Experiment/EditDOMMutationsModal.tsx
@@ -0,0 +1,130 @@
import { VisualChange } from "back-end/types/visual-changeset";
import { FC, useCallback, useState } from "react";
import Code from "@/components/SyntaxHighlighting/Code";
import Modal from "../Modal";

const EditDOMMutatonsModal: FC<{
visualChange: VisualChange;
close: () => void;
onSave: (newVisualChange: VisualChange) => void;
}> = ({ close, visualChange, onSave }) => {
const [newVisualChange, setNewVisualChange] = useState<VisualChange>(
visualChange
);

const deleteCustomJS = useCallback(() => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: useCallback for these seems unnecessary, as it's a very inexpensive operation

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair. It comes from a force of habit. I don't equate useCallback as a thing to use when something is expensive but rather when 'we don't need to redefine this function per render'

setNewVisualChange({
...newVisualChange,
js: "",
});
}, [newVisualChange, setNewVisualChange]);

const deleteGlobalCSS = useCallback(() => {
setNewVisualChange({
...newVisualChange,
css: "",
});
}, [newVisualChange, setNewVisualChange]);

const deleteDOMMutation = useCallback(
(index: number) => {
setNewVisualChange({
...newVisualChange,
domMutations: newVisualChange.domMutations.filter(
(_m, i) => i !== index
),
});
},
[newVisualChange, setNewVisualChange]
);

const onSubmit = () => {
onSave(newVisualChange);
};

return (
<Modal
open
close={close}
size="lg"
header="Remove Visual Changes"
submit={onSubmit}
cta="Save"
>
<div>
<div className="mb-4">
<h4>
Global CSS
{newVisualChange.css ? (
<small className="ml-2">
<a href="#" className="text-danger" onClick={deleteGlobalCSS}>
delete
bttf marked this conversation as resolved.
Show resolved Hide resolved
</a>
</small>
) : null}
</h4>
{newVisualChange.css ? (
<Code
language="css"
code={newVisualChange.css}
className="disabled"
/>
) : (
<div className="text-muted font-italic">(None)</div>
)}
</div>

<div className="mb-4">
<h4>
Custom JS
{newVisualChange.js ? (
<small className="ml-2">
<a href="#" className="text-danger" onClick={deleteCustomJS}>
delete
</a>
</small>
) : null}
</h4>
{newVisualChange.js ? (
<Code
language="javascript"
code={newVisualChange.js ?? ""}
className="disabled"
/>
) : (
<div className="text-muted font-italic">(None)</div>
)}
</div>

<div className="mb-4">
<h4>DOM Mutations</h4>

{newVisualChange.domMutations.length ? (
newVisualChange.domMutations.map((m, i) => (
<div key={i} className="d-flex flex-column align-items-end">
<a
className="text-danger"
href="#"
onClick={() => deleteDOMMutation(i)}
style={{ marginBottom: "-.5rem", fontSize: "0.75rem" }}
>
delete
</a>
<Code
language="json"
code={JSON.stringify(m)}
className="disabled"
containerClassName="w-100"
/>
</div>
))
) : (
<div className="text-muted font-italic">(None)</div>
)}
</div>
</div>
</Modal>
);
};

export default EditDOMMutatonsModal;
90 changes: 79 additions & 11 deletions packages/front-end/components/Experiment/VariationsTable.tsx
Expand Up @@ -5,11 +5,12 @@ import {
Variation,
} from "back-end/types/experiment";
import {
VisualChange,
VisualChangesetInterface,
VisualChangesetURLPattern,
} from "back-end/types/visual-changeset";
import React, { FC, Fragment, useState } from "react";
import { FaPlusCircle, FaTimesCircle } from "react-icons/fa";
import React, { FC, Fragment, useCallback, useState } from "react";
import { FaPencilAlt, FaPlusCircle, FaTimesCircle } from "react-icons/fa";
import { useAuth } from "@/services/auth";
import { useUser } from "@/services/UserContext";
import DeleteButton from "@/components/DeleteButton/DeleteButton";
Expand All @@ -22,6 +23,7 @@ import ScreenshotUpload from "../EditExperiment/ScreenshotUpload";
import { GBEdit } from "../Icons";
import OpenVisualEditorLink from "../OpenVisualEditorLink";
import VisualChangesetModal from "./VisualChangesetModal";
import EditDOMMutatonsModal from "./EditDOMMutationsModal";

interface Props {
experiment: ExperimentInterfaceStringDates;
Expand Down Expand Up @@ -130,10 +132,57 @@ const VariationsTable: FC<Props> = ({
setEditingVisualChangeset,
] = useState<VisualChangesetInterface | null>(null);

const [editingVisualChange, setEditingVisualChange] = useState<{
visualChangeset: VisualChangesetInterface;
visualChange: VisualChange;
visualChangeIndex: number;
} | null>(null);

const hasDescriptions = variations.some((v) => !!v.description?.trim());
const hasUniqueIDs = variations.some((v, i) => v.key !== i + "");
const hasLegacyVisualChanges = variations.some((v) => isLegacyVariation(v));

const deleteVisualChangeset = useCallback(
async (id: string) => {
await apiCall(`/visual-changesets/${id}`, {
method: "DELETE",
});
mutate();
track("Delete visual changeset", {
source: "visual-editor-ui",
});
},
[apiCall, mutate]
);

const updateVisualChange = useCallback(
async ({
visualChangeset,
visualChange,
index,
}: {
visualChangeset: VisualChangesetInterface;
visualChange: VisualChange;
index: number;
}) => {
const newVisualChangeset: VisualChangesetInterface = {
...visualChangeset,
visualChanges: visualChangeset.visualChanges.map((c, i) =>
i === index ? visualChange : c
),
};
await apiCall(`/visual-changesets/${visualChangeset.id}`, {
method: "PUT",
body: JSON.stringify(newVisualChangeset),
});
mutate();
track("Delete visual changeset", {
source: "visual-editor-ui",
});
},
[apiCall, mutate]
);

return (
<div className="w-100">
<div
Expand Down Expand Up @@ -322,15 +371,7 @@ const VariationsTable: FC<Props> = ({
)}
<DeleteButton
className="btn-sm ml-4"
onClick={async () => {
await apiCall(`/visual-changesets/${vc.id}`, {
method: "DELETE",
});
mutate();
track("Delete visual changeset", {
source: "visual-editor-ui",
});
}}
onClick={() => deleteVisualChangeset(vc.id)}
displayName="Visual Changes"
/>
</div>
Expand Down Expand Up @@ -374,6 +415,19 @@ const VariationsTable: FC<Props> = ({
<td key={j} className="px-4 py-1">
<div className="d-flex justify-content-between">
<div>
<a
href="#"
className="mr-2"
onClick={() =>
setEditingVisualChange({
visualChange: changes,
visualChangeIndex: j,
visualChangeset: vc,
})
}
>
<FaPencilAlt />
</a>
{numChanges} visual change
{numChanges === 1 ? "" : "s"}
</div>
Expand Down Expand Up @@ -446,6 +500,20 @@ const VariationsTable: FC<Props> = ({
</Link>
</div>
)}

{editingVisualChange ? (
<EditDOMMutatonsModal
visualChange={editingVisualChange.visualChange}
close={() => setEditingVisualChange(null)}
onSave={(newVisualChange) =>
updateVisualChange({
index: editingVisualChange.visualChangeIndex,
visualChange: newVisualChange,
visualChangeset: editingVisualChange.visualChangeset,
})
}
/>
) : null}
</div>
);
};
Expand Down
26 changes: 18 additions & 8 deletions packages/front-end/components/OpenVisualEditorLink.tsx
@@ -1,4 +1,4 @@
import { FC, useMemo, useState } from "react";
import { FC, useCallback, useMemo, useState } from "react";
import { FaExternalLinkAlt } from "react-icons/fa";
import { getApiHost } from "@/services/env";
import track from "@/services/track";
Expand Down Expand Up @@ -35,6 +35,14 @@ const OpenVisualEditorLink: FC<{
});
}, [visualEditorUrl, id, changeIndex, apiHost]);

const navigate = useCallback(() => {
track("Open visual editor", {
source: "visual-editor-ui",
status: "success",
});
window.location.href = url;
}, [url]);

return (
<>
<span
Expand Down Expand Up @@ -77,11 +85,7 @@ const OpenVisualEditorLink: FC<{
}

if (url) {
track("Open visual editor", {
source: "visual-editor-ui",
status: "success",
});
window.location.href = url;
navigate();
}
}}
>
Expand Down Expand Up @@ -120,8 +124,14 @@ const OpenVisualEditorLink: FC<{
}
>
{isChromeBrowser ? (
`You'll need to install the GrowthBook DevTools Chrome extension
to use the visual editor.`
<>
You&apos;ll need to install the GrowthBook DevTools Chrome
extension to use the visual editor.{" "}
<a href="#" onClick={navigate} target="_blank" rel="noreferrer">
Click here to proceed anyway
</a>
.
</>
) : (
<>
The Visual Editor is currently only supported in Chrome. We are
Expand Down