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

feat: clone campaign #859

Merged
merged 3 commits into from
Aug 15, 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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions src/components/Campaigns/CloneCampaign.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {
Box,
Button,
Chip,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@mui/material";
import {
CampaignFragment,
useCreateCampaignMutation,
} from "graphql/campaign.generated";
import { useHistory } from "react-router-dom";
import { useState } from "react";
import { AdvertiserCampaignsDocument } from "graphql/advertiser.generated";
import { createCampaignFromFragment } from "form/fragmentUtil";
import { useAdvertiser } from "auth/hooks/queries/useAdvertiser";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";

interface Props {
campaignFragment?: CampaignFragment | null;
useChip?: boolean;
}

export function CloneCampaign({ campaignFragment, useChip }: Props) {
const { advertiser } = useAdvertiser();
const history = useHistory();
const [open, setOpen] = useState(false);

const [copyCampaign, { loading }] = useCreateCampaignMutation({
refetchQueries: [
{
query: AdvertiserCampaignsDocument,
variables: { id: advertiser.id },
},
],
onCompleted(data) {
history.push(
`/user/main/adsmanager/advanced/${data.createCampaign.id}/settings`,
);
},
onError() {
alert(`Unable to clone campaign`);
},
});

return (
<Box>
{useChip ? (
<Chip
color="primary"
label="Clone"
onClick={() => {
setOpen(true);
}}
disabled={loading || !campaignFragment}
icon={<ContentCopyIcon fontSize="small" />}
/>
) : (
<Button
color="primary"
variant="text"
size="small"
onClick={(e) => {
e.preventDefault();
setOpen(true);
}}
disabled={loading || !campaignFragment}
startIcon={<ContentCopyIcon />}
>
Clone Campaign
</Button>
)}
<Dialog open={open} onClose={() => setOpen(false)}>
<DialogTitle>{`Copy campaign: "${campaignFragment?.name}"?`}</DialogTitle>
<DialogContent>
<DialogContentText>
Copying a campaign will take all properties including ad sets and
ads, and create a new draft campaign with them.
</DialogContentText>
{loading && <LinearProgress />}
</DialogContent>
<DialogActions>
<Button onClick={() => setOpen(false)} disabled={loading}>
Cancel
</Button>
<Button
disabled={loading}
onClick={(e) => {
e.preventDefault();
if (campaignFragment) {
copyCampaign({
variables: {
input: createCampaignFromFragment(campaignFragment),
},
});
} else {
alert("No campaign selected");
}
}}
>
Clone
</Button>
</DialogActions>
</Dialog>
</Box>
);
}
2 changes: 1 addition & 1 deletion src/components/Card/CardContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { SxProps } from "@mui/system";

export function CardContainer(
props: {
header?: string;
header?: ReactNode;
additionalAction?: ReactNode;
sx?: SxProps;
} & PropsWithChildren,
Expand Down
21 changes: 14 additions & 7 deletions src/components/Steps/ActionButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
import { Button, Stack } from "@mui/material";
import { useContext } from "react";
import { DraftContext } from "state/context";
import { useHistory } from "react-router-dom";
import { useFormikContext } from "formik";
import { CampaignForm } from "user/views/adsManager/types";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import RemoveIcon from "@mui/icons-material/Remove";
import { Link as RouterLink } from "react-router-dom";

export function ActionButtons() {
const history = useHistory();
const { values } = useFormikContext<CampaignForm>();
const { setDrafts } = useContext(DraftContext);

return (
<Stack mt={3} spacing={2} mr={1}>
<Button size="small" onClick={() => history.push("/user/main")}>
Return to dashboard
</Button>
{values.draftId !== undefined && (
<Button
startIcon={<RemoveIcon />}
component={RouterLink}
size="small"
color="error"
sx={{ mr: 1 }}
to="/user/main"
onClick={() => {
localStorage.removeItem(values.draftId!);
setDrafts();
history.push("/user/main");
}}
>
Discard campaign
</Button>
)}
<Button
size="small"
component={RouterLink}
to="/user/main"
startIcon={<ArrowBackIcon />}
>
Return to dashboard
</Button>
</Stack>
);
}
2 changes: 1 addition & 1 deletion src/components/Steps/StepDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { NextAndBack } from "components/Steps/NextAndBack";
import { useHistory } from "react-router-dom";
import { ActionButtons } from "components/Steps/ActionButtons";

const drawerWidth = 250;
const drawerWidth = 222;

interface Props {
steps: {
Expand Down
78 changes: 78 additions & 0 deletions src/form/fragmentUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { CreateAdSetInput, CreateCampaignInput } from "graphql/types";
import moment from "moment";
import { CampaignFragment } from "graphql/campaign.generated";
import { AdSetFragment } from "graphql/ad-set.generated";

export function createCampaignFromFragment(
data: CampaignFragment,
): CreateCampaignInput {
const adSets: CreateAdSetInput[] = data.adSets.map((adSet) =>
createAdSetFromFragment(adSet),
);

const two = moment().utc().add(3, "days");
return {
adSets: adSets && adSets.length > 0 ? adSets : undefined,
advertiserId: data.advertiser.id,
budget: data.budget,
currency: data.currency,
dailyBudget: data.dailyBudget,
dailyCap: data.dailyCap,
dayPartings: (data.dayPartings ?? []).map((d) => ({
dow: d.dow,
startMinute: d.startMinute,
endMinute: d.endMinute,
})),
dayProportion: data.dayProportion,
startAt: two.startOf("day").toISOString(),
endAt: two.endOf("day").toISOString(),
externalId: data.externalId,
format: data.format,
geoTargets: (data.geoTargets ?? []).map((g) => ({
code: g.code,
name: g.name,
})),
name: `${data.name} - Copy`,
pacingStrategy: data.pacingStrategy,
source: data.source.toLowerCase(),
state: "draft",
type: data.type,
paymentType: data.paymentType,
};
}

export function createAdSetFromFragment(
data: AdSetFragment,
campaignId?: string,
): CreateAdSetInput {
return {
campaignId,
ads: (data.ads ?? [])
.filter((ad) => ad.state !== "deleted")
.map((ad) => ({
creativeId: ad.creative.id,
price: ad.price,
priceType: ad.priceType,
})),
bannedKeywords: data.bannedKeywords,
billingType: data.billingType ?? "cpm",
conversions: (data.conversions ?? []).map((c) => ({
observationWindow: c.observationWindow,
type: c.type,
urlPattern: c.urlPattern,
})),
execution: data.execution,
keywordSimilarity: data.keywordSimilarity,
keywords: data.keywords,
name: `${data.name ? data.name : data.id.split("-")[0]} - Copy`,
negativeKeywords: data.negativeKeywords,
oses: (data.oses ?? []).map((o) => ({ name: o.name, code: o.code })),
perDay: data.perDay,
segments: (data.segments ?? []).map((o) => ({
name: o.name,
code: o.code,
})),
state: data.state,
totalMax: data.totalMax,
};
}
19 changes: 17 additions & 2 deletions src/graphql/ad-set.generated.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions src/graphql/ad-set.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ fragment AdSet on AdSet {
perDay
state
execution
keywords
keywordSimilarity
negativeKeywords
bannedKeywords
segments {
code
name
Expand Down
3 changes: 1 addition & 2 deletions src/graphql/advertiser.generated.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions src/graphql/analytics-overview.generated.tsx

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading