Added API integration voting functionality
This commit is contained in:
@@ -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,
|
||||
|
||||
+1
@@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
+16
@@ -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;
|
||||
@@ -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])
|
||||
}
|
||||
Reference in New Issue
Block a user