From 1c7d2a85a04e9627b033c1934c212112a4cf6f74 Mon Sep 17 00:00:00 2001 From: Suraj <102975939+Surajsuthar@users.noreply.github.com> Date: Sat, 8 Mar 2025 13:09:59 +0530 Subject: [PATCH] Feature/filter in email by domain id and api (#108) * feat: filter by domainId * feat: filter on apikey in email * Update pnpm-lock.yaml --------- Co-authored-by: KM Koushik --- .../migration.sql | 2 + apps/web/prisma/schema.prisma | 1 + .../src/app/(dashboard)/emails/email-list.tsx | 142 +++++++++++++----- apps/web/src/server/api/routers/email.ts | 8 +- pnpm-lock.yaml | 5 +- 5 files changed, 119 insertions(+), 39 deletions(-) create mode 100644 apps/web/prisma/migrations/20250215074030_added_apiid_to_email_table/migration.sql diff --git a/apps/web/prisma/migrations/20250215074030_added_apiid_to_email_table/migration.sql b/apps/web/prisma/migrations/20250215074030_added_apiid_to_email_table/migration.sql new file mode 100644 index 0000000..42d894f --- /dev/null +++ b/apps/web/prisma/migrations/20250215074030_added_apiid_to_email_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Email" ADD COLUMN "apiId" INTEGER; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index b25f9f7..839bf1c 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -197,6 +197,7 @@ model Email { latestStatus EmailStatus @default(QUEUED) teamId Int domainId Int? + apiId Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt scheduledAt DateTime? diff --git a/apps/web/src/app/(dashboard)/emails/email-list.tsx b/apps/web/src/app/(dashboard)/emails/email-list.tsx index af4cfdd..e9b1c0f 100644 --- a/apps/web/src/app/(dashboard)/emails/email-list.tsx +++ b/apps/web/src/app/(dashboard)/emails/email-list.tsx @@ -40,16 +40,17 @@ import { import { Input } from "@unsend/ui/src/input"; import { DEFAULT_QUERY_LIMIT } from "~/lib/constants"; import { useDebouncedCallback } from "use-debounce"; +import { useState } from "react"; /* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs */ const DynamicSheetWithNoSSR = dynamic( () => import("@unsend/ui/src/sheet").then((mod) => mod.Sheet), - { ssr: false } + { ssr: false }, ); const DynamicSheetContentWithNoSSR = dynamic( () => import("@unsend/ui/src/sheet").then((mod) => mod.SheetContent), - { ssr: false } + { ssr: false }, ); export default function EmailsList() { @@ -57,19 +58,46 @@ export default function EmailsList() { const [page, setPage] = useUrlState("page", "1"); const [status, setStatus] = useUrlState("status"); const [search, setSearch] = useUrlState("search"); + const [domainName, setDomainName] = useUrlState("domain"); + const [apiKeyName, setApiKeyName] = useUrlState("apikey"); + const [domain, setDomain] = useState(null); + const [apikey, setApikey] = useState(null); const pageNumber = Number(page); + const domainId = Number(domain); + const apiId = Number(apikey); const emailsQuery = api.email.emails.useQuery({ page: pageNumber, status: status?.toUpperCase() as EmailStatus, + domain: domainId, search, + apiId: apiId, }); + const { data: domainsQuery } = api.domain.domains.useQuery(); + const { data: apiKeysQuery } = api.apiKey.getApiKeys.useQuery(); + const handleSelectEmail = (emailId: string) => { setSelectedEmail(emailId); }; + const handleDomainName = (val: string) => { + setDomain(val === "All Domain" ? null : val); + const nameOfDomain = domainsQuery?.find( + (item) => item.id.toString() === val, + ); + setDomainName(nameOfDomain?.name || "All Domain"); + }; + + const handleApiKeyName = (val: string) => { + setApikey(val === "All ApiKey" ? null : val); + const nameOfApiKey = apiKeysQuery?.find( + (item) => item.id.toString() === val, + ); + setApiKeyName(nameOfApiKey?.name || "All ApiKey"); + }; + const handleSheetChange = (isOpen: boolean) => { if (!isOpen) { setSelectedEmail(null); @@ -89,39 +117,83 @@ export default function EmailsList() { defaultValue={search ?? ""} onChange={(e) => debouncedSearch(e.target.value)} /> - handleApiKeyName(val)} + > + + {apiKeyName ? apiKeyName : "All ApiKey"} + + + All ApiKey + {apiKeysQuery && + apiKeysQuery.map((apikey) => ( + + {apikey.name} + + ))} + {/* Light Dark System */} - - + + + + +
@@ -171,7 +243,7 @@ export default function EmailsList() { Scheduled at{" "} {formatDate( email.scheduledAt, - "MMM dd'th', hh:mm a" + "MMM dd'th', hh:mm a", )} @@ -187,7 +259,7 @@ export default function EmailsList() { {email.latestStatus !== "SCHEDULED" ? formatDate( email.scheduledAt ?? email.createdAt, - "MMM do, hh:mm a" + "MMM do, hh:mm a", ) : "--"} diff --git a/apps/web/src/server/api/routers/email.ts b/apps/web/src/server/api/routers/email.ts index 4ea1c10..6e070f3 100644 --- a/apps/web/src/server/api/routers/email.ts +++ b/apps/web/src/server/api/routers/email.ts @@ -21,7 +21,8 @@ export const emailRouter = createTRPCRouter({ status: z.enum(statuses).optional().nullable(), domain: z.number().optional(), search: z.string().optional().nullable(), - }) + apiId: z.number().optional(), + }), ) .query(async ({ ctx, input }) => { const page = input.page || 1; @@ -40,6 +41,7 @@ export const emailRouter = createTRPCRouter({ WHERE "teamId" = ${ctx.team.id} ${input.status ? Prisma.sql`AND "latestStatus"::text = ${input.status}` : Prisma.sql``} ${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``} + ${input.apiId ? Prisma.sql`AND "apiId" = ${input.apiId}` : Prisma.sql``} ${ input.search ? Prisma.sql`AND ( @@ -63,7 +65,7 @@ export const emailRouter = createTRPCRouter({ .input( z.object({ days: z.number().optional(), - }) + }), ) .query(async ({ ctx, input }) => { const { team } = ctx; @@ -146,7 +148,7 @@ export const emailRouter = createTRPCRouter({ clicked: 0, bounced: 0, complained: 0, - } + }, ); return { result: filledResult, totalCounts }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 356c21a..7bf1fd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -736,6 +736,7 @@ packages: preact-render-to-string: 5.2.3(preact@10.11.3) dev: false + /@auth/prisma-adapter@1.5.0(@prisma/client@6.3.1): resolution: {integrity: sha512-TNDY7z5VhC3E5xetb4qhTmSmjvyEurafaTM6dlYlhTGj+7K3xTki7fJ0oJwgkFsQ2fT1weJJJSBmjtH8Nqsnuw==} peerDependencies: @@ -8241,6 +8242,7 @@ packages: negotiator: 0.6.3 dev: true + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -8267,7 +8269,6 @@ packages: resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} engines: {node: '>=0.4.0'} hasBin: true - dev: false /add@2.0.6: resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} @@ -13724,6 +13725,7 @@ packages: - babel-plugin-macros dev: false + /next@14.2.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SF3TJnKdH43PMkCcErLPv+x/DY1YCklslk3ZmwaVoyUfDgHKexuKlf9sEfBQ69w+ue8jQ3msLb+hSj1T19hGag==} engines: {node: '>=18.17.0'} @@ -15720,6 +15722,7 @@ packages: engines: {node: '>=0.10.0'} dev: true + /resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} dev: true