Skip to content
108 linesCodeBlameRaw
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 */
8import { ArrowLeft } from "lucide-react";
9import { Link, data } from "react-router";
10
11import { connectorsFor } from "@g1t/contracts/connectors";
12import { customizePath } from "@g1t/contracts/customize";
13
14import type { Route } from "./+types/requests";
15import { useCustomize } from "./layout";
16import { NoAnswer, RequestRow, SectionHead } from "../../../components/customize";
17import { EmptyState } from "../../../components/ui";
18import { Card } from "../../../components/ui/card";
19import { addPath, customizeForm, openRequests } from "../../../lib/customize";
20import { page } from "../../../lib/meta";
21import { workspaceAgents } from "../../../lib/services.server";
22import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server";
23
24export function meta({ params, ...args }: Route.MetaArgs) {
25 return page(args, { title: `Requests · Customize · ${params.owner} · g1t` });
26}
27
28export 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
42export 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}