feat(integration): Add Google Sheets integration

This commit is contained in:
Baptiste Arnaud
2022-01-18 18:25:18 +01:00
parent 2814a352b2
commit f49b5143cf
67 changed files with 2560 additions and 391 deletions

View 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>
)
}

View File

@@ -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} />
}
)

View File

@@ -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

View 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>
)
}

View File

@@ -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"
>