♻️ Update how we store poll status (#957)

This commit is contained in:
Luke Vella 2023-12-05 14:43:48 +07:00 committed by GitHub
parent 7670db6778
commit 04211ac168
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 108 additions and 186 deletions

View file

@ -75,12 +75,7 @@ export function PollsPage() {
data.length > 0 ? (
<div className="mx-auto grid max-w-3xl gap-3 sm:gap-4">
{data.map((poll) => {
const { title, id: pollId, createdAt, closed: paused } = poll;
const status = poll.event
? "closed"
: paused
? "paused"
: "live";
const { title, id: pollId, createdAt, status } = poll;
return (
<div
key={poll.id}

View file

@ -34,8 +34,6 @@ export const EventCard = () => {
),
);
const status = poll?.event ? "closed" : poll?.closed ? "paused" : "live";
if (!poll) {
return null;
}
@ -49,7 +47,7 @@ export const EventCard = () => {
/>
<div className="bg-pattern p-4 sm:flex sm:flex-row-reverse sm:justify-between sm:px-6">
<div className="mb-2">
<PollStatusBadge status={status} />
<PollStatusBadge status={poll.status} />
</div>
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 sm:gap-6">

View file

@ -42,7 +42,7 @@ import {
import ManagePoll from "@/components/poll/manage-poll";
import NotificationsToggle from "@/components/poll/notifications-toggle";
import { LegacyPollContextProvider } from "@/components/poll/poll-context-provider";
import { PollStatus } from "@/components/poll-status";
import { PollStatusLabel } from "@/components/poll-status";
import { Skeleton } from "@/components/skeleton";
import { Trans } from "@/components/trans";
import { useUser } from "@/components/user-provider";
@ -53,7 +53,6 @@ import { NextPageWithLayout } from "../../types";
const StatusControl = () => {
const poll = usePoll();
const state = poll.event ? "closed" : poll.closed ? "paused" : "live";
const queryClient = trpc.useUtils();
const reopen = trpc.polls.reopen.useMutation({
onMutate: () => {
@ -110,7 +109,7 @@ const StatusControl = () => {
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild>
<Button>
<PollStatus status={state} />
<PollStatusLabel status={poll.status} />
<ChevronDownIcon className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>

View file

@ -1,11 +1,10 @@
import { PollStatus } from "@rallly/database";
import { cn } from "@rallly/ui";
import { CheckCircleIcon, PauseCircleIcon, RadioIcon } from "lucide-react";
import { Trans } from "@/components/trans";
import { IconComponent } from "@/types";
export type PollState = "live" | "paused" | "closed";
const LabelWithIcon = ({
icon: Icon,
children,
@ -23,11 +22,11 @@ const LabelWithIcon = ({
);
};
export const PollStatus = ({
export const PollStatusLabel = ({
status,
className,
}: {
status: PollState;
status: PollStatus;
className?: string;
}) => {
switch (status) {
@ -43,7 +42,7 @@ export const PollStatus = ({
<Trans i18nKey="pollStatusPaused" defaults="Paused" />
</LabelWithIcon>
);
case "closed":
case "finalized":
return (
<LabelWithIcon icon={CheckCircleIcon} className={className}>
<Trans i18nKey="pollStatusClosed" defaults="Finalized" />
@ -52,13 +51,13 @@ export const PollStatus = ({
}
};
export const PollStatusBadge = ({ status }: { status: PollState }) => {
export const PollStatusBadge = ({ status }: { status: PollStatus }) => {
return (
<PollStatus
<PollStatusLabel
className={cn("rounded-full border py-0.5 pl-1.5 pr-3 text-sm", {
"border-blue-500 text-blue-500": status === "live",
"border-gray-500 text-gray-500": status === "paused",
"border-green-500 text-green-500": status === "closed",
"border-green-500 text-green-500": status === "finalized",
})}
status={status}
/>

View file

@ -65,7 +65,7 @@ const NotificationsToggle: React.FunctionComponent = () => {
loading={watch.isLoading || unwatch.isLoading}
icon={isWatching ? BellRingIcon : BellOffIcon}
data-testid="notifications-toggle"
disabled={poll.demo || user.isGuest}
disabled={user.isGuest}
className="flex items-center gap-2 px-2.5"
onClick={async () => {
if (user.isGuest) {

View file

@ -1,4 +1,4 @@
import { User, VoteType } from "@rallly/database";
import { PollStatus, User, VoteType } from "@rallly/database";
export type GetPollApiResponse = {
id: string;
@ -9,10 +9,9 @@ export type GetPollApiResponse = {
user: User | null;
timeZone: string | null;
adminUrlId: string;
status: PollStatus;
participantUrlId: string;
closed: boolean;
legacy: boolean;
demo: boolean;
createdAt: Date;
deleted: boolean;
};

View file

@ -1,130 +0,0 @@
import { prisma, VoteType } from "@rallly/database";
import dayjs from "dayjs";
import { nanoid } from "nanoid";
const participantData: Array<{ name: string; votes: VoteType[] }> = [
{
name: "Reed",
votes: ["yes", "no", "yes", "no"],
},
{
name: "Susan",
votes: ["yes", "yes", "yes", "no"],
},
{
name: "Johnny",
votes: ["no", "no", "yes", "yes"],
},
{
name: "Ben",
votes: ["yes", "yes", "yes", "yes"],
},
];
const optionValues = ["2022-12-14", "2022-12-15", "2022-12-16", "2022-12-17"];
export const createPoll = async () => {
const pollId = nanoid();
const adminUrlId = nanoid();
const options: Array<{ start: Date; id: string }> = [];
for (let i = 0; i < optionValues.length; i++) {
options.push({ id: nanoid(), start: new Date(optionValues[i]) });
}
const participants: Array<{
name: string;
id: string;
userId: string;
createdAt: Date;
}> = [];
const votes: Array<{
optionId: string;
participantId: string;
type: VoteType;
}> = [];
for (let i = 0; i < participantData.length; i++) {
const { name, votes: participantVotes } = participantData[i];
const participantId = nanoid();
participants.push({
id: participantId,
name,
userId: "user-demo",
createdAt: dayjs()
.add(i * -1, "minutes")
.toDate(),
});
options.forEach((option, index) => {
votes.push({
optionId: option.id,
participantId,
type: participantVotes[index],
});
});
}
await prisma.poll.create({
data: {
id: pollId,
title: "Lunch Meeting",
location: "Starbucks, 901 New York Avenue",
description: `Hey everyone, please choose the dates when you are available to meet for our monthly get together. Looking forward to see you all!`,
demo: true,
adminUrlId,
participantUrlId: nanoid(),
userId: "guest-user",
options: {
createMany: {
data: options,
},
},
participants: {
createMany: {
data: participants,
},
},
votes: {
createMany: {
data: votes,
},
},
},
});
return pollId;
};
export const deletePoll = async (pollId: string) => {
await prisma.$transaction([
prisma.vote.deleteMany({
where: {
pollId,
},
}),
prisma.option.deleteMany({
where: {
pollId,
},
}),
prisma.participant.deleteMany({
where: {
pollId,
},
}),
prisma.comment.deleteMany({
where: {
pollId,
},
}),
prisma.poll.deleteMany({
where: {
id: pollId,
},
}),
]);
};

View file

@ -87,7 +87,6 @@ export const polls = router({
timeZone: input.timeZone,
location: input.location,
description: input.description,
demo: input.demo,
adminUrlId: adminToken,
participantUrlId,
userId: ctx.user.id,
@ -355,12 +354,11 @@ export const polls = router({
adminUrlId: true,
participantUrlId: true,
closed: true,
legacy: true,
status: true,
hideParticipants: true,
disableComments: true,
hideScores: true,
requireParticipantEmail: true,
demo: true,
options: {
select: {
id: true,
@ -436,6 +434,7 @@ export const polls = router({
timeZone: true,
adminUrlId: true,
participantUrlId: true,
status: true,
event: {
select: {
start: true,
@ -551,14 +550,20 @@ export const polls = router({
eventStart = eventStart.tz(poll.timeZone, true);
}
await prisma.event.create({
await prisma.poll.update({
where: {
id: input.pollId,
},
data: {
pollId: poll.id,
optionId: input.optionId,
start: eventStart.toDate(),
duration: option.duration,
title: poll.title,
userId: ctx.user.id,
event: {
create: {
optionId: input.optionId,
start: eventStart.toDate(),
duration: option.duration,
title: poll.title,
userId: ctx.user.id,
},
},
},
});
@ -721,18 +726,16 @@ export const polls = router({
)
.mutation(async ({ input }) => {
await prisma.$transaction([
prisma.event.delete({
where: {
pollId: input.pollId,
},
}),
prisma.poll.update({
where: {
id: input.pollId,
},
data: {
eventId: null,
closed: false,
event: {
delete: true,
},
status: "live",
closed: false, // @deprecated
},
}),
]);
@ -749,7 +752,8 @@ export const polls = router({
id: input.pollId,
},
data: {
closed: true,
closed: true, // TODO (Luke Vella) [2023-12-05]: Remove this
status: "paused",
},
});
}),
@ -827,6 +831,7 @@ export const polls = router({
},
data: {
closed: false,
status: "live",
},
});
}),

View file

@ -0,0 +1,50 @@
/*
Warnings:
- You are about to drop the column `demo` on the `polls` table. All the data in the column will be lost.
- You are about to drop the column `legacy` on the `polls` table. All the data in the column will be lost.
- A unique constraint covering the columns `[event_id]` on the table `polls` will be added. If there are existing duplicate values, this will fail.
*/
-- CreateEnum
CREATE TYPE "poll_status" AS ENUM ('live', 'paused', 'finalized');
-- AlterTable
ALTER TABLE "polls" DROP COLUMN "demo",
DROP COLUMN "legacy",
ADD COLUMN "status" "poll_status";
-- CreateIndex
CREATE UNIQUE INDEX "polls_event_id_key" ON "polls"("event_id");
-- Fix an issue where the "event_id" column was not being set
UPDATE "polls"
SET "event_id" = "events"."id"
FROM "events"
WHERE "events"."poll_id" = "polls"."id";
-- Set the "status" column to corressponding enum value
-- If "closed" is true, set to "paused"
-- If a poll has an "event_id", set to "finalized"
-- If a poll has a "deletedAt" date, set to "deleted"
-- Otherwise set to "live"
UPDATE "polls"
SET "status" = CASE
WHEN "closed" = true THEN 'paused'::poll_status
WHEN "event_id" IS NOT NULL THEN 'finalized'::poll_status
ELSE 'live'::poll_status
END;
-- Make the "status" column non-nullable and default to "live"
ALTER TABLE "polls"
ALTER COLUMN "status" SET NOT NULL,
ALTER COLUMN "status" SET DEFAULT 'live';
DROP INDEX "events_poll_id_idx";
-- DropIndex
DROP INDEX "events_poll_id_key";
-- AlterTable
ALTER TABLE "events" DROP COLUMN "poll_id";

View file

@ -117,6 +117,14 @@ enum ParticipantVisibility {
@@map("participant_visibility")
}
enum PollStatus {
live
paused
finalized
@@map("poll_status")
}
model Poll {
id String @id @unique @map("id")
createdAt DateTime @default(now()) @map("created_at")
@ -125,46 +133,45 @@ model Poll {
title String
description String?
location String?
user User? @relation(fields: [userId], references: [id])
userId String @map("user_id")
votes Vote[]
timeZone String? @map("time_zone")
options Option[]
participants Participant[]
watchers Watcher[]
demo Boolean @default(false)
comments Comment[]
legacy Boolean @default(false) // @deprecated
closed Boolean @default(false) // we use this to indicate whether a poll is paused
closed Boolean @default(false) // @deprecated
status PollStatus @default(live)
deleted Boolean @default(false)
deletedAt DateTime? @map("deleted_at")
touchedAt DateTime @default(now()) @map("touched_at")
participantUrlId String @unique @map("participant_url_id")
adminUrlId String @unique @map("admin_url_id")
eventId String? @map("event_id")
event Event?
eventId String? @unique @map("event_id")
hideParticipants Boolean @default(false) @map("hide_participants")
hideScores Boolean @default(false) @map("hide_scores")
disableComments Boolean @default(false) @map("disable_comments")
requireParticipantEmail Boolean @default(false) @map("require_participant_email")
user User? @relation(fields: [userId], references: [id])
event Event? @relation(fields: [eventId], references: [id])
options Option[]
participants Participant[]
watchers Watcher[]
comments Comment[]
votes Vote[]
@@index([userId], type: Hash)
@@map("polls")
}
model Event {
id String @id @default(cuid())
pollId String @unique @map("poll_id")
userId String @map("user_id")
user User @relation(fields: [userId], references: [id])
poll Poll @relation(fields: [pollId], references: [id])
optionId String @map("option_id")
title String
start DateTime @db.Timestamp(0)
duration Int @default(0) @map("duration_minutes")
createdAt DateTime @default(now()) @map("created_at")
@@index([pollId], type: Hash)
Poll Poll?
@@index([userId], type: Hash)
@@map("events")
}