diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 02abc179c..edfd48a8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,109 +1,3 @@ # Contributing to Typebot -You are considering contributing to Typebot. I thank you for this 🙏. - -Any contributions you make are **greatly appreciated**. It can be anything from typo fixes to new features. - -Let's [discuss](https://github.com/baptisteArno/typebot.io/discussions/new) about what you want to implement before creating a PR if you are unsure about the requirements or the vision of Typebot. - -Typebot is a Monorepo powered by [Turborepo](https://turborepo.org/). It is composed of 2 main applications: - -- the builder ([`./apps/builder`](apps/builder)), where you build your typebots -- the viewer ([`./apps/viewer`](./apps/viewer)), where your user answer the typebot - -These apps are built with awesome web technologies including [Typescript](https://www.typescriptlang.org/), [Next.js](https://nextjs.org/), [Prisma](https://www.prisma.io/), [Chakra UI](https://chakra-ui.com/), [Tailwind CSS](https://tailwindcss.com/). - -## Get started - -1. [Fork](https://help.github.com/articles/fork-a-repo/) this repository to your - own GitHub account and then - [clone](https://help.github.com/articles/cloning-a-repository/) it to your local device. - -2. Create a new branch: - - ```sh - git checkout -b MY_BRANCH_NAME - ``` - -## Running the project locally - -1. Install dependencies - - ```sh - cd typebot.io - pnpm i - ``` - -2. Set up environment variables - - Copy [`.env.dev.example`](./.env.dev.example) to `.env` - - Check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) if you want to enable more options - -3. Make sure you have [Docker](https://docs.docker.com/compose/install/) running - -4. Make sure you have Node.js installed. I suggest you use [`nvm`](https://github.com/nvm-sh/nvm) allowing you to manage different versions. Once you installed nvm, you can install and use the latest version of Node.js: `nvm install && nvm use` - -5. Start the builder and viewer - - ```sh - pnpm dev - ``` - - Builder is available at [`http://localhost:3000`](http://localhost:3000) - - Viewer is available at [`http://localhost:3001`](http://localhost:3001) - - Database inspector is available at [`http://localhost:5555`](http://localhost:5555) - - By default, you can easily authenticate in the builder using the "Github Sign In" button. For other options, check out the [Configuration guide](https://docs.typebot.io/self-hosting/configuration) - -6. (Optionnal) Start the landing page - - Copy [`apps/landing-page/.env.local.example`](apps/landing-page/.env.local.example) to `apps/landing-page/.env.local` - - ```sh - cd apps/landing-page - pnpm dev - ``` - -7. (Optionnal) Start the docs - - ```sh - cd apps/docs - pnpm start - ``` - -I know the project can be a bit hard to understand at first. I'm working on improving the documentation and the codebase to make it easier to contribute. If you have any questions, feel free to [open a discussion](https://github.com/baptisteArno/typebot.io/discussions/new) - -## How to create a new integration block - -The first step to create a new Typebot block is to define its schema. For this you need to - -1. Add your integration in the enum `IntegrationBlockType` in [`packages/schemas/features/blocks/integrations/enums.ts`](packages/schemas/features/blocks/integrations/enums.ts) -2. Create a new file in [`packages/schemas/features/blocks/integrations`](packages/schemas/features/blocks/integrations). - - Your schema should look like: - - ```ts - import { z } from 'zod' - import { blockBaseSchema } from '../baseSchemas' - - export const myIntegrationBlockSchema = blockBaseSchema.merge( - z.object({ - type: z.enum([IntegrationBlockType.MY_INTEGRATION]), - options: z.object({ - //... - }), - }) - ) - - export type MyIntegrationBlock = z.infer - ``` - -3. Add `myIntegrationBlockSchema` to `blockSchema` in `packages/schemas/features/blocks/schemas.ts` - -As soon as you have defined your schema, you can start implementing your block in the builder and the viewer. -Since the code is strictly typed, you should see exactly where you need to add your integration-specific code. - -To sum it up you need to create a folder in [`apps/builder/src/features/blocks/integrations`](apps/builder/src/features/blocks/integrations) and in [`apps/viewer/src/features/blocks/integrations`](apps/viewer/src/features/blocks/integrations) +All the content has been moved [here](https://docs.typebot.io/contribute/overview). ❤️ diff --git a/README.md b/README.md index b77d8a88c..35c5c5faa 100644 --- a/README.md +++ b/README.md @@ -90,24 +90,15 @@ You'll find a lot of resources to help you get started with Typebot in the [docu ## Self-hosting -Interested in self-hosting Typebot on your server? Take a look at the [self-hosting installation instructions](https://docs.typebot.io/self-hosting). +Interested in self-hosting Typebot on your server? Take a look at the [self-hosting installation instructions](https://docs.typebot.io/self-hosting/get-started). ## How to Contribute -You are awesome, lets build great software together. Head over to the [Contribute guidelines](https://github.com/baptisteArno/typebot.io/blob/main/CONTRIBUTING.md) to get started. 💪 - -### Claim these open bounties 👇 - - - - - Bounties of typebot - - +You are awesome, lets build great software together. Head over to the [Contribute docs](https://docs.typebot.io/contribute/overview) to get started. 💪 ## Run the project locally -Follow the [Get started](https://github.com/baptisteArno/typebot.io/blob/main/CONTRIBUTING.md#get-started) section of the Contributing guide. +Follow the [Local installation](https://docs.typebot.io/contribute/guides/local-installation) section of in the Contributing docs. ### Top contributors diff --git a/apps/builder/src/components/DropdownList.tsx b/apps/builder/src/components/DropdownList.tsx index b878bd775..8915035bf 100644 --- a/apps/builder/src/components/DropdownList.tsx +++ b/apps/builder/src/components/DropdownList.tsx @@ -51,7 +51,7 @@ export const DropdownList = ({ isRequired={isRequired} as={direction === 'column' ? Stack : HStack} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={props.width === 'full' || label ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/components/PrimitiveList.tsx b/apps/builder/src/components/PrimitiveList.tsx new file mode 100644 index 000000000..e41da46ef --- /dev/null +++ b/apps/builder/src/components/PrimitiveList.tsx @@ -0,0 +1,135 @@ +import { Box, Button, Fade, Flex, IconButton, Stack } from '@chakra-ui/react' +import { TrashIcon, PlusIcon } from '@/components/icons' +import React, { useEffect, useState } from 'react' +import { createId } from '@paralleldrive/cuid2' + +type ItemWithId = { id: string; value?: T } + +export type TableListItemProps = { + item: T + onItemChange: (item: T) => void +} + +type Props = { + initialItems?: T[] + addLabel?: string + newItemDefaultProps?: Partial + hasDefaultItem?: boolean + ComponentBetweenItems?: (props: unknown) => JSX.Element + onItemsChange: (items: T[]) => void + children: (props: TableListItemProps) => JSX.Element +} + +const addIdToItems = ( + items: T[] +): ItemWithId[] => items.map((item) => ({ id: createId(), value: item })) + +const removeIdFromItems = ( + items: ItemWithId[] +): T[] => items.map((item) => item.value as T) + +export const PrimitiveList = ({ + initialItems, + addLabel = 'Add', + hasDefaultItem, + children, + ComponentBetweenItems, + onItemsChange, +}: Props) => { + const [items, setItems] = useState[]>() + const [showDeleteIndex, setShowDeleteIndex] = useState(null) + + useEffect(() => { + if (items) return + if (initialItems) { + setItems(addIdToItems(initialItems)) + } else if (hasDefaultItem) { + setItems(addIdToItems([])) + } else { + setItems([]) + } + }, [hasDefaultItem, initialItems, items]) + + const createItem = () => { + if (!items) return + const newItems = [...items, { id: createId() }] + setItems(newItems) + onItemsChange(removeIdFromItems(newItems)) + } + + const updateItem = (itemIndex: number, newValue: T) => { + if (!items) return + const newItems = items.map((item, idx) => + idx === itemIndex ? { ...item, value: newValue } : item + ) + setItems(newItems) + onItemsChange(removeIdFromItems(newItems)) + } + + const deleteItem = (itemIndex: number) => () => { + if (!items) return + const newItems = [...items] + newItems.splice(itemIndex, 1) + setItems([...newItems]) + onItemsChange(removeIdFromItems([...newItems])) + } + + const handleMouseEnter = (itemIndex: number) => () => + setShowDeleteIndex(itemIndex) + + const handleCellChange = (itemIndex: number) => (item: T) => + updateItem(itemIndex, item) + + const handleMouseLeave = () => setShowDeleteIndex(null) + + return ( + + {items?.map((item, itemIndex) => ( + + {itemIndex !== 0 && ComponentBetweenItems && ( + + )} + + {children({ + item: item.value as T, + onItemChange: handleCellChange(itemIndex), + })} + + } + aria-label="Remove cell" + onClick={deleteItem(itemIndex)} + size="sm" + shadow="md" + /> + + + + ))} + + + ) +} diff --git a/apps/builder/src/components/inputs/NumberInput.tsx b/apps/builder/src/components/inputs/NumberInput.tsx index e127055ea..82bb2c688 100644 --- a/apps/builder/src/components/inputs/NumberInput.tsx +++ b/apps/builder/src/components/inputs/NumberInput.tsx @@ -109,7 +109,7 @@ export const NumberInput = ({ as={direction === 'column' ? Stack : HStack} isRequired={isRequired} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={label || props.width === 'full' ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/components/inputs/TextInput.tsx b/apps/builder/src/components/inputs/TextInput.tsx index 3c2a40493..e88d9d7af 100644 --- a/apps/builder/src/components/inputs/TextInput.tsx +++ b/apps/builder/src/components/inputs/TextInput.tsx @@ -35,6 +35,7 @@ export type TextInputProps = { placeholder?: string isDisabled?: boolean direction?: 'row' | 'column' + width?: 'full' } & Pick< InputProps, | 'autoComplete' @@ -66,6 +67,7 @@ export const TextInput = forwardRef(function TextInput( size, maxWidth, direction = 'column', + width, }: TextInputProps, ref ) { @@ -141,7 +143,7 @@ export const TextInput = forwardRef(function TextInput( isRequired={isRequired} as={direction === 'column' ? Stack : HStack} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={label || width === 'full' ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/components/inputs/Textarea.tsx b/apps/builder/src/components/inputs/Textarea.tsx index cb8783140..666b39689 100644 --- a/apps/builder/src/components/inputs/Textarea.tsx +++ b/apps/builder/src/components/inputs/Textarea.tsx @@ -28,7 +28,7 @@ type Props = { helperText?: ReactNode onChange: (value: string) => void direction?: 'row' | 'column' -} & Pick +} & Pick export const Textarea = ({ id, @@ -43,6 +43,7 @@ export const Textarea = ({ minH, helperText, direction = 'column', + width, }: Props) => { const inputRef = useRef(null) const [isTouched, setIsTouched] = useState(false) @@ -108,7 +109,7 @@ export const Textarea = ({ isRequired={isRequired} as={direction === 'column' ? Stack : HStack} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={label || width === 'full' ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/components/inputs/VariableSearchInput.tsx b/apps/builder/src/components/inputs/VariableSearchInput.tsx index be1211aea..7c7b4c254 100644 --- a/apps/builder/src/components/inputs/VariableSearchInput.tsx +++ b/apps/builder/src/components/inputs/VariableSearchInput.tsx @@ -46,6 +46,7 @@ type Props = { helperText?: ReactNode moreInfoTooltip?: string direction?: 'row' | 'column' + width?: 'full' } & Omit export const VariableSearchInput = ({ @@ -58,6 +59,7 @@ export const VariableSearchInput = ({ moreInfoTooltip, direction = 'column', isRequired, + width, ...inputProps }: Props) => { const focusedItemBgColor = useColorModeValue('gray.200', 'gray.700') @@ -196,7 +198,7 @@ export const VariableSearchInput = ({ isRequired={isRequired} as={direction === 'column' ? Stack : HStack} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={label || width === 'full' ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/features/forge/components/ForgeSelectInput.tsx b/apps/builder/src/features/forge/components/ForgeSelectInput.tsx index 393f3fe7e..c446ab254 100644 --- a/apps/builder/src/features/forge/components/ForgeSelectInput.tsx +++ b/apps/builder/src/features/forge/components/ForgeSelectInput.tsx @@ -25,6 +25,7 @@ type Props = { moreInfoTooltip?: string direction?: 'row' | 'column' isRequired?: boolean + width?: 'full' onChange: (value: string | undefined) => void } export const ForgeSelectInput = ({ @@ -38,6 +39,7 @@ export const ForgeSelectInput = ({ moreInfoTooltip, isRequired, direction = 'column', + width, onChange, }: Props) => { const { workspace } = useWorkspace() @@ -83,7 +85,7 @@ export const ForgeSelectInput = ({ isRequired={isRequired} as={direction === 'column' ? Stack : HStack} justifyContent="space-between" - width={label ? 'full' : 'auto'} + width={label || width === 'full' ? 'full' : 'auto'} spacing={direction === 'column' ? 2 : 3} > {label && ( diff --git a/apps/builder/src/features/forge/components/zodLayouts/ZodDiscriminatedUnionLayout.tsx b/apps/builder/src/features/forge/components/zodLayouts/ZodDiscriminatedUnionLayout.tsx index 84be75429..bc426db5f 100644 --- a/apps/builder/src/features/forge/components/zodLayouts/ZodDiscriminatedUnionLayout.tsx +++ b/apps/builder/src/features/forge/components/zodLayouts/ZodDiscriminatedUnionLayout.tsx @@ -31,10 +31,8 @@ export const ZodDiscriminatedUnionLayout = ({ currentItem={data?.[discriminant]} onItemSelect={(item) => onDataChange({ ...data, [discriminant]: item })} items={ - [...schema._def.optionsMap.keys()].filter( - (key) => - isDefined(key) && - !schema._def.optionsMap.get(key)?._def.layout?.isHidden + [...schema._def.optionsMap.keys()].filter((key) => + isDefined(key) ) as string[] } placeholder={dropdownPlaceholder} diff --git a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx index 3be21a970..bf9cebb6c 100644 --- a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx +++ b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx @@ -13,12 +13,15 @@ import { AccordionIcon, AccordionItem, AccordionPanel, + FormLabel, Stack, Text, } from '@chakra-ui/react' import { VariableSearchInput } from '@/components/inputs/VariableSearchInput' import { DropdownList } from '@/components/DropdownList' import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas' +import { PrimitiveList } from '@/components/PrimitiveList' +import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' const mdComponents = { a: ({ href, children }) => ( @@ -40,6 +43,8 @@ export const ZodFieldLayout = ({ isInAccordion, blockDef, blockOptions, + width, + propName, onDataChange, }: { data: any @@ -47,6 +52,8 @@ export const ZodFieldLayout = ({ isInAccordion?: boolean blockDef?: ForgedBlockDefinition blockOptions?: ForgedBlock['options'] + width?: 'full' + propName?: string onDataChange: (val: any) => void }) => { const layout = schema._def.layout as ZodLayoutMetadata | undefined @@ -54,7 +61,6 @@ export const ZodFieldLayout = ({ ? schema._def.innerType._def.typeName : schema._def.typeName - if (layout?.isHidden) return null switch (type) { case 'ZodObject': return ( @@ -125,6 +131,7 @@ export const ZodFieldLayout = ({ moreInfoTooltip={layout?.moreInfoTooltip} placeholder={layout?.placeholder} direction={layout?.direction} + width={width} /> ) } @@ -143,6 +150,18 @@ export const ZodFieldLayout = ({ isRequired={layout?.isRequired} moreInfoTooltip={layout?.moreInfoTooltip} onValueChange={onDataChange} + direction={layout?.direction} + width={width} + /> + ) + } + case 'ZodBoolean': { + return ( + ) } @@ -166,10 +185,11 @@ export const ZodFieldLayout = ({ } moreInfoTooltip={layout?.moreInfoTooltip} onChange={onDataChange} + width={width} /> ) } - if (layout?.input === 'variableDropdown') { + if (layout?.inputType === 'variableDropdown') { return ( ) : undefined } + width={width} /> ) } - if (layout?.input === 'textarea') { + if (layout?.inputType === 'textarea') { return (