Skip to main content
Glama

mcp-google-sheets

utils.ts9.02 kB
import { AxiosError } from 'axios'; import { clsx, type ClassValue } from 'clsx'; import dayjs from 'dayjs'; import i18next, { t } from 'i18next'; import JSZip from 'jszip'; import { useEffect, useRef, useState, RefObject } from 'react'; import { twMerge } from 'tailwind-merge'; import { LocalesEnum, Permission } from '@activepieces/shared'; import { authenticationSession } from './authentication-session'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; export const formatUtils = { emailRegex, convertEnumToHumanReadable(str: string) { const words = str.split(/[_.]/); return words .map( (word) => word.charAt(0).toUpperCase() + word.slice(1).toLocaleLowerCase(), ) .join(' '); }, formatNumber(number: number) { return new Intl.NumberFormat(i18next.language).format(number); }, formatDateOnlyOrFail(date: Date, fallback: string) { try { return this.formatDateOnly(date); } catch (error) { return fallback; } }, formatDateOnly(date: Date) { return Intl.DateTimeFormat(i18next.language, { month: 'numeric', day: 'numeric', year: 'numeric', }).format(date); }, formatDate(date: Date) { const now = dayjs(); const inputDate = dayjs(date); const isToday = inputDate.isSame(now, 'day'); const isYesterday = inputDate.isSame(now.subtract(1, 'day'), 'day'); const timeFormat = new Intl.DateTimeFormat(i18next.language, { hour: 'numeric', minute: 'numeric', hour12: true, }); if (isToday) { return `${t('Today')}, ${timeFormat.format(date)}`; } else if (isYesterday) { return `${t('Yesterday')}, ${timeFormat.format(date)}`; } return Intl.DateTimeFormat(i18next.language, { month: 'short', day: 'numeric', year: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true, }).format(date); }, formatDateToAgo(date: Date) { const now = dayjs(); const inputDate = dayjs(date); const diffInSeconds = now.diff(inputDate, 'second'); const diffInMinutes = now.diff(inputDate, 'minute'); const diffInHours = now.diff(inputDate, 'hour'); const diffInDays = now.diff(inputDate, 'day'); if (diffInSeconds < 60) { return `${diffInSeconds}s ago`; } if (diffInMinutes < 60) { return `${diffInMinutes}m ago`; } if (diffInHours < 24) { return `${diffInHours}h ago`; } if (diffInDays < 30) { return `${diffInDays}d ago`; } return inputDate.format('MMM D, YYYY'); }, formatDuration(durationMs: number | undefined, short?: boolean): string { if (durationMs === undefined) { return '-'; } if (durationMs < 1000) { const durationMsFormatted = Math.floor(durationMs); return short ? `${durationMsFormatted} ms` : `${durationMsFormatted} milliseconds`; } const seconds = Math.floor(durationMs / 1000); const minutes = Math.floor(seconds / 60); if (seconds < 60) { return short ? `${seconds} s` : `${seconds} seconds`; } if (minutes > 0) { const remainingSeconds = seconds % 60; return short ? `${minutes} min ${ remainingSeconds > 0 ? `${remainingSeconds} s` : '' }` : `${minutes} minutes${ remainingSeconds > 0 ? ` ${remainingSeconds} seconds` : '' }`; } return short ? `${seconds} s` : `${seconds} seconds`; }, }; export const validationUtils = { isValidationError: ( error: unknown, ): error is AxiosError<{ code?: string; params?: { message?: string } }> => { console.error('isValidationError', error); return ( error instanceof AxiosError && error.response?.status === 409 && error.response?.data?.code === 'VALIDATION' ); }, }; export function useForwardedRef<T>(ref: React.ForwardedRef<T>) { const innerRef = useRef<T>(null); useEffect(() => { if (!ref) return; if (typeof ref === 'function') { ref(innerRef.current); } else { ref.current = innerRef.current; } }); return innerRef; } export const localesMap = { [LocalesEnum.CHINESE_SIMPLIFIED]: '简体中文', [LocalesEnum.GERMAN]: 'Deutsch', [LocalesEnum.ENGLISH]: 'English', [LocalesEnum.SPANISH]: 'Español', [LocalesEnum.FRENCH]: 'Français', [LocalesEnum.JAPANESE]: '日本語', [LocalesEnum.DUTCH]: 'Nederlands', [LocalesEnum.PORTUGUESE]: 'Português', [LocalesEnum.CHINESE_TRADITIONAL]: '繁體中文', }; export const useElementSize = (ref: RefObject<HTMLElement>) => { const [size, setSize] = useState({ width: 0, height: 0 }); useEffect(() => { const handleResize = (entries: ResizeObserverEntry[]) => { if (entries[0]) { const { width, height } = entries[0].contentRect; setSize({ width, height }); } }; const resizeObserver = new ResizeObserver(handleResize); if (ref.current) { resizeObserver.observe(ref.current); } return () => { resizeObserver.disconnect(); }; }, [ref.current]); return size; }; export const isStepFileUrl = (json: unknown): json is string => { return ( Boolean(json) && typeof json === 'string' && (json.includes('/api/v1/step-files/') || json.includes('file://')) ); }; export const useTimeAgo = (date: Date) => { const [timeAgo, setTimeAgo] = useState(() => formatUtils.formatDateToAgo(date), ); useEffect(() => { const updateInterval = () => { const now = dayjs(); const inputDate = dayjs(date); const diffInSeconds = now.diff(inputDate, 'second'); // Update every second if less than a minute // Update every minute if less than an hour // Update every hour if less than a day // Update every day if more than a day if (diffInSeconds < 60) return 1000; if (diffInSeconds < 3600) return 60000; if (diffInSeconds < 86400) return 3600000; return 86400000; }; const intervalId = setInterval(() => { setTimeAgo(formatUtils.formatDateToAgo(date)); }, updateInterval()); return () => clearInterval(intervalId); }, [date]); return timeAgo; }; export const determineDefaultRoute = ( checkAccess: (permission: Permission) => boolean, ) => { if (checkAccess(Permission.READ_FLOW)) { return authenticationSession.appendProjectRoutePrefix('/flows'); } if (checkAccess(Permission.READ_RUN)) { return authenticationSession.appendProjectRoutePrefix('/runs'); } if (checkAccess(Permission.READ_ISSUES)) { return authenticationSession.appendProjectRoutePrefix('/issues'); } return authenticationSession.appendProjectRoutePrefix('/settings'); }; export const NEW_FLOW_QUERY_PARAM = 'newFlow'; export const NEW_TABLE_QUERY_PARAM = 'newTable'; export const NEW_MCP_QUERY_PARAM = 'newMcp'; export const parentWindow: Window = window.opener ?? window.parent; export const cleanLeadingSlash = (url: string) => { return url.startsWith('/') ? url.slice(1) : url; }; export const cleanTrailingSlash = (url: string) => { return url.endsWith('/') ? url.slice(0, -1) : url; }; export const combinePaths = ({ firstPath, secondPath, }: { firstPath: string; secondPath: string; }) => { const cleanedFirstPath = cleanTrailingSlash(firstPath); const cleanedSecondPath = cleanLeadingSlash(secondPath); return `${cleanedFirstPath}/${cleanedSecondPath}`; }; const getBlobType = (extension: 'json' | 'txt' | 'csv') => { switch (extension) { case 'csv': return 'text/csv'; case 'json': return 'application/json'; case 'txt': return 'text/plain'; default: return `text/plain`; } }; type downloadFileProps = | { obj: string; fileName: string; extension: 'json' | 'txt' | 'csv'; } | { obj: JSZip; fileName: string; extension: 'zip'; }; export const downloadFile = async ({ obj, fileName, extension, }: downloadFileProps) => { const blob = extension === 'zip' ? await obj.generateAsync({ type: 'blob' }) : //utf-8 with bom new Blob([new Uint8Array([0xef, 0xbb, 0xbf]), obj], { type: getBlobType(extension), }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = `${fileName}.${extension}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; export const wait = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; export const scrollToElementAndClickIt = (elementId: string) => { const element = document.getElementById(elementId); element?.scrollIntoView({ behavior: 'instant', block: 'start', }); element?.click(); };

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