fix(platform): address remaining PR review feedback for waitlist

Backend fixes:
- Fix optional field clearing by using model_fields_set
- Re-fetch waitlist data after join operation
- Only mark waitlist as DONE if all notifications succeed
- Fix race condition in email removal with transaction
- Rename waitlist_id to waitlistId for naming consistency

Frontend fixes:
- Migrate useWaitlistSection to generated API hooks
- Migrate JoinWaitlistModal to design system + generated hooks
- Migrate WaitlistSignupsDialog to design system + generated hooks
- Replace lucide-react icons with Phosphor in WaitlistTable
- Add proper error state in WaitlistSignupsDialog
- Update waitlistId naming across components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Nicholas Tindle
2026-01-12 14:43:10 -06:00
parent 9edfe0fb97
commit 738c7e2bef
9 changed files with 314 additions and 259 deletions

View File

@@ -1988,7 +1988,7 @@ def _waitlist_to_store_entry(
) -> store_model.StoreWaitlistEntry:
"""Convert a WaitlistEntry to StoreWaitlistEntry for public display."""
return store_model.StoreWaitlistEntry(
waitlist_id=waitlist.id,
waitlistId=waitlist.id,
slug=waitlist.slug,
name=waitlist.name,
subHeading=waitlist.subHeading,
@@ -2084,14 +2084,24 @@ async def add_user_to_waitlist(
logger.info(f"User {user_id} joined waitlist {waitlist_id}")
# If user was previously in email list, remove them
if email and email in (waitlist.unafilliatedEmailUsers or []):
updated_emails: list[str] = [
e for e in (waitlist.unafilliatedEmailUsers or []) if e != email
]
await prisma.models.WaitlistEntry.prisma().update(
where={"id": waitlist_id},
data={"unafilliatedEmailUsers": updated_emails},
)
# Use transaction to prevent race conditions
if email:
async with transaction() as tx:
current_waitlist = await tx.waitlistentry.find_unique(
where={"id": waitlist_id}
)
if current_waitlist and email in (
current_waitlist.unafilliatedEmailUsers or []
):
updated_emails: list[str] = [
e
for e in (current_waitlist.unafilliatedEmailUsers or [])
if e != email
]
await tx.waitlistentry.update(
where={"id": waitlist_id},
data={"unafilliatedEmailUsers": updated_emails},
)
elif email:
# Add email to unaffiliated list if not already present
# Use transaction to prevent race conditions with concurrent signups
@@ -2114,7 +2124,11 @@ async def add_user_to_waitlist(
else:
logger.debug(f"Email {email} already on waitlist {waitlist_id}")
return _waitlist_to_store_entry(waitlist)
# Re-fetch to return updated data
updated_waitlist = await prisma.models.WaitlistEntry.prisma().find_unique(
where={"id": waitlist_id}
)
return _waitlist_to_store_entry(updated_waitlist or waitlist)
except ValueError:
raise
@@ -2235,7 +2249,8 @@ async def update_waitlist_admin(
logger.info(f"Updating waitlist {waitlist_id}")
try:
# Build update data from non-None fields
# Build update data from explicitly provided fields
# Use model_fields_set to allow clearing fields by setting them to None
field_mappings = {
"name": data.name,
"slug": data.slug,
@@ -2248,11 +2263,11 @@ async def update_waitlist_admin(
"storeListingId": data.storeListingId,
}
update_data: dict[str, typing.Any] = {
k: v for k, v in field_mappings.items() if v is not None
k: v for k, v in field_mappings.items() if k in data.model_fields_set
}
# Handle status separately due to enum conversion
if data.status is not None:
if "status" in data.model_fields_set and data.status is not None:
update_data["status"] = prisma.enums.WaitlistExternalStatus(data.status)
if not update_data:
@@ -2408,8 +2423,12 @@ async def notify_waitlist_users_on_launch(
launched_at = datetime.now(tz=timezone.utc)
for waitlist in waitlists:
# Track notification results for this waitlist
users_to_notify = waitlist.joinedUsers or []
failed_user_ids: list[str] = []
# Notify registered users
for user in waitlist.joinedUsers or []:
for user in users_to_notify:
try:
notification_data = WaitlistLaunchData(
agent_name=agent_name,
@@ -2430,6 +2449,7 @@ async def notify_waitlist_users_on_launch(
logger.error(
f"Failed to send waitlist launch notification to user {user.id}: {e}"
)
failed_user_ids.append(user.id)
# Note: For unaffiliated email users, you would need to send emails directly
# since they don't have user IDs for the notification system.
@@ -2441,12 +2461,18 @@ async def notify_waitlist_users_on_launch(
f"unaffiliated email users that need email notifications"
)
# Update waitlist status to DONE
await prisma.models.WaitlistEntry.prisma().update(
where={"id": waitlist.id},
data={"status": prisma.enums.WaitlistExternalStatus.DONE},
)
logger.info(f"Updated waitlist {waitlist.id} status to DONE")
# Only mark waitlist as DONE if all registered user notifications succeeded
if not failed_user_ids:
await prisma.models.WaitlistEntry.prisma().update(
where={"id": waitlist.id},
data={"status": prisma.enums.WaitlistExternalStatus.DONE},
)
logger.info(f"Updated waitlist {waitlist.id} status to DONE")
else:
logger.warning(
f"Waitlist {waitlist.id} not marked as DONE due to "
f"{len(failed_user_ids)} failed notifications"
)
logger.info(
f"Sent {notification_count} waitlist launch notifications for store listing {store_listing_id}"

View File

@@ -220,7 +220,7 @@ class ReviewSubmissionRequest(pydantic.BaseModel):
class StoreWaitlistEntry(pydantic.BaseModel):
waitlist_id: str
waitlistId: str
storeListing: StoreListingWithVersions | None = None
owner: User | None = None
slug: str

View File

@@ -1,26 +1,9 @@
"use client";
import { useEffect, useState } from "react";
import { Button } from "@/components/atoms/Button/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/__legacy__/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/__legacy__/ui/table";
import { getWaitlistSignups } from "../actions";
import type { WaitlistSignupListResponse } from "@/lib/autogpt-server-api/types";
import { useToast } from "@/components/molecules/Toast/use-toast";
import { User, Mail, Download } from "lucide-react";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { User, Envelope, DownloadSimple } from "@phosphor-icons/react";
import { useGetV2GetWaitlistSignups } from "@/app/api/__generated__/endpoints/admin/admin";
type WaitlistSignupsDialogProps = {
waitlistId: string;
@@ -31,31 +14,13 @@ export function WaitlistSignupsDialog({
waitlistId,
onClose,
}: WaitlistSignupsDialogProps) {
const [loading, setLoading] = useState(true);
const [signups, setSignups] = useState<WaitlistSignupListResponse | null>(
null,
);
const { toast } = useToast();
const {
data: signupsResponse,
isLoading,
isError,
} = useGetV2GetWaitlistSignups(waitlistId);
useEffect(() => {
async function loadSignups() {
try {
const response = await getWaitlistSignups(waitlistId);
setSignups(response);
} catch (error) {
console.error("Error loading signups:", error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to load signups",
});
} finally {
setLoading(false);
}
}
loadSignups();
}, [waitlistId, toast]);
const signups = signupsResponse?.status === 200 ? signupsResponse.data : null;
function exportToCSV() {
if (!signups) return;
@@ -84,79 +49,108 @@ export function WaitlistSignupsDialog({
window.URL.revokeObjectURL(url);
}
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-[700px]">
<DialogHeader>
<DialogTitle>Waitlist Signups</DialogTitle>
<DialogDescription>
{signups
? `${signups.totalCount} total signups`
: "Loading signups..."}
</DialogDescription>
</DialogHeader>
function renderContent() {
if (isLoading) {
return <div className="py-10 text-center">Loading signups...</div>;
}
{loading ? (
<div className="py-10 text-center">Loading signups...</div>
) : signups && signups.signups.length > 0 ? (
<>
<div className="flex justify-end">
<Button variant="secondary" size="small" onClick={exportToCSV}>
<Download className="mr-2 h-4 w-4" />
Export CSV
</Button>
</div>
<div className="max-h-[400px] overflow-y-auto rounded-md border">
<Table>
<TableHeader className="bg-gray-50">
<TableRow>
<TableHead className="font-medium">Type</TableHead>
<TableHead className="font-medium">
Email / Username
</TableHead>
<TableHead className="font-medium">User ID</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{signups.signups.map((signup, index) => (
<TableRow key={index}>
<TableCell>
{signup.type === "user" ? (
<span className="flex items-center gap-1 text-blue-600">
<User className="h-4 w-4" /> User
</span>
) : (
<span className="flex items-center gap-1 text-gray-600">
<Mail className="h-4 w-4" /> Email
</span>
)}
</TableCell>
<TableCell>
{signup.type === "user"
? signup.username || signup.email
: signup.email}
</TableCell>
<TableCell className="font-mono text-sm">
{signup.userId || "-"}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</>
) : (
<div className="py-10 text-center text-gray-500">
No signups yet for this waitlist.
</div>
)}
if (isError) {
return (
<div className="py-10 text-center text-red-500">
Failed to load signups. Please try again.
</div>
);
}
if (!signups || signups.signups.length === 0) {
return (
<div className="py-10 text-center text-gray-500">
No signups yet for this waitlist.
</div>
);
}
return (
<>
<div className="flex justify-end">
<Button variant="secondary" size="small" onClick={exportToCSV}>
<DownloadSimple className="mr-2 h-4 w-4" size={16} />
Export CSV
</Button>
</div>
<div className="max-h-[400px] overflow-y-auto rounded-md border">
<table className="w-full">
<thead className="bg-gray-50 dark:bg-gray-800">
<tr>
<th className="px-4 py-3 text-left text-sm font-medium">
Type
</th>
<th className="px-4 py-3 text-left text-sm font-medium">
Email / Username
</th>
<th className="px-4 py-3 text-left text-sm font-medium">
User ID
</th>
</tr>
</thead>
<tbody className="divide-y">
{signups.signups.map((signup, index) => (
<tr key={index}>
<td className="px-4 py-3">
{signup.type === "user" ? (
<span className="flex items-center gap-1 text-blue-600">
<User className="h-4 w-4" size={16} /> User
</span>
) : (
<span className="flex items-center gap-1 text-gray-600">
<Envelope className="h-4 w-4" size={16} /> Email
</span>
)}
</td>
<td className="px-4 py-3">
{signup.type === "user"
? signup.username || signup.email
: signup.email}
</td>
<td className="px-4 py-3 font-mono text-sm">
{signup.userId || "-"}
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
);
}
return (
<Dialog
title="Waitlist Signups"
controlled={{
isOpen: true,
set: async (open) => {
if (!open) onClose();
},
}}
onClose={onClose}
styling={{ maxWidth: "700px" }}
>
<Dialog.Content>
<p className="mb-4 text-sm text-zinc-500">
{signups
? `${signups.totalCount} total signups`
: "Loading signups..."}
</p>
{renderContent()}
<Dialog.Footer>
<Button variant="secondary" onClick={onClose}>
Close
</Button>
</div>
</DialogContent>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -14,7 +14,7 @@ import { getWaitlistsAdmin, deleteWaitlist } from "../actions";
import type { WaitlistAdminResponse } from "@/lib/autogpt-server-api/types";
import { EditWaitlistDialog } from "./EditWaitlistDialog";
import { WaitlistSignupsDialog } from "./WaitlistSignupsDialog";
import { Trash2, Edit, Users, Link } from "lucide-react";
import { Trash, PencilSimple, Users, Link } from "@phosphor-icons/react";
import { useToast } from "@/components/molecules/Toast/use-toast";
export function WaitlistTable() {
@@ -136,7 +136,7 @@ export function WaitlistTable() {
<TableCell>
{waitlist.storeListingId ? (
<span className="text-green-600">
<Link className="inline h-4 w-4" /> Linked
<Link size={16} className="inline" /> Linked
</span>
) : (
<span className="text-gray-400">Not linked</span>
@@ -150,7 +150,7 @@ export function WaitlistTable() {
onClick={() => setViewingSignups(waitlist.id)}
title="View signups"
>
<Users className="h-4 w-4" />
<Users size={16} />
</Button>
<Button
variant="ghost"
@@ -158,7 +158,7 @@ export function WaitlistTable() {
onClick={() => setEditingWaitlist(waitlist)}
title="Edit"
>
<Edit className="h-4 w-4" />
<PencilSimple size={16} />
</Button>
<Button
variant="ghost"
@@ -166,7 +166,7 @@ export function WaitlistTable() {
onClick={() => handleDelete(waitlist.id)}
title="Delete"
>
<Trash2 className="h-4 w-4 text-red-500" />
<Trash size={16} className="text-red-500" />
</Button>
</div>
</TableCell>

View File

@@ -2,21 +2,13 @@
import { useState } from "react";
import { Button } from "@/components/atoms/Button/Button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/__legacy__/ui/dialog";
import { Input } from "@/components/__legacy__/ui/input";
import { Label } from "@/components/__legacy__/ui/label";
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
import { Dialog } from "@/components/molecules/Dialog/Dialog";
import { Input } from "@/components/atoms/Input/Input";
import type { StoreWaitlistEntry } from "@/app/api/__generated__/models/storeWaitlistEntry";
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
import { useToast } from "@/components/molecules/Toast/use-toast";
import BackendAPI from "@/lib/autogpt-server-api/client";
import { Check } from "lucide-react";
import { usePostV2AddSelfToTheAgentWaitlist } from "@/app/api/__generated__/endpoints/store/store";
import { Check } from "@phosphor-icons/react";
interface JoinWaitlistModalProps {
waitlist: StoreWaitlistEntry;
@@ -31,70 +23,101 @@ export function JoinWaitlistModal({
}: JoinWaitlistModalProps) {
const { user } = useSupabaseStore();
const [email, setEmail] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const { toast } = useToast();
const joinWaitlistMutation = usePostV2AddSelfToTheAgentWaitlist();
async function handleJoin() {
setLoading(true);
try {
const api = new BackendAPI();
await api.joinWaitlist(waitlist.waitlist_id, user ? undefined : email);
function handleJoin() {
joinWaitlistMutation.mutate(
{
waitlistId: waitlist.waitlistId,
data: { email: user ? undefined : email },
},
{
onSuccess: (response) => {
if (response.status === 200) {
setSuccess(true);
toast({
title: "You're on the list!",
description: `We'll notify you when ${waitlist.name} is ready.`,
});
setSuccess(true);
toast({
title: "You're on the list!",
description: `We'll notify you when ${waitlist.name} is ready.`,
});
// Close after a short delay to show success state
setTimeout(() => {
onSuccess?.();
onClose();
}, 1500);
} catch (error) {
console.error("Error joining waitlist:", error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to join waitlist. Please try again.",
});
} finally {
setLoading(false);
}
// Close after a short delay to show success state
setTimeout(() => {
onSuccess?.();
onClose();
}, 1500);
} else {
toast({
variant: "destructive",
title: "Error",
description: "Failed to join waitlist. Please try again.",
});
}
},
onError: () => {
toast({
variant: "destructive",
title: "Error",
description: "Failed to join waitlist. Please try again.",
});
},
},
);
}
if (success) {
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[400px]">
<Dialog
title=""
controlled={{
isOpen: true,
set: async (open) => {
if (!open) onClose();
},
}}
onClose={onClose}
styling={{ maxWidth: "400px" }}
>
<Dialog.Content>
<div className="flex flex-col items-center justify-center py-8">
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-green-100 dark:bg-green-900">
<Check className="h-8 w-8 text-green-600 dark:text-green-400" />
<Check
className="h-8 w-8 text-green-600 dark:text-green-400"
size={32}
weight="bold"
/>
</div>
<DialogTitle className="mb-2 text-center text-xl">
<h2 className="mb-2 text-center text-xl font-semibold">
You&apos;re on the list!
</DialogTitle>
<DialogDescription className="text-center">
</h2>
<p className="text-center text-zinc-500">
We&apos;ll notify you when {waitlist.name} is ready.
</DialogDescription>
</p>
</div>
</DialogContent>
</Dialog.Content>
</Dialog>
);
}
return (
<Dialog open={true} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[400px]">
<DialogHeader>
<DialogTitle>Join waitlist</DialogTitle>
<DialogDescription>
{user
? `Get notified when ${waitlist.name} is ready to use.`
: `Enter your email to get notified when ${waitlist.name} is ready.`}
</DialogDescription>
</DialogHeader>
<Dialog
title="Join waitlist"
controlled={{
isOpen: true,
set: async (open) => {
if (!open) onClose();
},
}}
onClose={onClose}
styling={{ maxWidth: "400px" }}
>
<Dialog.Content>
<p className="mb-4 text-sm text-zinc-500">
{user
? `Get notified when ${waitlist.name} is ready to use.`
: `Enter your email to get notified when ${waitlist.name} is ready.`}
</p>
<div className="py-4">
{user ? (
@@ -107,34 +130,32 @@ export function JoinWaitlistModal({
</p>
</div>
) : (
<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
</div>
<Input
id="email"
label="Email address"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
)}
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Dialog.Footer>
<Button type="button" variant="secondary" onClick={onClose}>
Cancel
</Button>
<Button
onClick={handleJoin}
loading={loading}
loading={joinWaitlistMutation.isPending}
disabled={!user && !email}
className="bg-neutral-800 text-white hover:bg-neutral-700 dark:bg-neutral-700 dark:hover:bg-neutral-600"
>
{user ? "Join waitlist" : "Join with email"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog.Footer>
</Dialog.Content>
</Dialog>
);
}

View File

@@ -9,7 +9,7 @@ import {
import { WaitlistCard } from "../WaitlistCard/WaitlistCard";
import { WaitlistDetailModal } from "../WaitlistDetailModal/WaitlistDetailModal";
import { JoinWaitlistModal } from "../JoinWaitlistModal/JoinWaitlistModal";
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
import type { StoreWaitlistEntry } from "@/app/api/__generated__/models/storeWaitlistEntry";
import { useWaitlistSection } from "./useWaitlistSection";
export function WaitlistSection() {
@@ -69,7 +69,7 @@ export function WaitlistSection() {
<CarouselContent>
{waitlists.map((waitlist) => (
<CarouselItem
key={waitlist.waitlist_id}
key={waitlist.waitlistId}
className="min-w-64 max-w-71"
>
<WaitlistCard
@@ -77,7 +77,7 @@ export function WaitlistSection() {
subHeading={waitlist.subHeading}
description={waitlist.description}
imageUrl={waitlist.imageUrls[0] || null}
isMember={joinedWaitlistIds.has(waitlist.waitlist_id)}
isMember={joinedWaitlistIds.has(waitlist.waitlistId)}
onCardClick={() => handleCardClick(waitlist)}
onJoinClick={() => handleJoinClick(waitlist)}
/>
@@ -90,12 +90,12 @@ export function WaitlistSection() {
<div className="hidden grid-cols-1 place-items-center gap-6 md:grid md:grid-cols-2 lg:grid-cols-3">
{waitlists.map((waitlist) => (
<WaitlistCard
key={waitlist.waitlist_id}
key={waitlist.waitlistId}
name={waitlist.name}
subHeading={waitlist.subHeading}
description={waitlist.description}
imageUrl={waitlist.imageUrls[0] || null}
isMember={joinedWaitlistIds.has(waitlist.waitlist_id)}
isMember={joinedWaitlistIds.has(waitlist.waitlistId)}
onCardClick={() => handleCardClick(waitlist)}
onJoinClick={() => handleJoinClick(waitlist)}
/>
@@ -107,7 +107,7 @@ export function WaitlistSection() {
{selectedWaitlist && (
<WaitlistDetailModal
waitlist={selectedWaitlist}
isMember={joinedWaitlistIds.has(selectedWaitlist.waitlist_id)}
isMember={joinedWaitlistIds.has(selectedWaitlist.waitlistId)}
onClose={() => setSelectedWaitlist(null)}
onJoin={handleJoinFromDetail}
/>
@@ -118,7 +118,7 @@ export function WaitlistSection() {
<JoinWaitlistModal
waitlist={joiningWaitlist}
onClose={() => setJoiningWaitlist(null)}
onSuccess={() => handleJoinSuccess(joiningWaitlist.waitlist_id)}
onSuccess={() => handleJoinSuccess(joiningWaitlist.waitlistId)}
/>
)}
</div>

View File

@@ -1,52 +1,56 @@
"use client";
import { useEffect, useState } from "react";
import BackendAPI from "@/lib/autogpt-server-api/client";
import { StoreWaitlistEntry } from "@/lib/autogpt-server-api/types";
import { useMemo } from "react";
import { useSupabaseStore } from "@/lib/supabase/hooks/useSupabaseStore";
import {
useGetV2GetTheAgentWaitlist,
useGetV2GetWaitlistIdsTheCurrentUserHasJoined,
} from "@/app/api/__generated__/endpoints/store/store";
import type { StoreWaitlistEntry } from "@/app/api/__generated__/models/storeWaitlistEntry";
import { useQueryClient } from "@tanstack/react-query";
export function useWaitlistSection() {
const { user } = useSupabaseStore();
const [waitlists, setWaitlists] = useState<StoreWaitlistEntry[]>([]);
const [joinedWaitlistIds, setJoinedWaitlistIds] = useState<Set<string>>(
new Set(),
);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);
const queryClient = useQueryClient();
useEffect(() => {
async function fetchData() {
try {
const api = new BackendAPI();
// Fetch waitlists
const {
data: waitlistsResponse,
isLoading: waitlistsLoading,
isError: waitlistsError,
} = useGetV2GetTheAgentWaitlist();
// Fetch waitlists
const response = await api.getWaitlists();
setWaitlists(response.listings);
// Fetch memberships if logged in
const { data: membershipsResponse, isLoading: membershipsLoading } =
useGetV2GetWaitlistIdsTheCurrentUserHasJoined({
query: {
enabled: !!user,
},
});
// Fetch memberships if logged in
if (user) {
try {
const memberships = await api.getMyWaitlistMemberships();
setJoinedWaitlistIds(new Set(memberships));
} catch (error) {
// Don't fail the whole component if membership fetch fails
console.error("Error fetching waitlist memberships:", error);
}
}
} catch (error) {
console.error("Error fetching waitlists:", error);
setHasError(true);
} finally {
setIsLoading(false);
}
const waitlists: StoreWaitlistEntry[] = useMemo(() => {
if (waitlistsResponse?.status === 200) {
return waitlistsResponse.data.listings;
}
return [];
}, [waitlistsResponse]);
fetchData();
}, [user]);
const joinedWaitlistIds: Set<string> = useMemo(() => {
if (membershipsResponse?.status === 200) {
return new Set(membershipsResponse.data);
}
return new Set();
}, [membershipsResponse]);
const isLoading = waitlistsLoading || (!!user && membershipsLoading);
const hasError = waitlistsError;
// Function to add a waitlist ID to joined set (called after successful join)
function markAsJoined(waitlistId: string) {
setJoinedWaitlistIds((prev) => new Set([...prev, waitlistId]));
function markAsJoined(_waitlistId: string) {
// Invalidate the memberships query to refetch
queryClient.invalidateQueries({
queryKey: ["getV2GetWaitlistIdsTheCurrentUserHasJoined"],
});
}
return { waitlists, joinedWaitlistIds, isLoading, hasError, markAsJoined };

View File

@@ -5185,9 +5185,7 @@
"content": {
"application/json": {
"schema": {
"type": "string",
"description": "The ID of the store listing",
"title": "Store Listing Id"
"$ref": "#/components/schemas/Body_postV2Link_waitlist_to_store_listing"
}
}
}
@@ -6915,6 +6913,18 @@
"type": "object",
"title": "Body_postV2Execute a preset"
},
"Body_postV2Link_waitlist_to_store_listing": {
"properties": {
"store_listing_id": {
"type": "string",
"title": "Store Listing Id",
"description": "The ID of the store listing"
}
},
"type": "object",
"required": ["store_listing_id"],
"title": "Body_postV2Link waitlist to store listing"
},
"Body_postV2Upload_submission_media": {
"properties": {
"file": { "type": "string", "format": "binary", "title": "File" }
@@ -10291,7 +10301,7 @@
},
"StoreWaitlistEntry": {
"properties": {
"waitlist_id": { "type": "string", "title": "Waitlist Id" },
"waitlistId": { "type": "string", "title": "Waitlistid" },
"storeListing": {
"anyOf": [
{ "$ref": "#/components/schemas/StoreListingWithVersions" },
@@ -10329,7 +10339,7 @@
},
"type": "object",
"required": [
"waitlist_id",
"waitlistId",
"slug",
"name",
"subHeading",

View File

@@ -1167,7 +1167,7 @@ export type WaitlistUpdateRequest = {
// Public Waitlist Types
export type StoreWaitlistEntry = {
waitlist_id: string;
waitlistId: string;
slug: string;
name: string;
subHeading: string;