Added API integration voting functionality

This commit is contained in:
Matt Aitken
2023-06-16 11:32:38 +01:00
parent b363a1e60d
commit 31a9164df1
7 changed files with 136 additions and 17 deletions
@@ -1,9 +1,8 @@
import React, { useState } from "react";
import {
ApiAuthenticationMethodApiKey,
Integration,
} from "~/services/externalApis/types";
import { Header1, Header2 } from "../primitives/Headers";
import React from "react";
import { Api } from "~/services/externalApis/apis";
import { Button } from "../primitives/Buttons";
import { Callout } from "../primitives/Callout";
import { Header1 } from "../primitives/Headers";
import { NamedIconInBox } from "../primitives/NamedIcon";
import {
Sheet,
@@ -12,21 +11,23 @@ import {
SheetHeader,
SheetTrigger,
} from "../primitives/Sheet";
import { RadioGroup, RadioGroupItem } from "../primitives/RadioButton";
import { ApiKeyHelp } from "./ApiKeyHelp";
import { CustomHelp } from "./CustomHelp";
import { SelectOAuthMethod } from "./SelectOAuthMethod";
import { Api } from "~/services/externalApis/apis";
import { Callout } from "../primitives/Callout";
import { Button } from "../primitives/Buttons";
import { CheckIcon } from "@heroicons/react/24/solid";
import { useFetcher } from "@remix-run/react";
import { Paragraph } from "../primitives/Paragraph";
export function NoIntegrationSheet({
api,
requested,
button,
}: {
api: Api;
requested: boolean;
button: React.ReactNode;
}) {
const fetcher = useFetcher();
const isLoading = fetcher.state !== "idle";
return (
<Sheet>
<SheetTrigger>{button}</SheetTrigger>
@@ -36,9 +37,29 @@ export function NoIntegrationSheet({
<NamedIconInBox name={api.identifier} className="h-9 w-9" />
<Header1>{api.name}</Header1>
</div>
<Button variant="primary/small">
I want an integration for {api.name}
</Button>
{requested ? (
<div className="flex items-center gap-1">
<CheckIcon className="h-4 w-4 text-green-500" />
<Paragraph variant="small">
We'll let you know when the integration is available
</Paragraph>
</div>
) : (
<fetcher.Form
method="post"
action={`/resources/apivote/${api.identifier}`}
>
<Button
variant="primary/small"
disabled={isLoading}
LeadingIcon={isLoading ? "spinner-white" : undefined}
>
{isLoading
? "Saving…"
: `I want an integration for ${api.name}`}
</Button>
</fetcher.Form>
)}
</SheetHeader>
<SheetBody>
<Callout variant="info">
@@ -13,7 +13,7 @@ export type IntegrationOrApi =
| ({
type: "integration";
} & Integration)
| ({ type: "api" } & Api);
| ({ type: "api" } & Api & { voted: boolean });
export class IntegrationsPresenter {
#prismaClient: PrismaClient;
@@ -132,13 +132,29 @@ export class IntegrationsPresenter {
integrationCatalog.getIntegrations()
).map((i) => ({ type: "integration" as const, ...i }));
//get all apis, some don't have integrations yet.
//get whether the user has voted for them or not
const votes = await this.#prismaClient.apiIntegrationVote.findMany({
select: {
apiIdentifier: true,
},
where: {
userId,
},
});
const apis = apisList
.filter((a) => !integrations.some((i) => i.identifier === a.identifier))
.map((a) => ({ type: "api" as const, ...a }));
.map((a) => ({
type: "api" as const,
...a,
voted: votes.some((v) => v.apiIdentifier === a.identifier),
}));
const options = [...integrations, ...apis].sort((a, b) =>
a.name.localeCompare(b.name)
);
return {
clients: clientsWithConnections,
options,
@@ -161,6 +161,7 @@ function PossibleIntegrationsList({
<NoIntegrationSheet
key={option.identifier}
api={option}
requested={option.voted}
button={
<AddIntegrationConnection
identifier={option.identifier}
@@ -0,0 +1,22 @@
import { ActionArgs, json } from "@remix-run/server-runtime";
import { z } from "zod";
import { ApiVoteService } from "~/services/apiVote.server";
import { requireUserId } from "~/services/session.server";
const ParamsSchema = z.object({
identifier: z.string(),
});
export async function action({ request, params }: ActionArgs) {
const userId = await requireUserId(request);
const { identifier } = ParamsSchema.parse(params);
const service = new ApiVoteService();
try {
const result = await service.call({ userId, identifier });
return json(result);
} catch (e) {
return json(e, { status: 400 });
}
}
@@ -0,0 +1,28 @@
import { PrismaClient, prisma } from "~/db.server";
export class ApiVoteService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call({
userId,
identifier,
}: {
userId: string;
identifier: string;
}) {
return this.#prismaClient.apiIntegrationVote.create({
data: {
user: {
connect: {
id: userId,
},
},
apiIdentifier: identifier,
},
});
}
}
@@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "ApiIntegrationVote" (
"id" TEXT NOT NULL,
"apiIdentifier" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ApiIntegrationVote_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "ApiIntegrationVote_apiIdentifier_userId_key" ON "ApiIntegrationVote"("apiIdentifier", "userId");
-- AddForeignKey
ALTER TABLE "ApiIntegrationVote" ADD CONSTRAINT "ApiIntegrationVote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+15
View File
@@ -36,6 +36,7 @@ model User {
orgMemberships OrgMember[]
sentInvites OrgMemberInvite[]
apiVotes ApiIntegrationVote[]
}
enum AuthenticationMethod {
@@ -927,3 +928,17 @@ model MissingApiConnection {
@@unique([apiConnectionClientId, connectionType, externalAccountId])
}
model ApiIntegrationVote {
id String @id @default(cuid())
apiIdentifier String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([apiIdentifier, userId])
}