feat(integration): ✨ Add Google Sheets integration
This commit is contained in:
104
apps/builder/components/shared/CredentialsDropdown.tsx
Normal file
104
apps/builder/components/shared/CredentialsDropdown.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
Button,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuButtonProps,
|
||||
MenuItem,
|
||||
MenuList,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@chakra-ui/react'
|
||||
import { ChevronLeftIcon, PlusIcon } from 'assets/icons'
|
||||
import React, { useEffect, useMemo } from 'react'
|
||||
import { useUser } from 'contexts/UserContext'
|
||||
import { CredentialsType } from 'db'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
type Props = Omit<MenuButtonProps, 'type'> & {
|
||||
type: CredentialsType
|
||||
currentCredentialsId?: string
|
||||
onCredentialsSelect: (credentialId: string) => void
|
||||
onCreateNewClick: () => void
|
||||
}
|
||||
|
||||
export const CredentialsDropdown = ({
|
||||
type,
|
||||
currentCredentialsId,
|
||||
onCredentialsSelect,
|
||||
onCreateNewClick,
|
||||
...props
|
||||
}: Props) => {
|
||||
const router = useRouter()
|
||||
const { credentials } = useUser()
|
||||
|
||||
const credentialsList = useMemo(() => {
|
||||
return credentials.filter((credential) => credential.type === type)
|
||||
}, [type, credentials])
|
||||
|
||||
const currentCredential = useMemo(
|
||||
() => credentials.find((c) => c.id === currentCredentialsId),
|
||||
[currentCredentialsId, credentials]
|
||||
)
|
||||
|
||||
const handleMenuItemClick = (credentialId: string) => () => {
|
||||
onCredentialsSelect(credentialId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return
|
||||
if (router.query.credentialsId) {
|
||||
handleMenuItemClick(router.query.credentialsId.toString())()
|
||||
clearQueryParams()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [router.isReady])
|
||||
|
||||
const clearQueryParams = () => {
|
||||
const hasQueryParams = router.asPath.includes('?')
|
||||
if (hasQueryParams)
|
||||
router.push(router.asPath.split('?')[0], undefined, { shallow: true })
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
colorScheme="gray"
|
||||
justifyContent="space-between"
|
||||
textAlign="left"
|
||||
{...props}
|
||||
>
|
||||
<Text isTruncated overflowY="visible" h="20px">
|
||||
{currentCredential ? currentCredential.name : 'Select an account'}
|
||||
</Text>
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{credentialsList.map((credentials) => (
|
||||
<MenuItem
|
||||
key={credentials.id}
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
onClick={handleMenuItemClick(credentials.id)}
|
||||
>
|
||||
{credentials.name}
|
||||
</MenuItem>
|
||||
))}
|
||||
<MenuItem
|
||||
maxW="500px"
|
||||
overflow="hidden"
|
||||
whiteSpace="nowrap"
|
||||
textOverflow="ellipsis"
|
||||
icon={<PlusIcon />}
|
||||
onClick={onCreateNewClick}
|
||||
>
|
||||
Connect new
|
||||
</MenuItem>
|
||||
</Stack>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import { Input, InputProps } from '@chakra-ui/react'
|
||||
import { ChangeEvent, useEffect, useState } from 'react'
|
||||
import {
|
||||
ChangeEvent,
|
||||
ForwardedRef,
|
||||
forwardRef,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
|
||||
type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
@@ -8,24 +14,31 @@ type Props = Omit<InputProps, 'onChange' | 'value'> & {
|
||||
onChange: (debouncedValue: string) => void
|
||||
}
|
||||
|
||||
export const DebouncedInput = ({
|
||||
delay,
|
||||
onChange,
|
||||
initialValue,
|
||||
...props
|
||||
}: Props) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
export const DebouncedInput = forwardRef(
|
||||
(
|
||||
{ delay, onChange, initialValue, ...props }: Props,
|
||||
ref: ForwardedRef<HTMLInputElement>
|
||||
) => {
|
||||
const [currentValue, setCurrentValue] = useState(initialValue)
|
||||
const [currentValueDebounced] = useDebounce(currentValue, delay)
|
||||
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
useEffect(() => {
|
||||
if (currentValueDebounced === initialValue) return
|
||||
onChange(currentValueDebounced)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentValueDebounced])
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setCurrentValue(e.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={ref}
|
||||
value={currentValue}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <Input {...props} value={currentValue} onChange={handleChange} />
|
||||
}
|
||||
)
|
||||
|
||||
@@ -11,15 +11,17 @@ import { ChevronLeftIcon } from 'assets/icons'
|
||||
import React from 'react'
|
||||
|
||||
type Props<T> = {
|
||||
currentItem: T
|
||||
currentItem?: T
|
||||
onItemSelect: (item: T) => void
|
||||
items: T[]
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const DropdownList = <T,>({
|
||||
currentItem,
|
||||
onItemSelect,
|
||||
items,
|
||||
placeholder = '',
|
||||
...props
|
||||
}: Props<T> & MenuButtonProps) => {
|
||||
const handleMenuItemClick = (operator: T) => () => {
|
||||
@@ -27,7 +29,7 @@ export const DropdownList = <T,>({
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Menu isLazy placement="bottom-end">
|
||||
<Menu isLazy placement="bottom-end" matchWidth>
|
||||
<MenuButton
|
||||
as={Button}
|
||||
rightIcon={<ChevronLeftIcon transform={'rotate(-90deg)'} />}
|
||||
@@ -37,9 +39,9 @@ export const DropdownList = <T,>({
|
||||
textAlign="left"
|
||||
{...props}
|
||||
>
|
||||
{currentItem}
|
||||
{currentItem ?? placeholder}
|
||||
</MenuButton>
|
||||
<MenuList maxW="500px">
|
||||
<MenuList maxW="500px" shadow="lg">
|
||||
<Stack maxH={'35vh'} overflowY="scroll" spacing="0">
|
||||
{items.map((item) => (
|
||||
<MenuItem
|
||||
|
||||
105
apps/builder/components/shared/InputWithVariable.tsx
Normal file
105
apps/builder/components/shared/InputWithVariable.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
IconButton,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputProps,
|
||||
InputRightElement,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@chakra-ui/react'
|
||||
import { UserIcon } from 'assets/icons'
|
||||
import { Variable } from 'models'
|
||||
import React, { useEffect, useRef, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { VariableSearchInput } from './VariableSearchInput'
|
||||
|
||||
export const InputWithVariable = ({
|
||||
initialValue,
|
||||
noAbsolute,
|
||||
onValueChange,
|
||||
...props
|
||||
}: {
|
||||
initialValue: string
|
||||
onValueChange: (value: string) => void
|
||||
noAbsolute?: boolean
|
||||
} & InputProps) => {
|
||||
const inputRef = useRef<HTMLInputElement | null>(null)
|
||||
const [value, setValue] = useState(initialValue)
|
||||
const [debouncedValue] = useDebounce(value, 100)
|
||||
const [carretPosition, setCarretPosition] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
onValueChange(debouncedValue)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [debouncedValue])
|
||||
|
||||
const handleVariableSelected = (variable: Variable) => {
|
||||
if (!inputRef.current) return
|
||||
const cursorPosition = carretPosition
|
||||
const textBeforeCursorPosition = inputRef.current.value.substring(
|
||||
0,
|
||||
cursorPosition
|
||||
)
|
||||
const textAfterCursorPosition = inputRef.current.value.substring(
|
||||
cursorPosition,
|
||||
inputRef.current.value.length
|
||||
)
|
||||
setValue(
|
||||
textBeforeCursorPosition +
|
||||
`{{${variable.name}}}` +
|
||||
textAfterCursorPosition
|
||||
)
|
||||
inputRef.current.focus()
|
||||
setTimeout(() => {
|
||||
if (!inputRef.current) return
|
||||
inputRef.current.selectionStart = inputRef.current.selectionEnd =
|
||||
carretPosition + `{{${variable.name}}}`.length
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleKeyUp = () => {
|
||||
if (!inputRef.current?.selectionStart) return
|
||||
setCarretPosition(inputRef.current.selectionStart)
|
||||
}
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(e.target.value)
|
||||
|
||||
return (
|
||||
<InputGroup>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
onKeyUp={handleKeyUp}
|
||||
onClick={handleKeyUp}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
{...props}
|
||||
bgColor={'white'}
|
||||
/>
|
||||
<InputRightElement
|
||||
pos={noAbsolute ? 'relative' : 'absolute'}
|
||||
zIndex={noAbsolute ? 'unset' : '1'}
|
||||
>
|
||||
<Popover matchWidth isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton
|
||||
aria-label="Insert a variable"
|
||||
icon={<UserIcon />}
|
||||
size="sm"
|
||||
pos="relative"
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent w="full">
|
||||
<VariableSearchInput
|
||||
onSelectVariable={handleVariableSelected}
|
||||
placeholder="Search for a variable"
|
||||
shadow="lg"
|
||||
isDefaultOpen
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
)
|
||||
}
|
||||
@@ -8,18 +8,21 @@ import {
|
||||
PopoverContent,
|
||||
Button,
|
||||
Text,
|
||||
InputProps,
|
||||
} from '@chakra-ui/react'
|
||||
import { useState, useRef, useEffect, ChangeEvent } from 'react'
|
||||
|
||||
type Props = {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
} & InputProps
|
||||
export const SearchableDropdown = ({
|
||||
selectedItem,
|
||||
items,
|
||||
onSelectItem,
|
||||
}: {
|
||||
selectedItem?: string
|
||||
items: string[]
|
||||
onSelectItem: (value: string) => void
|
||||
}) => {
|
||||
...inputProps
|
||||
}: Props) => {
|
||||
const { onOpen, onClose, isOpen } = useDisclosure()
|
||||
const [inputValue, setInputValue] = useState(selectedItem)
|
||||
const [filteredItems, setFilteredItems] = useState([
|
||||
@@ -64,19 +67,38 @@ export const SearchableDropdown = ({
|
||||
])
|
||||
}
|
||||
|
||||
const handleItemClick = (item: string) => () => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Flex ref={dropdownRef}>
|
||||
<Popover isOpen={isOpen} initialFocusRef={inputRef}>
|
||||
<Flex ref={dropdownRef} w="full">
|
||||
<Popover
|
||||
isOpen={isOpen}
|
||||
initialFocusRef={inputRef}
|
||||
matchWidth
|
||||
offset={[0, 0]}
|
||||
isLazy
|
||||
>
|
||||
<PopoverTrigger>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
value={inputValue}
|
||||
onChange={onInputChange}
|
||||
onClick={onOpen}
|
||||
w="300px"
|
||||
{...inputProps}
|
||||
/>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent maxH="35vh" overflowY="scroll" spacing="0" w="300px">
|
||||
<PopoverContent
|
||||
maxH="35vh"
|
||||
overflowY="scroll"
|
||||
spacing="0"
|
||||
role="menu"
|
||||
w="inherit"
|
||||
shadow="lg"
|
||||
>
|
||||
{filteredItems.length > 0 ? (
|
||||
<>
|
||||
{filteredItems.map((item, idx) => {
|
||||
@@ -84,15 +106,12 @@ export const SearchableDropdown = ({
|
||||
<Button
|
||||
minH="40px"
|
||||
key={idx}
|
||||
onClick={() => {
|
||||
setInputValue(item)
|
||||
onSelectItem(item)
|
||||
onClose()
|
||||
}}
|
||||
onClick={handleItemClick(item)}
|
||||
fontSize="16px"
|
||||
fontWeight="normal"
|
||||
rounded="none"
|
||||
colorScheme="gray"
|
||||
role="menuitem"
|
||||
variant="ghost"
|
||||
justifyContent="flex-start"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user