From e339f9d447d11f49f19616186f76b63c30ed2825 Mon Sep 17 00:00:00 2001 From: Rob Galanakis Date: Fri, 26 Jun 2026 10:27:36 -0700 Subject: [PATCH] Add marketing list csv uploads --- adminapp/src/api.js | 2 + adminapp/src/components/FileUploadInput.jsx | 56 +++++++++++++++++++ .../src/pages/MarketingListDetailPage.jsx | 14 +++++ lib/suma/admin_api/marketing_lists.rb | 28 +++++++++- lib/suma/marketing/list.rb | 33 +++++++++++ lib/suma/phone_number.rb | 10 ++++ lib/suma/spec_helpers/service.rb | 4 ++ spec/suma/admin_api/marketing_lists_spec.rb | 29 ++++++++++ spec/suma/marketing/list_spec.rb | 22 ++++++++ spec/suma/phone_number_spec.rb | 12 ++++ 10 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 adminapp/src/components/FileUploadInput.jsx diff --git a/adminapp/src/api.js b/adminapp/src/api.js index 6cef54e75..323a542a9 100644 --- a/adminapp/src/api.js +++ b/adminapp/src/api.js @@ -199,6 +199,8 @@ export default { post(`/adminapi/v1/marketing_lists/${id}/destroy`, data, ...args), rebuildMarketingList: ({ id, ...data }, ...args) => post(`/adminapi/v1/marketing_lists/${id}/rebuild`, data, ...args), + uploadingMarketingListCsv: ({ id, ...data }, ...args) => + postForm(`/adminapi/v1/marketing_lists/${id}/upload_csv`, data, ...args), getMarketingSmsBroadcasts: (data, ...args) => get(`/adminapi/v1/marketing_sms_broadcasts`, data, ...args), diff --git a/adminapp/src/components/FileUploadInput.jsx b/adminapp/src/components/FileUploadInput.jsx new file mode 100644 index 000000000..9795b1733 --- /dev/null +++ b/adminapp/src/components/FileUploadInput.jsx @@ -0,0 +1,56 @@ +import useErrorSnackbar from "../hooks/useErrorSnackbar"; +import useToggle from "../shared/react/useToggle"; +import UploadFileIcon from "@mui/icons-material/UploadFile"; +import { Button, CircularProgress } from "@mui/material"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import { useSnackbar } from "notistack"; +import React from "react"; + +export default function FileUploadInput({ accept, label, onUpload }) { + label = label || "Choose File"; + accept = accept || "*.*"; + const [file, setFile] = React.useState(""); + const uploading = useToggle(); + const { enqueueErrorSnackbar } = useErrorSnackbar(); + const { enqueueSnackbar } = useSnackbar(); + + function handleFileChange(e) { + setFile(e.target.files?.[0] ?? null); + } + + async function handleUpload(e) { + e.preventDefault(); + if (!file) { + return; + } + uploading.turnOn(); + + try { + await onUpload(file); + enqueueSnackbar(`Uploaded ${file.name}`, { variant: "success" }); + setFile(null); + } catch (err) { + enqueueErrorSnackbar(err); + } finally { + uploading.turnOff(); + } + } + + return ( + + + + + + ); +} diff --git a/adminapp/src/pages/MarketingListDetailPage.jsx b/adminapp/src/pages/MarketingListDetailPage.jsx index d4a1a3d0a..08ccea3b2 100644 --- a/adminapp/src/pages/MarketingListDetailPage.jsx +++ b/adminapp/src/pages/MarketingListDetailPage.jsx @@ -1,5 +1,6 @@ import api from "../api"; import AdminLink from "../components/AdminLink"; +import FileUploadInput from "../components/FileUploadInput"; import RelatedListRemote from "../components/RelatedListRemote"; import ResourceDetail from "../components/ResourceDetail"; import resourceDetailCommonFields from "../components/resourceDetailCommonFields"; @@ -25,6 +26,14 @@ export default function MarketingListDetailPage() { .finally(notBusy); } + function handleCsvUpload(file, setModel) { + busy(); + api + .uploadingMarketingListCsv({ id, file }) + .then((r) => setModel(r.data)) + .finally(notBusy); + } + if (isBusy) { return ; } @@ -49,6 +58,11 @@ export default function MarketingListDetailPage() { ]} > {(model, setModel) => [ + handleCsvUpload(f, setModel)} + />, model.managed && (