add email and subject search (#78)
This commit is contained in:
@@ -37,6 +37,9 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@unsend/ui/src/tooltip";
|
} from "@unsend/ui/src/tooltip";
|
||||||
|
import { Input } from "@unsend/ui/src/input";
|
||||||
|
import { DEFAULT_QUERY_LIMIT } from "~/lib/constants";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
/* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs */
|
/* Stupid hydrating error. And I so stupid to understand the stupid NextJS docs */
|
||||||
const DynamicSheetWithNoSSR = dynamic(
|
const DynamicSheetWithNoSSR = dynamic(
|
||||||
@@ -53,12 +56,14 @@ export default function EmailsList() {
|
|||||||
const [selectedEmail, setSelectedEmail] = useUrlState("emailId");
|
const [selectedEmail, setSelectedEmail] = useUrlState("emailId");
|
||||||
const [page, setPage] = useUrlState("page", "1");
|
const [page, setPage] = useUrlState("page", "1");
|
||||||
const [status, setStatus] = useUrlState("status");
|
const [status, setStatus] = useUrlState("status");
|
||||||
|
const [search, setSearch] = useUrlState("search");
|
||||||
|
|
||||||
const pageNumber = Number(page);
|
const pageNumber = Number(page);
|
||||||
|
|
||||||
const emailsQuery = api.email.emails.useQuery({
|
const emailsQuery = api.email.emails.useQuery({
|
||||||
page: pageNumber,
|
page: pageNumber,
|
||||||
status: status?.toUpperCase() as EmailStatus,
|
status: status?.toUpperCase() as EmailStatus,
|
||||||
|
search,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectEmail = (emailId: string) => {
|
const handleSelectEmail = (emailId: string) => {
|
||||||
@@ -71,9 +76,19 @@ export default function EmailsList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const debouncedSearch = useDebouncedCallback((value: string) => {
|
||||||
|
setSearch(value);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-10 flex flex-col gap-4">
|
<div className="mt-10 flex flex-col gap-4">
|
||||||
<div className="flex justify-end">
|
<div className="flex justify-between">
|
||||||
|
<Input
|
||||||
|
placeholder="Search by subject or email"
|
||||||
|
className="w-[350px] mr-4"
|
||||||
|
defaultValue={search ?? ""}
|
||||||
|
onChange={(e) => debouncedSearch(e.target.value)}
|
||||||
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={status ?? "All statuses"}
|
value={status ?? "All statuses"}
|
||||||
onValueChange={(val) =>
|
onValueChange={(val) =>
|
||||||
@@ -207,7 +222,7 @@ export default function EmailsList() {
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setPage((pageNumber + 1).toString())}
|
onClick={() => setPage((pageNumber + 1).toString())}
|
||||||
disabled={pageNumber >= (emailsQuery.data?.totalPage ?? 0)}
|
disabled={emailsQuery.data?.emails.length !== DEFAULT_QUERY_LIMIT}
|
||||||
>
|
>
|
||||||
Next
|
Next
|
||||||
</Button>
|
</Button>
|
||||||
|
1
apps/web/src/lib/constants/index.ts
Normal file
1
apps/web/src/lib/constants/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const DEFAULT_QUERY_LIMIT = 30;
|
@@ -1,6 +1,7 @@
|
|||||||
import { EmailStatus } from "@prisma/client";
|
import { EmailStatus, Prisma, Email } from "@prisma/client";
|
||||||
import { format, subDays } from "date-fns";
|
import { format, subDays } from "date-fns";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { DEFAULT_QUERY_LIMIT } from "~/lib/constants";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createTRPCRouter,
|
createTRPCRouter,
|
||||||
@@ -12,8 +13,6 @@ import { cancelEmail, updateEmail } from "~/server/service/email-service";
|
|||||||
|
|
||||||
const statuses = Object.values(EmailStatus) as [EmailStatus];
|
const statuses = Object.values(EmailStatus) as [EmailStatus];
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 30;
|
|
||||||
|
|
||||||
export const emailRouter = createTRPCRouter({
|
export const emailRouter = createTRPCRouter({
|
||||||
emails: teamProcedure
|
emails: teamProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -21,41 +20,43 @@ export const emailRouter = createTRPCRouter({
|
|||||||
page: z.number().optional(),
|
page: z.number().optional(),
|
||||||
status: z.enum(statuses).optional().nullable(),
|
status: z.enum(statuses).optional().nullable(),
|
||||||
domain: z.number().optional(),
|
domain: z.number().optional(),
|
||||||
|
search: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const page = input.page || 1;
|
const page = input.page || 1;
|
||||||
const limit = DEFAULT_LIMIT;
|
const limit = DEFAULT_QUERY_LIMIT;
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
const whereConditions = {
|
const emails = await db.$queryRaw<Array<Email>>`
|
||||||
teamId: ctx.team.id,
|
SELECT
|
||||||
...(input.status ? { latestStatus: input.status } : {}),
|
id,
|
||||||
...(input.domain ? { domainId: input.domain } : {}),
|
"createdAt",
|
||||||
};
|
"latestStatus",
|
||||||
|
subject,
|
||||||
|
"to",
|
||||||
|
"scheduledAt"
|
||||||
|
FROM "Email"
|
||||||
|
WHERE "teamId" = ${ctx.team.id}
|
||||||
|
${input.status ? Prisma.sql`AND "latestStatus" = ${input.status}` : Prisma.sql``}
|
||||||
|
${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``}
|
||||||
|
${
|
||||||
|
input.search
|
||||||
|
? Prisma.sql`AND (
|
||||||
|
"subject" ILIKE ${`%${input.search}%`}
|
||||||
|
OR EXISTS (
|
||||||
|
SELECT 1 FROM unnest("to") AS email
|
||||||
|
WHERE email ILIKE ${`%${input.search}%`}
|
||||||
|
)
|
||||||
|
)`
|
||||||
|
: Prisma.sql``
|
||||||
|
}
|
||||||
|
ORDER BY "createdAt" DESC
|
||||||
|
LIMIT ${DEFAULT_QUERY_LIMIT}
|
||||||
|
OFFSET ${offset}
|
||||||
|
`;
|
||||||
|
|
||||||
const countP = db.email.count({ where: whereConditions });
|
return { emails };
|
||||||
|
|
||||||
const emailsP = db.email.findMany({
|
|
||||||
where: whereConditions,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
latestStatus: true,
|
|
||||||
subject: true,
|
|
||||||
to: true,
|
|
||||||
scheduledAt: true,
|
|
||||||
},
|
|
||||||
orderBy: {
|
|
||||||
createdAt: "desc",
|
|
||||||
},
|
|
||||||
skip: offset,
|
|
||||||
take: limit,
|
|
||||||
});
|
|
||||||
|
|
||||||
const [emails, count] = await Promise.all([emailsP, countP]);
|
|
||||||
|
|
||||||
return { emails, totalPage: Math.ceil(count / limit) };
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
dashboard: teamProcedure
|
dashboard: teamProcedure
|
||||||
|
Reference in New Issue
Block a user