| 1 | /** |
| 2 | * Install requests: for an owner, everything members asked the workspace |
| 3 | * to add, with the way to add it, mark it added or turn it down; for |
| 4 | * anyone else, their own and how each was answered. Every Marketplace form |
| 5 | * posts here: asking, answering, and installing, switching off or removing |
| 6 | * an extension. |
| 7 | */ |
| 8 | import { Link, data } from "react-router"; |
| 9 | |
| 10 | import { connectorsFor } from "@g1t/contracts/connectors"; |
| 11 | |
| 12 | import type { Route } from "./+types/requests"; |
| 13 | import { RequestRow, SectionHead } from "../../../components/marketplace"; |
| 14 | import { EmptyState } from "../../../components/ui"; |
| 15 | import { Card } from "../../../components/ui/card"; |
| 16 | import { addPath, marketplaceForm, marketplacePath, openRequests } from "../../../lib/marketplace"; |
| 17 | import { workspaceAgents } from "../../../lib/services.server"; |
| 18 | import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server"; |
| 19 | import { useMarketplace } from "./layout"; |
| 20 | |
| 21 | export async function action({ params, context, request }: Route.ActionArgs) { |
| 22 | assertSameOrigin(request); |
| 23 | const viewer = requireUser(context, request); |
| 24 | const slug = params.owner.toLowerCase(); |
| 25 | if (!roleIn(viewer, slug)) throw data(null, { status: 404 }); |
| 26 | const asked = marketplaceForm(await request.formData()); |
| 27 | if (!asked) return data({ error: "Nothing to do." }, { status: 400 }); |
| 28 | const call = (): Promise<{ ok: true } | { ok: false; error: { code: string; message: string } }> => { |
| 29 | switch (asked.intent) { |
| 30 | case "request": |
| 31 | return workspaceAgents.requestInstall(slug, viewer, asked.listing, asked.note); |
| 32 | case "resolve": |
| 33 | return workspaceAgents.resolveInstallRequest(slug, viewer, asked.id, asked.status); |
| 34 | case "install": |
| 35 | return workspaceAgents.installExtension(slug, viewer, asked.extension); |
| 36 | case "switch": |
| 37 | return workspaceAgents.setExtensionEnabled(slug, viewer, asked.listing, asked.enabled); |
| 38 | case "uninstall": |
| 39 | return workspaceAgents.uninstallExtension(slug, viewer, asked.listing); |
| 40 | } |
| 41 | }; |
| 42 | const result = await call().catch(() => null); |
| 43 | if (!result) return data({ error: "The request didn't go through. Try again in a moment." }, { status: 503 }); |
| 44 | if (!result.ok) return data({ error: result.error.message }, { status: result.error.code === "conflict" ? 409 : 422 }); |
| 45 | return { error: null }; |
| 46 | } |
| 47 | |
| 48 | export default function MarketplaceRequests() { |
| 49 | const { slug, owner, requests } = useMarketplace(); |
| 50 | if (!requests) { |
| 51 | return <EmptyState title="Requests can't be shown right now">The agents service didn't answer. Reload in a minute.</EmptyState>; |
| 52 | } |
| 53 | const views = connectorsFor("workspace"); |
| 54 | const waiting = openRequests(requests.requests); |
| 55 | const answered = requests.requests.filter((request) => request.status !== "open"); |
| 56 | if (requests.requests.length === 0) { |
| 57 | return owner ? ( |
| 58 | <EmptyState title="No requests">When a member asks for an extension or an integration, it shows here, and you're notified.</EmptyState> |
| 59 | ) : ( |
| 60 | <EmptyState title="You haven't asked for anything"> |
| 61 | Find an extension or an integration and choose <b>Request</b>. The workspace's owners are notified, and you hear back when one answers. |
| 62 | </EmptyState> |
| 63 | ); |
| 64 | } |
| 65 | return ( |
| 66 | <div className="space-y-10"> |
| 67 | <section aria-labelledby="waiting"> |
| 68 | <SectionHead id="waiting" title={owner ? "Waiting on an owner" : "Waiting"} aside={`${waiting.length}`}> |
| 69 | {owner |
| 70 | ? "Installing an extension answers every request for it. After connecting an integration, mark its requests added." |
| 71 | : "Your requests the workspace's owners haven't answered yet."} |
| 72 | </SectionHead> |
| 73 | {waiting.length === 0 ? ( |
| 74 | <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted"> |
| 75 | <p>Nothing is waiting.</p> |
| 76 | </Card> |
| 77 | ) : ( |
| 78 | <Card asChild divided className="overflow-hidden"> |
| 79 | <ul> |
| 80 | {waiting.map((request) => ( |
| 81 | <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={owner ? addPath(request, slug, views) : null} /> |
| 82 | ))} |
| 83 | </ul> |
| 84 | </Card> |
| 85 | )} |
| 86 | </section> |
| 87 | {answered.length > 0 && ( |
| 88 | <section aria-labelledby="answered"> |
| 89 | <SectionHead id="answered" title="Answered" aside="Last 30 days" /> |
| 90 | <Card asChild divided className="overflow-hidden"> |
| 91 | <ul> |
| 92 | {answered.map((request) => ( |
| 93 | <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={null} /> |
| 94 | ))} |
| 95 | </ul> |
| 96 | </Card> |
| 97 | </section> |
| 98 | )} |
| 99 | {!owner && ( |
| 100 | <p className="text-sm text-muted"> |
| 101 | Looking for something else? <Link to={marketplacePath(slug)} className="text-accent hover:underline">Browse the Marketplace</Link>. |
| 102 | </p> |
| 103 | )} |
| 104 | </div> |
| 105 | ); |
| 106 | } |