mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Migrate documentation to Mintlify and configure 301 redirects (#15502)
## Summary Completes the migration of all documentation from twenty-website to a new Mintlify-powered documentation site at docs.twenty.com. ## Changes Made ### New Package: `twenty-docs` - ✅ Created new Mintlify documentation package - ✅ Migrated 95 content pages (user-guide, developers, twenty-ui) - ✅ Migrated 81 images - ✅ Converted all custom components to Mintlify native components - ✅ Configured navigation with 2 tabs and 94 pages - ✅ Added Helper AI Agent with searchArticles tool for docs search ### Updated: `twenty-website` - ✅ Added 11 redirect rules (301 permanent) in next.config.js - ✅ Removed all documentation content (111 files) - ✅ Removed documentation routes (user-guide, developers, twenty-ui) - ✅ Removed documentation components (9 files) - ✅ Updated keystatic.config.ts - ✅ Preserved all marketing/release pages ### Updated: Core Files - ✅ Updated README.md - docs links point to docs.twenty.com - ✅ Updated CONTRIBUTING.md - code quality link updated - ✅ Updated SupportDropdown.tsx - user guide link updated - ✅ Updated Footer.tsx - user guide link updated
This commit is contained in:
parent
5f13e6fcf4
commit
9f97be67b1
186 changed files with 254 additions and 11616 deletions
8
.github/CONTRIBUTING.md
vendored
8
.github/CONTRIBUTING.md
vendored
|
|
@ -1,21 +1,21 @@
|
|||
# Contributing to Twenty
|
||||
|
||||
Thanks for considering contributing to Twenty!
|
||||
Thanks for considering contributing to Twenty!
|
||||
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
Please make sure to go through the [documentation](https://docs.twenty.com) before.
|
||||
|
||||
<br>
|
||||
|
||||
|
||||
## Good first issues
|
||||
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
Good first issues are a great way to start contributing and get familiar with the codebase. You can find them on by filtering on the [good first issue](https://github.com/twentyhq/twenty/labels/good%20first%20issue) label.
|
||||
|
||||
## Issue assignment
|
||||
|
||||
To avoid conflicts, we follow these guidelines:
|
||||
|
||||
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
|
||||
1. For `Good First Issue` and `Experienced Contributor` issues without `size: long` labels, we'll merge the first PRs that meet our [code quality standards](https://docs.twenty.com/developers). **We don't assign contributors to these issues**. For `priority: high` issues, our core team will step in within days if no adequate contributions are received.
|
||||
2. For `size: long` Issues, assigned contributors have one week to submit their first draft PR.
|
||||
|
||||
## How to Contribute
|
||||
|
|
|
|||
18
README.md
18
README.md
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<h2 align="center" >The #1 Open-Source CRM </h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://twenty.com/developers">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<p align="center"><a href="https://twenty.com">🌐 Website</a> · <a href="https://docs.twenty.com">📚 Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/planner-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.png" width="12" height="12"/> Figma</a></p>
|
||||
<br />
|
||||
|
||||
|
||||
|
|
@ -24,11 +24,11 @@
|
|||
|
||||
<br />
|
||||
|
||||
# Installation
|
||||
# Installation
|
||||
|
||||
See:
|
||||
🚀 [Self-hosting](https://twenty.com/developers/section/self-hosting)
|
||||
🖥️ [Local Setup](https://twenty.com/developers/local-setup)
|
||||
See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ We built Twenty for three reasons:
|
|||
|
||||
# What You Can Do With Twenty
|
||||
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
Please feel free to flag any specific needs you have by creating an issue.
|
||||
|
||||
Below are a few features we have implemented to date:
|
||||
|
||||
|
|
@ -131,7 +131,7 @@ Below are a few features we have implemented to date:
|
|||
|
||||
- Star the repo
|
||||
- Subscribe to releases (watch -> custom -> releases)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Follow us on [Twitter](https://twitter.com/twentycrm) or [LinkedIn](https://www.linkedin.com/company/twenty/)
|
||||
- Join our [Discord](https://discord.gg/cx5n4Jzs57)
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
- Improve translations on [Crowdin](https://twenty.crowdin.com/twenty)
|
||||
- [Contributions](https://github.com/twentyhq/twenty/contribute) are, of course, most welcome!
|
||||
|
|
|
|||
94
packages/twenty-docs/README.md
Normal file
94
packages/twenty-docs/README.md
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
# Twenty Documentation
|
||||
|
||||
Official documentation for Twenty CRM, powered by [Mintlify](https://mintlify.com).
|
||||
|
||||
## 🌐 Live Site
|
||||
|
||||
Visit the documentation at [docs.twenty.com](https://docs.twenty.com)
|
||||
|
||||
## 📚 Content
|
||||
|
||||
This repository contains:
|
||||
- **User Guide** (46 pages) - Complete guide for Twenty users
|
||||
- **Developers** (24 pages) - Technical documentation for developers
|
||||
- **Twenty UI** (25 pages) - UI component library documentation
|
||||
|
||||
## 🚀 Local Development
|
||||
|
||||
To run the documentation locally:
|
||||
|
||||
```bash
|
||||
# From the twenty monorepo root
|
||||
npx nx run twenty-docs:dev
|
||||
```
|
||||
|
||||
The documentation will be available at `http://localhost:3000`
|
||||
|
||||
## 📝 Editing Content
|
||||
|
||||
### Adding/Editing Pages
|
||||
|
||||
1. Edit MDX files in the appropriate directory:
|
||||
- `user-guide/` - User documentation
|
||||
- `developers/` - Developer documentation
|
||||
- `twenty-ui/` - Component documentation
|
||||
|
||||
2. Update `docs.json` to add pages to navigation
|
||||
|
||||
### MDX Format
|
||||
|
||||
All documentation pages use MDX format with frontmatter:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: Page Title
|
||||
description: Page description
|
||||
image: /images/path/to/image.png
|
||||
---
|
||||
|
||||
Your content here...
|
||||
```
|
||||
|
||||
### Adding Images
|
||||
|
||||
1. Place images in the `/images/` directory
|
||||
2. Reference them in MDX: ``
|
||||
3. Or use Mintlify Frame component:
|
||||
```mdx
|
||||
<Frame>
|
||||
<img src="/images/your-image.png" alt="Description" />
|
||||
</Frame>
|
||||
```
|
||||
|
||||
## 🔧 Configuration
|
||||
|
||||
- `docs.json` - Main Mintlify configuration (navigation, theme, etc.)
|
||||
- `package.json` - Package dependencies and scripts
|
||||
- `project.json` - Nx workspace configuration
|
||||
|
||||
## 📦 Building
|
||||
|
||||
```bash
|
||||
# Build the documentation
|
||||
npx nx run twenty-docs:build
|
||||
```
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- [Twenty Website](https://twenty.com)
|
||||
- [GitHub Repository](https://github.com/twentyhq/twenty)
|
||||
- [Mintlify Documentation](https://mintlify.com/docs)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
To contribute to the documentation:
|
||||
|
||||
1. Fork the repository
|
||||
2. Make your changes in the `packages/twenty-docs` directory
|
||||
3. Test locally with `npx nx run twenty-docs:dev`
|
||||
4. Submit a pull request
|
||||
|
||||
## 📄 License
|
||||
|
||||
This documentation is part of the Twenty project and is licensed under [AGPL-3.0](../../LICENSE).
|
||||
|
||||
|
|
@ -73,15 +73,15 @@ The project has a clean and simple stack, with minimal boilerplate code.
|
|||
|
||||
### Routing
|
||||
|
||||
[React Router](https://reactrouter.com/) handles the routing.
|
||||
[React Router](https://reactrouter.com/) handles the routing.
|
||||
|
||||
To avoid unnecessary [re-renders](/contributor/frontend/best-practices#managing-re-renders) all the routing logic is in a `useEffect` in `PageChangeEffect`.
|
||||
To avoid unnecessary [re-renders](/developers/frontend-development/best-practices-front#managing-re-renders) all the routing logic is in a `useEffect` in `PageChangeEffect`.
|
||||
|
||||
### State Management
|
||||
|
||||
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management.
|
||||
|
||||
See [best practices](/developers/section/frontend-development/best-practices-front#state-management) for more information on state management.
|
||||
See [best practices](/developers/frontend-development/best-practices-front#state-management) for more information on state management.
|
||||
|
||||
## Testing
|
||||
|
||||
|
|
|
|||
|
|
@ -232,7 +232,7 @@ If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/
|
|||
|
||||
## Step 5: Setup environment variables
|
||||
|
||||
Use environment variables or `.env` files to configure your project. More info [here](https://twenty.com/developers/section/self-hosting/setup)
|
||||
Use environment variables or `.env` files to configure your project. More info [here](https://docs.twenty.com/developers/self-hosting/setup)
|
||||
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
```bash
|
||||
|
|
@ -301,4 +301,4 @@ You can log in using the default demo account: `tim@apple.dev` (password: `tim@a
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions.
|
||||
If you encounter any problem, check [Troubleshooting](https://docs.twenty.com/developers/self-hosting/troubleshooting) for solutions.
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ image: /images/user-guide/objects/objects.png
|
|||
</Frame>
|
||||
|
||||
<Warning>
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](https://twenty.com/developers/local-setup).
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](https://docs.twenty.com/developers/local-setup).
|
||||
</Warning>
|
||||
|
||||
## Overview
|
||||
|
|
@ -16,7 +16,7 @@ This guide provides step-by-step instructions to install and configure the Twent
|
|||
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
See docs [Setup Environment Variables](https://twenty.com/developers/section/self-hosting/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
See docs [Setup Environment Variables](https://docs.twenty.com/developers/self-hosting/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
|
||||
## System Requirements
|
||||
|
||||
|
|
@ -196,6 +196,6 @@ We strongly recommend setting up Twenty behind a reverse proxy with SSL terminat
|
|||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions.
|
||||
If you encounter any problem, check [Troubleshooting](https://docs.twenty.com/developers/self-hosting/troubleshooting) for solutions.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import OptionTable from '@site/src/theme/OptionTable'
|
|||
# Configuration Management
|
||||
|
||||
<Warning>
|
||||
**First time installing?** Follow the [Docker Compose installation guide](https://twenty.com/developers/section/self-hosting/docker-compose) to get Twenty running, then return here for configuration.
|
||||
**First time installing?** Follow the [Docker Compose installation guide](https://docs.twenty.com/developers/self-hosting/docker-compose) to get Twenty running, then return here for configuration.
|
||||
</Warning>
|
||||
|
||||
Twenty offers **two configuration modes** to suit different deployment needs:
|
||||
|
|
@ -27,7 +27,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
|||
**Most configuration happens through the UI** after installation:
|
||||
|
||||
1. Access your Twenty instance (usually `http://localhost:3000`)
|
||||
2. Go to **Settings / Admin Panel / Configuration Variables**
|
||||
2. Go to **Settings / Admin Panel / Configuration Variables**
|
||||
3. Configure integrations, email, storage, and more
|
||||
4. Changes take effect immediately (within 15 seconds for multi-container deployments)
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
|||
|
||||
**What you can configure through the admin panel:**
|
||||
- **Authentication** - Google/Microsoft OAuth, password settings
|
||||
- **Email** - SMTP settings, templates, verification
|
||||
- **Email** - SMTP settings, templates, verification
|
||||
- **Storage** - S3 configuration, local storage paths
|
||||
- **Integrations** - Gmail, Google Calendar, Microsoft services
|
||||
- **Workflow & Rate Limiting** - Execution limits, API throttling
|
||||
|
|
@ -46,7 +46,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
|||

|
||||
|
||||
<Warning>
|
||||
Each variable is documented with descriptions in your admin panel at **Settings → Admin Panel → Configuration Variables**.
|
||||
Each variable is documented with descriptions in your admin panel at **Settings → Admin Panel → Configuration Variables**.
|
||||
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and app secrets (`APP_SECRET`) can only be configured via `.env` file.
|
||||
|
||||
[Complete technical reference →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
|
|
@ -103,7 +103,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
|
|||
**Required scopes** (automatically configured):
|
||||
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
|
||||
- `https://www.googleapis.com/auth/calendar.events`
|
||||
- `https://www.googleapis.com/auth/gmail.readonly`
|
||||
- `https://www.googleapis.com/auth/gmail.readonly`
|
||||
- `https://www.googleapis.com/auth/profile.emails.read`
|
||||
|
||||
### If your app is in test mode
|
||||
|
|
@ -238,4 +238,4 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
|||
|
||||
<Warning>
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</Warning>
|
||||
</Warning>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const MyComponent = () => {
|
|||
|-------|------|-------------|--------|
|
||||
| variant | string | The color scheme variant. Options include `Dark`, `Light`, and `System` | light |
|
||||
| selected | boolean | If `true`, displays a checkmark to indicate the selected color scheme | |
|
||||
| additional props | `React.ComponentPropsWithoutRef<'div'>` | Standard HTML `div` element props |
|
||||
| additional props | `React.ComponentPropsWithoutRef<'div'>` | Standard HTML `div` element props | |
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ image: /images/user-guide/what-is-twenty/20.png
|
|||
<img src="/images/user-guide/what-is-twenty/20.png" alt="Header" />
|
||||
</Frame>
|
||||
|
||||
Allows users to pick a value from a list of predefined options.
|
||||
Allows users to pick a value from a list of predefined options.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Usage">
|
||||
|
|
@ -47,7 +47,7 @@ export const MyComponent = () => {
|
|||
| disabled | boolean | When set to `true`, disables user interaction with the component |
|
||||
| label | string | The label to describe the purpose of the `Select` component |
|
||||
| onChange | function | The function called when the selected values change |
|
||||
| options | array | |
|
||||
| options | array | Represents the options available for the `Selected` component. It's an array of objects where each object has a `value` (the unique identifier), `label` (the unique identifier), and an optional `Icon` |
|
||||
| value | string | Represents the currently selected value. It should match one of the `value` properties in the `options` array |
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ sectionInfo: Centralize communications and team collaboration
|
|||
<img src="/images/user-guide/emails/emails_header.png" alt="Header" />
|
||||
</Frame>
|
||||
|
||||
**Note**: To connect your email accounts and configure sync settings, visit [Email & Calendar Setup](/user-guide/section/settings/email-calendar-setup).
|
||||
**Note**: To connect your email accounts and configure sync settings, visit [Email & Calendar Setup](/user-guide/settings/email-calendar-setup).
|
||||
|
||||
## How Email Integration Works
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ Twenty automatically links emails from your connected mailboxes to the relevant
|
|||
Email conversations appear in three main objects:
|
||||
|
||||
- **People**: View all emails exchanged with a specific contact
|
||||
- **Companies**: See all emails related to a company and its employees
|
||||
- **Companies**: See all emails related to a company and its employees
|
||||
- **Opportunities**: Access email threads related to the company linked to this opportunity. Email threads from individual people on the opportunity are not shown yet.
|
||||
|
||||
### Viewing Email Threads
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ Manage your record-linked notes efficiently using the powerful **Notes** feature
|
|||
- **Team Handoffs**: Share context when transferring accounts between team members
|
||||
|
||||
### Automated Note Creation
|
||||
Use [Workflows](/user-guide/section/workflows/getting-started-workflows) to automatically create notes:
|
||||
Use [Workflows](/user-guide/workflows/getting-started-workflows) to automatically create notes:
|
||||
- **Call Recorder Integration**: Auto-generate meeting summaries from recorded calls
|
||||
- **Deal Handoff Notes**: Auto-create sales cycle summaries when handing off new customers to implementation teams
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ Use [Workflows](/user-guide/section/workflows/getting-started-workflows) to auto
|
|||
### Relations Field
|
||||
Notes include a **Relations** field that allows you to attach a single note to multiple records across different objects. For example, you can link one meeting note to:
|
||||
- The Person you met with
|
||||
- The Company they represent
|
||||
- The Company they represent
|
||||
- The Opportunity being discussed
|
||||
- Any relevant Tasks or other records
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ This morph many relationship ensures important information is accessible from al
|
|||
|
||||
Creating notes in the system is intuitive and dynamic. You can either:
|
||||
|
||||
- Navigate to the notes view and create a new record.
|
||||
- Navigate to the notes view and create a new record.
|
||||
- Go to a `Record page` and select the Notes tab and press the `New note` button.
|
||||
|
||||
<img src="/images/user-guide/notes/add-note.png"style={{width:'100%'}}/>
|
||||
|
|
@ -64,22 +64,22 @@ Start typing directly or press `/` to add elements like headings, files, or imag
|
|||
|
||||
You can format your notes right from the editor. Use Markdown syntax, press the `/` key or click on the `+` icon on the editor to see the different block options, such as headings, tables, and lists. You can also attach images to your note.
|
||||
|
||||
Highlight the text to see more formatting options like bold, italics, and alignment options.
|
||||
Highlight the text to see more formatting options like bold, italics, and alignment options.
|
||||
|
||||
You can also change the background color and text color of each block to highlight important things in your note. To do so, hover over the block you want to format and click on the `⋮` icon besides the `+` icon. Click on `Colors` to open up all color options for both the text and the background.
|
||||
|
||||
<div style={{padding:'69.01% 0 0 0', position:'relative', margin: '32px 0px 0px'}}>
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/927896302?autoplay=1&loop=1&autopause=0&background=1&app_id=58479"
|
||||
frameBorder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture; clipboard-write"
|
||||
<iframe
|
||||
src="https://player.vimeo.com/video/927896302?autoplay=1&loop=1&autopause=0&background=1&app_id=58479"
|
||||
frameBorder="0"
|
||||
allow="autoplay; fullscreen; picture-in-picture; clipboard-write"
|
||||
style={{
|
||||
position:'absolute',
|
||||
top:0,
|
||||
left:0,
|
||||
width:'100%',
|
||||
height:'100%',
|
||||
borderRadius: '16px',
|
||||
borderRadius: '16px',
|
||||
border:'2px solid black'
|
||||
}}
|
||||
title="Export data"
|
||||
|
|
@ -93,7 +93,7 @@ The system displays all your notes linked to a specific record under the Notes s
|
|||
|
||||
## Saving And Deleting
|
||||
|
||||
All edits and additions to the note are automatically saved.
|
||||
All edits and additions to the note are automatically saved.
|
||||
|
||||
To delete a note:
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ Beyond sales and customer success, Tasks support broader organizational needs:
|
|||
- **Operations**: Coordinate facility management, vendor relationships, and process improvements
|
||||
|
||||
### Automated Task Creation
|
||||
Use [Workflows](/user-guide/section/workflows/getting-started-workflows) to automatically create tasks:
|
||||
Use [Workflows](/user-guide/workflows/getting-started-workflows) to automatically create tasks:
|
||||
- **Deal Won Triggers**: Auto-create onboarding tasks assigned to CS team when opportunities close
|
||||
- **Email Reminders**: Set up weekly email reminders for tasks due this week (sent every Monday)
|
||||
- **Pipeline Automation**: Create follow-up tasks when deals stall in specific stages
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ The domain field is particularly important for company identification, and the e
|
|||
|
||||
### CSV imports
|
||||
|
||||
When you have existing data from spreadsheets or other systems, CSV import is your fastest option. You can prepare your data in Excel or Google Sheets, then upload it all at once. This is particularly useful when migrating from another CRM or when someone has been tracking contacts in spreadsheets. Our [Import/Export Data](/user-guide/section/getting-started/import-export-data) guide walks you through the process.
|
||||
When you have existing data from spreadsheets or other systems, CSV import is your fastest option. You can prepare your data in Excel or Google Sheets, then upload it all at once. This is particularly useful when migrating from another CRM or when someone has been tracking contacts in spreadsheets. Our [Import/Export Data](/user-guide/getting-started/import-export-data) guide walks you through the process.
|
||||
|
||||
### Automated data capture
|
||||
|
||||
|
|
@ -30,13 +30,13 @@ For ongoing lead generation, you can set up automated workflows that bring data
|
|||
|
||||
**Integration with other systems**: If you use other business tools, you can connect them to Twenty using API calls and workflows. This lets you automatically sync data between systems: for example, bringing in new customers from your billing system or leads from your marketing platform.
|
||||
|
||||
To learn more about setting up these automated data flows, check out our [Workflows](/user-guide/section/workflows) section.
|
||||
To learn more about setting up these automated data flows, check out our [Workflows](/user-guide/workflows/getting-started-workflows) section.
|
||||
|
||||
### Email and calendar sync
|
||||
|
||||
When you connect your mailbox and calendar to Twenty, the system can automatically create People and Companies records for people you email or meet with. If you send an email to someone who isn't already in your CRM, Twenty can create a new Person record for them. The same happens when you schedule meetings with new contacts through your calendar.
|
||||
|
||||
This is particularly useful for sales and business development teams who are constantly meeting new people. Instead of manually adding every new contact, Twenty captures them automatically as you communicate. Learn how to set this up in our [Emails and Calendars](/user-guide/section/collaboration/emails-and-calendars) guide.
|
||||
This is particularly useful for sales and business development teams who are constantly meeting new people. Instead of manually adding every new contact, Twenty captures them automatically as you communicate. Learn how to set this up in our [Emails and Calendars](/user-guide/collaboration/emails-and-calendars) guide.
|
||||
|
||||
### Reducing manual work
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ Even when adding data manually, you can use workflows to streamline the process.
|
|||
|
||||
Twenty automatically enforces uniqueness to keep your data organized. Each person's email address serves as a unique identifier: you can't have two people with the same email. Similarly, company domains are unique, so you won't accidentally create duplicate companies.
|
||||
|
||||
If your business needs other fields to be unique (like phone numbers, or reference codes), you can configure this in your data model. Head to our [Data Model](/user-guide/section/data-model/customize-your-data-model) section to learn how to set up additional uniqueness constraints for your specific needs.
|
||||
If your business needs other fields to be unique (like phone numbers, or reference codes), you can configure this in your data model. Head to our [Data Model](/user-guide/data-model/customize-your-data-model) section to learn how to set up additional uniqueness constraints for your specific needs.
|
||||
|
||||
### Handling duplicates
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ If you work with large organizations that have subsidiaries or multiple division
|
|||
|
||||
### Customizing your views
|
||||
|
||||
Different team members might need to see different information. You can create custom views that show different columns for different purposes: maybe your sales team needs to see deal stages while your support team focuses on contact details. Learn more about this in our [View Management](/user-guide/section/crm-essentials/view-management) article.
|
||||
Different team members might need to see different information. You can create custom views that show different columns for different purposes: maybe your sales team needs to see deal stages while your support team focuses on contact details. Learn more about this in our [View Management](/user-guide/crm-essentials/view-management) article.
|
||||
|
||||
## Working with records
|
||||
|
||||
|
|
@ -79,15 +79,15 @@ When you open a Person or Company record, you'll see all their information organ
|
|||
- **Emails**: Email threads with this contact (when your team has connected their mailboxes)
|
||||
- **Calendar**: Meetings and appointments with this contact
|
||||
|
||||
The Email and Calendar tabs are particularly powerful: they automatically show all email exchanges and meetings that anyone on your team has had with this contact, as long as they've connected their mailbox to Twenty. You can learn more about setting this up in our [Emails and Calendars](/user-guide/section/collaboration/emails-and-calendars) guide.
|
||||
The Email and Calendar tabs are particularly powerful: they automatically show all email exchanges and meetings that anyone on your team has had with this contact, as long as they've connected their mailbox to Twenty. You can learn more about setting this up in our [Emails and Calendars](/user-guide/collaboration/emails-and-calendars) guide.
|
||||
|
||||
### Adding the fields you need
|
||||
|
||||
The standard fields might not capture everything important for your business. If you need additional information: like customer segments, referral sources, or industry-specific data: you can add custom fields or modify existing ones. Head to our [Data Model](/user-guide/section/data-model/customize-your-data-model) section to learn how to customize your setup.
|
||||
The standard fields might not capture everything important for your business. If you need additional information: like customer segments, referral sources, or industry-specific data: you can add custom fields or modify existing ones. Head to our [Data Model](/user-guide/data-model/customize-your-data-model) section to learn how to customize your setup.
|
||||
|
||||
## Managing deleted records
|
||||
|
||||
When you delete a record in Twenty, it's not gone forever. Records are "soft deleted," which means they're hidden but can be restored if needed.
|
||||
When you delete a record in Twenty, it's not gone forever. Records are "soft deleted," which means they're hidden but can be restored if needed.
|
||||
|
||||
To access deleted records, open the command menu `Cmd+K` on Mac, `Ctrl+K` on Windows, then click `See deleted records`. From there, you can either restore records or permanently delete them if you're sure you don't need them.
|
||||
|
||||
|
|
|
|||
|
|
@ -92,13 +92,13 @@ You can also hide all the fields and get an overview of all the opportunities at
|
|||
|
||||
### Automation with workflows
|
||||
|
||||
Use [Workflows](/user-guide/section/workflows/getting-started-workflows) to automate your pipeline:
|
||||
Use [Workflows](/user-guide/workflows/getting-started-workflows) to automate your pipeline:
|
||||
- **Automatic stage progression**: Move deals based on activities
|
||||
- **Notifications**: Alert team members of stage changes
|
||||
- **Task creation**: Generate follow-up tasks for each stage
|
||||
|
||||
### Multiple pipelines
|
||||
|
||||
You can create different pipelines for various business lines, market segments, or specialized sales teams by creating new views. Each view can show different opportunities with specific filters and stages tailored to your needs. Learn how to create those views in our [View Management](/user-guide/section/crm-essentials/view-management) guide.
|
||||
You can create different pipelines for various business lines, market segments, or specialized sales teams by creating new views. Each view can show different opportunities with specific filters and stages tailored to your needs. Learn how to create those views in our [View Management](/user-guide/crm-essentials/view-management) guide.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,5 +12,5 @@ sectionInfo: "Essential CRM features for managing leads, sales, and customers"
|
|||
|
||||
GTM teams often need advanced sales capabilities like lead scoring, data enrichment, round robin, territory assignment, automated reminders, and email sequences. While these aren't built-in features in Twenty, they can all be configured and tailored to your specific needs using our flexible workflow system.
|
||||
|
||||
Visit our [Workflows section](/user-guide/section/workflows) to learn how to build these automations step by step. For detailed examples, see our [Internal Automations](/user-guide/section/workflows/internal-automations) and [External Tool Integration](/user-guide/section/workflows/external-tool-integration) guides.
|
||||
Visit our [Workflows section](/user-guide/workflows/getting-started-workflows) to learn how to build these automations step by step. For detailed examples, see our [Internal Automations](/user-guide/workflows/internal-automations) and [External Tool Integration](/user-guide/workflows/external-tool-integration) guides.
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ The standard table format that displays records in rows and columns. This is per
|
|||
Organizes your records by grouping them based on a select field. For example, you can group opportunities by stage, companies by locations, or any other select field. This helps you see patterns and organize related records together.
|
||||
|
||||
### Kanban Layout
|
||||
A visual board where each column represents a stage and each record appears as a card. This layout is ideal for managing pipelines and workflows where records move through different stages. For more details on using Kanban views for pipeline management, see our [Pipeline](/user-guide/section/crm-essentials/pipeline) article.
|
||||
A visual board where each column represents a stage and each record appears as a card. This layout is ideal for managing pipelines and workflows where records move through different stages. For more details on using Kanban views for pipeline management, see our [Pipeline](/user-guide/crm-essentials/pipeline) article.
|
||||
|
||||
<div style={{padding:'69.01% 0 0 0', position:'relative', margin: '32px 0px 0px'}}>
|
||||
<iframe
|
||||
|
|
@ -198,5 +198,5 @@ To modify or remove views, open the view dropdown menu and hover over the view y
|
|||
|
||||
Views saved as favorites appear just under Settings in your navigation, giving you instant access to your most important views. Use the view dropdown menu to organize your views by dragging them into the order that works best for your workflow.
|
||||
|
||||
For more advanced data organization, see our [Data Model section](/user-guide/section/data-model) to learn about customizing fields and objects.
|
||||
For more advanced data organization, see our [Data Model section](/user-guide/data-model/customize-your-data-model) to learn about customizing fields and objects.
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,6 @@ You can create as many custom objects and fields as you need - the price won't c
|
|||
|
||||
## Need More Help?
|
||||
|
||||
Check our [implementation services](/user-guide/section/getting-started/implementation-services) to get help with complex data model design.
|
||||
Check our [implementation services](/user-guide/getting-started/implementation-services) to get help with complex data model design.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,6 @@ If you get an error when setting uniqueness, check for duplicate values in your
|
|||
- **Each object has one main display field**: This field appears in the leftmost column and represents the record when linked to other objects. It must be a text field. For example, People uses `Name` as the main field, so when you link a person to a company, you'll see their name in the company's view.
|
||||
|
||||
### Relation Fields
|
||||
- **Connect objects together**: Relation fields link records from different objects. For detailed information on creating and managing relationships, see our [Relation Fields](/user-guide/section/data-model/object-relations) article.
|
||||
- **Connect objects together**: Relation fields link records from different objects. For detailed information on creating and managing relationships, see our [Relation Fields](/user-guide/data-model/relation-fields) article.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -88,6 +88,6 @@ The singular and plural names must be different. This is required for our GraphQ
|
|||
|
||||
<img src="/images/user-guide/objects/customize-fields.png"style={{width:'100%'}}/>
|
||||
|
||||
**Note:** If you're not sure whether a new object or field is needed, check [this article](/user-guide/section/data-model/customize-your-data-model) for guidance on designing your data model.
|
||||
**Note:** If you're not sure whether a new object or field is needed, check [this article](/user-guide/data-model/customize-your-data-model) for guidance on designing your data model.
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ Here are a few tips:
|
|||
- You can deactivate standard fields and objects you do not want to use.
|
||||
- You can hide fields from views: don't be afraid of creating fields, you won't have to display all of them.
|
||||
|
||||
Read [this article](https://twenty.com/user-guide/section/data-model/customize-your-data-model) to learn how to design your data model.
|
||||
Read [this article](https://docs.twenty.com/user-guide/data-model/customize-your-data-model) to learn how to design your data model.
|
||||
|
||||
## 2. Bring your data in
|
||||
Bringing your existing data into Twenty gives your team context from the start.
|
||||
|
|
@ -48,7 +48,7 @@ Use the Command menu (```Cmd + K``` or ```Ctrl + K```) to import People, Compani
|
|||
- Remove duplicate emails for People or duplicate domains for Companies
|
||||
- Review and fix errors (highlighted in yellow) before importing
|
||||
|
||||
Read [this article](/user-guide/section/getting-started/import-export-data) to learn more about data import.
|
||||
Read [this article](/user-guide/getting-started/import-export-data) to learn more about data import.
|
||||
|
||||
## 3. Create your first view
|
||||
Creating different views is key to make the data actionable for your team.
|
||||
|
|
@ -73,5 +73,5 @@ Here is how to proceed:
|
|||
|
||||
|
||||
## What's next?
|
||||
Start creating automations using [workflows](https://twenty.com/user-guide/section/workflows/getting-started-workflows).
|
||||
Start creating automations using [workflows](https://docs.twenty.com/user-guide/workflows/getting-started-workflows).
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ Use the dropdown menu at the top left of the main layout to switch between the d
|
|||
- Save filtered views to reuse them later
|
||||
- Favourite views for fast access
|
||||
|
||||
If you're new to Views, read our [View Management](/user-guide/section/crm-essentials/view-management) article to learn how to create and customize them.
|
||||
If you're new to Views, read our [View Management](/user-guide/crm-essentials/view-management) article to learn how to create and customize them.
|
||||
|
||||
## Settings
|
||||
Open your Settings from the top left to:
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ sectionInfo: A brief guide to grasp the basics of Twenty
|
|||
- **List active workflows** and automations
|
||||
|
||||
### 2. Create Your New Data Model
|
||||
Follow our [data model guide](/user-guide/section/data-model/customize-your-data-model) to:
|
||||
Follow our [data model guide](/user-guide/data-model/customize-your-data-model) to:
|
||||
- **Design the data model** you need
|
||||
- **Map existing fields** to Twenty's standard objects / fields
|
||||
- **Identify the custom objects / fields** that you will need
|
||||
|
|
@ -31,7 +31,7 @@ Follow our [data model guide](/user-guide/section/data-model/customize-your-data
|
|||
2. **People** second (linked to companies)
|
||||
3. **Opportunities** third (linked to people/companies)
|
||||
|
||||
Use the CSV import via the Command Menu `Cmd + K` (Mac) or `Ctrl + K` (Windows). See our [data import guide](/user-guide/section/getting-started/import-export-data) for detailed instructions.
|
||||
Use the CSV import via the Command Menu `Cmd + K` (Mac) or `Ctrl + K` (Windows). See our [data import guide](/user-guide/getting-started/import-export-data) for detailed instructions.
|
||||
|
||||
### 2. Recreate Workflows
|
||||
- **Start simple** - recreate your most critical automations first
|
||||
|
|
@ -55,14 +55,14 @@ To import relations between records using the csv import function, you can use t
|
|||
- **Use email addresses** to link People records
|
||||
- **Use domain names** to link Company records
|
||||
- **Use any other field you set as unique**, which can be done in the Data Model section.
|
||||
Read our [import-export data guide](/user-guide/section/getting-started/import-export-data) for detailed instructions on creating relationships during CSV import.
|
||||
Read our [import-export data guide](/user-guide/getting-started/import-export-data) for detailed instructions on creating relationships during CSV import.
|
||||
|
||||
## Professional Help
|
||||
### Our Services
|
||||
- **4-hour onboarding packs** for guided migration
|
||||
- **Implementation partners** for more advanced projects
|
||||
|
||||
Discover our [implementation services](/user-guide/section/getting-started/implementation-services).
|
||||
Discover our [implementation services](/user-guide/getting-started/implementation-services).
|
||||
|
||||
## Migrating from Self-Hosted to Cloud
|
||||
|
||||
|
|
|
|||
|
|
@ -34,27 +34,27 @@ Open-source is the bedrock of our approach, ensuring that Twenty evolves with it
|
|||
|
||||
## Main Features
|
||||
|
||||
**Contact Management:** Efficiently store and manage customer data. [Learn more](/user-guide/section/crm-essentials/contact-and-account-management).
|
||||
**Contact Management:** Efficiently store and manage customer data. [Learn more](/user-guide/crm-essentials/contact-and-account-management).
|
||||
|
||||
**Custom Objects:** Create and customize objects to fit your business needs. [Details](/user-guide/section/data-model/objects).
|
||||
**Custom Objects:** Create and customize objects to fit your business needs. [Details](/user-guide/data-model/objects).
|
||||
|
||||
**Custom Fields:** Tailor data fields to capture and organize information specific to your operations. [Understand more](/user-guide/section/data-model/fields).
|
||||
**Custom Fields:** Tailor data fields to capture and organize information specific to your operations. [Understand more](/user-guide/data-model/fields).
|
||||
|
||||
**Deal Management:** Track and manage your sales opportunities through customizable [Pipeline stages](/user-guide/section/crm-essentials/pipeline).
|
||||
**Deal Management:** Track and manage your sales opportunities through customizable [Pipeline stages](/user-guide/crm-essentials/pipeline).
|
||||
|
||||
**Kanban & Table Views:** Make data actionable with [flexible table views](/user-guide/section/crm-essentials/view-management).
|
||||
**Kanban & Table Views:** Make data actionable with [flexible table views](/user-guide/crm-essentials/view-management).
|
||||
|
||||
**Workflows:** Automate your business processes and integrate with external tools using powerful workflow automation. [Get started](/user-guide/section/workflows/getting-started-workflows).
|
||||
**Workflows:** Automate your business processes and integrate with external tools using powerful workflow automation. [Get started](/user-guide/workflows/getting-started-workflows).
|
||||
|
||||
**Email Integration:** View the emails of a specific customer or company within your workspace. [Synchronize your mailbox](/user-guide/section/collaboration/emails-and-calendars).
|
||||
**Email Integration:** View the emails of a specific customer or company within your workspace. [Synchronize your mailbox](/user-guide/collaboration/emails-and-calendars).
|
||||
|
||||
**Notes:** Create detailed notes for each record to share knowledge more effectively. [Add notes](/user-guide/section/collaboration/notes).
|
||||
**Notes:** Create detailed notes for each record to share knowledge more effectively. [Add notes](/user-guide/collaboration/notes).
|
||||
|
||||
**Tasks:** Schedule tasks to track customer interactions. [See how](/user-guide/section/collaboration/tasks).
|
||||
**Tasks:** Schedule tasks to track customer interactions. [See how](/user-guide/collaboration/tasks).
|
||||
|
||||
**Permissions:** Control access and manage user roles with flexible workspace and object-level permissions. [Configure permissions](/user-guide/section/settings/permissions).
|
||||
**Permissions:** Control access and manage user roles with flexible workspace and object-level permissions. [Configure permissions](/user-guide/settings/permissions).
|
||||
|
||||
**API & Webhooks:** Connect to other apps and automate workflows with API and Webhooks. [Start integrating](/user-guide/section/integrations-api/api-webhooks).
|
||||
**API & Webhooks:** Connect to other apps and automate workflows with API and Webhooks. [Start integrating](/user-guide/integrations-api/api-webhooks).
|
||||
|
||||
|
||||
## Join now
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ Since your API key gives access to sensitive information, you shouldn't share it
|
|||
|
||||
Webhooks allow for immediate updates to your specified URL about changes or events related to your customer data.
|
||||
|
||||
For example, when an Opportunity moves to "Closed Won", a webhook can automatically trigger invoice creation in your accounting system. Note that this type of automation can also be achieved using Twenty's in-app [Workflows feature](/user-guide/section/workflows/getting-started-workflows), which offers triggers based on field updates for internal automation.
|
||||
For example, when an Opportunity moves to "Closed Won", a webhook can automatically trigger invoice creation in your accounting system. Note that this type of automation can also be achieved using Twenty's in-app [Workflows feature](/user-guide/workflows/getting-started-workflows), which offers triggers based on field updates for internal automation.
|
||||
|
||||
Webhooks are ideal for integrating with external systems, while Workflows support both internal automation and external tool connections via webhook triggers, code nodes, and HTTP nodes.
|
||||
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ Your documentation is unique to your workspace because it reflects your custom o
|
|||
|
||||
## Next Steps
|
||||
|
||||
- **[API & Webhooks Setup](/user-guide/section/integrations-api/api-webhooks)**: Learn how to create API keys and webhooks
|
||||
- **[API & Webhooks Setup](/user-guide/integrations-api/api-webhooks)**: Learn how to create API keys and webhooks
|
||||
- **Custom Documentation**: Access your personalized API docs via Settings → API & Webhooks
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ Twenty currently offers native integration with:
|
|||
- **Email & Calendar**: Connect Gmail, Outlook, or SMTP/CalDAV providers
|
||||
- **API Access**: Use our REST and GraphQL APIs to build custom integrations
|
||||
|
||||
For email and calendar setup, visit [Email & Calendar Setup](/user-guide/section/settings/email-calendar-setup).
|
||||
For email and calendar setup, visit [Email & Calendar Setup](/user-guide/settings/email-calendar-setup).
|
||||
|
||||
### Workflows (Recommended)
|
||||
The primary way to connect Twenty to other tools is through our in-app **Workflows** feature:
|
||||
|
|
@ -24,7 +24,7 @@ The primary way to connect Twenty to other tools is through our in-app **Workflo
|
|||
- **Webhook Triggers**: Receive data from external systems
|
||||
- **Field Update Triggers**: Automate actions based on CRM changes
|
||||
|
||||
Learn more in our [Workflows section](/user-guide/section/workflows/getting-started-workflows).
|
||||
Learn more in our [Workflows section](/user-guide/workflows/getting-started-workflows).
|
||||
|
||||
### Zapier Integration (Legacy)
|
||||
We maintain a Zapier integration for users who prefer no-code automation:
|
||||
|
|
|
|||
|
|
@ -34,6 +34,6 @@ sectionInfo: Configure your Twenty workspace settings and preferences
|
|||
|
||||
## Need Help?
|
||||
|
||||
For role and permission management, check the [Permissions article](/user-guide/section/settings/permissions).
|
||||
For role and permission management, check the [Permissions article](/user-guide/settings/permissions).
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ Note: If a role is deleted, any workspace member assigned to it will be automati
|
|||
3. Select which role new members should automatically receive
|
||||
4. New workspace members will be assigned this role when they join
|
||||
|
||||
**Note**: You can only assign roles to existing workspace members. To invite new members, use [Member Management](/user-guide/section/settings/member-management).
|
||||
**Note**: You can only assign roles to existing workspace members. To invite new members, use [Member Management](/user-guide/settings/member-management).
|
||||
|
||||
## Customize Permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ Access experimental features before they're officially released:
|
|||
2. View release notes and access the **Lab** tab for beta features
|
||||
|
||||
|
||||
For more community resources, check our [Resources section](/user-guide/section/resources).
|
||||
For more community resources, check our [Resources section](/user-guide/resources/glossary).
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -190,8 +190,8 @@ Below are workflow examples you could roll out to connect Twenty with the rest o
|
|||
- Use HTTPS for all external API calls
|
||||
- Be mindful of API rate limits - use scheduled workflows when possible
|
||||
- Consider batch updates "On a Schedule" when real-time processing isn't required
|
||||
- Remember the 100 concurrent workflow limit per workspace - use "Bulk" availability for manual triggers when processing multiple records (see [Workflow Features](/user-guide/section/workflows/workflow-features) for details)
|
||||
- Remember the 100 concurrent workflow limit per workspace - use "Bulk" availability for manual triggers when processing multiple records (see [Workflow Features](/user-guide/workflows/workflow-features) for details)
|
||||
- Test with sample data before activating workflows
|
||||
|
||||
For troubleshooting integration issues, see our [Workflow Troubleshooting](/user-guide/section/workflows/workflow-troubleshooting) guide. For help implementing complex integrations, consider our [Professional Services](/user-guide/section/workflows/professional-services).
|
||||
For troubleshooting integration issues, see our [Workflow Troubleshooting](/user-guide/workflows/workflow-troubleshooting) guide. For help implementing complex integrations, consider our [Professional Services](/user-guide/workflows/professional-services).
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ Examples of what you can automate internally:
|
|||
- **Sales processes**: Stage-based updates, churn management, stale opportunity alerts
|
||||
- **Productivity**: Weekly task reminders, meeting follow-ups, cross-object field synchronization
|
||||
|
||||
For detailed examples and step-by-step guidance, see our [Internal Automations](/user-guide/section/workflows/internal-automations) guide.
|
||||
For detailed examples and step-by-step guidance, see our [Internal Automations](/user-guide/workflows/internal-automations) guide.
|
||||
|
||||
|
||||
## 2. External Integrations
|
||||
|
|
@ -43,10 +43,10 @@ Examples of what you can integrate:
|
|||
- **Data ingestion**: Webform submissions, product data sync, meeting notes, data enrichment
|
||||
- **Data distribution**: Newsletter management, email sequences, lead scoring, invoice generation
|
||||
|
||||
For detailed integration patterns and implementation guidance, see our [External Tool Integration](/user-guide/section/workflows/external-tool-integration) guide.
|
||||
For detailed integration patterns and implementation guidance, see our [External Tool Integration](/user-guide/workflows/external-tool-integration) guide.
|
||||
|
||||
### What if I don't want to build those connections?
|
||||
We offer [Professional Services](/user-guide/section/workflows/professional-services) to help you create the automations you need.
|
||||
We offer [Professional Services](/user-guide/workflows/professional-services) to help you create the automations you need.
|
||||
Depending on the scope of your project, we will suggest an [Onboarding pack](https://twenty.com/onboarding-packages) or we will put you in contact with our certified implementation partners. They can create your data model, migrate your data, build your workflows.
|
||||
|
||||
### I'm not sure the connection I need is feasible
|
||||
|
|
|
|||
|
|
@ -146,6 +146,6 @@ Below are workflow examples you could roll out to automate repetitive tasks to r
|
|||
- Update Record to add country code if missing
|
||||
|
||||
|
||||
For more complex automation needs, consider our [Professional Services](/user-guide/section/workflows/professional-services) or explore [External Tool Integration](/user-guide/section/workflows/external-tool-integration).
|
||||
For more complex automation needs, consider our [Professional Services](/user-guide/workflows/professional-services) or explore [External Tool Integration](/user-guide/workflows/external-tool-integration).
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ If you need more credits beyond your plan allocation:
|
|||
- **Minimize HTTP Requests**: Use efficient search criteria and combine operations where possible
|
||||
- **Batch Processing**: Use bulk operations and Iterator actions efficiently
|
||||
- **Error Handling**: Implement proper error handling to prevent unnecessary retries
|
||||
- **Manual Trigger Optimization**: For manual triggers, choose `Bulk` availability to process multiple records in a single workflow run (see [Workflow Features](/user-guide/section/workflows/workflow-features) for details)
|
||||
- **Manual Trigger Optimization**: For manual triggers, choose `Bulk` availability to process multiple records in a single workflow run (see [Workflow Features](/user-guide/workflows/workflow-features) for details)
|
||||
|
||||
### Action Optimization
|
||||
- Use basic internal operations when possible (lower credit consumption)
|
||||
|
|
|
|||
|
|
@ -269,5 +269,5 @@ A **Run** is a record of workflow execution containing:
|
|||
- **Update Management**: Test changes in draft before activation
|
||||
- **Team Coordination**: Document workflows for team members
|
||||
|
||||
For practical examples of these features in action, see our [Internal Automations](/user-guide/section/workflows/internal-automations) and [External Tool Integration](/user-guide/section/workflows/external-tool-integration) guides.
|
||||
For practical examples of these features in action, see our [Internal Automations](/user-guide/workflows/internal-automations) and [External Tool Integration](/user-guide/workflows/external-tool-integration) guides.
|
||||
|
||||
|
|
|
|||
|
|
@ -103,12 +103,12 @@ If you don't see the Workflows section, this is due to a permissions issue. Cont
|
|||
## Getting Help
|
||||
|
||||
### Self-Service Resources
|
||||
- Review [Workflow Features](/user-guide/section/workflows/workflow-features) for technical details
|
||||
- Check [Workflow Credits](/user-guide/section/workflows/workflow-credits) for optimization tips
|
||||
- Explore [Internal Automations](/user-guide/section/workflows/internal-automations) and [External Tool Integration](/user-guide/section/workflows/external-tool-integration) for examples
|
||||
- Review [Workflow Features](/user-guide/workflows/workflow-features) for technical details
|
||||
- Check [Workflow Credits](/user-guide/workflows/workflow-credits) for optimization tips
|
||||
- Explore [Internal Automations](/user-guide/workflows/internal-automations) and [External Tool Integration](/user-guide/workflows/external-tool-integration) for examples
|
||||
|
||||
### Professional Support
|
||||
- Contact our [Professional Services](/user-guide/section/workflows/professional-services) for complex troubleshooting
|
||||
- Contact our [Professional Services](/user-guide/workflows/professional-services) for complex troubleshooting
|
||||
- Reach out to support via contact@twenty.com for technical assistance
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const Footer = ({ i18n }: FooterProps) => {
|
|||
<Column>
|
||||
<ShadowText>
|
||||
<Link
|
||||
href="https://twenty.com/user-guide"
|
||||
href="https://docs.twenty.com/user-guide/introduction"
|
||||
value={i18n._('User guide')}
|
||||
aria-label={i18n._("Read Twenty's user guide")}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const SupportDropdown = () => {
|
|||
};
|
||||
|
||||
const handleUserGuide = () => {
|
||||
window.open('https://twenty.com/user-guide', '_blank');
|
||||
window.open('https://docs.twenty.com/user-guide/introduction', '_blank');
|
||||
closeDropdown(dropdownId);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { collection, config, fields } from '@keystatic/core';
|
||||
import { wrapper } from '@keystatic/core/content-components';
|
||||
|
||||
export default config({
|
||||
storage: {
|
||||
|
|
@ -11,27 +10,6 @@ export default config({
|
|||
pathPrefix: 'packages/twenty-website',
|
||||
},
|
||||
collections: {
|
||||
developers: collection({
|
||||
label: 'Technical documentation',
|
||||
slugField: 'title',
|
||||
path: 'src/content/developers/**',
|
||||
format: { contentField: 'content' },
|
||||
schema: {
|
||||
title: fields.slug({ name: { label: 'Title' } }),
|
||||
icon: fields.text({ label: 'Icon' }),
|
||||
image: fields.text({ label: 'Image' }),
|
||||
info: fields.text({ label: 'Info' }),
|
||||
content: fields.mdx({
|
||||
label: 'Content',
|
||||
components: {
|
||||
ArticleEditContent: wrapper({
|
||||
label: 'ArticleEditContent',
|
||||
schema: {},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
releases: collection({
|
||||
label: 'Releases',
|
||||
slugField: 'release',
|
||||
|
|
|
|||
|
|
@ -9,6 +9,67 @@ const nextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
{
|
||||
source: '/user-guide',
|
||||
destination: 'https://docs.twenty.com/user-guide/introduction',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/user-guide/section/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/user-guide/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/user-guide/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/user-guide/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
{
|
||||
source: '/developers',
|
||||
destination: 'https://docs.twenty.com/developers/introduction',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/developers/section/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/developers/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/developers/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/developers/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/developers/:slug',
|
||||
destination: 'https://docs.twenty.com/developers/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
|
||||
{
|
||||
source: '/twenty-ui',
|
||||
destination: 'https://docs.twenty.com/twenty-ui/introduction',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/twenty-ui/section/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/twenty-ui/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/twenty-ui/:folder/:slug*',
|
||||
destination: 'https://docs.twenty.com/twenty-ui/:folder/:slug*',
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: '/twenty-ui/:slug',
|
||||
destination: 'https://docs.twenty.com/twenty-ui/:slug',
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const formattedSlug = formatSlug(slug);
|
||||
const basePath = '/src/content/developers';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DocsSlug(props: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await props.params;
|
||||
const basePath = '/src/content/developers';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { type ReactNode } from 'react';
|
||||
|
||||
import { DocsMainLayout } from '@/app/_components/docs/DocsMainLayout';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export default function DocsLayout({ children }: { children: ReactNode }) {
|
||||
const filePath = 'src/content/developers/';
|
||||
const getAllArticles = true;
|
||||
const docsIndex = getDocsArticles(filePath, getAllArticles);
|
||||
return <DocsMainLayout docsIndex={docsIndex}>{children}</DocsMainLayout>;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Twenty - Docs',
|
||||
description: 'Twenty is a CRM designed to fit your unique business needs.',
|
||||
icons: '/images/core/logo.svg',
|
||||
};
|
||||
export default async function DocsHome() {
|
||||
const filePath = 'src/content/developers/';
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
|
||||
return <DocsMain docsArticleCards={docsArticleCards} />;
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ folder: string; documentation: string }> }): Promise<Metadata> {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/developers/${folder}`;
|
||||
const formattedSlug = formatSlug(documentation);
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DocsSlug(props: { params: Promise<{ folder: string; documentation: string }> }) {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/developers/${folder}`;
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ folder: string }> }): Promise<Metadata> {
|
||||
const { folder } = await props.params;
|
||||
const formattedSlug = formatSlug(folder);
|
||||
const basePath = '/src/content/developers';
|
||||
const mainPost = await fetchArticleFromSlug(folder, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function DocsSlug(props: { params: Promise<{ folder: string }> }) {
|
||||
const { folder } = await props.params;
|
||||
const filePath = `src/content/developers/${folder}/`;
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
const isSection = true;
|
||||
const hasOnlyEmptySections = docsArticleCards.every(
|
||||
(article) => article.topic === 'Empty Section',
|
||||
);
|
||||
if (!docsArticleCards || hasOnlyEmptySections) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsMain docsArticleCards={docsArticleCards} isSection={isSection} />;
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata(props: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const formattedSlug = formatSlug(slug);
|
||||
const basePath = '/src/content/twenty-ui';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function TwentyUISlug(props: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await props.params;
|
||||
const basePath = '/src/content/twenty-ui';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return mainPost && <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { type ReactNode } from 'react';
|
||||
|
||||
import { DocsMainLayout } from '@/app/_components/docs/DocsMainLayout';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export default function TwentyUILayout({ children }: { children: ReactNode }) {
|
||||
const filePath = 'src/content/twenty-ui/';
|
||||
const getAllArticles = true;
|
||||
const docsIndex = getDocsArticles(filePath, getAllArticles);
|
||||
return <DocsMainLayout docsIndex={docsIndex}>{children}</DocsMainLayout>;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Twenty - Twenty UI',
|
||||
description: 'Twenty is a CRM designed to fit your unique business needs.',
|
||||
icons: '/images/core/logo.svg',
|
||||
};
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export default async function TwentyUIHome() {
|
||||
const filePath = 'src/content/twenty-ui/';
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
|
||||
return <DocsMain docsArticleCards={docsArticleCards} />;
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ folder: string; documentation: string }> },
|
||||
): Promise<Metadata> {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/twenty-ui/${folder}`;
|
||||
const formattedSlug = formatSlug(documentation);
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function TwentyUISlug(
|
||||
props: { params: Promise<{ folder: string; documentation: string }> },
|
||||
) {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/twenty-ui/${folder}`;
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return mainPost && <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ folder: string }> },
|
||||
): Promise<Metadata> {
|
||||
const { folder } = await props.params;
|
||||
const formattedSlug = formatSlug(folder);
|
||||
const basePath = '/src/content/twenty-ui';
|
||||
const mainPost = await fetchArticleFromSlug(folder, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function TwentyUISlug(
|
||||
props: { params: Promise<{ folder: string }> },
|
||||
) {
|
||||
const { folder } = await props.params;
|
||||
const filePath = `src/content/twenty-ui/${folder}/`;
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
const isSection = true;
|
||||
const hasOnlyEmptySections = docsArticleCards.every(
|
||||
(article) => article.topic === 'Empty Section',
|
||||
);
|
||||
if (!docsArticleCards || hasOnlyEmptySections) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsMain docsArticleCards={docsArticleCards} isSection={isSection} />;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ slug: string }> },
|
||||
): Promise<Metadata> {
|
||||
const { slug } = await props.params;
|
||||
const formattedSlug = formatSlug(slug);
|
||||
const basePath = '/src/content/user-guide';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function UserGuideSlug(
|
||||
props: { params: Promise<{ slug: string }> },
|
||||
) {
|
||||
const { slug } = await props.params;
|
||||
const basePath = '/src/content/user-guide';
|
||||
const mainPost = await fetchArticleFromSlug(slug, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,122 +0,0 @@
|
|||
.DocSearch-Hit-source{
|
||||
color: #1c1e21;
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
color: #1c1e21;
|
||||
}
|
||||
|
||||
.DocSearch-Logo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.DocSearch-Footer{
|
||||
flex-direction: row;
|
||||
box-shadow: none;
|
||||
border: 1px solid #14141414;
|
||||
}
|
||||
|
||||
.DocSearch-Form {
|
||||
box-shadow: none;
|
||||
border: 1px solid #141414;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.DocSearch-Modal {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.DocSearch-Hits {
|
||||
width: 100%;
|
||||
margin-bottom: 2px !important;
|
||||
}
|
||||
|
||||
.DocSearch-Hit[aria-selected=true] mark {
|
||||
color: #1961ED !important;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit a {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hits mark {
|
||||
background-color: #E8EFFD;
|
||||
color: #1961ED;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action h2{
|
||||
font-size: .9em;
|
||||
margin: 0 0 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action p{
|
||||
font-size: 12px;
|
||||
margin: 0;
|
||||
width: 90%;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-family: var(--font-inter);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.DocSearch-Button {
|
||||
margin: 0px;
|
||||
min-height: 36px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #14141414;
|
||||
}
|
||||
|
||||
.DocSearch-Button:hover {
|
||||
color: #B3B3B3;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Button-Placeholder{
|
||||
color: #B3B3B3;
|
||||
}
|
||||
|
||||
.DocSearch-Search-Icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
color: #B3B3B3 !important;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-source {
|
||||
background: none;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-inter);
|
||||
}
|
||||
|
||||
.DocSearch-Button-Placeholder {
|
||||
font-weight: 500;
|
||||
font-family: var(--font-gabarito);
|
||||
}
|
||||
|
||||
.DocSearch-Button-Keys {
|
||||
display: none
|
||||
}
|
||||
|
||||
:root {
|
||||
--docsearch-primary-color: #1c1e21;
|
||||
--docsearch-highlight-color: #1414140F;
|
||||
--docsearch-hit-active-color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.anchor {
|
||||
scroll-margin-top: calc(80px);
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export function useHeadsObserver(location: string) {
|
||||
const [activeId, setActiveId] = useState('');
|
||||
const observer = useRef<IntersectionObserver | null>(null);
|
||||
useEffect(() => {
|
||||
const handleObsever = (entries: any[]) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry?.isIntersecting) {
|
||||
setActiveId(entry.target.id);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
observer.current = new IntersectionObserver(handleObsever, {
|
||||
rootMargin: '0% 0% -85% 0px',
|
||||
});
|
||||
|
||||
const elements = document.querySelectorAll('h2, h3, h4, h5');
|
||||
elements.forEach((elem) => observer.current?.observe(elem));
|
||||
return () => observer.current?.disconnect();
|
||||
}, [location]);
|
||||
|
||||
return { activeId };
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { type ReactNode } from 'react';
|
||||
|
||||
import { DocsMainLayout } from '@/app/_components/docs/DocsMainLayout';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export default function UserGuideLayout({ children }: { children: ReactNode }) {
|
||||
const filePath = 'src/content/user-guide/';
|
||||
const getAllArticles = true;
|
||||
const docsIndex = getDocsArticles(filePath, getAllArticles);
|
||||
return <DocsMainLayout docsIndex={docsIndex}>{children}</DocsMainLayout>;
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
|
||||
export const metadata = {
|
||||
title: 'Twenty - User Guide',
|
||||
description:
|
||||
'Discover how to use Twenty CRM effectively with our detailed user guide. Explore ways to customize features, manage tasks, integrate emails, and navigate the system with ease.',
|
||||
icons: '/images/core/logo.svg',
|
||||
};
|
||||
|
||||
export default async function UserGuideHome() {
|
||||
const filePath = 'src/content/user-guide/';
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
|
||||
return <DocsMain docsArticleCards={docsArticleCards} />;
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsContent from '@/app/_components/docs/DocsContent';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ folder: string; documentation: string }> },
|
||||
): Promise<Metadata> {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/user-guide/${folder}`;
|
||||
const formattedSlug = formatSlug(documentation);
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function UserGuideSlug(
|
||||
props: { params: Promise<{ folder: string; documentation: string }> },
|
||||
) {
|
||||
const { folder, documentation } = await props.params;
|
||||
const basePath = `/src/content/user-guide/${folder}`;
|
||||
const mainPost = await fetchArticleFromSlug(documentation, basePath);
|
||||
if (!mainPost) {
|
||||
notFound();
|
||||
}
|
||||
return mainPost && <DocsContent item={mainPost} />;
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { type Metadata } from 'next';
|
||||
import { notFound } from 'next/navigation';
|
||||
|
||||
import DocsMain from '@/app/_components/docs/DocsMain';
|
||||
import { getDocsArticles } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { fetchArticleFromSlug } from '@/shared-utils/fetchArticleFromSlug';
|
||||
import { formatSlug } from '@/shared-utils/formatSlug';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ folder: string }> },
|
||||
): Promise<Metadata> {
|
||||
const { folder } = await props.params;
|
||||
const formattedSlug = formatSlug(folder);
|
||||
const basePath = '/src/content/user-guide';
|
||||
const mainPost = await fetchArticleFromSlug(folder, basePath);
|
||||
return {
|
||||
title: 'Twenty - ' + formattedSlug,
|
||||
description: mainPost?.itemInfo?.info,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function UserGuideSlug(
|
||||
props: { params: Promise<{ folder: string }> },
|
||||
) {
|
||||
const { folder } = await props.params;
|
||||
const filePath = `src/content/user-guide/${folder}/`;
|
||||
const docsArticleCards = getDocsArticles(filePath);
|
||||
const isSection = true;
|
||||
const hasOnlyEmptySections = docsArticleCards.every(
|
||||
(article) => article.topic === 'Empty Section',
|
||||
);
|
||||
if (!docsArticleCards || hasOnlyEmptySections) {
|
||||
notFound();
|
||||
}
|
||||
return <DocsMain docsArticleCards={docsArticleCards} isSection={isSection} />;
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { DocSearch } from '@docsearch/react';
|
||||
import { env } from 'next-runtime-env';
|
||||
|
||||
interface AlgoliaDocSearchProps {
|
||||
pathname: string;
|
||||
}
|
||||
|
||||
export const AlgoliaDocSearch = ({ pathname }: AlgoliaDocSearchProps) => {
|
||||
const indexName = pathname.includes('user-guide')
|
||||
? 'user-guide'
|
||||
: 'developer-docs';
|
||||
return (
|
||||
<DocSearch
|
||||
hitComponent={({ hit }: { hit: any }) => (
|
||||
<section className="DocSearch-Hits">
|
||||
<a href={hit.url}>
|
||||
<div className="DocSearch-Hit-Container">
|
||||
<div className="DocSearch-Hit-icon">
|
||||
<svg width="20" height="20" viewBox="0 0 20 20">
|
||||
<path
|
||||
d="M13 13h4-4V8H7v5h6v4-4H7V8H3h4V3v5h6V3v5h4-4v5zm-6 0v4-4H3h4z"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="DocSearch-Hit-action">
|
||||
<h2>
|
||||
{hit.hierarchy.lvl1 ? hit.hierarchy.lvl1 : hit.hierarchy.lvl0}
|
||||
</h2>
|
||||
<p
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: hit?._snippetResult?.content?.value || '',
|
||||
}}
|
||||
></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</section>
|
||||
)}
|
||||
appId={env('NEXT_PUBLIC_ALGOLIA_APP_ID') ?? ''}
|
||||
apiKey={env('NEXT_PUBLIC_ALGOLIA_API_KEY') ?? ''}
|
||||
indexName={`twenty-${indexName}`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import dynamic from 'next/dynamic';
|
||||
|
||||
type ClientOnlyProps = { children: any };
|
||||
|
||||
const ClientOnly = (props: ClientOnlyProps) => {
|
||||
const { children } = props;
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default dynamic(() => Promise.resolve(ClientOnly), {
|
||||
ssr: false,
|
||||
});
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { getCardPath } from '@/shared-utils/getCardPath';
|
||||
|
||||
const StyledContainer = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: ${Theme.border.color.plain};
|
||||
border: 2px solid ${Theme.border.color.plain};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
gap: ${Theme.spacing(4)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: -8px 8px 0px -4px ${Theme.color.gray60};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
font-size: ${Theme.font.size.lg};
|
||||
color: ${Theme.text.color.primary};
|
||||
padding: 0 16px;
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
@media (max-width: 800px) {
|
||||
font-size: ${Theme.font.size.base};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.div`
|
||||
font-size: ${Theme.font.size.xs};
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-family: ${Theme.font.family};
|
||||
margin: 0 16px 24px;
|
||||
font-weight: ${Theme.font.weight.regular};
|
||||
line-height: 21px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
`;
|
||||
|
||||
const StyledImage = styled.img`
|
||||
border-bottom: 1.5px solid #14141429;
|
||||
height: 160px;
|
||||
border-top-right-radius: 6px;
|
||||
border-top-left-radius: 6px;
|
||||
`;
|
||||
|
||||
export default function DocsCard({
|
||||
card,
|
||||
isSection = false,
|
||||
}: {
|
||||
card: DocsArticlesProps;
|
||||
isSection?: boolean;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const path = getCardPath(card, pathname, isSection);
|
||||
|
||||
if (card.title) {
|
||||
return (
|
||||
<StyledContainer href={path}>
|
||||
<StyledImage src={card.image} alt={card.title} />
|
||||
<StyledHeading>{card.title}</StyledHeading>
|
||||
<StyledSubHeading>{card.info}</StyledSubHeading>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,170 +0,0 @@
|
|||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import Image from 'next/image';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { ArticleContent } from '@/app/_components/ui/layout/articles/ArticleContent';
|
||||
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type FileContent } from '@/app/_server-utils/get-posts';
|
||||
import { getUriAndLabel } from '@/shared-utils/pathUtils';
|
||||
|
||||
const StyledContainer = styled('div')`
|
||||
${mq({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
fontFamily: `${Theme.font.family}`,
|
||||
})};
|
||||
width: 100%;
|
||||
min-height: calc(100vh - 50px);
|
||||
|
||||
@media (min-width: 990px) {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
@media (max-width: 450px) {
|
||||
width: 100%;
|
||||
padding: ${Theme.spacing(10)} 32px ${Theme.spacing(20)};
|
||||
}
|
||||
|
||||
@media (min-width: 451px) and (max-width: 800px) {
|
||||
padding: ${Theme.spacing(10)} 50px ${Theme.spacing(20)};
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
@media (min-width: 801px) and (max-width: 1500px) {
|
||||
max-width: 720px;
|
||||
min-width: calc(100% - 184px);
|
||||
margin: ${Theme.spacing(10)} 92px ${Theme.spacing(20)};
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
max-width: 720px;
|
||||
margin: ${Theme.spacing(10)} auto ${Theme.spacing(20)};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(8)};
|
||||
@media (min-width: 450px) and (max-width: 800px) {
|
||||
width: 340px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.h1`
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
font-family: var(--font-gabarito);
|
||||
margin: 0px;
|
||||
@media (max-width: 800px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSection = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionTitle = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
padding: ${Theme.spacing(2)} 0px;
|
||||
color: ${Theme.text.color.secondary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
font-family: var(--font-gabarito);
|
||||
`;
|
||||
|
||||
const StyledHeaderInfoSectionSub = styled.p`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${Theme.spacing(4)};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
line-height: 1.8;
|
||||
margin: 0px;
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div`
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: ${Theme.background.transparent.medium};
|
||||
`;
|
||||
|
||||
const StyledImageContainer = styled.div<{ loaded: string }>`
|
||||
position: relative;
|
||||
width: 100%;
|
||||
padding-top: 46%;
|
||||
overflow: hidden;
|
||||
border-radius: 16px;
|
||||
border: 2px solid rgba(20, 20, 20, 0.08);
|
||||
background: #fafafa;
|
||||
transition: border-color 150ms ease-in-out;
|
||||
${({ loaded }) =>
|
||||
loaded === 'true' &&
|
||||
`border-color: ${Theme.text.color.primary};
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledImage = styled(Image)<{ loaded: string }>`
|
||||
opacity: ${({ loaded }) => (loaded === 'true' ? 1 : 0)};
|
||||
transition: opacity 250ms ease-in-out;
|
||||
`;
|
||||
|
||||
export default function DocsContent({ item }: { item: FileContent }) {
|
||||
const pathname = usePathname();
|
||||
const { uri, label } = getUriAndLabel(pathname);
|
||||
const [imageLoaded, setImageLoaded] = useState(false);
|
||||
|
||||
const BREADCRUMB_ITEMS = [
|
||||
{
|
||||
uri: uri,
|
||||
label: label,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledWrapper>
|
||||
<StyledHeader>
|
||||
<Breadcrumbs
|
||||
items={BREADCRUMB_ITEMS}
|
||||
activePage={item.itemInfo.title}
|
||||
separator="/"
|
||||
/>
|
||||
<StyledHeading>{item.itemInfo.title}</StyledHeading>
|
||||
<StyledImageContainer loaded={imageLoaded.toString()}>
|
||||
{item.itemInfo.image && (
|
||||
<StyledImage
|
||||
id={`img-${item.itemInfo.title}`}
|
||||
src={item.itemInfo.image}
|
||||
alt={item.itemInfo.title}
|
||||
fill
|
||||
style={{ objectFit: 'cover' }}
|
||||
onLoad={() => setImageLoaded(true)}
|
||||
loaded={imageLoaded.toString()}
|
||||
unoptimized
|
||||
/>
|
||||
)}
|
||||
</StyledImageContainer>
|
||||
<StyledHeaderInfoSection>
|
||||
<StyledHeaderInfoSectionTitle>
|
||||
In this article
|
||||
</StyledHeaderInfoSectionTitle>
|
||||
<StyledHeaderInfoSectionSub>
|
||||
{item.itemInfo.info}
|
||||
</StyledHeaderInfoSectionSub>
|
||||
</StyledHeaderInfoSection>
|
||||
<StyledRectangle />
|
||||
</StyledHeader>
|
||||
<ArticleContent>{item.content}</ArticleContent>
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import DocsCard from '@/app/_components/docs/DocsCard';
|
||||
import { Breadcrumbs } from '@/app/_components/ui/layout/Breadcrumbs';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { constructSections } from '@/shared-utils/constructSections';
|
||||
import { filterDocsIndex } from '@/shared-utils/filterDocsIndex';
|
||||
import { getUriAndLabel } from '@/shared-utils/pathUtils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${mq({
|
||||
width: ['100%', '60%', '60%'],
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
})};
|
||||
@media (min-width: 1500px) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
padding: ${Theme.spacing(10)} 92px ${Theme.spacing(20)};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 450px) {
|
||||
padding: ${Theme.spacing(10)} 32px ${Theme.spacing(20)};
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) and (max-width: 800px) {
|
||||
padding: ${Theme.spacing(10)} 50px ${Theme.spacing(20)};
|
||||
align-items: flex-start;
|
||||
width: 440px;
|
||||
}
|
||||
|
||||
@media (min-width: 1500px) {
|
||||
width: 720px;
|
||||
padding: ${Theme.spacing(10)} 0px ${Theme.spacing(20)};
|
||||
margin-right: 300px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 450px) and (max-width: 800px) {
|
||||
width: 340px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSection = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
@media (min-width: 801px) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0px;
|
||||
width: 100%;
|
||||
@media (min-width: 450px) and (max-width: 1200px) {
|
||||
width: 340px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
@media (min-width: 450px) and (max-width: 800px) {
|
||||
margin-bottom: 24px;
|
||||
width: 340px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.h1`
|
||||
line-height: 38px;
|
||||
font-weight: 700;
|
||||
font-size: 40px;
|
||||
color: ${Theme.text.color.primary};
|
||||
margin: 0px;
|
||||
margin-top: 32px;
|
||||
@media (max-width: 800px) {
|
||||
font-size: 28px;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSubHeading = styled.h1`
|
||||
line-height: 28.8px;
|
||||
font-size: ${Theme.font.size.lg};
|
||||
font-weight: ${Theme.font.weight.regular};
|
||||
color: ${Theme.text.color.tertiary};
|
||||
@media (max-width: 800px) {
|
||||
font-size: ${Theme.font.size.sm};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledContent = styled.div`
|
||||
${mq({
|
||||
width: '100%',
|
||||
paddingTop: `${Theme.spacing(6)}`,
|
||||
display: ['flex', 'flex', 'grid'],
|
||||
flexDirection: 'column',
|
||||
gridTemplateRows: 'auto auto',
|
||||
gridTemplateColumns: 'auto auto',
|
||||
gap: `${Theme.spacing(6)}`,
|
||||
})};
|
||||
@media (min-width: 450px) {
|
||||
justify-content: flex-start;
|
||||
width: 340px;
|
||||
}
|
||||
`;
|
||||
|
||||
interface DocsProps {
|
||||
docsArticleCards: DocsArticlesProps[];
|
||||
isSection?: boolean;
|
||||
}
|
||||
|
||||
export default function DocsMain({
|
||||
docsArticleCards,
|
||||
isSection = false,
|
||||
}: DocsProps) {
|
||||
const sections = constructSections(docsArticleCards, isSection);
|
||||
const pathname = usePathname();
|
||||
const { uri, label } = getUriAndLabel(pathname);
|
||||
|
||||
const BREADCRUMB_ITEMS = [
|
||||
{
|
||||
uri: uri,
|
||||
label: label,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledWrapper>
|
||||
{isSection ? (
|
||||
<Breadcrumbs
|
||||
items={BREADCRUMB_ITEMS}
|
||||
activePage={sections[0].name}
|
||||
separator="/"
|
||||
/>
|
||||
) : (
|
||||
<StyledTitle>{label}</StyledTitle>
|
||||
)}
|
||||
{sections.map((section, index) => {
|
||||
const filteredArticles = isSection
|
||||
? docsArticleCards
|
||||
: filterDocsIndex(docsArticleCards, section.name);
|
||||
return (
|
||||
<StyledSection key={index}>
|
||||
<StyledHeader>
|
||||
<StyledHeading>{section.name}</StyledHeading>
|
||||
<StyledSubHeading>{section.info}</StyledSubHeading>
|
||||
</StyledHeader>
|
||||
<StyledContent>
|
||||
{filteredArticles.map((card) => (
|
||||
<DocsCard
|
||||
key={card.title}
|
||||
card={card}
|
||||
isSection={isSection}
|
||||
/>
|
||||
))}
|
||||
</StyledContent>
|
||||
</StyledSection>
|
||||
);
|
||||
})}
|
||||
</StyledWrapper>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
'use client';
|
||||
import { type ReactNode } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
import DocsSidebar from '@/app/_components/docs/DocsSideBar';
|
||||
import DocsTableContents from '@/app/_components/docs/TableContent';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import {
|
||||
isPlaygroundPage,
|
||||
shouldShowEmptySidebar,
|
||||
} from '@/shared-utils/pathUtils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid ${Theme.background.transparent.medium};
|
||||
min-height: calc(100vh - 50px);
|
||||
`;
|
||||
|
||||
const StyledEmptySideBar = styled.div`
|
||||
${mq({
|
||||
width: '20%',
|
||||
display: ['none', 'none', ''],
|
||||
})};
|
||||
`;
|
||||
|
||||
export const DocsMainLayout = ({
|
||||
children,
|
||||
docsIndex,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
docsIndex: DocsArticlesProps[];
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{!isPlaygroundPage(pathname) && <DocsSidebar docsIndex={docsIndex} />}
|
||||
{children}
|
||||
{shouldShowEmptySidebar(pathname) ? (
|
||||
<StyledEmptySideBar />
|
||||
) : (
|
||||
<DocsTableContents />
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import { AlgoliaDocSearch } from '@/app/_components/docs/AlgoliaDocSearch';
|
||||
import DocsSidebarSection from '@/app/_components/docs/DocsSidebarSection';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { getSectionIcon } from '@/shared-utils/getSectionIcons';
|
||||
|
||||
import '@docsearch/css';
|
||||
import '../../(public)/user-guide/algolia.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${mq({
|
||||
display: ['none', 'flex', 'flex'],
|
||||
flexDirection: 'column',
|
||||
background: `${Theme.background.secondary}`,
|
||||
borderRight: `1px solid ${Theme.background.transparent.medium}`,
|
||||
padding: `${Theme.spacing(10)} ${Theme.spacing(4)}`,
|
||||
gap: `${Theme.spacing(6)}`,
|
||||
})}
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
overflow: scroll;
|
||||
overflow-x: hidden;
|
||||
height: calc(100vh - 60px);
|
||||
position: sticky;
|
||||
top: 64px;
|
||||
`;
|
||||
|
||||
const StyledHeading = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.secondary};
|
||||
border: 1px solid ${Theme.text.color.secondary};
|
||||
border-radius: ${Theme.border.radius.sm};
|
||||
padding: ${Theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledHeadingText = styled.h1`
|
||||
cursor: pointer;
|
||||
font-size: ${Theme.font.size.sm};
|
||||
font-weight: ${Theme.font.weight.medium};
|
||||
color: ${Theme.text.color.secondary};
|
||||
`;
|
||||
|
||||
const DocsSidebar = ({ docsIndex }: { docsIndex: DocsArticlesProps[] }) => {
|
||||
const router = useRouter();
|
||||
const pathName = usePathname();
|
||||
const path = pathName.includes('user-guide')
|
||||
? '/user-guide'
|
||||
: pathName.includes('developers')
|
||||
? '/developers'
|
||||
: '/twenty-ui';
|
||||
|
||||
const sections = Array.from(
|
||||
new Set(docsIndex.map((guide) => guide.section)),
|
||||
).map((section) => ({
|
||||
name: section,
|
||||
icon: getSectionIcon(section),
|
||||
guides: docsIndex.filter((guide) => {
|
||||
const isInSection = guide.section === section;
|
||||
const hasFiles = guide.numberOfFiles > 0;
|
||||
const isNotSingleFileTopic = !(
|
||||
guide.numberOfFiles > 1 && guide.topic === guide.title
|
||||
);
|
||||
|
||||
return isInSection && hasFiles && isNotSingleFileTopic;
|
||||
}),
|
||||
}));
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<AlgoliaDocSearch pathname={pathName} />
|
||||
{sections.map((section) => (
|
||||
<div key={section.name}>
|
||||
<StyledHeading>
|
||||
<StyledIconContainer>{section.icon}</StyledIconContainer>
|
||||
<StyledHeadingText
|
||||
onClick={() =>
|
||||
router.push(
|
||||
section.name === 'User Guide'
|
||||
? '/user-guide'
|
||||
: section.name === 'Developers'
|
||||
? '/developers'
|
||||
: path,
|
||||
)
|
||||
}
|
||||
>
|
||||
{section.name}
|
||||
</StyledHeadingText>
|
||||
</StyledHeading>
|
||||
<DocsSidebarSection docsIndex={section.guides} />
|
||||
</div>
|
||||
))}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsSidebar;
|
||||
|
|
@ -1,214 +0,0 @@
|
|||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconPoint } from '@tabler/icons-react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
|
||||
import { IconChevronDown, IconChevronRight } from '@/app/_components/ui/icons';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
import { type DocsArticlesProps } from '@/content/user-guide/constants/getDocsArticles';
|
||||
import { groupArticlesByTopic } from '@/content/user-guide/constants/groupArticlesByTopic';
|
||||
import { getCardPath } from '@/shared-utils/getCardPath';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledIndex = styled.div`
|
||||
margin-bottom: 8px;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: ${Theme.spacing(2)};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
margin-top: 8px;
|
||||
padding-bottom: ${Theme.spacing(2)};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const StyledSubTopicItem = styled.a<{ isselected: boolean }>`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: ${Theme.spacing(8)};
|
||||
color: ${(props) =>
|
||||
props.isselected ? Theme.text.color.primary : Theme.text.color.secondary};
|
||||
font-weight: ${(props) =>
|
||||
props.isselected ? Theme.font.weight.medium : Theme.font.weight.regular};
|
||||
font-family: ${Theme.font.family};
|
||||
font-size: ${Theme.font.size.xs};
|
||||
gap: 19px;
|
||||
padding: ${(props) =>
|
||||
props.isselected ? '6px 12px 6px 11px' : '0px 12px 0px 11px'};
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.background.transparent.light
|
||||
: Theme.background.secondary};
|
||||
border-radius: ${Theme.border.radius.md};
|
||||
text-decoration: none;
|
||||
&:focus,
|
||||
&:hover,
|
||||
&:visited,
|
||||
&:link,
|
||||
&:active {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #1414140a;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: #1414140f;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIcon = styled.div`
|
||||
padding: 0px 4px 0px 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
margin-top: 3px;
|
||||
margin-left: -8px;
|
||||
color: ${Theme.color.gray30};
|
||||
`;
|
||||
|
||||
const StyledCardTitle = styled.p`
|
||||
margin: 0px -5px;
|
||||
color: ${Theme.color.gray30};
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
const StyledRectangle = styled.div<{ isselected: boolean; isHovered: boolean }>`
|
||||
height: ${(props) =>
|
||||
props.isselected ? '95%' : props.isHovered ? '70%' : '100%'};
|
||||
width: 2px;
|
||||
background: ${(props) =>
|
||||
props.isselected
|
||||
? Theme.border.color.plain
|
||||
: props.isHovered
|
||||
? Theme.background.transparent.strong
|
||||
: Theme.background.transparent.light};
|
||||
transition: height 0.2s ease-in-out;
|
||||
`;
|
||||
|
||||
interface TopicsState {
|
||||
[topic: string]: boolean;
|
||||
}
|
||||
|
||||
const DocsSidebarSection = ({
|
||||
docsIndex,
|
||||
}: {
|
||||
docsIndex: DocsArticlesProps[];
|
||||
}) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const topics = groupArticlesByTopic(docsIndex);
|
||||
const [hoveredItem, setHoveredItem] = useState<string | null>(null);
|
||||
const path = pathname.includes('user-guide')
|
||||
? '/user-guide/'
|
||||
: pathname.includes('developers')
|
||||
? '/developers/'
|
||||
: '/twenty-ui/';
|
||||
|
||||
const initializeUnfoldedState = () => {
|
||||
const unfoldedState: TopicsState = {};
|
||||
Object.keys(topics).forEach((topic) => {
|
||||
const containsCurrentArticle = topics[topic].some((card) => {
|
||||
const topicPath = card.topic.toLowerCase().replace(/\s+/g, '-');
|
||||
return pathname.includes(topicPath);
|
||||
});
|
||||
unfoldedState[topic] = containsCurrentArticle;
|
||||
});
|
||||
return unfoldedState;
|
||||
};
|
||||
|
||||
const [unfolded, setUnfolded] = useState<TopicsState>(
|
||||
initializeUnfoldedState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newUnfoldedState = initializeUnfoldedState();
|
||||
setUnfolded(newUnfoldedState);
|
||||
}, [pathname]);
|
||||
|
||||
const toggleFold = (topic: string) => {
|
||||
setUnfolded((prev: TopicsState) => ({ ...prev, [topic]: !prev[topic] }));
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{Object.entries(topics).map(([topic, cards]) => {
|
||||
const hasMultipleFiles = cards.some((card) => card.numberOfFiles > 1);
|
||||
|
||||
return (
|
||||
<StyledIndex key={topic}>
|
||||
{hasMultipleFiles ? (
|
||||
<StyledTitle onClick={() => toggleFold(topic)}>
|
||||
{unfolded[topic] ? (
|
||||
<StyledIcon>
|
||||
<IconChevronDown size={Theme.icon.size.md} />
|
||||
</StyledIcon>
|
||||
) : (
|
||||
<StyledIcon>
|
||||
<IconChevronRight size={Theme.icon.size.md} />
|
||||
</StyledIcon>
|
||||
)}
|
||||
<div>{topic}</div>
|
||||
</StyledTitle>
|
||||
) : null}
|
||||
|
||||
{(unfolded[topic] || !hasMultipleFiles) &&
|
||||
cards.map((card) => {
|
||||
const sectionName = card.topic
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-');
|
||||
const routerPath = getCardPath(card, path, false, sectionName);
|
||||
const isselected = pathname === routerPath;
|
||||
return (
|
||||
<StyledSubTopicItem
|
||||
key={card.title}
|
||||
isselected={isselected}
|
||||
href={routerPath}
|
||||
onClick={() => router.push(routerPath)}
|
||||
onMouseEnter={() => setHoveredItem(card.title)}
|
||||
onMouseLeave={() => setHoveredItem(null)}
|
||||
>
|
||||
{card.numberOfFiles > 1 ? (
|
||||
<>
|
||||
<StyledRectangle
|
||||
isselected={isselected}
|
||||
isHovered={hoveredItem === card.title}
|
||||
/>
|
||||
{card.title}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<StyledIconContainer>
|
||||
<IconPoint size={Theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<StyledCardTitle>{card.title}</StyledCardTitle>
|
||||
</>
|
||||
)}
|
||||
</StyledSubTopicItem>
|
||||
);
|
||||
})}
|
||||
</StyledIndex>
|
||||
);
|
||||
})}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsSidebarSection;
|
||||
|
|
@ -1,161 +0,0 @@
|
|||
'use client';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useHeadsObserver } from '@/app/(public)/user-guide/hooks/useHeadsObserver';
|
||||
import ClientOnly from '@/app/_components/docs/ClientOnly';
|
||||
import mq from '@/app/_components/ui/theme/mq';
|
||||
import { Theme } from '@/app/_components/ui/theme/theme';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
${mq({
|
||||
display: ['none', 'none', 'flex'],
|
||||
flexDirection: 'column',
|
||||
background: `${Theme.background.secondary}`,
|
||||
borderLeft: `1px solid ${Theme.background.transparent.medium}`,
|
||||
padding: `0px ${Theme.spacing(6)}`,
|
||||
})};
|
||||
width: 300px;
|
||||
min-width: 300px;
|
||||
`;
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
width: 220px;
|
||||
min-width: 220px;
|
||||
align-self: flex-start;
|
||||
padding: 32px 0px;
|
||||
position: -webkit-sticky;
|
||||
position: sticky;
|
||||
top: 70px;
|
||||
max-height: calc(100vh - 70px);
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const StyledUnorderedList = styled.ul`
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledList = styled.li`
|
||||
margin: 12px 0px;
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
text-decoration: none;
|
||||
font-size: 12px;
|
||||
font-family: var(--font-inter);
|
||||
color: ${Theme.text.color.tertiary};
|
||||
&:hover {
|
||||
color: ${Theme.text.color.secondary};
|
||||
}
|
||||
&:active {
|
||||
color: ${Theme.text.color.primary};
|
||||
font-weight: 500 !important;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledHeadingText = styled.div`
|
||||
font-size: ${Theme.font.size.sm};
|
||||
color: ${Theme.text.color.quarternary};
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const getStyledHeading = (level: number) => {
|
||||
switch (level) {
|
||||
case 3:
|
||||
return {
|
||||
marginLeft: 10,
|
||||
};
|
||||
case 4:
|
||||
return {
|
||||
marginLeft: 20,
|
||||
};
|
||||
case 5:
|
||||
return {
|
||||
marginLeft: 30,
|
||||
};
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
interface HeadingType {
|
||||
id: string;
|
||||
elem: HTMLElement;
|
||||
className: string;
|
||||
text: string;
|
||||
level: number;
|
||||
}
|
||||
|
||||
const DocsTableContents = () => {
|
||||
const [headings, setHeadings] = useState<HeadingType[]>([]);
|
||||
const pathname = usePathname();
|
||||
const { activeId } = useHeadsObserver(pathname);
|
||||
|
||||
useEffect(() => {
|
||||
const nodes: HTMLElement[] = Array.from(
|
||||
document.querySelectorAll('h2, h3, h4, h5'),
|
||||
).filter((elem) => (elem as HTMLElement).id !== 'edit') as HTMLElement[];
|
||||
|
||||
const elements: HeadingType[] = nodes.map(
|
||||
(elem): HeadingType => ({
|
||||
id: elem.id,
|
||||
elem: elem,
|
||||
className: elem.className,
|
||||
text: elem.innerText,
|
||||
level: Number(elem.nodeName.charAt(1)),
|
||||
}),
|
||||
);
|
||||
setHeadings(elements);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledNav>
|
||||
<StyledHeadingText>Table of Content</StyledHeadingText>
|
||||
|
||||
<ClientOnly>
|
||||
{!!headings?.length && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<StyledUnorderedList>
|
||||
{headings.map((heading) => (
|
||||
<StyledList
|
||||
key={heading.id}
|
||||
style={getStyledHeading(heading.level)}
|
||||
>
|
||||
<StyledLink
|
||||
href={`#${heading.id}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const yOffset = -70;
|
||||
const y =
|
||||
heading.elem.getBoundingClientRect().top +
|
||||
window.scrollY +
|
||||
yOffset;
|
||||
|
||||
window.scrollTo({ top: y, behavior: 'smooth' });
|
||||
}}
|
||||
style={{
|
||||
fontWeight: activeId === heading.id ? 'bold' : 'normal',
|
||||
}}
|
||||
>
|
||||
{heading.text}
|
||||
</StyledLink>
|
||||
</StyledList>
|
||||
))}
|
||||
</StyledUnorderedList>
|
||||
</motion.div>
|
||||
)}
|
||||
</ClientOnly>
|
||||
</StyledNav>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default DocsTableContents;
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
---
|
||||
title: API
|
||||
icon: IconApi
|
||||
image: /images/docs/getting-started/api.png
|
||||
info: Discover how to use our APIs.
|
||||
---
|
||||
|
||||
## Overview
|
||||
The Twenty API allows developers to interact programmatically with the Twenty CRM platform. Using the API, you can integrate Twenty with other systems, automate data synchronization, and build custom solutions around your customer data. The API provides endpoints to **create, read, update, and delete** core CRM objects (such as people and companies) as well as access metadata configuration.
|
||||
|
||||
**API Playground:** You can now access the API Playground within the app's settings. To try out API calls in real-time, log in to your Twenty workspace and navigate to **Settings → APIs & Webhooks**. This opens the in-app API Playground and the settings for API keys.
|
||||
**[Go to API Settings](https://app.twenty.com/settings)**
|
||||
|
||||
## Authentication
|
||||
Twenty’s API uses API keys for authentication. Every request to protected endpoints must include an API key in the header.
|
||||
|
||||
* **API Keys:** You can generate a new API key from your Twenty app’s **API settings** page. Each API key is a secret token that grants access to your CRM data, so keep it safe. If a key is compromised, revoke it from the settings and generate a new one.
|
||||
* **Auth Header:** Once you have an API key, include it in the `Authorization` header of your HTTP requests. Use the Bearer token scheme. For example:
|
||||
```
|
||||
Authorization: Bearer YOUR_API_KEY
|
||||
```
|
||||
|
||||
Replace `YOUR_API_KEY` with the key you obtained. This header must be present on **all API requests**. If the token is missing or invalid, the API will respond with an authentication error (HTTP 401 Unauthorized).
|
||||
|
||||
## API Endpoints
|
||||
All resources can be accessed and via REST or GraphQL.
|
||||
|
||||
* **Cloud:** `https://api.twenty.com/` or your custom domain / sub-domain
|
||||
* **Self-Hosted Instances:** If you are running Twenty on your own server, use your own domain in place of `api.twenty.com` (for example, `https://<your-domain>/rest/`).
|
||||
|
||||
Endpoints are grouped into two categories: **Core API** and **Metadata API**. The **Core API** deals with primary CRM data (e.g. people, companies, notes, tasks), while the **Metadata API** covers configuration data (like custom fields or object definitions). Most integrations will primarily use the Core API.
|
||||
|
||||
### Core API
|
||||
Accessed on `/rest/` or `/graphql/`.
|
||||
The **Core API** serves as a unified interface for managing core CRM entities (people, companies, notes, tasks) and their relationships, offering **both REST and GraphQL** interaction models.
|
||||
|
||||
### Metadata API
|
||||
Accessed on `/rest/metadata/` or `/metadata/`.
|
||||
The Metadata API endpoints allow you to retrieve information about your schema and settings. For instance, you can fetch definitions of custom fields, object schemas, etc.
|
||||
|
||||
* **Example Endpoints:**
|
||||
|
||||
* `GET /rest/metadata/objects` – List all object types and their metadata (fields, relationships).
|
||||
* `GET /rest/metadata/objects/{objectName}` – Get metadata for a specific object (e.g., `people`, `companies`).
|
||||
* `GET /rest/metadata/picklists` – Retrieve picklist (dropdown) field options defined in the CRM.
|
||||
|
||||
Typically, the metadata endpoints are used to understand the structure of data (for dynamic integrations or form-building) rather than to manage actual records. They are read-only in most cases. Authentication is required for these as well (use your API key).
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
---
|
||||
title: Webhooks
|
||||
icon: IconApi
|
||||
image: /images/docs/getting-started/webhooks.png
|
||||
info: Discover how to use our Webhooks.
|
||||
---
|
||||
|
||||
## Overview
|
||||
Webhooks in Twenty complement the API by enabling **real-time notifications** to your own applications when certain events happen in your CRM. Instead of continuously polling the API for changes, you can set up webhooks to have Twenty **push** data to your system whenever specific events occur (for example, when a new record is created or an existing record is updated). This helps keep external systems in sync with Twenty instantly and efficiently.
|
||||
|
||||
With webhooks, Twenty will send an HTTP POST request to a URL you specify, containing details about the event. You can then handle that data in your application (e.g., to update your external database, trigger workflows, or send alerts).
|
||||
|
||||
## Setting Up a Webhook
|
||||
To create a webhook in Twenty, use the **APIs & Webhooks** settings in your Twenty app:
|
||||
|
||||
1. **Navigate to Settings:** In your Twenty application, go to **Settings → APIs & Webhooks**.
|
||||
2. **Create a Webhook:** Under **Webhooks** click on **+ Create webhook**.
|
||||
3. **Enter URL:** Provide the endpoint URL on your server where you want Twenty to send webhook requests. This should be a publicly accessible URL that can handle POST requests.
|
||||
4. **Save:** Click **Save** to create the webhook. The new webhook will be active immediately.
|
||||
|
||||
You can create multiple webhooks if you need to send different events to different endpoints. Each webhook is essentially a subscription for all relevant events (at this time, Twenty sends all event types to the given URL; filtering specific event types may be configurable in the UI). If you ever need to remove a webhook, you can delete it from the same settings page (select the webhook and choose delete).
|
||||
|
||||
## Events and Payloads
|
||||
Once a webhook is set up, Twenty will send an HTTP POST request to your specified URL whenever a trigger event occurs in your CRM data. Common events that trigger webhooks include:
|
||||
|
||||
* **Record Created:** e.g. a new person is added (`person.created`), a new company is created (`company.created`), a note is created (`note.created`), etc.
|
||||
* **Record Updated:** e.g. an existing person's information is updated (`person.updated`), a company record is edited (`company.updated`), etc.
|
||||
* **Record Deleted:** e.g. a person or company is deleted (`person.deleted`, `company.deleted`).
|
||||
* **Other Events:** If applicable, other object events or custom triggers (for instance, if tasks or other objects are updated, similar event types would be used like `task.created`, `note.updated`, etc.).
|
||||
|
||||
The webhook POST request contains a JSON payload in its body. The payload will generally include at least two things: the type of event, and the data related to that event (often the record that was created/updated). For example, a webhook for a newly created person might send a payload like:
|
||||
|
||||
```
|
||||
{
|
||||
"event": "person.created",
|
||||
"data": {
|
||||
"id": "abc12345",
|
||||
"firstName": "Alice",
|
||||
"lastName": "Doe",
|
||||
"email": "alice@example.com",
|
||||
"createdAt": "2025-02-10T15:30:45Z",
|
||||
"createdBy": "user_123"
|
||||
},
|
||||
"timestamp": "2025-02-10T15:30:50Z"
|
||||
}
|
||||
```
|
||||
|
||||
In this example:
|
||||
|
||||
* `"event"` specifies what happened (`person.created`).
|
||||
* `"data"` contains the new record's details (the same information you would get if you requested that person via the API).
|
||||
* `"timestamp"` is when the event occurred (in UTC).
|
||||
|
||||
Your endpoint should be prepared to receive such JSON data via POST. Typically, you'll parse the JSON, look at the `"event"` type to understand what happened, and then use the `"data"` accordingly (e.g., create a new contact in your system, or update an existing one).
|
||||
|
||||
**Note:** It's important to respond with a **2xx HTTP status** from your webhook endpoint to acknowledge successful receipt. If the Twenty webhook sender does not get a 2xx response, it may consider the delivery failed. (In the future, retry logic might attempt to resend failed webhooks, so always strive to return a 200 OK as quickly as possible after processing the data.)
|
||||
|
||||
## Webhook Validation
|
||||
|
||||
To ensure the security of your webhook endpoints, Twenty includes a signature in the `X-Twenty-Webhook-Signature` header.
|
||||
|
||||
This signature is an HMAC SHA256 hash of the request payload, computed using your webhook secret.
|
||||
|
||||
To validate the signature, you'll need to:
|
||||
1. Concatenate the timestamp (from `X-Twenty-Webhook-Timestamp` header), a colon, and the JSON string of the payload
|
||||
2. Compute the HMAC SHA256 hash using your webhook secret as the key ()
|
||||
3. Compare the resulting hex digest with the signature header
|
||||
|
||||
Here's an example in Node.js:
|
||||
|
||||
```javascript
|
||||
const crypto = require("crypto");
|
||||
const timestamp = "1735066639761";
|
||||
const payload = JSON.stringify({...});
|
||||
const secret = "your-secret";
|
||||
const stringToSign = `${timestamp}:${JSON.stringify(payload)}`;
|
||||
const signature = crypto.createHmac("sha256", secret)
|
||||
.update(stringToSign)
|
||||
.digest("hex");
|
||||
```
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Backend Development
|
||||
icon: TbTerminal
|
||||
image: /images/user-guide/kanban-views/kanban.png
|
||||
info: NestJS, Custom Objects, Queues...
|
||||
---
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
---
|
||||
title: Best Practices
|
||||
icon: TbChecklist
|
||||
image: /images/user-guide/tips/light-bulb.png
|
||||
---
|
||||
|
||||
This document outlines the best practices you should follow when working on the backend.
|
||||
|
||||
## Follow a modular approach
|
||||
|
||||
The backend follows a modular approach, which is a fundamental principle when working with NestJS. Make sure you break down your code into reusable modules to maintain a clean and organized codebase.
|
||||
Each module should encapsulate a particular feature or functionality and have a well-defined scope. This modular approach enables clear separation of concerns and removes unnecessary complexities.
|
||||
|
||||
## Expose services to use in modules
|
||||
|
||||
Always create services that have a clear and single responsibility, which enhances code readability and maintainability. Name the services descriptively and consistently.
|
||||
|
||||
You should also expose services that you want to use in other modules. Exposing services to other modules is possible through NestJS's powerful dependency injection system, and promotes loose coupling between components.
|
||||
|
||||
## Avoid using `any` type
|
||||
|
||||
When you declare a variable as `any`, TypeScript's type checker doesn't perform any type checking, making it possible to assign any type of values to the variable. TypeScript uses type inference to determine the type of variable based on the value. By declaring it as `any`, TypeScript can no longer infer the type. This makes it hard to catch type-related errors during development, leading to runtime errors and makes the code less maintainable, less reliable, and harder to understand for others.
|
||||
|
||||
This is why everything should have a type. So if you create a new object with a first name and last name, you should create an interface or type that contains a first name and last name that defines the shape of the object you are manipulating.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
---
|
||||
title: Custom Objects
|
||||
icon: TbAugmentedReality
|
||||
image: /images/user-guide/objects/objects.png
|
||||
---
|
||||
|
||||
Objects are structures that allow you to store data (records, attributes, and values) specific to an organization. Twenty provides both standard and custom objects.
|
||||
|
||||
Standard objects are in-built objects with a set of attributes available for all users. Examples of standard objects in Twenty include Company and Person. Standard objects have standard fields that are also available for all Twenty users, like Company.displayName.
|
||||
|
||||
Custom objects are objects that you can create to store information that is unique to your organization. They are not built-in; members of your workspace can create and customize custom objects to hold information that standard objects aren't suitable for.
|
||||
|
||||
|
||||
## High-level schema
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="High level schema" />
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
## How it works
|
||||
|
||||
Custom objects come from metadata tables that determine the shape, name, and type of the objects. All this information is present in the metadata schema database, consisting of tables:
|
||||
|
||||
- **DataSource**: Details where the data is present.
|
||||
- **Object**: Describes the object and links to a DataSource.
|
||||
- **Field**: Outlines an Object's fields and connects to the Object.
|
||||
|
||||
To add a custom object, the workspaceMember will query the /metadata API. This updates the metadata accordingly and computes a GraphQL schema based on the metadata, storing it in a GQL cache for later use.
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Query the /metadata API to add custom objects" />
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
To fetch data, the process involves making queries through the /graphql endpoint and passing them through the Query Resolver.
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="Query the /graphql endpoint to fetch data" />
|
||||
</div>
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
---
|
||||
title: Feature Flags
|
||||
icon: TbFlag
|
||||
image: /images/user-guide/table-views/table.png
|
||||
---
|
||||
|
||||
Feature flags are used to hide experimental features. For Twenty, they are set on workspace level and not on a user level.
|
||||
|
||||
## Adding a new feature flag
|
||||
|
||||
In `FeatureFlagKey.ts` add the feature flag:
|
||||
|
||||
```ts
|
||||
type FeatureFlagKey =
|
||||
| 'IS_FEATURENAME_ENABLED'
|
||||
| ...;
|
||||
```
|
||||
|
||||
Also add it to the enum in `feature-flag.entity.ts`:
|
||||
|
||||
```ts
|
||||
enum FeatureFlagKeys {
|
||||
IsFeatureNameEnabled = 'IS_FEATURENAME_ENABLED',
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
To apply a feature flag on a **backend** feature use:
|
||||
|
||||
```ts
|
||||
@Gate({
|
||||
featureFlag: 'IS_FEATURENAME_ENABLED',
|
||||
})
|
||||
```
|
||||
|
||||
To apply a feature flag on a **frontend** feature use:
|
||||
|
||||
```ts
|
||||
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
|
||||
```
|
||||
|
||||
|
||||
## Configure feature flags for the deployment
|
||||
|
||||
Change the corresponding record in the Table `core.featureFlag`:
|
||||
|
||||
| id | key | workspaceId | value |
|
||||
|----------|--------------------------|---------------|--------|
|
||||
| Random | `IS_FEATURENAME_ENABLED` | WorkspaceID | `true` |
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,130 +0,0 @@
|
|||
---
|
||||
title: Folder Architecture
|
||||
info: A detailed look into our server folder architecture
|
||||
icon: TbFolder
|
||||
image: /images/user-guide/fields/field.png
|
||||
---
|
||||
|
||||
|
||||
The backend directory structure is as follows:
|
||||
|
||||
```
|
||||
server
|
||||
└───ability
|
||||
└───constants
|
||||
└───core
|
||||
└───database
|
||||
└───decorators
|
||||
└───filters
|
||||
└───guards
|
||||
└───health
|
||||
└───integrations
|
||||
└───metadata
|
||||
└───workspace
|
||||
└───utils
|
||||
```
|
||||
|
||||
## Ability
|
||||
|
||||
Defines permissions and includes handlers for each entity.
|
||||
|
||||
## Decorators
|
||||
|
||||
Defines custom decorators in NestJS for added functionality.
|
||||
|
||||
See [custom decorators](https://docs.nestjs.com/custom-decorators) for more details.
|
||||
|
||||
## Filters
|
||||
|
||||
Includes exception filters to handle exceptions that might occur in GraphQL endpoints.
|
||||
|
||||
## Guards
|
||||
|
||||
See [guards](https://docs.nestjs.com/guards) for more details.
|
||||
|
||||
## Health
|
||||
|
||||
Includes a publicly available REST API (healthz) that returns a JSON to confirm whether the database is working as expected.
|
||||
|
||||
## Metadata
|
||||
|
||||
Defines custom objects and makes available a GraphQL API (graphql/metadata).
|
||||
|
||||
## Workspace
|
||||
|
||||
Generates and serves custom GraphQL schema based on the metadata.
|
||||
|
||||
### Workspace Directory Structure
|
||||
|
||||
```
|
||||
workspace
|
||||
|
||||
└───workspace-schema-builder
|
||||
└───factories
|
||||
└───graphql-types
|
||||
└───database
|
||||
└───interfaces
|
||||
└───object-definitions
|
||||
└───services
|
||||
└───storage
|
||||
└───utils
|
||||
└───workspace-resolver-builder
|
||||
└───factories
|
||||
└───interfaces
|
||||
└───workspace-query-builder
|
||||
└───factories
|
||||
└───interfaces
|
||||
└───workspace-query-runner
|
||||
└───interfaces
|
||||
└───utils
|
||||
└───workspace-datasource
|
||||
└───workspace-manager
|
||||
└───workspace-migration-runner
|
||||
└───utils
|
||||
└───workspace.module.ts
|
||||
└───workspace.factory.spec.ts
|
||||
└───workspace.factory.ts
|
||||
```
|
||||
|
||||
|
||||
The root of the workspace directory includes the `workspace.factory.ts`, a file containing the `createGraphQLSchema` function. This function generates workspace-specific schema by using the metadata to tailor a schema for individual workspaces. By separating the schema and resolver construction, we use the `makeExecutableSchema` function, which combines these discrete elements.
|
||||
|
||||
This strategy is not just about organization, but also helps with optimization, such as caching generated type definitions to enhance performance and scalability.
|
||||
|
||||
### Workspace Schema builder
|
||||
|
||||
Generates the GraphQL schema, and includes:
|
||||
|
||||
#### Factories:
|
||||
|
||||
Specialised constructors to generate GraphQL-related constructs.
|
||||
- The type.factory translates field metadata into GraphQL types using `TypeMapperService`.
|
||||
- The type-definition.factory creates GraphQL input or output objects derived from `objectMetadata`.
|
||||
|
||||
#### GraphQL Types
|
||||
|
||||
Includes enumerations, inputs, objects, and scalars, and serves as the building blocks for the schema construction.
|
||||
|
||||
#### Interfaces and Object Definitions
|
||||
|
||||
Contains the blueprints for GraphQL entities, and includes both predefined and custom types like `MONEY` or `URL`.
|
||||
|
||||
#### Services
|
||||
|
||||
Contains the service responsible for associating FieldMetadataType with its appropriate GraphQL scalar or query modifiers.
|
||||
|
||||
#### Storage
|
||||
|
||||
Includes the `TypeDefinitionsStorage` class that contains reusable type definitions, preventing duplication of GraphQL types.
|
||||
|
||||
### Workspace Resolver Builder
|
||||
|
||||
Creates resolver functions for querying and mutating the GraphQL schema.
|
||||
|
||||
Each factory in this directory is responsible for producing a distinct resolver type, such as the `FindManyResolverFactory`, designed for adaptable application across various tables.
|
||||
|
||||
### Workspace Query Runner
|
||||
|
||||
Runs the generated queries on the database and parses the result.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
---
|
||||
title: Message Queue
|
||||
icon: TbSchema
|
||||
image: /images/user-guide/emails/emails_header.png
|
||||
---
|
||||
|
||||
Queues facilitate async operations to be performed. They can be used for performing background tasks such as sending a welcome email on register.
|
||||
Each use case will have its own queue class extended from `MessageQueueServiceBase`.
|
||||
|
||||
Currently, we only support `bull-mq`[bull-mq](https://bullmq.io/) as the queue driver.
|
||||
|
||||
## Steps to create and use a new queue
|
||||
|
||||
1. Add a queue name for your new queue under enum `MESSAGE_QUEUES`.
|
||||
2. Provide the factory implementation of the queue with the queue name as the dependency token.
|
||||
3. Inject the queue that you created in the required module/service with the queue name as the dependency token.
|
||||
4. Add worker class with token based injection just like producer.
|
||||
|
||||
### Example usage
|
||||
```ts
|
||||
class Resolver {
|
||||
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
|
||||
|
||||
async onSomeAction() {
|
||||
//business logic
|
||||
await this.queue.add(someData);
|
||||
}
|
||||
}
|
||||
|
||||
//async worker
|
||||
class CustomWorker {
|
||||
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
|
||||
this.initWorker();
|
||||
}
|
||||
|
||||
async initWorker() {
|
||||
await this.queue.work(async ({ id, data }) => {
|
||||
//worker logic
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
---
|
||||
title: Backend Commands
|
||||
icon: TbTerminal
|
||||
image: /images/user-guide/kanban-views/kanban.png
|
||||
---
|
||||
|
||||
## Useful commands
|
||||
|
||||
These commands should be executed from packages/twenty-server folder.
|
||||
From any other folder you can run `npx nx <command> twenty-server` (or `npx nx run twenty-server:<command>`).
|
||||
|
||||
### First time setup
|
||||
|
||||
```
|
||||
npx nx database:reset twenty-server # setup the database with dev seeds
|
||||
```
|
||||
|
||||
### Starting the server
|
||||
|
||||
```
|
||||
npx nx run twenty-server:start
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```
|
||||
npx nx run twenty-server:lint # pass --fix to fix lint errors
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```
|
||||
npx nx run twenty-server:test:unit # run unit tests
|
||||
npx nx run twenty-server:test:integration # run integration tests
|
||||
```
|
||||
Note: you can run `npx nx run twenty-server:test:integration:with-db-reset` in case you need to reset the database before running the integration tests.
|
||||
|
||||
### Resetting the database
|
||||
|
||||
If you want to reset and seed the database, you can run the following command:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:database:reset
|
||||
```
|
||||
|
||||
### Migrations
|
||||
|
||||
#### For objects in Core/Metadata schemas (TypeORM)
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:typeorm migration:generate src/database/typeorm/core/migrations/nameOfYourMigration -d src/database/typeorm/core/core.datasource.ts
|
||||
```
|
||||
|
||||
#### For Workspace objects
|
||||
|
||||
There are no migrations files, migration are generated automatically for each workspace,
|
||||
stored in the database, and applied with this command
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<ArticleWarning>
|
||||
|
||||
This will drop the database and re-run the migrations and seed.
|
||||
|
||||
Make sure to back up any data you want to keep before running this command.
|
||||
|
||||
</ArticleWarning>
|
||||
|
||||
## Tech Stack
|
||||
|
||||
Twenty primarily uses NestJS for the backend.
|
||||
|
||||
Prisma was the first ORM we used. But in order to allow users to create custom fields and custom objects, a lower-level made more sense as we need to have fine-grained control. The project now uses TypeORM.
|
||||
|
||||
Here's what the tech stack now looks like.
|
||||
|
||||
|
||||
**Core**
|
||||
- [NestJS](https://nestjs.com/)
|
||||
- [TypeORM](https://typeorm.io/)
|
||||
- [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
|
||||
|
||||
**Database**
|
||||
- [Postgres](https://www.postgresql.org/)
|
||||
|
||||
**Third-party integrations**
|
||||
- [Sentry](https://sentry.io/welcome/) for tracking bugs
|
||||
|
||||
**Testing**
|
||||
- [Jest](https://jestjs.io/)
|
||||
|
||||
**Tooling**
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- [ESLint](https://eslint.org/)
|
||||
|
||||
**Development**
|
||||
- [AWS EKS](https://aws.amazon.com/eks/)
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
---
|
||||
title: Zapier App
|
||||
icon: TbBrandZapier
|
||||
image: /images/user-guide/integrations/plug.png
|
||||
---
|
||||
|
||||
Effortlessly sync Twenty with 3000+ apps using [Zapier](https://zapier.com/). Automate tasks, boost productivity, and supercharge your customer relationships!
|
||||
|
||||
## About Zapier
|
||||
|
||||
Zapier is a tool that allows you to automate workflows by connecting the apps that your team uses every day. The fundamental concept of Zapier is automation workflows, called Zaps, and include triggers and actions.
|
||||
|
||||
You can learn more about how Zapier works [here](https://zapier.com/how-it-works).
|
||||
|
||||
## Setup
|
||||
|
||||
### Step 1: Install Zapier packages
|
||||
|
||||
```bash
|
||||
cd packages/twenty-zapier
|
||||
|
||||
yarn
|
||||
```
|
||||
|
||||
### Step 2: Login with the CLI
|
||||
|
||||
Use your Zapier credentials to log in using the CLI:
|
||||
|
||||
```bash
|
||||
zapier login
|
||||
```
|
||||
|
||||
### Step 3: Set environment variables
|
||||
|
||||
From the `packages/twenty-zapier` folder, run:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
Run the application locally, go to [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks), and generate an API key.
|
||||
|
||||
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
|
||||
|
||||
## Development
|
||||
|
||||
<ArticleWarning>
|
||||
|
||||
Make sure to run `yarn build` before any `zapier` command.
|
||||
|
||||
</ArticleWarning>
|
||||
|
||||
### Test
|
||||
```bash
|
||||
yarn test
|
||||
```
|
||||
### Lint
|
||||
```bash
|
||||
yarn format
|
||||
```
|
||||
### Watch and compile as you edit code
|
||||
```bash
|
||||
yarn watch
|
||||
```
|
||||
### Validate your Zapier app
|
||||
```bash
|
||||
yarn validate
|
||||
```
|
||||
### Deploy your Zapier app
|
||||
```bash
|
||||
yarn deploy
|
||||
```
|
||||
### List all Zapier CLI commands
|
||||
```bash
|
||||
zapier
|
||||
```
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
---
|
||||
title: Bugs and Requests
|
||||
icon: TbBug
|
||||
image: /images/user-guide/api/api.png
|
||||
info: Ask for help on GitHub or Discord
|
||||
---
|
||||
|
||||
## Reporting Bugs
|
||||
To report a bug, please [create an issue on GitHub](https://github.com/twentyhq/twenty/issues/new).
|
||||
|
||||
You can also ask for help on [Discord](https://discord.gg/cx5n4Jzs57).
|
||||
|
||||
## Feature Requests
|
||||
|
||||
If you're not sure if it's a bug, and you feel it's closer to a feature request, then you should probably [open a discussion instead](https://github.com/twentyhq/twenty/discussions/new).
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
export const DOCS_INDEX = {
|
||||
'Getting started': {
|
||||
'Local Setup': [{ fileName: 'local-setup' }],
|
||||
'Self-Hosting': [
|
||||
{ fileName: 'self-hosting' },
|
||||
{ fileName: 'docker-compose' },
|
||||
{ fileName: 'upgrade-guide' },
|
||||
{ fileName: 'setup' },
|
||||
{ fileName: 'cloud-providers' },
|
||||
{ fileName: 'troubleshooting' },
|
||||
],
|
||||
'API and Webhooks': [{ fileName: 'api' }, { fileName: 'webhooks' }],
|
||||
},
|
||||
Contributing: {
|
||||
'Bugs and Requests': [{ fileName: 'bug-and-requests' }],
|
||||
'Frontend Development': [
|
||||
{ fileName: 'storybook' },
|
||||
{ fileName: 'components' },
|
||||
{ fileName: 'frontend-development' },
|
||||
{ fileName: 'frontend-commands' },
|
||||
{ fileName: 'work-with-figma' },
|
||||
{ fileName: 'best-practices-front' },
|
||||
{ fileName: 'style-guide' },
|
||||
{ fileName: 'folder-architecture-front' },
|
||||
{ fileName: 'hotkeys' },
|
||||
],
|
||||
'Backend Development': [
|
||||
{ fileName: 'backend-development' },
|
||||
{ fileName: 'server-commands' },
|
||||
{ fileName: 'feature-flags' },
|
||||
{ fileName: 'folder-architecture-server' },
|
||||
{ fileName: 'zapier' },
|
||||
{ fileName: 'best-practices-server' },
|
||||
{ fileName: 'custom-objects' },
|
||||
{ fileName: 'queue' },
|
||||
],
|
||||
},
|
||||
'User Guide': {
|
||||
'Empty Section': [],
|
||||
},
|
||||
};
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Frontend Development
|
||||
icon: TbTerminal2
|
||||
image: /images/user-guide/create-workspace/workspace-cover.png
|
||||
info: Storybook, Figma, React Best Practices...
|
||||
---
|
||||
|
|
@ -1,330 +0,0 @@
|
|||
---
|
||||
title: Best Practices
|
||||
icon: TbChecklist
|
||||
image: /images/user-guide/tips/light-bulb.png
|
||||
---
|
||||
|
||||
This document outlines the best practices you should follow when working on the frontend.
|
||||
|
||||
## State management
|
||||
|
||||
React and Recoil handle state management in the codebase.
|
||||
|
||||
### Use `useRecoilState` to store state
|
||||
|
||||
It's good practice to create as many atoms as you need to store your state.
|
||||
|
||||
<ArticleWarning>
|
||||
|
||||
It's better to use extra atoms than trying to be too concise with props drilling.
|
||||
|
||||
</ArticleWarning>
|
||||
|
||||
```tsx
|
||||
export const myAtomState = atom({
|
||||
key: 'myAtomState',
|
||||
default: 'default value',
|
||||
});
|
||||
|
||||
export const MyComponent = () => {
|
||||
const [myAtom, setMyAtom] = useRecoilState(myAtomState);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input
|
||||
value={myAtom}
|
||||
onChange={(e) => setMyAtom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Do not use `useRef` to store state
|
||||
|
||||
Avoid using `useRef` to store state.
|
||||
|
||||
If you want to store state, you should use `useState` or `useRecoilState`.
|
||||
|
||||
See [how to manage re-renders](#managing-re-renders) if you feel like you need `useRef` to prevent some re-renders from happening.
|
||||
|
||||
## Managing re-renders
|
||||
|
||||
Re-renders can be hard to manage in React.
|
||||
|
||||
Here are some rules to follow to avoid unnecessary re-renders.
|
||||
|
||||
Keep in mind that you can **always** avoid re-renders by understanding their cause.
|
||||
|
||||
### Work at the root level
|
||||
|
||||
Avoiding re-renders in new features is now made easy by eliminating them at the root level.
|
||||
|
||||
The `PageChangeEffect` sidecar component contains just one `useEffect` that holds all the logic to execute on a page change.
|
||||
|
||||
That way you know that there's just one place that can trigger a re-render.
|
||||
|
||||
### Always think twice before adding `useEffect` in your codebase
|
||||
|
||||
Re-renders are often caused by unnecessary `useEffect`.
|
||||
|
||||
You should think whether you need `useEffect`, or if you can move the logic in a event handler function.
|
||||
|
||||
You'll find it generally easy to move the logic in a `handleClick` or `handleChange` function.
|
||||
|
||||
You can also find them in libraries like Apollo: `onCompleted`, `onError`, etc.
|
||||
|
||||
### Use a sibling component to extract `useEffect` or data fetching logic
|
||||
|
||||
If you feel like you need to add a `useEffect` in your root component, you should consider extracting it in a sidecar component.
|
||||
|
||||
You can apply the same for data fetching logic, with Apollo hooks.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, will cause re-renders even if data is not changing,
|
||||
// because useEffect needs to be re-evaluated
|
||||
export const PageComponent = () => {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
const [someDependency] = useRecoilState(someDependencyState);
|
||||
|
||||
useEffect(() => {
|
||||
if(someDependency !== data) {
|
||||
setData(someDependency);
|
||||
}
|
||||
}, [someDependency]);
|
||||
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
export const App = () => (
|
||||
<RecoilRoot>
|
||||
<PageComponent />
|
||||
</RecoilRoot>
|
||||
);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, will not cause re-renders if data is not changing,
|
||||
// because useEffect is re-evaluated in another sibling component
|
||||
export const PageComponent = () => {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
|
||||
return <div>{data}</div>;
|
||||
};
|
||||
|
||||
export const PageData = () => {
|
||||
const [data, setData] = useRecoilState(dataState);
|
||||
const [someDependency] = useRecoilState(someDependencyState);
|
||||
|
||||
useEffect(() => {
|
||||
if(someDependency !== data) {
|
||||
setData(someDependency);
|
||||
}
|
||||
}, [someDependency]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export const App = () => (
|
||||
<RecoilRoot>
|
||||
<PageData />
|
||||
<PageComponent />
|
||||
</RecoilRoot>
|
||||
);
|
||||
```
|
||||
|
||||
### Use recoil family states and recoil family selectors
|
||||
|
||||
Recoil family states and selectors are a great way to avoid re-renders.
|
||||
|
||||
They are useful when you need to store a list of items.
|
||||
|
||||
### You shouldn't use `React.memo(MyComponent)`
|
||||
|
||||
Avoid using `React.memo()` because it does not solve the cause of the re-render, but instead breaks the re-render chain, which can lead to unexpected behavior and make the code very hard to refactor.
|
||||
|
||||
### Limit `useCallback` or `useMemo` usage
|
||||
|
||||
They are often not necessary and will make the code harder to read and maintain for a gain of performance that is unnoticeable.
|
||||
|
||||
## Console.logs
|
||||
|
||||
`console.log` statements are valuable during development, offering real-time insights into variable values and code flow. But, leaving them in production code can lead to several issues:
|
||||
|
||||
1. **Performance**: Excessive logging can affect the runtime performance, especially on client-side applications.
|
||||
|
||||
2. **Security**: Logging sensitive data can expose critical information to anyone who inspects the browser's console.
|
||||
|
||||
3. **Cleanliness**: Filling up the console with logs can obscure important warnings or errors that developers or tools need to see.
|
||||
|
||||
4. **Professionalism**: End users or clients checking the console and seeing a myriad of log statements might question the code's quality and polish.
|
||||
|
||||
Make sure you remove all `console.logs` before pushing the code to production.
|
||||
|
||||
## Naming
|
||||
|
||||
### Variable Naming
|
||||
|
||||
Variable names ought to precisely depict the purpose or function of the variable.
|
||||
|
||||
#### The issue with generic names
|
||||
Generic names in programming are not ideal because they lack specificity, leading to ambiguity and reduced code readability. Such names fail to convey the variable or function's purpose, making it challenging for developers to understand the code's intent without deeper investigation. This can result in increased debugging time, higher susceptibility to errors, and difficulties in maintenance and collaboration. Meanwhile, descriptive naming makes the code self-explanatory and easier to navigate, enhancing code quality and developer productivity.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, uses a generic name that doesn't communicate its
|
||||
// purpose or content clearly
|
||||
const [value, setValue] = useState('');
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, uses a descriptive name
|
||||
const [email, setEmail] = useState('');
|
||||
```
|
||||
|
||||
#### Some words to avoid in variable names
|
||||
|
||||
- dummy
|
||||
|
||||
### Event handlers
|
||||
|
||||
Event handler names should start with `handle`, while `on` is a prefix used to name events in components props.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const onEmailChange = (val: string) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good
|
||||
const handleEmailChange = (val: string) => {
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
## Optional Props
|
||||
|
||||
Avoid passing the default value for an optional prop.
|
||||
|
||||
**EXAMPLE**
|
||||
|
||||
Take the`EmailField` component defined below:
|
||||
|
||||
```tsx
|
||||
type EmailFieldProps = {
|
||||
value: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
|
||||
<TextInput value={value} disabled={disabled} fullWidth />
|
||||
);
|
||||
```
|
||||
|
||||
**Usage**
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, passing in the same value as the default value adds no value
|
||||
const Form = () => <EmailField value="username@email.com" disabled={false} />;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, assumes the default value
|
||||
const Form = () => <EmailField value="username@email.com" />;
|
||||
```
|
||||
|
||||
## Component as props
|
||||
|
||||
Try as much as possible to pass uninstantiated components as props, so children can decide on their own of what props they need to pass.
|
||||
|
||||
The most common example for that is icon components:
|
||||
|
||||
```tsx
|
||||
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
|
||||
|
||||
// In MyComponent
|
||||
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<MyIcon size={theme.icon.size.md}>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
For React to understand that the component is a component, you need to use PascalCase, to later instantiate it with `<MyIcon>`
|
||||
|
||||
## Prop Drilling: Keep It Minimal
|
||||
|
||||
Prop drilling, in the React context, refers to the practice of passing state variables and their setters through many component layers, even if intermediary components don't use them. While sometimes necessary, excessive prop drilling can lead to:
|
||||
|
||||
1. **Decreased Readability**: Tracing where a prop originates or where it's utilized can become convoluted in a deeply nested component structure.
|
||||
|
||||
2. **Maintenance Challenges**: Changes in one component's prop structure might require adjustments in several components, even if they don't directly use the prop.
|
||||
|
||||
3. **Reduced Component Reusability**: A component receiving a lot of props solely for passing them down becomes less general-purpose and harder to reuse in different contexts.
|
||||
|
||||
If you feel that you are using excessive prop drilling, see [state management best practices](#state-management).
|
||||
|
||||
## Imports
|
||||
|
||||
When importing, opt for the designated aliases rather than specifying complete or relative paths.
|
||||
|
||||
**The Aliases**
|
||||
|
||||
```js
|
||||
{
|
||||
alias: {
|
||||
"~": path.resolve(__dirname, "src"),
|
||||
"@": path.resolve(__dirname, "src/modules"),
|
||||
"@testing": path.resolve(__dirname, "src/testing"),
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Usage**
|
||||
```tsx
|
||||
// ❌ Bad, specifies the entire relative path
|
||||
import {
|
||||
CatalogDecorator
|
||||
} from '../../../../../testing/decorators/CatalogDecorator';
|
||||
import {
|
||||
ComponentDecorator
|
||||
} from '../../../../../testing/decorators/ComponentDecorator';
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, utilises the designated aliases
|
||||
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
```
|
||||
|
||||
## Schema Validation
|
||||
|
||||
[Zod](https://github.com/colinhacks/zod) is the schema validator for untyped objects:
|
||||
|
||||
```js
|
||||
const validationSchema = z
|
||||
.object({
|
||||
exist: z.boolean(),
|
||||
email: z
|
||||
.string()
|
||||
.email('Email must be a valid email'),
|
||||
password: z
|
||||
.string()
|
||||
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
||||
})
|
||||
.required();
|
||||
|
||||
type Form = z.infer<typeof validationSchema>;
|
||||
```
|
||||
|
||||
|
||||
## Breaking Changes
|
||||
|
||||
Always perform thorough manual testing before proceeding to guarantee that modifications haven’t caused disruptions elsewhere, given that tests have not yet been extensively integrated.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,114 +0,0 @@
|
|||
---
|
||||
title: Folder Architecture
|
||||
info: A detailed look into our folder architecture
|
||||
icon: TbFolder
|
||||
image: /images/user-guide/fields/field.png
|
||||
---
|
||||
|
||||
In this guide, you will explore the details of the project directory structure and how it contributes to the organization and maintainability of Twenty.
|
||||
|
||||
By following this folder architecture convention, it's easier to find the files related to specific features and ensure that the application is scalable and maintainable.
|
||||
|
||||
```
|
||||
front
|
||||
└───modules
|
||||
│ └───module1
|
||||
│ │ └───submodule1
|
||||
│ └───module2
|
||||
│ └───ui
|
||||
│ │ └───display
|
||||
│ │ └───inputs
|
||||
│ │ │ └───buttons
|
||||
│ │ └───...
|
||||
└───pages
|
||||
└───...
|
||||
```
|
||||
|
||||
## Pages
|
||||
|
||||
Includes the top-level components defined by the application routes. They import more low-level components from the modules folder (more details below).
|
||||
|
||||
## Modules
|
||||
|
||||
Each module represents a feature or a group of feature, comprising its specific components, states, and operational logic.
|
||||
They should all follow the structure below. You can nest modules within modules (referred to as submodules) and the same rules will apply.
|
||||
|
||||
```
|
||||
module1
|
||||
└───components
|
||||
│ └───component1
|
||||
│ └───component2
|
||||
└───constants
|
||||
└───contexts
|
||||
└───graphql
|
||||
│ └───fragments
|
||||
│ └───queries
|
||||
│ └───mutations
|
||||
└───hooks
|
||||
│ └───internal
|
||||
└───states
|
||||
│ └───selectors
|
||||
└───types
|
||||
└───utils
|
||||
```
|
||||
|
||||
### Contexts
|
||||
|
||||
A context is a way to pass data through the component tree without having to pass props down manually at every level.
|
||||
|
||||
See [React Context](https://react.dev/reference/react#context-hooks) for more details.
|
||||
|
||||
### GraphQL
|
||||
|
||||
Includes fragments, queries, and mutations.
|
||||
|
||||
See [GraphQL](https://graphql.org/learn/) for more details.
|
||||
|
||||
- Fragments
|
||||
|
||||
A fragment is a reusable piece of a query, which you can use in different places. By using fragments, it's easier to avoid duplicating code.
|
||||
|
||||
See [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) for more details.
|
||||
|
||||
- Queries
|
||||
|
||||
See [GraphQL Queries](https://graphql.org/learn/queries/) for more details.
|
||||
|
||||
- Mutations
|
||||
|
||||
See [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) for more details.
|
||||
|
||||
### Hooks
|
||||
|
||||
See [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) for more details.
|
||||
|
||||
### States
|
||||
|
||||
Contains the state management logic. [RecoilJS](https://recoiljs.org) handles this.
|
||||
|
||||
- Selectors: See [RecoilJS Selectors](https://recoiljs.org/docs/basic-tutorial/selectors) for more details.
|
||||
|
||||
React's built-in state management still handles state within a component.
|
||||
|
||||
### Utils
|
||||
|
||||
Should just contain reusable pure functions. Otherwise, create custom hooks in the `hooks` folder.
|
||||
|
||||
|
||||
## UI
|
||||
|
||||
Contains all the reusable UI components used in the application.
|
||||
|
||||
This folder can contain sub-folders, like `data`, `display`, `feedback`, and `input` for specific types of components. Each component should be self-contained and reusable, so that you can use it in different parts of the application.
|
||||
|
||||
By separating the UI components from the other components in the `modules` folder, it's easier to maintain a consistent design and to make changes to the UI without affecting other parts (business logic) of the codebase.
|
||||
|
||||
## Interface and dependencies
|
||||
|
||||
You can import other module code from any module except for the `ui` folder. This will keep its code easy to test.
|
||||
|
||||
### Internal
|
||||
|
||||
Each part (hooks, states, ...) of a module can have an `internal` folder, which contains parts that are just used within the module.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
---
|
||||
title: Frontend Commands
|
||||
icon: TbTerminal2
|
||||
image: /images/user-guide/create-workspace/workspace-cover.png
|
||||
---
|
||||
|
||||
## Useful commands
|
||||
|
||||
### Starting the app
|
||||
|
||||
```bash
|
||||
npx nx start twenty-front
|
||||
```
|
||||
|
||||
### Regenerate graphql schema based on API graphql schema
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
```
|
||||
OR
|
||||
```bash
|
||||
npx nx run twenty-front:graphql:generate
|
||||
```
|
||||
|
||||
### Lint
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lint # pass --fix to fix lint errors
|
||||
```
|
||||
|
||||
## Translations
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:lingui:extract
|
||||
npx nx run twenty-front:lingui:compile
|
||||
```
|
||||
|
||||
### Test
|
||||
|
||||
```bash
|
||||
npx nx run twenty-front:test # run jest tests
|
||||
npx nx run twenty-front:storybook:serve:dev # run storybook
|
||||
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
|
||||
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
The project has a clean and simple stack, with minimal boilerplate code.
|
||||
|
||||
**App**
|
||||
|
||||
- [React](https://react.dev/)
|
||||
- [Apollo](https://www.apollographql.com/docs/)
|
||||
- [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
|
||||
- [Recoil](https://recoiljs.org/docs/introduction/core-concepts)
|
||||
- [TypeScript](https://www.typescriptlang.org/)
|
||||
|
||||
**Testing**
|
||||
|
||||
- [Jest](https://jestjs.io/)
|
||||
- [Storybook](https://storybook.js.org/)
|
||||
|
||||
**Tooling**
|
||||
|
||||
- [Yarn](https://yarnpkg.com/)
|
||||
- [Craco](https://craco.js.org/docs/)
|
||||
- [ESLint](https://eslint.org/)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Routing
|
||||
|
||||
[React Router](https://reactrouter.com/) handles the routing.
|
||||
|
||||
To avoid unnecessary [re-renders](/contributor/frontend/best-practices#managing-re-renders) all the routing logic is in a `useEffect` in `PageChangeEffect`.
|
||||
|
||||
### State Management
|
||||
|
||||
[Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management.
|
||||
|
||||
See [best practices](/developers/section/frontend-development/best-practices-front#state-management) for more information on state management.
|
||||
|
||||
## Testing
|
||||
|
||||
[Jest](https://jestjs.io/) serves as the tool for unit testing while [Storybook](https://storybook.js.org/) is for component testing.
|
||||
|
||||
Jest is mainly for testing utility functions, and not components themselves.
|
||||
|
||||
Storybook is for testing the behavior of isolated components, as well as displaying the design system.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,178 +0,0 @@
|
|||
---
|
||||
title: Hotkeys
|
||||
icon: TbKeyboard
|
||||
image: /images/user-guide/table-views/table.png
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
When you need to listen to a hotkey, you would normally use the `onKeyDown` event listener.
|
||||
|
||||
In `twenty-front` however, you might have conflicts between same hotkeys that are used in different components, mounted at the same time.
|
||||
|
||||
For example, if you have a page that listens for the Enter key, and a modal that listens for the Enter key, with a Select component inside that modal that listens for the Enter key, you might have a conflict when all are mounted at the same time.
|
||||
|
||||
## The `useScopedHotkeys` hook
|
||||
|
||||
To handle this problem, we have a custom hook that makes it possible to listen to hotkeys without any conflict.
|
||||
|
||||
You place it in a component, and it will listen to the hotkeys only when the component is mounted AND when the specified **hotkey scope** is active.
|
||||
|
||||
## How to listen for hotkeys in practice?
|
||||
|
||||
There are two steps involved in setting up hotkey listening :
|
||||
1. Set the [hotkey scope](#what-is-a-hotkey-scope-) that will listen to hotkeys
|
||||
2. Use the `useScopedHotkeys` hook to listen to hotkeys
|
||||
|
||||
Setting up hotkey scopes is required even in simple pages, because other UI elements like left menu or command menu might also listen to hotkeys.
|
||||
|
||||
## Use cases for hotkeys
|
||||
|
||||
In general, you'll have two use cases that require hotkeys :
|
||||
1. In a page or a component mounted in a page
|
||||
2. In a modal-type component that takes the focus due to a user action
|
||||
|
||||
The second use case can happen recursively : a dropdown in a modal for example.
|
||||
|
||||
### Listening to hotkeys in a page
|
||||
|
||||
Example :
|
||||
|
||||
```tsx
|
||||
const PageListeningEnter = () => {
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
// 1. Set the hotkey scope in a useEffect
|
||||
useEffect(() => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
ExampleHotkeyScopes.ExampleEnterPage,
|
||||
);
|
||||
|
||||
// Revert to the previous hotkey scope when the component is unmounted
|
||||
return () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
}, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);
|
||||
|
||||
// 2. Use the useScopedHotkeys hook
|
||||
useScopedHotkeys(
|
||||
Key.Enter,
|
||||
() => {
|
||||
// Some logic executed on this page when the user presses Enter
|
||||
// ...
|
||||
},
|
||||
ExampleHotkeyScopes.ExampleEnterPage,
|
||||
);
|
||||
|
||||
return <div>My page that listens for Enter</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Listening to hotkeys in a modal-type component
|
||||
|
||||
For this example we'll use a modal component that listens for the Escape key to tell its parent to close it.
|
||||
|
||||
Here the user interaction is changing the scope.
|
||||
|
||||
```tsx
|
||||
const ExamplePageWithModal = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const handleOpenModalClick = () => {
|
||||
// 1. Set the hotkey scope when user opens the modal
|
||||
setShowModal(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
ExampleHotkeyScopes.ExampleModal,
|
||||
);
|
||||
};
|
||||
|
||||
const handleModalClose = () => {
|
||||
// 1. Revert to the previous hotkey scope when the modal is closed
|
||||
setShowModal(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
|
||||
return <div>
|
||||
<h1>My page with a modal</h1>
|
||||
<button onClick={handleOpenModalClick}>Open modal</button>
|
||||
{showModal && <MyModalComponent onClose={handleModalClose} />}
|
||||
</div>;
|
||||
};
|
||||
```
|
||||
|
||||
Then in the modal component :
|
||||
|
||||
```tsx
|
||||
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
|
||||
// 2. Use the useScopedHotkeys hook to listen for Escape.
|
||||
// Note that escape is a common hotkey that could be used by many other components
|
||||
// So it's important to use a hotkey scope to avoid conflicts
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
onClose()
|
||||
},
|
||||
ExampleHotkeyScopes.ExampleModal,
|
||||
);
|
||||
|
||||
return <div>My modal component</div>;
|
||||
};
|
||||
```
|
||||
|
||||
It's important to use this pattern when you're not sure that just using a useEffect with mount/unmount will be enough to avoid conflicts.
|
||||
|
||||
Those conflicts can be hard to debug, and it might happen more often than not with useEffects.
|
||||
|
||||
## What is a hotkey scope?
|
||||
|
||||
A hotkey scope is a string that represents a context in which the hotkeys are active. It is generally encoded as an enum.
|
||||
|
||||
When you change the hotkey scope, the hotkeys that are listening to this scope will be enabled and the hotkeys that are listening to other scopes will be disabled.
|
||||
|
||||
You can set only one scope at a time.
|
||||
|
||||
As an example, the hotkey scopes for each page are defined in the `PageHotkeyScope` enum:
|
||||
|
||||
```tsx
|
||||
export enum PageHotkeyScope {
|
||||
Settings = 'settings',
|
||||
CreateWorkspace = 'create-workspace',
|
||||
SignInUp = 'sign-in-up',
|
||||
CreateProfile = 'create-profile',
|
||||
PlanRequired = 'plan-required',
|
||||
ShowPage = 'show-page',
|
||||
PersonShowPage = 'person-show-page',
|
||||
CompanyShowPage = 'company-show-page',
|
||||
CompaniesPage = 'companies-page',
|
||||
PeoplePage = 'people-page',
|
||||
OpportunitiesPage = 'opportunities-page',
|
||||
ProfilePage = 'profile-page',
|
||||
WorkspaceMemberPage = 'workspace-member-page',
|
||||
TaskPage = 'task-page',
|
||||
}
|
||||
```
|
||||
|
||||
Internally, the currently selected scope is stored in a Recoil state that is shared across the application :
|
||||
|
||||
```tsx
|
||||
export const currentHotkeyScopeState = createState<HotkeyScope>({
|
||||
key: 'currentHotkeyScopeState',
|
||||
defaultValue: INITIAL_HOTKEYS_SCOPE,
|
||||
});
|
||||
```
|
||||
|
||||
But this Recoil state should never be handled manually ! We'll see how to use it in the next section.
|
||||
|
||||
## How is it working internally?
|
||||
|
||||
We made a thin wrapper on top of [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro) that makes it more performant and avoids unnecessary re-renders.
|
||||
|
||||
We also create a Recoil state to handle the hotkey scope state and make it available everywhere in the application.
|
||||
|
|
@ -1,293 +0,0 @@
|
|||
---
|
||||
title: Style Guide
|
||||
icon: TbPencil
|
||||
image: /images/user-guide/notes/notes_header.png
|
||||
---
|
||||
|
||||
This document includes the rules to follow when writing code.
|
||||
|
||||
The goal here is to have a consistent codebase, which is easy to read and easy to maintain.
|
||||
|
||||
For this, it's better to be a bit more verbose than to be too concise.
|
||||
|
||||
Always keep in mind that people read code more often than they write it, specially on an open source project, where anyone can contribute.
|
||||
|
||||
There are a lot of rules that are not defined here, but that are automatically checked by linters.
|
||||
|
||||
## React
|
||||
|
||||
### Use functional components
|
||||
|
||||
Always use TSX functional components.
|
||||
|
||||
Do not use default `import` with `const`, because it's harder to read and harder to import with code completion.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, harder to read, harder to import with code completion
|
||||
const MyComponent = () => {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
|
||||
export default MyComponent;
|
||||
|
||||
// ✅ Good, easy to read, easy to import with code completion
|
||||
export function MyComponent() {
|
||||
return <div>Hello World</div>;
|
||||
};
|
||||
```
|
||||
|
||||
### Props
|
||||
|
||||
Create the type of the props and call it `(ComponentName)Props` if there's no need to export it.
|
||||
|
||||
Use props destructuring.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, no type
|
||||
export const MyComponent = (props) => <div>Hello {props.name}</div>;
|
||||
|
||||
// ✅ Good, type
|
||||
type MyComponentProps = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
|
||||
```
|
||||
|
||||
#### Refrain from using `React.FC` or `React.FunctionComponent` to define prop types
|
||||
|
||||
```tsx
|
||||
/* ❌ - Bad, defines the component type annotations with `FC`
|
||||
* - With `React.FC`, the component implicitly accepts a `children` prop
|
||||
* even if it's not defined in the prop type. This might not always be
|
||||
* desirable, especially if the component doesn't intend to render
|
||||
* children.
|
||||
*/
|
||||
const EmailField: React.FC<{
|
||||
value: string;
|
||||
}> = ({ value }) => <TextInput value={value} disabled fullWidth />;
|
||||
```
|
||||
|
||||
```tsx
|
||||
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
|
||||
* component's props
|
||||
* - This method doesn't automatically include the children prop. If
|
||||
* you want to include it, you have to specify it in OwnProps.
|
||||
*/
|
||||
type EmailFieldProps = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
const EmailField = ({ value }: EmailFieldProps) => (
|
||||
<TextInput value={value} disabled fullWidth />
|
||||
);
|
||||
```
|
||||
|
||||
#### No Single Variable Prop Spreading in JSX Elements
|
||||
|
||||
Avoid using single variable prop spreading in JSX elements, like `{...props}`. This practice often results in code that is less readable and harder to maintain because it's unclear which props the component is receiving.
|
||||
|
||||
```tsx
|
||||
/* ❌ - Bad, spreads a single variable prop into the underlying component
|
||||
*/
|
||||
const MyComponent = (props: OwnProps) => {
|
||||
return <OtherComponent {...props} />;
|
||||
}
|
||||
```
|
||||
|
||||
```tsx
|
||||
/* ✅ - Good, Explicitly lists all props
|
||||
* - Enhances readability and maintainability
|
||||
*/
|
||||
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
|
||||
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
|
||||
};
|
||||
```
|
||||
|
||||
Rationale:
|
||||
- At a glance, it's clearer which props the code passes down, making it easier to understand and maintain.
|
||||
- It helps to prevent tight coupling between components via their props.
|
||||
- Linting tools make it easier to identify misspelled or unused props when you list props explicitly.
|
||||
|
||||
## JavaScript
|
||||
|
||||
### Use nullish-coalescing operator `??`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, can return 'default' even if value is 0 or ''
|
||||
const value = process.env.MY_VALUE || 'default';
|
||||
|
||||
// ✅ Good, will return 'default' only if value is null or undefined
|
||||
const value = process.env.MY_VALUE ?? 'default';
|
||||
```
|
||||
|
||||
### Use optional chaining `?.`
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
onClick && onClick();
|
||||
|
||||
// ✅ Good
|
||||
onClick?.();
|
||||
```
|
||||
|
||||
## TypeScript
|
||||
|
||||
### Use `type` instead of `interface`
|
||||
|
||||
Always use `type` instead of `interface`, because they almost always overlap, and `type` is more flexible.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
interface MyInterface {
|
||||
name: string;
|
||||
}
|
||||
|
||||
// ✅ Good
|
||||
type MyType = {
|
||||
name: string;
|
||||
};
|
||||
```
|
||||
|
||||
### Use string literals instead of enums
|
||||
|
||||
[String literals](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) are the go-to way to handle enum-like values in TypeScript. They are easier to extend with Pick and Omit, and offer a better developer experience, specially with code completion.
|
||||
|
||||
You can see why TypeScript recommends avoiding enums [here](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, utilizes an enum
|
||||
enum Color {
|
||||
Red = "red",
|
||||
Green = "green",
|
||||
Blue = "blue",
|
||||
}
|
||||
|
||||
let color = Color.Red;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, utilizes a string literal
|
||||
|
||||
let color: "red" | "green" | "blue" = "red";
|
||||
```
|
||||
|
||||
#### GraphQL and internal libraries
|
||||
|
||||
You should use enums that GraphQL codegen generates.
|
||||
|
||||
It's also better to use an enum when using an internal library, so the internal library doesn't have to expose a string literal type that is not related to the internal API.
|
||||
|
||||
Example:
|
||||
|
||||
```TSX
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
RelationPickerHotkeyScope.RelationPicker,
|
||||
);
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
### Use StyledComponents
|
||||
|
||||
Style the components with [styled-components](https://emotion.sh/docs/styled).
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
<div className="my-class">Hello World</div>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good
|
||||
const StyledTitle = styled.div`
|
||||
color: red;
|
||||
`;
|
||||
```
|
||||
|
||||
Prefix styled components with "Styled" to differentiate them from "real" components.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
const Title = styled.div`
|
||||
color: red;
|
||||
`;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good
|
||||
const StyledTitle = styled.div`
|
||||
color: red;
|
||||
`;
|
||||
```
|
||||
|
||||
### Theming
|
||||
|
||||
Utilizing the theme for the majority of component styling is the preferred approach.
|
||||
|
||||
#### Units of measurement
|
||||
|
||||
Avoid using `px` or `rem` values directly within the styled components. The necessary values are generally already defined in the theme, so it’s recommended to make use of the theme for these purposes.
|
||||
|
||||
#### Colors
|
||||
|
||||
Refrain from introducing new colors; instead, use the existing palette from the theme. Should there be a situation where the palette does not align, please leave a comment so that the team can rectify it.
|
||||
|
||||
|
||||
```tsx
|
||||
// ❌ Bad, directly specifies style values without utilizing the theme
|
||||
const StyledButton = styled.button`
|
||||
color: #333333;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
margin-left: 4px;
|
||||
border-radius: 50px;
|
||||
`;
|
||||
```
|
||||
|
||||
```tsx
|
||||
// ✅ Good, utilizes the theme
|
||||
const StyledButton = styled.button`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
border-radius: ${({ theme }) => theme.border.rounded};
|
||||
`;
|
||||
```
|
||||
## Enforcing No-Type Imports
|
||||
|
||||
Avoid type imports. To enforce this standard, an ESLint rule checks for and reports any type imports. This helps maintain consistency and readability in the TypeScript code.
|
||||
|
||||
```tsx
|
||||
// ❌ Bad
|
||||
import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
// ❌ Bad
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
// ✅ Good
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
```
|
||||
|
||||
### Why No-Type Imports
|
||||
|
||||
- **Consistency**: By avoiding type imports and using a single approach for both type and value imports, the codebase remains consistent in its module import style.
|
||||
|
||||
- **Readability**: No-type imports improve code readability by making it clear when you're importing values or types. This reduces ambiguity and makes it easier to understand the purpose of imported symbols.
|
||||
|
||||
- **Maintainability**: It enhances codebase maintainability because developers can identify and locate type-only imports when reviewing or modifying code.
|
||||
|
||||
### ESLint Rule
|
||||
|
||||
An ESLint rule, `@typescript-eslint/consistent-type-imports`, enforces the no-type import standard. This rule will generate errors or warnings for any type import violations.
|
||||
|
||||
Please note that this rule specifically addresses rare edge cases where unintentional type imports occur. TypeScript itself discourages this practice, as mentioned in the [TypeScript 3.8 release notes](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). In most situations, you should not need to use type-only imports.
|
||||
|
||||
To ensure your code complies with this rule, make sure to run ESLint as part of your development workflow.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
---
|
||||
title: Work with Figma
|
||||
info: Learn how you can collaborate with Twenty's Figma
|
||||
icon: TbBrandFigma
|
||||
image: /images/user-guide/objects/objects.png
|
||||
---
|
||||
|
||||
Figma is a collaborative interface design tool that aids in bridging the communication barrier between designers and developers.
|
||||
This guide explains how you can collaborate with Figma.
|
||||
|
||||
## Access
|
||||
|
||||
1. **Access the shared link:** You can access the project's Figma file [here](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
|
||||
2. **Sign in:** If you're not already signed in, Figma will prompt you to do so.
|
||||
Key features are only available to logged-in users, such as the developer mode and the ability to select a dedicated frame.
|
||||
|
||||
<ArticleWarning>
|
||||
|
||||
You will not be able to collaborate effectively without an account.
|
||||
|
||||
</ArticleWarning>
|
||||
|
||||
|
||||
## Figma structure
|
||||
|
||||
On the left sidebar, you can access the different pages of Twenty's Figma. This is how they're organized:
|
||||
|
||||
- **Components page:** This is the first page. The designer uses it to create and organize the reusable design elements used throughout the design file. For example, buttons, icons, symbols, or any other reusable components. It serves to maintain consistency across the design.
|
||||
- **Main page:** The second page is the main page, which shows the complete user interface of the project. You can press ***Play*** to use the full app prototype.
|
||||
- **Features pages:** The other pages are typically dedicated to features in progress. They contain the design of specific features or modules of the application or website. They are typically still in progress.
|
||||
|
||||
## Useful Tips
|
||||
|
||||
With read-only access, you can't edit the design, but you can access all features that will be useful to convert the designs into code.
|
||||
|
||||
### Use the Dev mode
|
||||
|
||||
Figma's Dev Mode enhances developers' productivity by providing easy design navigation, effective asset management, efficient communication tools, toolbox integrations, quick code snippets, and key layer information, bridging the gap between design and development. You can learn more about Dev Mode [here](https://www.figma.com/dev-mode/).
|
||||
|
||||
Switch to the "Developer" mode in the right part of the toolbar to see design specs, copy CSS, and access assets.
|
||||
|
||||
### Use the Prototype
|
||||
|
||||
Click on any element on the canvas and press the “Play” button at the top right edge of the interface to access the prototype view. Prototype mode allows you to interact with the design as if it were the final product. It demonstrates the flow between screens and how interface elements like buttons, links, or menus behave when interacted with.
|
||||
|
||||
1. **Understanding transitions and animations:** In the Prototype mode, you can view any transitions or animations added by a designer between screens or UI elements, providing clear visual instructions to developers on the intended behavior and style.
|
||||
2. **Implementation clarification:** A prototype can also help reduce ambiguities. Developers can interact with it to gain a better understanding of the functionality or appearance of particular elements.
|
||||
|
||||
For more comprehensive details and guidance on learning the Figma platform, you can visit the official [Figma Documentation](https://help.figma.com/hc/en-us).
|
||||
|
||||
### Measure distances
|
||||
|
||||
Select an element, hold `Option` key (Mac) or `Alt` key (Windows), then hover over another element to see the distance between them.
|
||||
|
||||
### Figma extension for VSCode (Recommended)
|
||||
|
||||
[Figma for VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension)
|
||||
lets you navigate and inspect design files, collaborate with designers, track changes, and speed up implementation - all without leaving your text editor.
|
||||
It's part of our recommended extensions.
|
||||
|
||||
## Collaboration
|
||||
|
||||
1. **Using Comments:** You are welcome to use the comment feature by clicking on the bubble icon in the left part of the toolbar.
|
||||
2. **Cursor chat:** A nice feature of Figma is the Cursor chat. Just press `;` on Mac and `/` on Windows to send a message if you see someone else using Figma as the same time as you.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Getting Started
|
||||
icon: IconUsers
|
||||
info: Discover Twenty, an open-source CRM, its features, benefits, system requirements, and how to get involved.
|
||||
image: /images/user-guide/what-is-twenty/20.png
|
||||
---
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: GraphQL APIs
|
||||
icon: TbRocket
|
||||
image: /images/user-guide/api/api.png
|
||||
info: The most powerful way to build integrations
|
||||
---
|
||||
|
|
@ -1,304 +0,0 @@
|
|||
---
|
||||
title: Local Setup
|
||||
icon: TbDeviceDesktop
|
||||
image: /images/user-guide/fields/field.png
|
||||
info: The guide for contributors (or curious developers) who want to run Twenty locally (on laptop, PC...)
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
<ArticleTabs label1="Linux and MacOS" label2="Windows (WSL)">
|
||||
<ArticleTab>
|
||||
|
||||
Before you can install and use Twenty, make sure you install the following on your computer:
|
||||
- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||
- [Node v24.5.0](https://nodejs.org/en/download)
|
||||
- [yarn v4](https://yarnpkg.com/getting-started/install)
|
||||
- [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
|
||||
|
||||
<ArticleWarning>
|
||||
`npm` won't work, you should use `yarn` instead. Yarn is now shipped with Node.js, so you don't need to install it separately.
|
||||
You only have to run `corepack enable` to enable Yarn if you haven't done it yet.
|
||||
</ArticleWarning>
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
1. Install WSL
|
||||
Open PowerShell as Administrator and run:
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
You should now see a prompt to restart your computer. If not, restart it manually.
|
||||
|
||||
Upon restart, a powershell window will open and install Ubuntu. This may take up some time.
|
||||
You'll see a prompt to create a username and password for your Ubuntu installation.
|
||||
|
||||
2. Install and configure git
|
||||
|
||||
```bash
|
||||
sudo apt-get install git
|
||||
|
||||
git config --global user.name "Your Name"
|
||||
|
||||
git config --global user.email "youremail@domain.com"
|
||||
```
|
||||
|
||||
3. Install nvm, node.js and yarn
|
||||
<ArticleWarning>
|
||||
|
||||
Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contributors use the same version.
|
||||
|
||||
</ArticleWarning>
|
||||
|
||||
```bash
|
||||
sudo apt-get install curl
|
||||
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
```
|
||||
Close and reopen your terminal to use nvm. Then run the following commands.
|
||||
|
||||
```bash
|
||||
|
||||
nvm install # installs recommended node version
|
||||
|
||||
nvm use # use recommended node version
|
||||
|
||||
corepack enable
|
||||
```
|
||||
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Git Clone
|
||||
|
||||
In your terminal, run the following command.
|
||||
|
||||
|
||||
<ArticleTabs label1="SSH (Recommended)" label2="HTTPS">
|
||||
<ArticleTab>
|
||||
If you haven't already set up SSH keys, you can learn how to do so [here](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
|
||||
```bash
|
||||
git clone git@github.com:twentyhq/twenty.git
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
|
||||
```bash
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
```
|
||||
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
## Step 2: Position yourself at the root
|
||||
|
||||
```bash
|
||||
cd twenty
|
||||
```
|
||||
|
||||
You should run all commands in the following steps from the root of the project.
|
||||
|
||||
## Step 3: Set up a PostgreSQL Database
|
||||
|
||||
<ArticleTabs label1="Linux" label2="Mac OS" label3="Windows (WSL)">
|
||||
<ArticleTab>
|
||||
<b>Option 1 (preferred):</b> To provision your database locally:
|
||||
Use the following link to install Postgresql on your Linux machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors.
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
```bash
|
||||
make postgres-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
<b>Option 1 (preferred):</b> To provision your database locally with `brew`:
|
||||
|
||||
```bash
|
||||
brew install postgresql@16
|
||||
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"
|
||||
brew services start postgresql@16
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
|
||||
You can verify if the PostgreSQL server is running by executing:
|
||||
```bash
|
||||
brew services list
|
||||
```
|
||||
|
||||
The installer might not create the `postgres` user by default when installing
|
||||
via Homebrew on MacOS. Instead, it creates a PostgreSQL role that matches your macOS
|
||||
username (e.g., "john").
|
||||
To check and create the `postgres` user if necessary, follow these steps:
|
||||
```bash
|
||||
# Connect to PostgreSQL
|
||||
psql postgres
|
||||
or
|
||||
psql -U $(whoami) -d postgres
|
||||
```
|
||||
|
||||
Once at the psql prompt (postgres=#), run:
|
||||
```bash
|
||||
# List existing PostgreSQL roles
|
||||
\du
|
||||
```
|
||||
You'll see output similar to:
|
||||
```bash
|
||||
Role name | Attributes | Member of
|
||||
-----------+-------------+-----------
|
||||
john | Superuser | {}
|
||||
```
|
||||
|
||||
If you do not see a `postgres` role listed, proceed to the next step.
|
||||
Create the `postgres` role manually:
|
||||
```bash
|
||||
CREATE ROLE postgres WITH SUPERUSER LOGIN;
|
||||
```
|
||||
This creates a superuser role named `postgres` with login access.
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
```bash
|
||||
make postgres-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
All the following steps are to be run in the WSL terminal (within your virtual machine)
|
||||
|
||||
<b>Option 1:</b> To provision your Postgresql locally:
|
||||
Use the following link to install Postgresql on your Linux virtual machine: [Postgresql Installation](https://www.postgresql.org/download/linux/)
|
||||
```bash
|
||||
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
|
||||
```
|
||||
Note: You might need to add `sudo -u postgres` to the command before `psql` to avoid permission errors.
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
Running Docker on WSL adds an extra layer of complexity.
|
||||
Only use this option if you are comfortable with the extra steps involved, including turning on [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
|
||||
```bash
|
||||
make postgres-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
You can now access the database at [localhost:5432](localhost:5432), with user `postgres` and password `postgres` .
|
||||
|
||||
## Step 4: Set up a Redis Database (cache)
|
||||
Twenty requires a redis cache to provide the best performance
|
||||
|
||||
<ArticleTabs label1="Linux" label2="Mac OS" label3="Windows (WSL)">
|
||||
<ArticleTab>
|
||||
<b>Option 1:</b> To provision your Redis locally:
|
||||
Use the following link to install Redis on your Linux machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
```bash
|
||||
make redis-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
<b>Option 1 (preferred):</b> To provision your Redis locally with `brew`:
|
||||
```bash
|
||||
brew install redis
|
||||
```
|
||||
Start your redis server:
|
||||
```brew services start redis```
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
```bash
|
||||
make redis-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
<b>Option 1:</b> To provision your Redis locally:
|
||||
Use the following link to install Redis on your Linux virtual machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
|
||||
|
||||
<b>Option 2:</b> If you have docker installed:
|
||||
```bash
|
||||
make redis-on-docker
|
||||
```
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
If you need a Client GUI, we recommend [redis insight](https://redis.io/insight/) (free version available)
|
||||
|
||||
## Step 5: Setup environment variables
|
||||
|
||||
Use environment variables or `.env` files to configure your project. More info [here](https://twenty.com/developers/section/self-hosting/setup)
|
||||
|
||||
Copy the `.env.example` files in `/front` and `/server`:
|
||||
```bash
|
||||
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
|
||||
cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
|
||||
```
|
||||
|
||||
## Step 6: Installing dependencies
|
||||
To build Twenty server and seed some data into your database, run the following command:
|
||||
```bash
|
||||
yarn
|
||||
```
|
||||
Note that `npm` or `pnpm` won't work
|
||||
|
||||
## Step 7: Running the project
|
||||
|
||||
<ArticleTabs label1="Linux" label2="Mac OS" label3="Windows (WSL)">
|
||||
<ArticleTab>
|
||||
Depending on your Linux distribution, Redis server might be started automatically.
|
||||
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
Redis should already be running. If not, run:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
</ArticleTab>
|
||||
<ArticleTab>
|
||||
Depending on your Linux distribution, Redis server might be started automatically.
|
||||
If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro.
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
|
||||
Set up your database with the following command:
|
||||
```bash
|
||||
npx nx database:reset twenty-server
|
||||
```
|
||||
|
||||
Start the server, the worker and the frontend services:
|
||||
```bash
|
||||
npx nx start twenty-server
|
||||
npx nx worker twenty-server
|
||||
npx nx start twenty-front
|
||||
```
|
||||
|
||||
Alternatively, you can start all services at once:
|
||||
```bash
|
||||
npx nx start
|
||||
```
|
||||
|
||||
## Step 8: Use Twenty
|
||||
|
||||
**Frontend**
|
||||
|
||||
Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001).
|
||||
You can log in using the default demo account: `tim@apple.dev` (password: `tim@apple.dev`)
|
||||
|
||||
**Backend**
|
||||
|
||||
- Twenty's server will be up and running at [http://localhost:3000](http://localhost:3000)
|
||||
- The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql)
|
||||
- The REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest)
|
||||
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions.
|
||||
|
||||
<ArticleEditContent />
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Rest APIs
|
||||
icon: TbRocket
|
||||
image: /images/user-guide/what-is-twenty/20.png
|
||||
info: The simplest way to build integrations
|
||||
---
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Self-Hosting
|
||||
icon: TbServer
|
||||
image: /images/user-guide/integrations/plug.png
|
||||
info: Learn how to host Twenty on your own server
|
||||
---
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
---
|
||||
title: Other methods
|
||||
icon: TbCloud
|
||||
image: /images/user-guide/notes/notes_header.png
|
||||
---
|
||||
|
||||
<ArticleWarning>
|
||||
This document is maintained by the community. It might contain issues.
|
||||
</ArticleWarning>
|
||||
|
||||
## Kubernetes via Terraform and Manifests
|
||||
|
||||
Community-led documentation for Kubernetes deployment is available [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s)
|
||||
|
||||
### Coolify
|
||||
|
||||
Deploy Twenty on servers using Coolify. (official image on Coolify will be available soon)
|
||||
|
||||
[Coolify documentation](https://coolify.io/docs/get-started/introduction)
|
||||
|
||||
|
||||
### EasyPanel
|
||||
|
||||
Deploy Twenty on EasyPanel with the community maintained template below.
|
||||
|
||||
[Deploy on EasyPanel](https://easypanel.io/docs/templates/twenty)
|
||||
|
||||
### Elest.io
|
||||
|
||||
Deploy Twenty on servers with Elest.io using link below.
|
||||
|
||||
[Deploy on Elest.io](https://elest.io/open-source/twenty)
|
||||
|
||||
### Twenty on Railway
|
||||
|
||||
Deploy Twenty on Railway with the community maintained template below.
|
||||
|
||||
[](https://railway.com/deploy/nAL3hA)
|
||||
|
||||
## Others
|
||||
|
||||
Please feel free to Open a PR to add more Cloud Provider options.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,199 +0,0 @@
|
|||
---
|
||||
title: 1-Click w/ Docker Compose
|
||||
icon: TbBrandDocker
|
||||
image: /images/user-guide/objects/objects.png
|
||||
---
|
||||
|
||||
<ArticleWarning>
|
||||
Docker containers are for production hosting or self-hosting, for the contribution please check the [Local Setup](https://twenty.com/developers/local-setup).
|
||||
</ArticleWarning>
|
||||
|
||||
## Overview
|
||||
|
||||
This guide provides step-by-step instructions to install and configure the Twenty application using Docker Compose. The aim is to make the process straightforward and prevent common pitfalls that could break your setup.
|
||||
|
||||
**Important:** Only modify settings explicitly mentioned in this guide. Altering other configurations may lead to issues.
|
||||
|
||||
See docs [Setup Environment Variables](https://twenty.com/developers/section/self-hosting/setup) for advanced configuration. All environment variables must be declared in the docker-compose.yml file at the server and / or worker level depending on the variable.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- RAM: Ensure your environment has at least 2GB of RAM. Insufficient memory can cause processes to crash.
|
||||
- Docker & Docker Compose: Make sure both are installed and up-to-date.
|
||||
|
||||
## Option 1: One-line script
|
||||
|
||||
Install the latest stable version of Twenty with a single command:
|
||||
```bash
|
||||
bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
|
||||
```
|
||||
|
||||
To install a specific version or branch:
|
||||
```bash
|
||||
VERSION=vx.y.z BRANCH=branch-name bash <(curl -sL https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/scripts/install.sh)
|
||||
```
|
||||
- Replace x.y.z with the desired version number.
|
||||
- Replace branch-name with the name of the branch you want to install.
|
||||
|
||||
## Option 2: Manual steps
|
||||
Follow these steps for a manual setup.
|
||||
|
||||
### Step 1: Set Up the Environment File
|
||||
|
||||
1. **Create the .env File**
|
||||
|
||||
Copy the example environment file to a new .env file in your working directory:
|
||||
```bash
|
||||
curl -o .env https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/.env.example
|
||||
```
|
||||
|
||||
2. **Generate Secret Tokens**
|
||||
|
||||
Run the following command to generate a unique random string:
|
||||
```bash
|
||||
openssl rand -base64 32
|
||||
```
|
||||
**Important:** Keep this value secret / do not share it.
|
||||
|
||||
3. **Update the `.env`**
|
||||
|
||||
Replace the placeholder value in your .env file with the generated token:
|
||||
|
||||
```ini
|
||||
APP_SECRET=first_random_string
|
||||
```
|
||||
|
||||
4. **Set the Postgres Password**
|
||||
|
||||
Update the `PG_DATABASE_PASSWORD` value in the .env file with a strong password without special characters.
|
||||
|
||||
```ini
|
||||
PG_DATABASE_PASSWORD=my_strong_password
|
||||
```
|
||||
|
||||
### Step 2: Obtain the Docker Compose File
|
||||
|
||||
Download the `docker-compose.yml` file to your working directory:
|
||||
|
||||
```bash
|
||||
curl -o docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/refs/heads/main/packages/twenty-docker/docker-compose.yml
|
||||
```
|
||||
|
||||
### Step 3: Launch the Application
|
||||
|
||||
Start the Docker containers:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
### Step 4: Access the Application
|
||||
|
||||
If you host twentyCRM on your own computer, open your browser and navigate to [http://localhost:3000](http://localhost:3000).
|
||||
|
||||
If you host it on a server, check that the server is running and that everything is ok with
|
||||
```bash
|
||||
curl http://localhost:3000
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Expose Twenty to External Access
|
||||
|
||||
By default, Twenty runs on `localhost` at port `3000`. To access it via an external domain or IP address, you need to configure the `SERVER_URL` in your `.env` file.
|
||||
|
||||
#### Understanding `SERVER_URL`
|
||||
|
||||
- **Protocol:** Use `http` or `https` depending on your setup.
|
||||
- Use `http` if you haven't set up SSL.
|
||||
- Use `https` if you have SSL configured.
|
||||
- **Domain/IP:** This is the domain name or IP address where your application is accessible.
|
||||
- **Port:** Include the port number if you're not using the default ports (`80` for `http`, `443` for `https`).
|
||||
|
||||
### SSL Requirements
|
||||
|
||||
SSL (HTTPS) is required for certain browser features to work properly. While these features might work during local development (as browsers treat localhost differently), a proper SSL setup is needed when hosting Twenty on a regular domain.
|
||||
|
||||
For example, the clipboard API might require a secure context - some features like copy buttons throughout the application might not work without HTTPS enabled.
|
||||
|
||||
We strongly recommend setting up Twenty behind a reverse proxy with SSL termination for optimal security and functionality.
|
||||
|
||||
#### Configuring `SERVER_URL`
|
||||
|
||||
1. **Determine Your Access URL**
|
||||
- **Without Reverse Proxy (Direct Access):**
|
||||
|
||||
If you're accessing the application directly without a reverse proxy:
|
||||
```ini
|
||||
SERVER_URL=http://your-domain-or-ip:3000
|
||||
```
|
||||
|
||||
- **With Reverse Proxy (Standard Ports):**
|
||||
|
||||
If you're using a reverse proxy like Nginx or Traefik and have SSL configured:
|
||||
```ini
|
||||
SERVER_URL=https://your-domain-or-ip
|
||||
```
|
||||
|
||||
- **With Reverse Proxy (Custom Ports):**
|
||||
|
||||
If you're using non-standard ports:
|
||||
```ini
|
||||
SERVER_URL=https://your-domain-or-ip:custom-port
|
||||
````
|
||||
|
||||
2. **Update the `.env` File**
|
||||
|
||||
Open your `.env` file and update the `SERVER_URL`:
|
||||
|
||||
```ini
|
||||
SERVER_URL=http(s)://your-domain-or-ip:your-port
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
- Direct access without SSL:
|
||||
```ini
|
||||
SERVER_URL=http://123.45.67.89:3000
|
||||
```
|
||||
- Access via domain with SSL:
|
||||
```ini
|
||||
SERVER_URL=https://mytwentyapp.com
|
||||
```
|
||||
|
||||
3. **Restart the Application**
|
||||
|
||||
For changes to take effect, restart the Docker containers:
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### Considerations
|
||||
|
||||
- **Reverse Proxy Configuration:**
|
||||
|
||||
Ensure your reverse proxy forwards requests to the correct internal port (`3000` by default). Configure SSL termination and any necessary headers.
|
||||
|
||||
- **Firewall Settings:**
|
||||
|
||||
Open necessary ports in your firewall to allow external access.
|
||||
|
||||
- **Consistency:**
|
||||
|
||||
The `SERVER_URL` must match how users access your application in their browsers.
|
||||
|
||||
#### Persistence
|
||||
|
||||
- **Data Volumes:**
|
||||
|
||||
The Docker Compose configuration uses volumes to persist data for the database and server storage.
|
||||
|
||||
- **Stateless Environments:**
|
||||
|
||||
If deploying to a stateless environment (e.g., certain cloud services), configure external storage to persist data.
|
||||
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any problem, check [Troubleshooting](https://twenty.com/developers/section/self-hosting/troubleshooting) for solutions.
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,239 +0,0 @@
|
|||
---
|
||||
title: Setup
|
||||
icon: TbServer
|
||||
image: /images/user-guide/table-views/table.png
|
||||
---
|
||||
|
||||
import OptionTable from '@site/src/theme/OptionTable'
|
||||
|
||||
# Configuration Management
|
||||
|
||||
<ArticleWarning>
|
||||
**First time installing?** Follow the [Docker Compose installation guide](https://twenty.com/developers/section/self-hosting/docker-compose) to get Twenty running, then return here for configuration.
|
||||
</ArticleWarning>
|
||||
|
||||
Twenty offers **two configuration modes** to suit different deployment needs:
|
||||
|
||||
**Admin panel access:** Only users with admin privileges (`canAccessFullAdminPanel: true`) can access the configuration interface.
|
||||
|
||||
## 1. Admin Panel Configuration (Default)
|
||||
|
||||
```bash
|
||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # default
|
||||
```
|
||||
|
||||
**Most configuration happens through the UI** after installation:
|
||||
|
||||
1. Access your Twenty instance (usually `http://localhost:3000`)
|
||||
2. Go to **Settings / Admin Panel / Configuration Variables**
|
||||
3. Configure integrations, email, storage, and more
|
||||
4. Changes take effect immediately (within 15 seconds for multi-container deployments)
|
||||
|
||||
<ArticleWarning>
|
||||
**Multi-Container Deployments:** When using database configuration (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`), both server and worker containers read from the same database. Admin panel changes affect both automatically, eliminating the need to duplicate environment variables between containers (except for infrastructure variables).
|
||||
</ArticleWarning>
|
||||
|
||||
**What you can configure through the admin panel:**
|
||||
- **Authentication** - Google/Microsoft OAuth, password settings
|
||||
- **Email** - SMTP settings, templates, verification
|
||||
- **Storage** - S3 configuration, local storage paths
|
||||
- **Integrations** - Gmail, Google Calendar, Microsoft services
|
||||
- **Workflow & Rate Limiting** - Execution limits, API throttling
|
||||
- **And much more...**
|
||||
|
||||

|
||||
|
||||
<ArticleWarning>
|
||||
Each variable is documented with descriptions in your admin panel at **Settings → Admin Panel → Configuration Variables**.
|
||||
Some infrastructure settings like database connections (`PG_DATABASE_URL`), server URLs (`SERVER_URL`), and app secrets (`APP_SECRET`) can only be configured via `.env` file.
|
||||
|
||||
[Complete technical reference →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</ArticleWarning>
|
||||
|
||||
## 2. Environment-Only Configuration
|
||||
|
||||
```bash
|
||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED=false
|
||||
```
|
||||
|
||||
**All configuration managed through `.env` files:**
|
||||
|
||||
1. Set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false` in your `.env` file
|
||||
2. Add all configuration variables to your `.env` file
|
||||
3. Restart containers for changes to take effect
|
||||
4. Admin panel will show current values but cannot modify them
|
||||
|
||||
## Gmail & Google Calendar Integration
|
||||
|
||||
### Create Google Cloud Project
|
||||
|
||||
1. Go to [Google Cloud Console](https://console.cloud.google.com/)
|
||||
2. Create a new project or select existing one
|
||||
3. Enable these APIs:
|
||||
- [Gmail API](https://console.cloud.google.com/apis/library/gmail.googleapis.com)
|
||||
- [Google Calendar API](https://console.cloud.google.com/apis/library/calendar-json.googleapis.com)
|
||||
- [People API](https://console.cloud.google.com/apis/library/people.googleapis.com)
|
||||
|
||||
### Configure OAuth
|
||||
|
||||
1. Go to [Credentials](https://console.cloud.google.com/apis/credentials)
|
||||
2. Create OAuth 2.0 Client ID
|
||||
3. Add these redirect URIs:
|
||||
- `https://<your-domain>/auth/google/redirect` (for SSO)
|
||||
- `https://<your-domain>/auth/google-apis/get-access-token` (for integrations)
|
||||
|
||||
### Configure in Twenty
|
||||
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Google Auth** section
|
||||
3. Set these variables:
|
||||
- `MESSAGING_PROVIDER_GMAIL_ENABLED=true`
|
||||
- `CALENDAR_PROVIDER_GOOGLE_ENABLED=true`
|
||||
- `AUTH_GOOGLE_CLIENT_ID=<client-id>`
|
||||
- `AUTH_GOOGLE_CLIENT_SECRET=<client-secret>`
|
||||
- `AUTH_GOOGLE_CALLBACK_URL=https://<your-domain>/auth/google/redirect`
|
||||
- `AUTH_GOOGLE_APIS_CALLBACK_URL=https://<your-domain>/auth/google-apis/get-access-token`
|
||||
|
||||
<ArticleWarning>
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</ArticleWarning>
|
||||
|
||||
**Required scopes** (automatically configured):
|
||||
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts#L4-L10)
|
||||
- `https://www.googleapis.com/auth/calendar.events`
|
||||
- `https://www.googleapis.com/auth/gmail.readonly`
|
||||
- `https://www.googleapis.com/auth/profile.emails.read`
|
||||
|
||||
### If your app is in test mode
|
||||
|
||||
If your app is in test mode, you will need to add test users to your project.
|
||||
|
||||
Under [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent), add your test users to the "Test users" section.
|
||||
|
||||
## Microsoft 365 Integration
|
||||
|
||||
<ArticleWarning>
|
||||
Users must have a [Microsoft 365 Licence](https://admin.microsoft.com/Adminportal/Home) to be able to use the Calendar and Messaging API. They will not be able to sync their account on Twenty without one.
|
||||
</ArticleWarning>
|
||||
|
||||
### Create a project in Microsoft Azure
|
||||
|
||||
You will need to create a project in [Microsoft Azure](https://portal.azure.com/#view/Microsoft_AAD_IAM/AppGalleryBladeV2) and get the credentials.
|
||||
|
||||
### Enable APIs
|
||||
|
||||
On Microsoft Azure Console enable the following APIs in "Permissions":
|
||||
|
||||
- Microsoft Graph: Mail.ReadWrite
|
||||
- Microsoft Graph: Mail.Send
|
||||
- Microsoft Graph: Calendars.Read
|
||||
- Microsoft Graph: User.Read
|
||||
- Microsoft Graph: openid
|
||||
- Microsoft Graph: email
|
||||
- Microsoft Graph: profile
|
||||
- Microsoft Graph: offline_access
|
||||
|
||||
Note: "Mail.ReadWrite" and "Mail.Send" are only mandatory if you want to send emails using our workflow actions. You can use "Mail.Read" instead if you only want to receive emails.
|
||||
|
||||
### Authorized redirect URIs
|
||||
|
||||
You need to add the following redirect URIs to your project:
|
||||
- `https://<your-domain>/auth/microsoft/redirect` if you want to use Microsoft SSO
|
||||
- `https://<your-domain>/auth/microsoft-apis/get-access-token`
|
||||
|
||||
### Configure in Twenty
|
||||
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Microsoft Auth** section
|
||||
3. Set these variables:
|
||||
- `MESSAGING_PROVIDER_MICROSOFT_ENABLED=true`
|
||||
- `CALENDAR_PROVIDER_MICROSOFT_ENABLED=true`
|
||||
- `AUTH_MICROSOFT_ENABLED=true`
|
||||
- `AUTH_MICROSOFT_CLIENT_ID=<client-id>`
|
||||
- `AUTH_MICROSOFT_CLIENT_SECRET=<client-secret>`
|
||||
- `AUTH_MICROSOFT_CALLBACK_URL=https://<your-domain>/auth/microsoft/redirect`
|
||||
- `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://<your-domain>/auth/microsoft-apis/get-access-token`
|
||||
|
||||
<ArticleWarning>
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</ArticleWarning>
|
||||
|
||||
### Configure scopes
|
||||
[See relevant source code](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/auth/utils/get-microsoft-apis-oauth-scopes.ts#L2-L9)
|
||||
- 'openid'
|
||||
- 'email'
|
||||
- 'profile'
|
||||
- 'offline_access'
|
||||
- 'Mail.ReadWrite'
|
||||
- 'Mail.Send'
|
||||
- 'Calendars.Read'
|
||||
|
||||
### If your app is in test mode
|
||||
|
||||
If your app is in test mode, you will need to add test users to your project.
|
||||
|
||||
Add your test users to the "Users and groups" section.
|
||||
|
||||
## Background Jobs for Calendar & Messaging
|
||||
|
||||
After configuring Gmail, Google Calendar, or Microsoft 365 integrations, you need to start the background jobs that sync data.
|
||||
|
||||
Register the following recurring jobs in your worker container:
|
||||
```bash
|
||||
# from your worker container
|
||||
yarn command:prod cron:messaging:messages-import
|
||||
yarn command:prod cron:messaging:message-list-fetch
|
||||
yarn command:prod cron:calendar:calendar-event-list-fetch
|
||||
yarn command:prod cron:calendar:calendar-events-import
|
||||
yarn command:prod cron:messaging:ongoing-stale
|
||||
yarn command:prod cron:calendar:ongoing-stale
|
||||
yarn command:prod cron:workflow:automated-cron-trigger
|
||||
```
|
||||
|
||||
## Email Configuration
|
||||
|
||||
1. Go to **Settings → Admin Panel → Configuration Variables**
|
||||
2. Find the **Email** section
|
||||
3. Configure your SMTP settings:
|
||||
|
||||
<ArticleTabs label1="Gmail" label2="Office365" label3="Smtp4dev">
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
You will need to provision an [App Password](https://support.google.com/accounts/answer/185833).
|
||||
- EMAIL_DRIVER=smtp
|
||||
- EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
- EMAIL_SMTP_PORT=465
|
||||
- EMAIL_SMTP_USER=gmail_email_address
|
||||
- EMAIL_SMTP_PASSWORD='gmail_app_password'
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
Keep in mind that if you have 2FA enabled, you will need to provision an [App Password](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
|
||||
- EMAIL_DRIVER=smtp
|
||||
- EMAIL_SMTP_HOST=smtp.office365.com
|
||||
- EMAIL_SMTP_PORT=587
|
||||
- EMAIL_SMTP_USER=office365_email_address
|
||||
- EMAIL_SMTP_PASSWORD='office365_password'
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
**smtp4dev** is a fake SMTP email server for development and testing.
|
||||
- Run the smtp4dev image: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
|
||||
- Access the smtp4dev ui here: [http://localhost:8090](http://localhost:8090)
|
||||
- Set the following variables:
|
||||
- EMAIL_DRIVER=smtp
|
||||
- EMAIL_SMTP_HOST=localhost
|
||||
- EMAIL_SMTP_PORT=2525
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
</ArticleTabs>
|
||||
|
||||
<ArticleWarning>
|
||||
**Environment-only mode:** If you set `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`, add these variables to your `.env` file instead.
|
||||
</ArticleWarning>
|
||||
|
|
@ -1,218 +0,0 @@
|
|||
---
|
||||
title: Troubleshooting
|
||||
icon: TbCloud
|
||||
image: /images/user-guide/what-is-twenty/20.png
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you encounter any problem while setting up environment for development, upgrading your instance or self-hosting,
|
||||
here are some solutions for common problems.
|
||||
|
||||
### Self-hosting
|
||||
|
||||
#### First install results in `password authentication failed for user "postgres"`
|
||||
|
||||
🚨 **IMPORTANT: This solution is ONLY for fresh installations** 🚨
|
||||
If you have an existing Twenty instance with production data, **DO NOT** follow these steps as they will permanently delete your database!
|
||||
|
||||
While installing Twenty for the first time, you might want to change the default database password.
|
||||
The password you set during the first installation becomes permanently stored in the database volume. If you later try to change this password in your configuration without removing the old volume, you'll get authentication errors because the database is still using the original password.
|
||||
|
||||
⚠️ WARNING: Following steps will PERMANENTLY DELETE all database data! ⚠️
|
||||
Only proceed if this is a fresh installation with no important data.
|
||||
|
||||
In order to update the `PG_DATABASE_PASSWORD` you need to:
|
||||
```sh
|
||||
# Update the PG_DATABASE_PASSWORD in .env
|
||||
docker compose down --volumes
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
#### CR line breaks found [Windows]
|
||||
|
||||
This is due to the line break characters of Windows and the git configuration. Try running:
|
||||
|
||||
```
|
||||
git config --global core.autocrlf false
|
||||
```
|
||||
|
||||
Then delete the repository and clone it again.
|
||||
|
||||
#### Missing metadata schema
|
||||
|
||||
During Twenty installation, you need to provision your postgres database with the right schemas, extensions, and users.
|
||||
If you're successful in running this provisioning, you should have `default` and `metadata` schemas in your database.
|
||||
If you don't, make sure you don't have more than one postgres instance running on your computer.
|
||||
|
||||
#### Cannot find module 'twenty-emails' or its corresponding type declarations.
|
||||
|
||||
You have to build the package `twenty-emails` before running the initialization of the database with `npx nx run twenty-emails:build`
|
||||
|
||||
#### Missing twenty-x package
|
||||
|
||||
Make sure to run yarn in the root directory and then run `npx nx server:dev twenty-server`. If this still doesn't work try building the missing package manually.
|
||||
|
||||
#### Lint on Save not working
|
||||
|
||||
This should work out of the box with the eslint extension installed. If this doesn't work try adding this to your vscode setting (on the dev container scope):
|
||||
|
||||
```
|
||||
"editor.codeActionsOnSave": {
|
||||
|
||||
"source.fixAll.eslint": "explicit"
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
#### While running `npx nx start` or `npx nx start twenty-front`, Out of memory error is thrown
|
||||
|
||||
In `packages/twenty-front/.env` uncomment `VITE_DISABLE_TYPESCRIPT_CHECKER=true` and `VITE_DISABLE_ESLINT_CHECKER=true` to disable background checks thus reducing amount of needed RAM.
|
||||
|
||||
**If it does not work:**
|
||||
Run only the services you need, instead of `npx nx start`. For instance, if you work on the server, run only `npx nx worker twenty-server`
|
||||
|
||||
**If it does not work:**
|
||||
If you tried to run only `npx nx run twenty-server:start` on WSL and it's failing with the below memory error:
|
||||
|
||||
`FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory`
|
||||
|
||||
Workaround is to execute below command in terminal or add it in .bashrc profile to get setup automatically:
|
||||
|
||||
`export NODE_OPTIONS="--max-old-space-size=8192"`
|
||||
|
||||
The --max-old-space-size=8192 flag sets an upper limit of 8GB for the Node.js heap; usage scales with application demand.
|
||||
Reference: https://stackoverflow.com/questions/56982005/where-do-i-set-node-options-max-old-space-size-2048
|
||||
|
||||
**If it does not work:**
|
||||
Investigate which processes are taking you most of your machine RAM. At Twenty, we noticed that some VScode extensions were taking a lot of RAM so we temporarily disable them.
|
||||
|
||||
**If it does not work:**
|
||||
Restart your machine helps to clean up ghost processes.
|
||||
|
||||
#### While running `npx nx start` there are weird [0] and [1] in logs
|
||||
|
||||
That's expected as command `npx nx start` is running more commands under the hood
|
||||
|
||||
#### No emails are sent
|
||||
Most of the time, it's because the `worker` is not running in the background. Try to run
|
||||
```
|
||||
npx nx worker twenty-server
|
||||
```
|
||||
|
||||
#### Cannot connect my Microsoft 365 account
|
||||
|
||||
Most of the time, it's because your admin has not enabled the Microsoft 365 Licence for your account. Check [https://admin.microsoft.com/](https://admin.microsoft.com/Adminportal/Home).
|
||||
|
||||
If you have an error code `AADSTS50020`, it probably means that you are using a personal Microsoft account. This is not supported yet. More info [here](https://learn.microsoft.com/fr-fr/troubleshoot/entra/entra-id/app-integration/error-code-aadsts50020-user-account-identity-provider-does-not-exist)
|
||||
|
||||
#### While running `yarn` warnings appear in console
|
||||
|
||||
Warnings are informing about pulling additional dependencies which aren't explicitly stated in `package.json`, so as long as no breaking error appears, everything should work as expected.
|
||||
|
||||
#### When user accesses login page, error about unauthorized user trying to access workspace appears in logs
|
||||
|
||||
That's expected as user is unauthorized when logged out since its identity is not verified.
|
||||
|
||||
#### How to check if your worker is running?
|
||||
- Go to [webhook-test.com](https://webhook-test.com/) and copy **Your Unique Webhook URL**.
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test.jpg" alt="Webhook test" />
|
||||
</div>
|
||||
- Open your Twenty app, navigate to `/settings`, and enable the **Advanced** toggle at the bottom left of the screen.
|
||||
- Create a new webhook.
|
||||
- Paste **Your Unique Webhook URL** in the **Endpoint Url** field in Twenty. Set the **Filters** to `Companies` and `Created`.
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-settings.jpg" alt="Webhook settings" />
|
||||
</div>
|
||||
- Go to `/objects/companies` and create a new company record.
|
||||
- Return to [webhook-test.com](https://webhook-test.com/) and check if a new **POST request** has been received.
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/developers/self-hosting/webhook-test-result.jpg" alt="Webhook test result" />
|
||||
</div>
|
||||
- If a **POST request** is received, your worker is running successfully. Otherwise, you need to troubleshoot your worker.
|
||||
|
||||
#### Front-end fails to start and returns error TS5042: Option 'project' cannot be mixed with source files on a command line
|
||||
|
||||
Comment out checker plugin in `packages/twenty-ui/vite-config.ts` like in example below
|
||||
```
|
||||
plugins: [
|
||||
react({ jsxImportSource: '@emotion/react' }),
|
||||
tsconfigPaths(),
|
||||
svgr(),
|
||||
dts(dtsConfig),
|
||||
// checker(checkersConfig),
|
||||
wyw({
|
||||
include: [
|
||||
'**/OverflowingTextWithTooltip.tsx',
|
||||
'**/Chip.tsx',
|
||||
'**/Tag.tsx',
|
||||
'**/Avatar.tsx',
|
||||
'**/AvatarChip.tsx',
|
||||
],
|
||||
babelOptions: {
|
||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||
},
|
||||
}),
|
||||
],
|
||||
```
|
||||
|
||||
#### Admin panel not accessible
|
||||
|
||||
Run `UPDATE core."user" SET "canAccessFullAdminPanel" = TRUE WHERE email = 'you@yourdomain.com';` in database container to get access to admin panel.
|
||||
|
||||
|
||||
|
||||
### 1-click Docker compose
|
||||
|
||||
#### Unable to Log In
|
||||
|
||||
If you can't log in after setup:
|
||||
1. Run the following commands:
|
||||
```bash
|
||||
docker exec -it twenty-server-1 yarn
|
||||
docker exec -it twenty-server-1 npx nx database:reset --configuration=no-seed
|
||||
```
|
||||
2. Restart the Docker containers:
|
||||
```bash
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Note the database:reset command will completely erase your database and recreate it from scratch.
|
||||
|
||||
#### Connection Issues Behind a Reverse Proxy
|
||||
|
||||
If you're running Twenty behind a reverse proxy and experiencing connection issues:
|
||||
|
||||
1. **Verify SERVER_URL:**
|
||||
|
||||
Ensure `SERVER_URL` in your `.env` file matches your external access URL, including `https` if SSL is enabled.
|
||||
|
||||
2. **Check Reverse Proxy Settings:**
|
||||
|
||||
- Confirm that your reverse proxy is correctly forwarding requests to the Twenty server.
|
||||
- Ensure headers like `X-Forwarded-For` and `X-Forwarded-Proto` are properly set.
|
||||
|
||||
3. **Restart Services:**
|
||||
|
||||
After making changes, restart both the reverse proxy and Twenty containers.
|
||||
|
||||
#### Error when uploading an image - permission denied
|
||||
|
||||
Switching the data folder ownership on the host from root to another user and group resolves this problem.
|
||||
|
||||
## Getting Help
|
||||
|
||||
If you encounter issues not covered in this guide:
|
||||
|
||||
- Check Logs:
|
||||
|
||||
View container logs for error messages:
|
||||
```bash
|
||||
docker compose logs
|
||||
```
|
||||
|
||||
- Community Support:
|
||||
|
||||
Reach out to the [Twenty community](https://github.com/twentyhq/twenty/issues) or [support channels](https://discord.gg/cx5n4Jzs57) for assistance.
|
||||
|
|
@ -1,388 +0,0 @@
|
|||
---
|
||||
title: Upgrade guide
|
||||
icon: TbCloud
|
||||
image: /images/user-guide/notes/notes_header.png
|
||||
---
|
||||
|
||||
|
||||
## General guidelines
|
||||
|
||||
**Always make sure to back up your database before starting the upgrade process** by running `docker exec -it <db_container_name_or_id> pg_dumpall -U <postgres_user> > databases_backup.sql`.
|
||||
|
||||
To restore backup, run `cat databases_backup.sql | docker exec -i <db_container_name_or_id> psql -U <postgres_user>`.
|
||||
|
||||
If you used Docker Compose, follow these steps:
|
||||
|
||||
1. In a terminal, on the host where Twenty is running, turn off Twenty: `docker compose down`
|
||||
|
||||
2. Upgrade the version by changing the `TAG` value in the .env file near your docker-compose. ( We recommend consuming `major.minor` version such as `v0.53` )
|
||||
|
||||
3. Bring Twenty back online with `docker compose up -d`
|
||||
|
||||
If you want to upgrade your instance by few versions, e.g. from v0.33.0 to v0.35.0, you have to upgrade your instance sequentially, in this example from v0.33.0 to v0.34.0, then from v0.34.0 to v0.35.0.
|
||||
|
||||
**Make sure that after each upgraded version you have non-corrupted backup.**
|
||||
|
||||
## Version-specific upgrade steps
|
||||
|
||||
## v1.0
|
||||
|
||||
Hello Twenty v1.0! 🎉
|
||||
|
||||
## v0.60
|
||||
|
||||
### Performance Enhancements
|
||||
All interactions with the metadata API have been optimized for better performance, particularly for object metadata manipulation and workspace creation operations.
|
||||
|
||||
We've refactored our caching strategy to prioritize cache hits over database queries when possible, significantly improving the performance of metadata API operations.
|
||||
|
||||
If you encounter any runtime issues after upgrading, you may need to flush your cache to ensure it's synchronized with the latest changes. Run this command in your twenty-server container:
|
||||
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
```
|
||||
|
||||
### v0.55
|
||||
|
||||
Upgrade your Twenty instance to use v0.55 image
|
||||
|
||||
You don't need to run any command anymore, the new image will automatically care about running all required migrations.
|
||||
|
||||
|
||||
### `User does not have permission` error
|
||||
|
||||
If you encounter authorization errors on most requests after upgrading, you may need to flush your cache to recompute the latest permissions.
|
||||
|
||||
In your `twenty-server` container, run:
|
||||
```bash
|
||||
yarn command:prod cache:flush
|
||||
```
|
||||
|
||||
This issue is specific to this Twenty version and should not be required for future upgrades.
|
||||
|
||||
### v0.54
|
||||
|
||||
Since version `0.53`, no manual actions needed.
|
||||
|
||||
#### Metadata schema deprecation
|
||||
|
||||
We've merged the `metadata` schema into the `core` one to simplify data retrieval from `TypeORM`.
|
||||
We have merged the `migrate` command step within the `upgrade` command. We do not recommend running `migrate` manually within any of your server/worker containers.
|
||||
|
||||
### Since v0.53
|
||||
|
||||
Starting from `0.53`, upgrade is programmatically done within the `DockerFile`, this means from now on, you shouldn't have to run any command manually anymore.
|
||||
|
||||
Make sure to keep upgrading your instance sequentially, without skipping any major version (e.g. `0.43.3` to `0.44.0` is allowed, but `0.43.1` to `0.45.0` isn't), else could lead to workspace version desynchronization that could result in runtime error and missing functionality.
|
||||
|
||||
To check if a workspace has been correctly migrated you can review its version in database in `core.workspace` table.
|
||||
|
||||
It should always be in the range of your current Twenty's instance `major.minor` version, you can view your instance version in the admin panel (at `/settings/admin-panel`, accessible if your user has `canAccessFullAdminPanel` property set to true in the database) or by running `echo $APP_VERSION` in your `twenty-server` container.
|
||||
|
||||
|
||||
To fix a desynchronized workspace version, you will have to upgrade from the corresponding twenty's version following related upgrade guide sequentially and so on until it reaches desired version.
|
||||
|
||||
#### `auditLog` removal
|
||||
|
||||
We've removed the auditLog standard object, which means your backup size might be significantly reduced after this migration.
|
||||
|
||||
### v0.51 to v0.52
|
||||
|
||||
Upgrade your Twenty instance to use v0.52 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
#### I have a workspace blocked in version between `0.52.0` and `0.52.6`
|
||||
|
||||
Unfortunately `0.52.0` and `0.52.6` have been completely removed from dockerHub.
|
||||
You will have to manually update your workspace version to `0.51.0` in database and upgrade using twenty version `0.52.11` following its just above upgrade guide.
|
||||
|
||||
|
||||
### v0.50 to v0.51
|
||||
|
||||
Upgrade your Twenty instance to use v0.51 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.44.0 to v0.50.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.50.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
#### Docker-compose.yml mutation
|
||||
|
||||
This version includes a `docker-compose.yml` mutation to give `worker` service access to the `server-local-data` volume.
|
||||
Please update your local `docker-compose.yml` with [v0.50.0 docker-compose.yml](https://github.com/twentyhq/twenty/blob/v0.50.0/packages/twenty-docker/docker-compose.yml)
|
||||
|
||||
### v0.43.0 to v0.44.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.44.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
### v0.42.0 to v0.43.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.43.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade
|
||||
```
|
||||
|
||||
In this version, we have also switched to postgres:16 image in docker-compose.yml.
|
||||
|
||||
#### (Option 1) Database migration
|
||||
|
||||
Keeping the existing postgres-spilo image is fine, but you will have to freeze the version in your docker-compose.yml to be 0.43.0.
|
||||
|
||||
#### (Option 2) Database migration
|
||||
|
||||
If you want to migrate your database to the new postgres:16 image, please follow these steps:
|
||||
|
||||
1. Dump your database from the old postgres-spilo container
|
||||
|
||||
```
|
||||
docker exec -it twenty-db-1 sh
|
||||
pg_dump -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} > databases_backup.sql
|
||||
exit
|
||||
docker cp twenty-db-1:/home/postgres/databases_backup.sql .
|
||||
```
|
||||
|
||||
Make sure your dump file is not empty.
|
||||
|
||||
2. Upgrade your docker-compose.yml to use postgres:16 image as in the [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) file.
|
||||
|
||||
3. Restore the database to the new postgres:16 container
|
||||
|
||||
```
|
||||
docker cp databases_backup.sql twenty-db-1:/databases_backup.sql
|
||||
docker exec -it twenty-db-1 sh
|
||||
psql -U {YOUR_POSTGRES_USER} -d {YOUR_POSTGRES_DB} -f databases_backup.sql
|
||||
exit
|
||||
```
|
||||
|
||||
### v0.41.0 to v0.42.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.42.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.42
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `FRONT_PORT`, `FRONT_PROTOCOL`, `FRONT_DOMAIN`, `PORT`
|
||||
- Added: `FRONTEND_URL`, `NODE_PORT`, `MAX_NUMBER_OF_WORKSPACES_DELETED_PER_EXECUTION`, `MESSAGING_PROVIDER_MICROSOFT_ENABLED`, `CALENDAR_PROVIDER_MICROSOFT_ENABLED`, `IS_MICROSOFT_SYNC_ENABLED`
|
||||
|
||||
### v0.40.0 to v0.41.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.41.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.41
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `AUTH_MICROSOFT_TENANT_ID`
|
||||
|
||||
|
||||
### v0.35.0 to v0.40.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.40.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.40
|
||||
```
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Added: `IS_EMAIL_VERIFICATION_REQUIRED`, `EMAIL_VERIFICATION_TOKEN_EXPIRES_IN`, `WORKFLOW_EXEC_THROTTLE_LIMIT`, `WORKFLOW_EXEC_THROTTLE_TTL`
|
||||
|
||||
### v0.34.0 to v0.35.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.35.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.35
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.35` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- We replaced `ENABLE_DB_MIGRATIONS` with `DISABLE_DB_MIGRATIONS` (default value is now `false`, you probably don't have to set anything)
|
||||
|
||||
### v0.33.0 to v0.34.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.34.0 image
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.34
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.34` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
- Removed: `FRONT_BASE_URL`
|
||||
- Added: `FRONT_DOMAIN`, `FRONT_PROTOCOL`, `FRONT_PORT`
|
||||
|
||||
We have updated the way we handle the frontend URL.
|
||||
You can now set the frontend URL using the `FRONT_DOMAIN`, `FRONT_PROTOCOL` and `FRONT_PORT` variables.
|
||||
If FRONT_DOMAIN is not set, the frontend URL will fall back to `SERVER_URL`.
|
||||
|
||||
### v0.32.0 to v0.33.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.33.0 image
|
||||
|
||||
```
|
||||
yarn command:prod cache:flush
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.33
|
||||
```
|
||||
|
||||
The `yarn command:prod cache:flush` command will flush the Redis cache.
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.33` takes care of the data migration of all workspaces.
|
||||
|
||||
Starting from this version, twenty-postgres image for DB became deprecated and twenty-postgres-spilo is used instead.
|
||||
If you want to keep using twenty-postgres image, simply replace `twentycrm/twenty-postgres:${TAG}` with `twentycrm/twenty-postgres` in docker-compose.yml.
|
||||
|
||||
### v0.31.0 to v0.32.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.32.0 image
|
||||
|
||||
**Schema and data migration**
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.32
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.32` takes care of the data migration of all workspaces.
|
||||
|
||||
**Environment Variables**
|
||||
|
||||
We have updated the way we handle the Redis connection.
|
||||
|
||||
- Removed: `REDIS_HOST`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`
|
||||
- Added: `REDIS_URL`
|
||||
|
||||
Update your `.env` file to use the new `REDIS_URL` variable instead of the individual Redis connection parameters.
|
||||
|
||||
We have also simplified the way we handle the JWT tokens.
|
||||
|
||||
- Removed: `ACCESS_TOKEN_SECRET`, `LOGIN_TOKEN_SECRET`, `REFRESH_TOKEN_SECRET`, `FILE_TOKEN_SECRET`
|
||||
- Added: `APP_SECRET`
|
||||
|
||||
Update your `.env` file to use the new `APP_SECRET` variable instead of the individual tokens secrets (you can use the same secret as before or generate a new random string)
|
||||
|
||||
**Connected Account**
|
||||
|
||||
If you are using connected account to synchronize your Google emails and calendars, you will need to activate the [People API](https://developers.google.com/people) on your Google Admin console.
|
||||
|
||||
|
||||
### v0.30.0 to v0.31.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.31.0 image
|
||||
|
||||
**Schema and data migration**:
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.31
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.31` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.24.0 to v0.30.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.30.0 image
|
||||
|
||||
**Breaking change**:
|
||||
To enhance performances, Twenty now requires redis cache to be configured. We have updated our [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) to reflect this.
|
||||
Make sure to update your configuration and to update your environment variables accordingly:
|
||||
```
|
||||
REDIS_HOST={your-redis-host}
|
||||
REDIS_PORT={your-redis-port}
|
||||
CACHE_STORAGE_TYPE=redis
|
||||
```
|
||||
|
||||
**Schema and data migration**:
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.30
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.30` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.23.0 to v0.24.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.24.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.24
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas)
|
||||
The `yarn command:prod upgrade-0.24` takes care of the data migration of all workspaces.
|
||||
|
||||
### v0.22.0 to v0.23.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.23.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod upgrade-0.23
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod upgrade-0.23` takes care of the data migration, including transferring activities to tasks/notes.
|
||||
|
||||
### v0.21.0 to v0.22.0
|
||||
|
||||
Upgrade your Twenty instance to use v0.22.0 image
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```
|
||||
yarn database:migrate:prod
|
||||
yarn command:prod workspace:sync-metadata -f
|
||||
yarn command:prod upgrade-0.22
|
||||
```
|
||||
|
||||
The `yarn database:migrate:prod` command will apply the migrations to the Database.
|
||||
The `yarn command:prod workspace:sync-metadata -f` command will sync the definition of standard objects to the metadata tables and apply to required migrations to existing workspaces.
|
||||
The `yarn command:prod upgrade-0.22` command will apply specific data transformations to adapt to the new object defaultRequestInstrumentationOptions.
|
||||
|
||||
|
||||
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: UI Kit
|
||||
icon: TbServer
|
||||
image: /images/user-guide/table-views/table.png
|
||||
---
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Twenty UI
|
||||
icon: TbRocket
|
||||
image: /images/user-guide/objects/objects.png
|
||||
---
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
title: Storybook
|
||||
icon: TbRocket
|
||||
image: /images/user-guide/glossary/glossary.png
|
||||
---
|
||||
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
export const TWENTY_UI_INDEX = {
|
||||
Components: {
|
||||
Display: [
|
||||
{ fileName: 'display' },
|
||||
{ fileName: 'checkmark' },
|
||||
{ fileName: 'chip' },
|
||||
{ fileName: 'icons' },
|
||||
{ fileName: 'soon-pill' },
|
||||
{ fileName: 'tag' },
|
||||
{ fileName: 'app-tooltip' },
|
||||
],
|
||||
Feedback: [{ fileName: 'progress-bar' }],
|
||||
Input: [
|
||||
{ fileName: 'input' },
|
||||
{ fileName: 'buttons' },
|
||||
{ fileName: 'color-scheme' },
|
||||
{ fileName: 'text' },
|
||||
{ fileName: 'checkbox' },
|
||||
{ fileName: 'icon-picker' },
|
||||
{ fileName: 'image-input' },
|
||||
{ fileName: 'radio' },
|
||||
{ fileName: 'select' },
|
||||
{ fileName: 'toggle' },
|
||||
{ fileName: 'block-editor' },
|
||||
],
|
||||
Navigation: [
|
||||
{ fileName: 'navigation' },
|
||||
{ fileName: 'breadcrumb' },
|
||||
{ fileName: 'links' },
|
||||
{ fileName: 'menu-item' },
|
||||
{ fileName: 'navigation-bar' },
|
||||
{ fileName: 'step-bar' },
|
||||
],
|
||||
},
|
||||
Developers: {
|
||||
'Empty Section': [],
|
||||
},
|
||||
};
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
title: Display
|
||||
icon: IconUsers
|
||||
image: /images/user-guide/views/filter.png
|
||||
---
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
---
|
||||
title: App Tooltip
|
||||
icon: TbTooltip
|
||||
image: /images/user-guide/tips/light-bulb.png
|
||||
---
|
||||
|
||||
A brief message that displays additional information when a user interacts with an element.
|
||||
|
||||
|
||||
<ArticleTabs label1="Usage" label2="Props">
|
||||
<ArticleTab>
|
||||
|
||||
<SandpackEditor content={`import { AppTooltip } from "@/ui/display/tooltip/AppTooltip";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<p id="hoverText" style={{ display: "inline-block" }}>
|
||||
Customer Insights
|
||||
</p>
|
||||
<AppTooltip
|
||||
className
|
||||
anchorSelect="#hoverText"
|
||||
content="Explore customer behavior and preferences"
|
||||
delayHide={0}
|
||||
offset={6}
|
||||
noArrow={false}
|
||||
isOpen={true}
|
||||
place="bottom"
|
||||
positionStrategy="absolute"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};`} />
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
<ArticlePropsTable options={[
|
||||
['className', 'string', 'Optional CSS class for additional styling'],
|
||||
['anchorSelect', 'CSS selector', 'Selector for the tooltip anchor (the element that triggers the tooltip)'],
|
||||
['content', 'string', 'The content you want to display within the tooltip'],
|
||||
['delayHide', 'number', 'The delay in seconds before hiding the tooltip after the cursor leaves the anchor'],
|
||||
['offset', 'number', 'The offset in pixels for positioning the tooltip'],
|
||||
['noArrow', 'boolean', 'If `true`, hides the arrow on the tooltip'],
|
||||
['isOpen', 'boolean', 'If `true`, the tooltip is open by default'],
|
||||
['place', '`PlacesType` string from `react-tooltip`', 'Specifies the placement of the tooltip. Values include `bottom`, `left`, `right`, `top`, `top-start`, `top-end`, `right-start`, `right-end`, `bottom-start`, `bottom-end`, `left-start`, and `left-end`'],
|
||||
['positionStrategy', '`PositionStrategy` string from `react-tooltip`', 'Position strategy for the tooltip. Has two values: `absolute` and `fixed`']
|
||||
]} />
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
</ArticleTabs>
|
||||
|
||||
## Overflowing Text with Tooltip
|
||||
|
||||
Handles overflowing text and displays a tooltip when the text overflows.
|
||||
|
||||
<ArticleTabs label1="Usage" label2="Props">
|
||||
<ArticleTab>
|
||||
|
||||
<SandpackEditor content={`import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
const crmTaskDescription =
|
||||
'Follow up with client regarding their recent product inquiry. Discuss pricing options, address any concerns, and provide additional product information. Record the details of the conversation in the CRM for future reference.';
|
||||
|
||||
return <OverflowingTextWithTooltip text={crmTaskDescription} />;
|
||||
};`} />
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
<ArticlePropsTable options={[
|
||||
['text', 'string', 'The content you want to display in the overflowing text area']
|
||||
]} />
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
</ArticleTabs>
|
||||
|
||||
<ArticleEditContent></ArticleEditContent>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue