Merge pull request #186 from himanshu-ntml/main

Fix contact update response
This commit is contained in:
Dries Augustyns
2025-06-05 11:21:29 +02:00
committed by GitHub
2 changed files with 115 additions and 88 deletions
+9 -10
View File
@@ -248,12 +248,17 @@ export class Contacts {
const { id, email, subscribed, data } = ContactSchemas.manage.parse(req.body); const { id, email, subscribed, data } = ContactSchemas.manage.parse(req.body);
const contact = id ? await ContactService.id(id) : await ContactService.email(project.id, email as string); let contact = id ? await ContactService.id(id) : await ContactService.email(project.id, email as string);
if (!contact || contact.projectId !== project.id) { if (!contact || contact.projectId !== project.id) {
throw new NotFound("contact"); throw new NotFound("contact");
} }
const updateData: Record<string, unknown> = {
email,
subscribed: subscribed ?? contact.subscribed,
};
if (data) { if (data) {
const givenUserData = Object.entries(data); const givenUserData = Object.entries(data);
const dataToUpdate = JSON.parse(contact.data ?? "{}"); const dataToUpdate = JSON.parse(contact.data ?? "{}");
@@ -266,18 +271,12 @@ export class Contacts {
} }
}); });
await prisma.contact.update({ updateData.data = JSON.stringify(dataToUpdate);
where: { id: contact.id },
data: { data: JSON.stringify(dataToUpdate) },
});
} }
await prisma.contact.update({ contact = await prisma.contact.update({
where: { id: contact.id }, where: { id: contact.id },
data: { data: updateData,
email,
subscribed: subscribed ?? contact.subscribed,
},
}); });
await redis.del(Keys.Project.contacts(project.id)); await redis.del(Keys.Project.contacts(project.id));
+106 -78
View File
@@ -9,7 +9,7 @@ import React, { useState } from "react";
import { type FieldError, useFieldArray, useForm } from "react-hook-form"; import { type FieldError, useFieldArray, useForm } from "react-hook-form";
import { toast } from "sonner"; import { toast } from "sonner";
import { z } from "zod"; import { z } from "zod";
import { Card, Empty, FullscreenLoader, Modal, Skeleton, Table, Toggle } from "../../components"; import { Card, Empty, FullscreenLoader, Modal, Skeleton, Table, Toggle, Dropdown } from "../../components";
import { Dashboard } from "../../layouts"; import { Dashboard } from "../../layouts";
import { searchContacts, useContacts } from "../../lib/hooks/contacts"; import { searchContacts, useContacts } from "../../lib/hooks/contacts";
import { useActiveProject } from "../../lib/hooks/projects"; import { useActiveProject } from "../../lib/hooks/projects";
@@ -31,8 +31,9 @@ interface ContactValues {
* *
*/ */
export default function Index() { export default function Index() {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [query, setQuery] = useState<string>(); const [query, setQuery] = useState<string>();
const [statusFilter, setStatusFilter] = useState<string>("all");
const project = useActiveProject(); const project = useActiveProject();
const { data: user } = useUser(); const { data: user } = useUser();
@@ -125,47 +126,56 @@ export default function Index() {
return <Skeleton type={"table"} />; return <Skeleton type={"table"} />;
} }
if (search && query !== undefined) { if (search && query !== undefined) {
if (search.contacts.length > 0) { const filtered = search.contacts
return ( .filter((c) =>
<> statusFilter === "all"
<Table ? true
values={search.contacts : statusFilter === "subscribed"
.sort((a, b) => { ? c.subscribed
const aTrigger = a.triggers.length > 0 ? a.triggers.sort()[0].createdAt : a.createdAt; : !c.subscribed,
);
const bTrigger = b.triggers.length > 0 ? b.triggers.sort()[0].createdAt : b.createdAt; if (filtered.length > 0) {
return (
<>
<Table
values={filtered
.sort((a, b) => {
const aTrigger = a.triggers.length > 0 ? a.triggers.sort()[0].createdAt : a.createdAt;
return bTrigger > aTrigger ? 1 : -1; const bTrigger = b.triggers.length > 0 ? b.triggers.sort()[0].createdAt : b.createdAt;
})
.map((u) => { return bTrigger > aTrigger ? 1 : -1;
return { })
Email: u.email, .map((u) => {
"Last Activity": dayjs() return {
.to( Email: u.email,
[...u.triggers, ...u.emails].length > 0 "Last Activity": dayjs()
? [...u.triggers, ...u.emails].sort((a, b) => { .to(
return a.createdAt > b.createdAt ? -1 : 1; [...u.triggers, ...u.emails].length > 0
})[0].createdAt ? [...u.triggers, ...u.emails].sort((a, b) => {
: u.createdAt, return a.createdAt > b.createdAt ? -1 : 1;
) })[0].createdAt
.toString(), : u.createdAt,
Subscribed: u.subscribed, )
Edit: ( .toString(),
<Link href={`/contacts/${u.id}`} className={"transition hover:text-neutral-800"}> Subscribed: u.subscribed,
<Edit2 size={18} /> Edit: (
</Link> <Link href={`/contacts/${u.id}`} className={"transition hover:text-neutral-800"}>
), <Edit2 size={18} />
}; </Link>
})} ),
/> };
</> })}
); />
} </>
return ( );
<> }
<Empty return (
icon={ <>
<Empty
icon={
<svg width="24" height="24" fill="none" viewBox="0 0 24 24"> <svg width="24" height="24" fill="none" viewBox="0 0 24 24">
<path <path
stroke="currentColor" stroke="currentColor"
@@ -183,24 +193,32 @@ export default function Index() {
); );
} }
if (contacts) { if (contacts) {
if (contacts.contacts.length > 0) { const filtered = contacts.contacts.filter((c) =>
return ( statusFilter === "all"
<> ? true
<Table : statusFilter === "subscribed"
values={contacts.contacts ? c.subscribed
.sort((a, b) => { : !c.subscribed,
const aTrigger = a.triggers.length > 0 ? a.triggers.sort()[0].createdAt : a.createdAt; );
const bTrigger = b.triggers.length > 0 ? b.triggers.sort()[0].createdAt : b.createdAt; if (filtered.length > 0) {
return (
<>
<Table
values={filtered
.sort((a, b) => {
const aTrigger = a.triggers.length > 0 ? a.triggers.sort()[0].createdAt : a.createdAt;
const bTrigger = b.triggers.length > 0 ? b.triggers.sort()[0].createdAt : b.createdAt;
return bTrigger > aTrigger ? 1 : -1; return bTrigger > aTrigger ? 1 : -1;
}) })
.map((u) => { .map((u) => {
return { return {
Email: u.email, Email: u.email,
"Last Activity": dayjs() "Last Activity": dayjs()
.to( .to(
u.triggers.length > 0 u.triggers.length > 0
? u.triggers.sort((a, b) => { ? u.triggers.sort((a, b) => {
return a.createdAt > b.createdAt ? -1 : 1; return a.createdAt > b.createdAt ? -1 : 1;
@@ -215,16 +233,16 @@ export default function Index() {
</Link> </Link>
), ),
}; };
})} })}
/> />
<nav className="flex items-center justify-between py-3" aria-label="Pagination"> <nav className="flex items-center justify-between py-3" aria-label="Pagination">
<div className="hidden sm:block"> <div className="hidden sm:block">
<p className="text-sm text-neutral-700"> <p className="text-sm text-neutral-700">
Showing <span className="font-medium">{(page - 1) * 20}</span> to{" "} Showing <span className="font-medium">{(page - 1) * 20}</span> to{" "}
<span className="font-medium">{page * 20}</span> of <span className="font-medium">{contacts.count}</span>{" "} <span className="font-medium">{page * 20}</span> of <span className="font-medium">{filtered.length}</span>{" "}
contacts contacts
</p> </p>
</div> </div>
<div className="flex flex-1 justify-between gap-1 sm:justify-end"> <div className="flex flex-1 justify-between gap-1 sm:justify-end">
{page > 1 && ( {page > 1 && (
<button <button
@@ -441,18 +459,28 @@ export default function Index() {
title={"Contacts"} title={"Contacts"}
description={"View and manage your contacts"} description={"View and manage your contacts"}
actions={ actions={
<div className={"grid w-full gap-3 md:w-fit md:grid-cols-2"}> <div className={"grid w-full gap-3 md:w-fit md:grid-cols-3"}>
<input <input
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
autoComplete={"off"} autoComplete={"off"}
type="search" type="search"
placeholder={"Search email or metadata"} placeholder={"Search email or metadata"}
className={ className={
"rounded border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm" "rounded border-neutral-300 transition ease-in-out focus:border-neutral-800 focus:ring-neutral-800 sm:text-sm"
} }
/> />
<motion.button <Dropdown
onChange={(v) => setStatusFilter(v)}
values={[
{ name: "All", value: "all" },
{ name: "Subscribed", value: "subscribed" },
{ name: "Unsubscribed", value: "unsubscribed" },
]}
selectedValue={statusFilter}
/>
<motion.button
onClick={() => setContactModal(true)} onClick={() => setContactModal(true)}
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}