Skip to main content
Glama

mcp-google-sheets

install-piece-dialog.tsx10.8 kB
import { typeboxResolver } from '@hookform/resolvers/typebox'; import { Static, Type } from '@sinclair/typebox'; import { useMutation } from '@tanstack/react-query'; import { HttpStatusCode } from 'axios'; import { t } from 'i18next'; import { Plus } from 'lucide-react'; import pako from 'pako'; import { useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { ApMarkdown } from '@/components/custom/markdown'; import { Button } from '@/components/ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { toast } from '@/components/ui/use-toast'; import { flagsHooks } from '@/hooks/flags-hooks'; import { platformHooks } from '@/hooks/platform-hooks'; import { api } from '@/lib/api'; import { AddPieceRequestBody, ApFlagId, PackageType, PieceScope, } from '@activepieces/shared'; import { piecesApi } from '../lib/pieces-api'; const FormSchema = Type.Object( { packageType: Type.Enum(PackageType), pieceName: Type.Optional(Type.String()), scope: Type.Enum(PieceScope), pieceVersion: Type.Optional(Type.String()), pieceArchive: Type.Optional(Type.Any()), }, { errorMessage: { required: t('Please select a package type'), }, }, ); type InstallPieceDialogProps = { onInstallPiece: () => void; scope: PieceScope; }; const InstallPieceDialog = ({ onInstallPiece, scope, }: InstallPieceDialogProps) => { const { platform } = platformHooks.useCurrentPlatform(); const isEnabled = platform.plan.managePiecesEnabled; const [isOpen, setIsOpen] = useState(false); const { data: privatePiecesEnabled } = flagsHooks.useFlag<boolean>( ApFlagId.PRIVATE_PIECES_ENABLED, ); const form = useForm<Static<typeof FormSchema>>({ resolver: typeboxResolver(FormSchema), defaultValues: { scope, packageType: PackageType.REGISTRY, }, }); const handleArchiveUpload = async (file: File) => { if (file && file.name.endsWith('.tgz')) { try { const fileBuffer = await file.arrayBuffer(); const decompressedData = pako.ungzip(new Uint8Array(fileBuffer)); const text = new TextDecoder().decode(decompressedData); // Look for package.json content in the decompressed data const packageJsonMatch = text.match( /package\.json.*?{[^}]*"name"\s*:\s*"([^"]+)".*?"version"\s*:\s*"([^"]+)"/s, ); if (packageJsonMatch) { form.setValue('pieceName', packageJsonMatch[1]); form.setValue('pieceVersion', packageJsonMatch[2]); } else { form.setError('pieceArchive', { message: t('package.json not found in archive'), }); } } catch (error) { console.error('Error processing file:', error); form.setError('pieceArchive', { message: t('Error processing archive file'), }); } } else { form.setError('pieceArchive', { message: t('Please upload a .tgz file'), }); } }; const { mutate, isPending } = useMutation<void, Error, AddPieceRequestBody>({ mutationFn: async (data) => { form.clearErrors(); if (data.packageType === PackageType.REGISTRY) { if (!data.pieceName) { form.setError('pieceName', { message: t('Piece name is required for NPM Registry'), }); } if (!data.pieceVersion) { form.setError('pieceVersion', { message: t('Piece version is required for NPM Registry'), }); } if (!data.pieceName || !data.pieceVersion) { return; } } await piecesApi.install(data); }, onSuccess: () => { setIsOpen(false); form.reset(); onInstallPiece(); toast({ title: t('Success'), description: t('Piece installed'), duration: 3000, }); }, onError: (error) => { if (api.isError(error)) { switch (error.response?.status) { case HttpStatusCode.Conflict: form.setError('root.serverError', { message: t( 'A piece with this name and version is already installed. Please update the version number in package.json and try again.', ), }); break; default: form.setError('root.serverError', { message: t('Something went wrong, please try again later'), }); break; } } }, }); return ( <Dialog open={isOpen} onOpenChange={(open) => setIsOpen(open)}> <DialogTrigger asChild> <Button size="sm" className="flex items-center justify-center gap-2"> <Plus className="size-4" /> {t('Install Piece')} </Button> </DialogTrigger> <DialogContent> <DialogHeader> <DialogTitle>{t('Install a piece')}</DialogTitle> <DialogDescription> <ApMarkdown markdown={ 'Use this to install a [custom piece]("https://www.activepieces.com/docs/developers/building-pieces/create-action") that you (or someone else) created. Once the piece is installed, you can use it in the flow builder.\n\nWarning: Make sure you trust the author as the piece will have access to your flow data and it might not be compatible with the current version of Activepieces.' } /> </DialogDescription> </DialogHeader> <FormProvider {...form}> <form className="flex flex-col gap-4" onSubmit={form.handleSubmit((data) => mutate(data as AddPieceRequestBody), )} > <FormField name="packageType" control={form.control} render={({ field }) => ( <FormItem> <FormLabel htmlFor="packageType"> {t('Package Type')} </FormLabel> <Select value={field.value} onValueChange={(value) => { field.onChange(value); if (value === PackageType.ARCHIVE) { form.setValue('pieceName', undefined); form.setValue('pieceVersion', undefined); } form.clearErrors(); }} defaultValue={PackageType.REGISTRY} > <SelectTrigger> <SelectValue defaultValue={PackageType.REGISTRY} /> </SelectTrigger> <SelectContent> <SelectGroup> <SelectItem value={PackageType.REGISTRY}> {t('NPM Registry')} </SelectItem> <SelectItem value={PackageType.ARCHIVE} disabled={!isEnabled || !privatePiecesEnabled} > {t('Packed Archive (.tgz)')} </SelectItem> </SelectGroup> </SelectContent> </Select> <FormMessage /> </FormItem> )} /> {form.watch('packageType') === PackageType.REGISTRY && ( <> <FormField name="pieceName" control={form.control} render={({ field }) => ( <FormItem> <FormLabel htmlFor="pieceName"> {t('Piece Name')} </FormLabel> <Input {...field} value={field.value || ''} id="pieceName" type="text" placeholder="@activepieces/piece-name" className="rounded-sm" /> <FormMessage /> </FormItem> )} /> <FormField name="pieceVersion" control={form.control} render={({ field }) => ( <FormItem> <FormLabel htmlFor="pieceVersion"> {t('Piece Version')} </FormLabel> <Input {...field} value={field.value || ''} id="pieceVersion" type="text" placeholder="0.0.1" className="rounded-sm" /> <FormMessage /> </FormItem> )} /> </> )} {form.watch('packageType') === PackageType.ARCHIVE && ( <FormField name="pieceArchive" control={form.control} render={({ field: { value, onChange, ...fieldProps } }) => ( <FormItem> <FormLabel htmlFor="pieceArchive"> {t('Package Archive')} </FormLabel> <Input {...fieldProps} id="pieceArchive" type="file" onChange={(event) => { const file = event.target.files?.[0]; if (file) { onChange(file); handleArchiveUpload(file); } }} placeholder={t('Package archive')} className="rounded-sm" /> <FormMessage /> </FormItem> )} /> )} {form?.formState?.errors?.root?.serverError && ( <FormMessage> {form.formState.errors.root.serverError.message} </FormMessage> )} <Button loading={isPending} type="submit"> {t('Install')} </Button> </form> </FormProvider> </DialogContent> </Dialog> ); }; export { InstallPieceDialog };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/activepieces/activepieces'

If you have feedback or need assistance with the MCP directory API, please join our Discord server