From 04211ac1689a46f810b40d73117ee4fb83a7f18c Mon Sep 17 00:00:00 2001 From: Luke Vella Date: Tue, 5 Dec 2023 14:43:48 +0700 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Update=20how=20we=20store?= =?UTF-8?q?=20poll=20status=20(#957)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/[locale]/(admin)/polls/polls-page.tsx | 7 +- apps/web/src/components/event-card.tsx | 4 +- .../src/components/layouts/poll-layout.tsx | 5 +- apps/web/src/components/poll-status.tsx | 15 +- .../components/poll/notifications-toggle.tsx | 2 +- apps/web/src/utils/trpc/types.ts | 5 +- apps/web/tests/mocks.ts | 130 ------------------ packages/backend/trpc/routers/polls.ts | 41 +++--- .../20231205043530_poll_status/migration.sql | 50 +++++++ packages/database/prisma/schema.prisma | 35 +++-- 10 files changed, 108 insertions(+), 186 deletions(-) delete mode 100644 apps/web/tests/mocks.ts create mode 100644 packages/database/prisma/migrations/20231205043530_poll_status/migration.sql diff --git a/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx b/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx index 8e5d2a2b2..435fd582a 100644 --- a/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx +++ b/apps/web/src/app/[locale]/(admin)/polls/polls-page.tsx @@ -75,12 +75,7 @@ export function PollsPage() { data.length > 0 ? (
{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 (
{ ), ); - const status = poll?.event ? "closed" : poll?.closed ? "paused" : "live"; - if (!poll) { return null; } @@ -49,7 +47,7 @@ export const EventCard = () => { />
- +
diff --git a/apps/web/src/components/layouts/poll-layout.tsx b/apps/web/src/components/layouts/poll-layout.tsx index cd3f08116..6d788eccc 100644 --- a/apps/web/src/components/layouts/poll-layout.tsx +++ b/apps/web/src/components/layouts/poll-layout.tsx @@ -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 = () => { diff --git a/apps/web/src/components/poll-status.tsx b/apps/web/src/components/poll-status.tsx index dddd60216..e56f395bd 100644 --- a/apps/web/src/components/poll-status.tsx +++ b/apps/web/src/components/poll-status.tsx @@ -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 = ({ ); - case "closed": + case "finalized": return ( @@ -52,13 +51,13 @@ export const PollStatus = ({ } }; -export const PollStatusBadge = ({ status }: { status: PollState }) => { +export const PollStatusBadge = ({ status }: { status: PollStatus }) => { return ( - diff --git a/apps/web/src/components/poll/notifications-toggle.tsx b/apps/web/src/components/poll/notifications-toggle.tsx index c00e8d0e7..367f53659 100644 --- a/apps/web/src/components/poll/notifications-toggle.tsx +++ b/apps/web/src/components/poll/notifications-toggle.tsx @@ -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) { diff --git a/apps/web/src/utils/trpc/types.ts b/apps/web/src/utils/trpc/types.ts index b91d6cccc..29b46336e 100644 --- a/apps/web/src/utils/trpc/types.ts +++ b/apps/web/src/utils/trpc/types.ts @@ -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; }; diff --git a/apps/web/tests/mocks.ts b/apps/web/tests/mocks.ts deleted file mode 100644 index b2a1a7631..000000000 --- a/apps/web/tests/mocks.ts +++ /dev/null @@ -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, - }, - }), - ]); -}; diff --git a/packages/backend/trpc/routers/polls.ts b/packages/backend/trpc/routers/polls.ts index f0f5281e8..67619f160 100644 --- a/packages/backend/trpc/routers/polls.ts +++ b/packages/backend/trpc/routers/polls.ts @@ -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", }, }); }), diff --git a/packages/database/prisma/migrations/20231205043530_poll_status/migration.sql b/packages/database/prisma/migrations/20231205043530_poll_status/migration.sql new file mode 100644 index 000000000..c3a4cd2b1 --- /dev/null +++ b/packages/database/prisma/migrations/20231205043530_poll_status/migration.sql @@ -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"; \ No newline at end of file diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index e1cb017f6..b0cc8d7b3 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -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") }