| 1 | /** |
| 2 | * Install requests: for an owner, everything members asked the workspace |
| 3 | * to add (a plugin, a skill, a connector, an integration), with the way to |
| 4 | * add it, mark it added or turn it down; for anyone else, their own and how |
| 5 | * each was answered. The Request buttons across Customize post here, and |
| 6 | * so do the answers. |
| 7 | */ |
| 8 | import { ArrowLeft } from "lucide-react"; |
| 9 | import { Link, data } from "react-router"; |
| 10 | |
| 11 | import { connectorsFor } from "@g1t/contracts/connectors"; |
| 12 | import { customizePath } from "@g1t/contracts/customize"; |
| 13 | |
| 14 | import type { Route } from "./+types/requests"; |
| 15 | import { useCustomize } from "./layout"; |
| 16 | import { NoAnswer, RequestRow, SectionHead } from "../../../components/customize"; |
| 17 | import { EmptyState } from "../../../components/ui"; |
| 18 | import { Card } from "../../../components/ui/card"; |
| 19 | import { addPath, customizeForm, openRequests } from "../../../lib/customize"; |
| 20 | import { page } from "../../../lib/meta"; |
| 21 | import { workspaceAgents } from "../../../lib/services.server"; |
| 22 | import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server"; |
| 23 | |
| 24 | export function meta({ params, ...args }: Route.MetaArgs) { |
| 25 | return page(args, { title: `Requests · Customize · ${params.owner} · g1t` }); |
| 26 | } |
| 27 | |
| 28 | export async function action({ params, context, request }: Route.ActionArgs) { |
| 29 | assertSameOrigin(request); |
| 30 | const viewer = requireUser(context, request); |
| 31 | const slug = params.owner.toLowerCase(); |
| 32 | if (!roleIn(viewer, slug)) throw data(null, { status: 404 }); |
| 33 | const asked = customizeForm(await request.formData()); |
| 34 | if (!asked) return data({ error: "Nothing to do." }, { status: 400 }); |
| 35 | const call = asked.intent === "request" ? workspaceAgents.requestInstall(slug, viewer, asked.listing, asked.note) : workspaceAgents.resolveInstallRequest(slug, viewer, asked.id, asked.status); |
| 36 | const result = await call.catch(() => null); |
| 37 | if (!result) return data({ error: "The request didn't go through. Try again in a moment." }, { status: 503 }); |
| 38 | if (!result.ok) return data({ error: result.error.message }, { status: result.error.code === "conflict" ? 409 : 422 }); |
| 39 | return { error: null }; |
| 40 | } |
| 41 | |
| 42 | export default function CustomizeRequests() { |
| 43 | const { slug, owner, requests } = useCustomize(); |
| 44 | const back = ( |
| 45 | <Link to={customizePath("skills", slug)} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg"> |
| 46 | <ArrowLeft size={14} /> |
| 47 | Customize |
| 48 | </Link> |
| 49 | ); |
| 50 | if (!requests) { |
| 51 | return ( |
| 52 | <div className="space-y-6"> |
| 53 | {back} |
| 54 | <NoAnswer what="Requests" /> |
| 55 | </div> |
| 56 | ); |
| 57 | } |
| 58 | const views = connectorsFor("workspace"); |
| 59 | const waiting = openRequests(requests.requests); |
| 60 | const answered = requests.requests.filter((request) => request.status !== "open"); |
| 61 | return ( |
| 62 | <div className="space-y-8"> |
| 63 | {back} |
| 64 | {requests.requests.length === 0 ? ( |
| 65 | owner ? ( |
| 66 | <EmptyState title="No requests">When a member asks for a plugin, a skill or a connector, it shows here, and you're notified.</EmptyState> |
| 67 | ) : ( |
| 68 | <EmptyState title="You haven't asked for anything"> |
| 69 | Find a plugin, a skill or a connector in Discover and choose <b>Request</b>. The workspace's owners are notified, and you hear back when one answers. |
| 70 | </EmptyState> |
| 71 | ) |
| 72 | ) : ( |
| 73 | <> |
| 74 | <section aria-labelledby="waiting"> |
| 75 | <SectionHead id="waiting" title={owner ? "Waiting on an owner" : "Waiting"} aside={`${waiting.length}`}> |
| 76 | {owner ? "Installing a plugin or connecting a connector answers every request for it. Anything added another way, mark added." : "Your requests the workspace's owners haven't answered yet."} |
| 77 | </SectionHead> |
| 78 | {waiting.length === 0 ? ( |
| 79 | <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted"> |
| 80 | <p>Nothing is waiting.</p> |
| 81 | </Card> |
| 82 | ) : ( |
| 83 | <Card asChild divided className="overflow-hidden"> |
| 84 | <ul> |
| 85 | {waiting.map((request) => ( |
| 86 | <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={owner ? addPath(request, slug, views) : null} /> |
| 87 | ))} |
| 88 | </ul> |
| 89 | </Card> |
| 90 | )} |
| 91 | </section> |
| 92 | {answered.length > 0 && ( |
| 93 | <section aria-labelledby="answered"> |
| 94 | <SectionHead id="answered" title="Answered" aside="Last 30 days" /> |
| 95 | <Card asChild divided className="overflow-hidden"> |
| 96 | <ul> |
| 97 | {answered.map((request) => ( |
| 98 | <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={null} /> |
| 99 | ))} |
| 100 | </ul> |
| 101 | </Card> |
| 102 | </section> |
| 103 | )} |
| 104 | </> |
| 105 | )} |
| 106 | </div> |
| 107 | ); |
| 108 | } |