Merge branch 'main' into o365sync

This commit is contained in:
bosiraphael
2024-11-04 11:41:52 +01:00
495 changed files with 32295 additions and 28768 deletions

View File

@@ -223,4 +223,4 @@ jobs:
uses: ./.github/workflows/actions/nx-affected
with:
tag: scope:frontend
tasks: ${{ matrix.task }}
tasks: ${{ matrix.task }}

View File

@@ -41,10 +41,7 @@ jobs:
cp .env.example .env
echo "Generating secrets..."
echo "# === Randomly generated secrets ===" >>.env
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
echo "Starting server..."

34
.github/workflows/ci-tinybird.yaml vendored Normal file
View File

@@ -0,0 +1,34 @@
name: CI Tinybird
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: |
package.json
packages/twenty-tinybird/**
- name: Skip if no relevant changes
if: steps.changed-files.outputs.any_changed == 'false'
run: echo "No relevant changes. Skipping CI."
- name: Check twenty-tinybird package
uses: tinybirdco/ci/.github/workflows/ci.yml@main
with:
data_project_dir: packages/twenty-tinybird
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
tb_host: https://api.eu-central-1.aws.tinybird.co

View File

@@ -23,16 +23,9 @@ jobs:
if: github.event.action != 'closed'
steps:
- uses: actions/checkout@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'packages/twenty-utils/**'
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Utils / Run Danger.js
if: steps.changed-files.outputs.changed == 'true'
run: cd packages/twenty-utils && npx nx danger:ci
env:
DANGER_GITHUB_API_TOKEN: ${{ github.token }}
@@ -42,16 +35,9 @@ jobs:
if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps:
- uses: actions/checkout@v4
- name: Check for changed files
id: changed-files
uses: tj-actions/changed-files@v11
with:
files: 'packages/twenty-utils/**'
- name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
uses: ./.github/workflows/actions/yarn-install
- name: Run congratulate-dangerfile.js
if: steps.changed-files.outputs.changed == 'true'
run: cd packages/twenty-utils && npx nx danger:congratulate
env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -9,7 +9,6 @@
.nx/installation
.nx/cache
projectStructure.cache.json
.pnp.*
.yarn/*
@@ -30,3 +29,4 @@ storybook-static
.nyc_output
test-results/
dump.rdb
.tinyb

View File

@@ -1,12 +1,3 @@
<p align="center">
<a href="https://oss.gg/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/twentyhq/twenty/blob/33be2dbbe14eea00445010ecb9cd53ed603c01d5/packages/twenty-website/public/images/readme/Github%20Read-me%20banner.png">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/twentyhq/twenty/blob/33be2dbbe14eea00445010ecb9cd53ed603c01d5/packages/twenty-website/public/images/readme/Github%20Read-me%20banner.png">
<img src="./packages/twenty-website/public/images/readme/Github%20Read-me%20banner.png" alt="Hacktoberfest" />
</picture>
</a>
</p>
<br>
<p align="center">

View File

@@ -91,10 +91,7 @@ fi
# Generate random strings for secrets
echo "# === Randomly generated secrets ===" >>.env
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "LOGIN_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "REFRESH_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "FILE_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env
echo "APP_SECRET=$(openssl rand -base64 32)" >>.env
echo "" >>.env
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env

View File

@@ -1,23 +0,0 @@
**Side Quest**: Create a YouTube Video about Twenty showcasing a specific way to use Twenty effectively.
**Points**: 750 Points
**Proof**: Add your oss handle and YouTube video link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » YouTube Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) YouTube Link: [YouTube](https://twenty.com/)
» 19-October-2024 by [Thefool76](https://oss.gg/thefool76) YouTube Link: [YouTube](https://youtu.be/KuAycGuW698?si=q-YxcukbbYuO8BWf)
---

View File

@@ -1,27 +0,0 @@
**Side Quest**: Write a blog post about sharing your experience using Twenty in a detailed format on any platform.
**Points**: 750 Points
**Proof**: Add your oss handle and blog link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/)
» 19-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/twenty-crm-a-fresh-start-for-modern-businesses)
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/twenty-crm-a-fresh-start-for-modern-businesses-46kf)
» 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) blog Link: [blog](https://open.substack.com/pub/rajeevdewangan/p/comprehensive-guide-to-self-hosting?r=4lly3x&utm_campaign=post&utm_medium=web&showWelcomeOnShare=true)
» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/twenty-crm-modern-solution-for-modern-problems-a0b65fec9d6c)

View File

@@ -1,28 +0,0 @@
**Side Quest**: Write a blog post about self-hosting Twenty in a detailed format on any platform.
**Points**: 750 Points
**Proof**: Add your oss handle and blog link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/)
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/streamlined-self-hosting-with-twenty-crm-1-click-docker-compose-setup-188o)
» 24-October-2024 by [Shrey](https://oss.gg/shreykx) guide link : [https://github.com/shreykx/newfolder/blob/8046bc7373b8632b7fc2bfa28c360b86f8890a81/twentyguide.md]
» 23-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/a-detailed-guide-to-self-host-twenty-crm-on-you-local-server)
» 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/detailed-guide-on-self-hosting-twenty-crm-on-your-server-troubleshooting-and-best-practices-1f2ca15cd6eb)
---

View File

@@ -1,23 +0,0 @@
**Side Quest**: Create a promotional video for Twenty and share it on social media.
**Points**: 750 Points
**Proof**: Add your oss handle and video link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/)
» 24-October-2024 by [Thefool76](https://oss.gg/thefool76) video Link: [video](https://youtube.com/shorts/lC4oqm7UlCI?si=Md-nsfK9F6Shzjkv)
---

View File

@@ -1,35 +0,0 @@
**Side Quest**: Design a promotional poster of Twenty and share it on social media.
**Points**: 50 Points
**Proof**: Add your oss handle and poster link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » poster Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) poster Link: [poster](https://twenty.com/)
» 11-October-2024 by [thefool76](https://oss.gg/thefool76) poster Link: [poster](https://drive.google.com/file/d/1cIC1eitvY6zKVTXKq2LnVrS_2Ho9H8-P/view?usp=sharing)
» 12-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) poster Link: [poster](https://x.com/ion_finisher/status/1845168965963628802)
» 14-October-2024 by [AliYar-Khan](https://oss.gg/AliYar-Khan) poster Link: [poster](https://x.com/Mr_Programmer14/status/1845888855183884352)
» 16-October-2024 by [Harsh BHat](https://oss.gg/harshsbhat) poster Link: [poster](https://x.com/HarshBhatX/status/1846233330435477531)
» 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697)
» 20-October-2024 by [Naprila](https://oss.gg/Naprila) poster Link: [poster](https://x.com/mkprasad_821/status/1848037527921254625)
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) poster Link: [poster](https://x.com/sateshcharans/status/1848358958970396727)
» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) poster Link: [poster](https://drive.google.com/file/d/1IFtzwzKa0C_hT9cL4o3ChsKwVNRP33G_/view?usp=sharing) - [Tweet Link](https://x.com/zia_webdev/status/1848764487081619470)

View File

@@ -1,37 +0,0 @@
**Side Quest**: Design/Create new Twenty logo, tweet your design, and mention @twentycrm.
**Points**: 50 Points
**Proof**: Create a logo upload it on any of the platform and add your oss handle and logo link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » Logo Link: https://link.to/content » tweet Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 08-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) Logo Link: [logo](https://drive.google.com/drive/folders/13k22xMnX2fhnWK94vas_hO1t-ImqXcHZ?usp=drive_link) » tweet Link: [tweet](https://x.com/adityadeshlahre/status/1843354963176718374)
» 11-October-2024 by [thefool76](https://oss.gg/thefool76) Logo Link: [logo](https://drive.google.com/file/d/1DxSwNY_i90kGgWzPQj5SxScBz_6r02l4/view?usp=sharing) » tweet Link: [tweet](https://x.com/thefool1135/status/1844693487067034008)
» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) Logo Link: [logo](https://drive.google.com/drive/folders/1yaegQ7Hr8YraMNs50AHZmDprvzLn6A90?usp=sharing) » tweet Link: [tweet](https://x.com/zia_webdev/status/1848754055717212388)
» 13-October-2024 by [Atharva_404](https://oss.gg/Atharva-3000) Logo Link: [logo](https://drive.google.com/drive/folders/1XB7ELR7kPA4x7Fx5RQr8wo5etdZAZgcs?usp=drive_link) » tweet Link: [tweet](https://x.com/0x_atharva/status/1845421218914095453)
» 13-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) Logo Link: [logo](https://drive.google.com/file/d/1l9vE8CIjW9KfdioI5WKzxrdmvO8LR4j7/view?usp=drive_link) » tweet Link: [tweet](https://x.com/ion_finisher/status/1845466470429442163)
» 16-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) Logo Link: [logo](https://drive.google.com/file/d/1jmqwNvlSyWSY1-pCG63TAtDvCoVa8xg-/view?usp=sharing) » tweet Link: [tweet](https://x.com/HarshBhatX/status/1846234658712772977)
» 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) » tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201)
» 20-October-2024 by [Naprila](https://oss.gg/Naprila) Logo Link: [logo](https://drive.google.com/file/d/105fWXNtOkOPkU31AV0FDZKOdrJ8XLwBb/view?usp=drivesdk) » tweet Link: [tweet](https://x.com/mkprasad_821/status/1847978789713695133)
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Logo Link: [logo](https://drive.google.com/file/d/1fwvOcg8oQZC3NlTNV8EcyJxh9v_OYdpY/view?usp=sharing) » tweet Link: [tweet](https://x.com/sateshcharans/status/1848344729483690455)
---

View File

@@ -1,24 +0,0 @@
**Side Quest**: Duplicate the Figma file from the main repo and customize the variables to create a unique interface theme for Twenty. <br/>
**Points**: 750 Points <br/>
**Proof**: Add your oss handle and Figma link to the list below. <br/>
**Figma Link**: https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?t=YIFyswta6Xf6sSYK-0
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE] Figma Link: https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?t=YIFyswta6Xf6sSYK-0
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/)
» 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) Figma Link: [Figma](https://www.figma.com/design/XE21QdkFuy0IJHtmW7TURa/Twenty-(rajeevDewangan)?node-id=0-1&node-type=canvas&t=BYBulCT6hpJu6E8G-0)
» 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) Figma Link: [Figma](https://www.figma.com/design/HqYQrzel3e2TjzujwfdCXZ/Twenty-(Copy)---Khaan25?node-id=478-19796&t=QTB8gzKTudbVNeNs-1)
---

View File

@@ -1,22 +0,0 @@
**Side Quest**: Develop a script to facilitate the migration of data from another CRM to Twenty.
**Points**: 750 Points
**Proof**: Add your oss handle and record video and share link to the list below. In video show the working proof of your created script.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/)
» 22-October-2024 by [FaheemOnHub](https://oss.gg/FaheemOnHub) video Link: [video](https://drive.google.com/file/d/1bR59Q5gqoqHjzgdrF6K68U2hloexkQYM/view)
---

View File

@@ -1,21 +0,0 @@
**Side Quest**: Develop an integration for Raycast that enables users to create records on any object within Twenty directly from Raycast.
**Points**: 1500 Points
**Proof**: Add your oss handle and record video and share link to the list below. In video show the workflow of the your integration created and perform some task.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/)
---

View File

@@ -1,21 +0,0 @@
**Side Quest**: Create an n8n workflow that empowers Twenty by connecting it to another tool.
**Points**: 750 Points
**Proof**: Add your oss handle and template link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » template Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) template Link: [template](https://twenty.com/)
---

View File

@@ -1,21 +0,0 @@
**Side Quest**: Write a comprehensive guide on how to integrate Twenty with marketing automation tool (n8n, Zapier). Include a concrete use case and explain how to leverage AI to write API requests for non-developers and share it.
**Points**: 1500 Points
**Proof**: Add your oss handle and guide link to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR oss.gg HANDLE » guide Link: https://link.to/content
---
////////////////////////////
Your turn 👇
////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) guide Link: [guide](https://twenty.com/)
---

View File

@@ -1,62 +0,0 @@
**Side Quest**: Like & Re-Tweet oss.gg Launch Tweet. Quote-tweet it tagging @twentycrm to say youll be contributing.
**Points**: 50 Points
**Proof**: Add a screenshot of the retweet to the PR description. Add a link to your retweet in the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
» Link to Tweet: https://x.com/...
---
////////////////////////////
Your turn 👇
////////////////////////////
» 13-October-2024 by Vanshika Dargan
» Link to Tweet: https://x.com/VanshikaDargan/status/1845467453108949123
» 13-October-2024 by Utsav Bhattarai
» Link to Tweet: https://x.com/utsavdotdev/status/1845417863462649900
» 10-October-2024 by Devansh Baghel
» Link to Tweet: https://x.com/DevanshBaghel5/status/1844359648037748954
» 11-October-2024 by Bhavesh Mishra
» Link to Tweet: https://x.com/thefool1135/status/1844453425188405326
» 11-October-2024 by Chirag Arora
» Link to Tweet: https://x.com/Chirag8023/status/1844689900668682699
» 11-October-2024 by Aritra Sadhukhan
» Link to Tweet: https://x.com/AritraDevelops/status/1844670236512878646
» 13-October-2024 by Nabhag Motivaras
» Link to Tweet: https://x.com/NabhagMotivaras/status/1845449144695218357
» 13-October-2024 by Ali Yar Khan
» Link to Tweet: https://x.com/Mr_Programmer14/status/1845527862549577860
» 13-October-2024 by Yash Parmar
» Link to Tweet: https://x.com/yashp3020/status/1845720834716959009
» 16-October-2024 by Harsh Bhat
» Link to Tweet: https://x.com/HarshBhatX/status/1846252536241508392
» 20-October-2024 by Naprila
» Link to Tweet: https://x.com/mkprasad_821/status/1847886807314120762
» 22-October-2024 by Zia Ur Rehman Khan
» Link to Tweet: https://x.com/zia_webdev/status/1848659210243871165x
» 22-October-2024 by Ritansh Rajput
» Link to Tweet: https://x.com/Ritansh_Dev/status/1848641904511975838
» 23-October-2024 by Rajeev Dewangan
» Link to Tweet: https://x.com/rajeevdew/status/1849109074685907374

View File

@@ -1,39 +0,0 @@
**Side Quest**: Share a tweet about your favorite feature in Twenty. Tweet about your favorite feature in Twenty and mention @twentycrm.
**Points**: 50 Points
**Proof**: Add a screenshot of the tweet to the PR description. Add a link to your tweet in the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
» Link to Tweet: https://x.com/...
---
////////////////////////////
Your turn 👇
////////////////////////////
» 10-October-2024 by Devansh Baghel
» Link to Tweet: https://x.com/DevanshBaghel5/status/1844384722119704972
» 11-October-2024 by Bhavesh Mishra
» Link to Tweet: https://x.com/thefool1135/status/1844456500380696969
» 13-October-2024 by Ali Yar Khan
» Link to Tweet: https://x.com/Mr_Programmer14/status/1845530448245711197
» 16-October-2024 by Harsh Bhat
» Link to Tweet: https://x.com/HarshBhatX/status/1846075312691413066
» 20-October-2024 by Naprila
» Link to Tweet: https://x.com/mkprasad_821/status/1847895747707953205
» 22-October-2024 by Zia Ur Rehman Khan
» Link to Tweet: https://x.com/zia_webdev/status/1848660000190697633
» 23-October-2024 by Rajeev Dewangan
» Link to Tweet: https://x.com/rajeevdew/status/1849110473272442991

View File

@@ -1,23 +0,0 @@
**Side Quest**: Create a bug report. Use the Twenty bug issue template to report a bug in detail, including steps to reproduce it.
**Points**: 50-150 Points
**Proof**: Add a link to your bug report in the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
» Link to bug report: https://github.com/twentyhq/twenty/issues/...
---
////////////////////////////
Your turn 👇
////////////////////////////
» 10-October-2024 by Devansh Baghel
» Link to bug report: https://github.com/twentyhq/twenty/issues/7560
---

View File

@@ -1,48 +0,0 @@
**Side Quest**: Meme Magic: Craft a meme where the number twenty plays a role. Tweet it, and tag @twentycrm.
**Points**: 150 Points
**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
» Link to Tweet: https://x.com/...
---
////////////////////////////
Your turn 👇
////////////////////////////
» 10-October-2024 by Teddy ASSIH
» Link to Tweet: https://x.com/ion_finisher/status/1844389252253299173
» 11-October-2024 by Bhavesh Mishra
» Link to Tweet: https://x.com/thefool1135/status/1844458836402503931
» 12-October-2024 by Chirag Arora
» Link to Tweet: https://x.com/Chirag8023/status/1845108226527994222
» 13-October-2024 by Ali Yar Khan
» Link to Tweet: https://x.com/Mr_Programmer14/status/1845537662587072697
» 14-October-2024 by Yash Parmar
» Link to Tweet: [https://x.com/yashp3020/status/1845108226527994222](https://x.com/yashp3020/status/1845720142702842093)
» 16-October-2024 by Harsh Bhat
» Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899
» 20-October-2024 by Poorvi Bajpai
» Link to Tweet: https://x.com/poorvi_bajpai/status/1847881362038308992
» 20-October-2024 by Satesh Charan
» Link to Tweet: https://x.com/sateshcharans/status/1847760124267389357
» 20-October-2024 by Naprila
» Link to Tweet: https://x.com/mkprasad_821/status/1847900277510123706
» 22-October-2024 by Zia Ur Rehman Khan
» Link to Tweet: https://x.com/zia_webdev/status/1846954638953926675

View File

@@ -1,45 +0,0 @@
**Side Quest**: Gif Magic: Create a gif related to Twenty. Tweet it, and tag @twentycrm.
**Points**: 150 Points
**Proof**: Add a screenshot of GIF on Giphy to the PR description. Add a link to your GIPHY in the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
» Link to gif: https://giphy.com/...
---
////////////////////////////
Your turn 👇
////////////////////////////
» 10-October-2024 by Teddy ASSIH
» Link to gif: https://giphy.com/gifs/oss-crm-twenty-VWDHAIlGTbc6Nqdza9
» 11-October-2024 by Bhavesh Mishra
» Link to gif: https://shorturl.at/yln9H
» 12-October-2024 by Chirag Arora
» Link to gif: https://giphy.com/gifs/yCJIS2MGbBdifbnuj0
» 13-October-2024 by Nabhag Motivaras
» Link to gif: https://giphy.com/gifs/twenty-twentycrm-opensourcecrm-wCcsmnJuzzzGrfuf9B
» 15-October-2024 by Ali Yar Khan
» Link to gif: https://giphy.com/gifs/Q3f7T107wSsMJlT7aj
» 16-October-2024 by Harsh Bhat
» Link to gif: https://giphy.com/gifs/oss-twentycrm-mgoYSDrjIalUL7XJzm
» 20-October-2024 by Satesh Charan
» Link to gif: https://giphy.com/gifs/rXjvGBrTqu7vvhEsvR
» 20-October-2024 by Naprila
» Link to gif: https://giphy.com/gifs/uiTAwFJ0BWQsQb7jbM
» 22-October-2024 by Zia Ur Rehman Khan
» Link to gif: https://giphy.com/gifs/MG5FQSrG1mxf1N5qnA

View File

@@ -1,21 +0,0 @@
**Side Quest**: Complete all Twenty side quests
**Points**: 300 Points
**Proof**: Add screenshots for each side quest to the PR description. Add your name to the list below.
Please follow the following schema:
---
» 05-April-2024 by YOUR NAME
////////////////////////////
Your turn 👇
////////////////////////////
» 01-October-2024 by X
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan)
» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25)

View File

@@ -298,7 +298,7 @@
"eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^5.1.2",
"eslint-plugin-project-structure": "^3.7.2",
"eslint-plugin-project-structure": "^3.9.1",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4",

View File

@@ -8,10 +8,7 @@ REDIS_URL=redis://redis:6379
SERVER_URL=http://localhost:3000
# Use openssl rand -base64 32 for each secret
# ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
# LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
# REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
# FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh
# APP_SECRET=replace_me_with_a_random_string
SIGN_IN_PREFILLED=true

View File

@@ -35,10 +35,7 @@ services:
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET}
APP_SECRET: ${APP_SECRET}
depends_on:
change-vol-ownership:
condition: service_completed_successfully
@@ -67,10 +64,7 @@ services:
STORAGE_S3_NAME: ${STORAGE_S3_NAME}
STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT}
ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
FILE_TOKEN_SECRET: ${FILE_TOKEN_SECRET}
APP_SECRET: ${APP_SECRET}
depends_on:
db:
condition: service_healthy

View File

@@ -55,26 +55,11 @@ spec:
value: "7d"
- name: "LOGIN_TOKEN_EXPIRES_IN"
value: "1h"
- name: ACCESS_TOKEN_SECRET
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: accessToken
- name: LOGIN_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: loginToken
- name: REFRESH_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: refreshToken
- name: FILE_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: fileToken
ports:
- containerPort: 3000
name: http-tcp

View File

@@ -42,26 +42,11 @@ spec:
value: "redis"
- name: "REDIS_URL"
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
- name: ACCESS_TOKEN_SECRET
- name: APP_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: accessToken
- name: LOGIN_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: loginToken
- name: REFRESH_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: refreshToken
- name: FILE_TOKEN_SECRET
valueFrom:
secretKeyRef:
name: tokens
key: fileToken
command:
- yarn
- worker:prod

View File

@@ -91,7 +91,7 @@ resource "kubernetes_deployment" "twentycrm_server" {
value = "1h"
}
env {
name = "ACCESS_TOKEN_SECRET"
name = "APP_SECRET"
value_from {
secret_key_ref {
name = "tokens"
@@ -100,36 +100,6 @@ resource "kubernetes_deployment" "twentycrm_server" {
}
}
env {
name = "LOGIN_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "loginToken"
}
}
}
env {
name = "REFRESH_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "refreshToken"
}
}
}
env {
name = "FILE_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "fileToken"
}
}
}
port {
container_port = 3000
protocol = "TCP"

View File

@@ -78,7 +78,7 @@ resource "kubernetes_deployment" "twentycrm_worker" {
}
env {
name = "ACCESS_TOKEN_SECRET"
name = "APP_SECRET"
value_from {
secret_key_ref {
name = "tokens"
@@ -87,36 +87,6 @@ resource "kubernetes_deployment" "twentycrm_worker" {
}
}
env {
name = "LOGIN_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "loginToken"
}
}
}
env {
name = "REFRESH_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "refreshToken"
}
}
}
env {
name = "FILE_TOKEN_SECRET"
value_from {
secret_key_ref {
name = "tokens"
key = "fileToken"
}
}
}
resources {
requests = {
cpu = "250m"

View File

@@ -41,4 +41,7 @@ dist-ssr
*.sw?
.vite/
.nyc_output/
.nyc_output/
# eslint-plugin-project-structure
projectStructure.cache.json

View File

@@ -1,57 +1,45 @@
{
"$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json",
"projectRoot": "packages/twenty-front",
"structureRoot": "src",
"regexParameters": {
"camelCase": "^[a-z]+[A-Za-z0-9]+"
"camelCase": "^[a-z]+([A-Za-z0-9]+)+",
"kebab-case": "[a-z][a-z0-9]*(?:-[a-z0-9]+)*"
},
"structure": [
{
"name": "packages",
"children": [
{
"name": "twenty-front",
"children": [
{ "name": "*", "children": [] },
{ "name": "*" },
{
"name": "src",
"children": [
{ "name": "*", "children": [] },
{ "name": "*" },
{
"name": "modules",
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "types", "ruleId": "doNotCheckLeafFolderRule" }
]
}
]
}
]
}
]
}
{ "name": "*" },
{ "name": "*", "children": [] },
{ "name": "modules", "ruleId": "modulesFolderRule" }
],
"rules": {
"modulesFolderRule": {
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "types", "children": [] }
]
},
"moduleFolderRule": {
"name": "^(?!utils$|hooks$|states$|types$|graphql$|components$|effect-components$|constants$|validation-schemas$|contexts$|scopes$|services$|errors$)[a-z][a-z0-9]**(?:-[a-z0-9]+)**$",
"name": "{kebab-case}",
"folderRecursionLimit": 6,
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "hooks", "ruleId": "hooksLeafFolderRule" },
{ "name": "utils", "ruleId": "utilsLeafFolderRule" },
{ "name": "states", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "types", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "graphql", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "components", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "effect-components", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "constants", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "validation-schemas", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "contexts", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "scopes", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "services", "ruleId": "doNotCheckLeafFolderRule" },
{ "name": "errors", "ruleId": "doNotCheckLeafFolderRule" }
{ "name": "states", "children": [] },
{ "name": "types", "children": [] },
{ "name": "graphql", "children": [] },
{ "name": "components", "children": [] },
{ "name": "effect-components", "children": [] },
{ "name": "constants", "children": [] },
{ "name": "validation-schemas", "children": [] },
{ "name": "contexts", "children": [] },
{ "name": "scopes", "children": [] },
{ "name": "services", "children": [] },
{ "name": "errors", "children": [] }
]
},
"hooksLeafFolderRule": {
"folderRecursionLimit": 2,
"children": [
@@ -63,12 +51,8 @@
{ "name": "internal", "ruleId": "hooksLeafFolderRule" }
]
},
"doNotCheckLeafFolderRule": {
"folderRecursionLimit": 1,
"children": [{ "name": "*" }, { "name": "*", "children": [] }]
},
"utilsLeafFolderRule": {
"folderRecursionLimit": 1,
"children": [
{ "name": "{camelCase}.ts" },
{

View File

@@ -10,7 +10,7 @@ const modulesCoverage = {
branches: 25,
statements: 49,
lines: 50,
functions: 40,
functions: 38,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
};

View File

@@ -162,6 +162,11 @@ export type ClientConfig = {
support: Support;
};
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON']['input'];
};
export type CreateAppTokenInput = {
expiresAt: Scalars['DateTime']['input'];
};
@@ -529,6 +534,7 @@ export type Mutation = {
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
computeStepOutputSchema: Scalars['JSON']['output'];
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
createOneField: Field;
@@ -625,6 +631,11 @@ export type MutationCheckoutSessionArgs = {
};
export type MutationComputeStepOutputSchemaArgs = {
input: ComputeStepOutputSchemaInput;
};
export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput;
};

View File

@@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@@ -155,6 +155,11 @@ export type ClientConfig = {
support: Support;
};
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
};
export type CreateServerlessFunctionInput = {
description?: InputMaybe<Scalars['String']>;
name: Scalars['String'];
@@ -424,6 +429,7 @@ export type Mutation = {
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
computeStepOutputSchema: Scalars['JSON'];
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
createOneObject: Object;
@@ -509,6 +515,11 @@ export type MutationCheckoutSessionArgs = {
};
export type MutationComputeStepOutputSchemaArgs = {
input: ComputeStepOutputSchemaInput;
};
export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput;
};
@@ -1272,6 +1283,7 @@ export type Workspace = {
displayName?: Maybe<Scalars['String']>;
domainName?: Maybe<Scalars['String']>;
featureFlags?: Maybe<Array<FeatureFlag>>;
hasValidEntrepriseKey: Scalars['Boolean'];
id: Scalars['UUID'];
inviteHash?: Maybe<Scalars['String']>;
isPublicInviteLinkEnabled: Scalars['Boolean'];
@@ -1677,7 +1689,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
appToken: Scalars['String'];
@@ -1710,7 +1722,7 @@ export type VerifyMutationVariables = Exact<{
}>;
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@@ -1797,7 +1809,7 @@ export type ListSsoIdentityProvidersByWorkspaceIdQueryVariables = Exact<{ [key:
export type ListSsoIdentityProvidersByWorkspaceIdQuery = { __typename?: 'Query', listSSOIdentityProvidersByWorkspaceId: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdpType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -1814,7 +1826,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, analyticsTinybirdJwt?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, isPublicInviteLinkEnabled: boolean, hasValidEntrepriseKey: boolean, metadataVersion: number, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
@@ -1823,6 +1835,13 @@ export type ActivateWorkflowVersionMutationVariables = Exact<{
export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean };
export type ComputeStepOutputSchemaMutationVariables = Exact<{
input: ComputeStepOutputSchemaInput;
}>;
export type ComputeStepOutputSchemaMutation = { __typename?: 'Mutation', computeStepOutputSchema: any };
export type DeactivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String'];
}>;
@@ -2044,6 +2063,7 @@ export const UserQueryFragmentFragmentDoc = gql`
allowImpersonation
activationStatus
isPublicInviteLinkEnabled
hasValidEntrepriseKey
featureFlags {
id
key
@@ -3443,6 +3463,37 @@ export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.Mutation
export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>;
export const ComputeStepOutputSchemaDocument = gql`
mutation ComputeStepOutputSchema($input: ComputeStepOutputSchemaInput!) {
computeStepOutputSchema(input: $input)
}
`;
export type ComputeStepOutputSchemaMutationFn = Apollo.MutationFunction<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>;
/**
* __useComputeStepOutputSchemaMutation__
*
* To run a mutation, you first call `useComputeStepOutputSchemaMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useComputeStepOutputSchemaMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [computeStepOutputSchemaMutation, { data, loading, error }] = useComputeStepOutputSchemaMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useComputeStepOutputSchemaMutation(baseOptions?: Apollo.MutationHookOptions<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>(ComputeStepOutputSchemaDocument, options);
}
export type ComputeStepOutputSchemaMutationHookResult = ReturnType<typeof useComputeStepOutputSchemaMutation>;
export type ComputeStepOutputSchemaMutationResult = Apollo.MutationResult<ComputeStepOutputSchemaMutation>;
export type ComputeStepOutputSchemaMutationOptions = Apollo.BaseMutationOptions<ComputeStepOutputSchemaMutation, ComputeStepOutputSchemaMutationVariables>;
export const DeactivateWorkflowVersionDocument = gql`
mutation DeactivateWorkflowVersion($workflowVersionId: String!) {
deactivateWorkflowVersion(workflowVersionId: $workflowVersionId)

View File

@@ -10,6 +10,6 @@ html {
}
/* https://stackoverflow.com/questions/44543157/how-to-hide-the-google-invisible-recaptcha-badge */
.grecaptcha-badge {
.grecaptcha-badge {
visibility: hidden !important;
}

View File

@@ -1,5 +1,5 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters';
@@ -12,17 +12,15 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useContext, useEffect, useState } from 'react';
import { IconTrash, isDefined } from 'twenty-ui';
export const DeleteRecordsActionEffect = ({
position,
objectMetadataItem,
actionMenuType,
}: {
position: number;
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@@ -93,6 +91,9 @@ export const DeleteRecordsActionEffect = ({
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0;
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
useEffect(() => {
if (canDelete) {
addActionMenuEntry({
@@ -101,6 +102,7 @@ export const DeleteRecordsActionEffect = ({
position,
Icon: IconTrash,
accent: 'danger',
isPinned: true,
onClick: () => {
setIsDeleteRecordsModalOpen(true);
},
@@ -120,17 +122,14 @@ export const DeleteRecordsActionEffect = ({
} can be recovered from the Options menu.`}
onConfirmClick={() => {
handleDeleteClick();
if (actionMenuType === 'recordShow') {
onActionExecutedCallback?.();
if (isInRightDrawer) {
closeRightDrawer();
}
}}
deleteButtonText={`Delete ${
contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`}
modalVariant={
actionMenuType === 'recordShow' ? 'tertiary' : 'primary'
}
/>
),
});
@@ -142,13 +141,14 @@ export const DeleteRecordsActionEffect = ({
removeActionMenuEntry('delete');
};
}, [
actionMenuType,
addActionMenuEntry,
canDelete,
closeRightDrawer,
contextStoreNumberOfSelectedRecords,
handleDeleteClick,
isDeleteRecordsModalOpen,
isInRightDrawer,
onActionExecutedCallback,
position,
removeActionMenuEntry,
]);

View File

@@ -1,27 +0,0 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect];
export const MultipleRecordsActionMenuEntriesSetter = ({
objectMetadataItem,
actionMenuType,
}: {
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
))}
</>
);
};

View File

@@ -1,16 +1,23 @@
import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordActionMenuEntriesSetter = ({
actionMenuType,
}: {
actionMenuType: ActionMenuType;
}) => {
const singleRecordActionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
const multipleRecordActionEffects = [
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
export const RecordActionMenuEntriesSetter = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState,
);
@@ -33,19 +40,20 @@ export const RecordActionMenuEntriesSetter = ({
return null;
}
if (contextStoreNumberOfSelectedRecords === 1) {
return (
<SingleRecordActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
);
}
const actions =
contextStoreNumberOfSelectedRecords === 1
? singleRecordActionEffects
: multipleRecordActionEffects;
return (
<MultipleRecordsActionMenuEntriesSetter
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
<>
{actions.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
/>
))}
</>
);
};

View File

@@ -1,31 +0,0 @@
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const SingleRecordActionMenuEntriesSetter = ({
objectMetadataItem,
actionMenuType,
}: {
objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => {
const actionEffects = [
ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
return (
<>
{actionEffects.map((ActionEffect, index) => (
<ActionEffect
key={index}
position={index}
objectMetadataItem={objectMetadataItem}
actionMenuType={actionMenuType}
/>
))}
</>
);
};

View File

@@ -3,16 +3,12 @@ import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMen
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexActionMenu = ({
actionMenuId,
}: {
actionMenuId: string;
}) => {
export const RecordIndexActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
@@ -20,15 +16,18 @@ export const RecordIndexActionMenu = ({
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordIndexActionMenuBar />
<RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter actionMenuType="recordIndex" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@@ -1,5 +1,6 @@
import styled from '@emotion/styled';
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
@@ -30,7 +31,9 @@ export const RecordIndexActionMenuBar = () => {
actionMenuEntriesComponentSelector,
);
if (actionMenuEntries.length === 0) {
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (pinnedEntries.length === 0) {
return null;
}
@@ -42,9 +45,10 @@ export const RecordIndexActionMenuBar = () => {
}}
>
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{actionMenuEntries.map((entry, index) => (
{pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} />
))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar>
);
};

View File

@@ -0,0 +1,53 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconLayoutSidebarRightExpand } from 'twenty-ui';
const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none;
&:hover {
background: ${({ theme }) => theme.background.tertiary};
}
`;
const StyledButtonLabel = styled.div`
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledShortcutLabel = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
const StyledSeparator = styled.div<{ size: 'sm' | 'md' }>`
background: ${({ theme }) => theme.border.color.light};
height: ${({ theme, size }) => theme.spacing(size === 'sm' ? 4 : 8)};
margin: 0 ${({ theme }) => theme.spacing(1)};
width: 1px;
`;
export const RecordIndexActionMenuBarAllActionsButton = () => {
const theme = useTheme();
const { openCommandMenu } = useCommandMenu();
return (
<>
<StyledSeparator size="md" />
<StyledButton onClick={() => openCommandMenu()}>
<IconLayoutSidebarRightExpand size={theme.icon.size.md} />
<StyledButtonLabel>All Actions</StyledButtonLabel>
<StyledSeparator size="sm" />
<StyledShortcutLabel>K</StyledShortcutLabel>
</StyledButton>
</>
);
};

View File

@@ -2,31 +2,24 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
type RecordIndexActionMenuBarEntryProps = {
entry: ActionMenuEntry;
};
const StyledButton = styled.div<{ accent: MenuItemAccent }>`
const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) =>
props.accent === 'danger'
? props.theme.color.red
: props.theme.font.color.secondary};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
justify-content: center;
padding: ${({ theme }) => theme.spacing(2)};
transition: background 0.1s ease;
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none;
&:hover {
background: ${({ theme, accent }) =>
accent === 'danger'
? theme.background.danger
: theme.background.tertiary};
background: ${({ theme }) => theme.background.tertiary};
}
`;
@@ -40,10 +33,7 @@ export const RecordIndexActionMenuBarEntry = ({
}: RecordIndexActionMenuBarEntryProps) => {
const theme = useTheme();
return (
<StyledButton
accent={entry.accent ?? 'default'}
onClick={() => entry.onClick?.()}
>
<StyledButton onClick={() => entry.onClick?.()}>
{entry.Icon && <entry.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{entry.label}</StyledButtonLabel>
</StyledButton>

View File

@@ -1,9 +1,13 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
@@ -19,12 +23,22 @@ export const RecordIndexActionMenuEffect = () => {
const { openActionBar, closeActionBar } = useActionMenu(actionMenuId);
// Using closeActionBar here was causing a bug because it goes back to the
// previous hotkey scope, and we don't want that here.
const setIsBottomBarOpened = useSetRecoilComponentStateV2(
isBottomBarOpenedComponentState,
`action-bar-${actionMenuId}`,
);
const isDropdownOpen = useRecoilValue(
extractComponentState(
isDropdownOpenComponentState,
`action-menu-dropdown-${actionMenuId}`,
),
);
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => {
if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) {
@@ -43,5 +57,11 @@ export const RecordIndexActionMenuEffect = () => {
isDropdownOpen,
]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
setIsBottomBarOpened(false);
}
}, [isRightDrawerOpen, isCommandMenuOpened, setIsBottomBarOpened]);
return null;
};

View File

@@ -1,30 +1,53 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
export const RecordShowActionMenu = ({
actionMenuId,
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}: {
actionMenuId: string;
isFavorite: boolean;
handleFavoriteButtonClick: () => void;
record: ObjectRecord | undefined;
objectMetadataItem: ObjectMetadataItem;
objectNameSingular: string;
}) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
// TODO: refactor RecordShowPageBaseHeader to use the context store
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: actionMenuId }}
<ActionMenuContext.Provider
value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
>
<RecordShowActionMenuBar />
<RecordShowPageBaseHeader
{...{
isFavorite,
handleFavoriteButtonClick,
record,
objectMetadataItem,
objectNameSingular,
}}
/>
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter actionMenuType="recordShow" />
</ActionMenuComponentInstanceContext.Provider>
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);

View File

@@ -0,0 +1,30 @@
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowRightDrawerActionMenu = () => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState,
);
return (
<>
{contextStoreCurrentObjectMetadataId && (
<ActionMenuContext.Provider
value={{
isInRightDrawer: true,
onActionExecutedCallback: () => {},
}}
>
<RecordShowRightDrawerActionMenuBar />
<ActionMenuConfirmationModals />
<RecordActionMenuEntriesSetter />
</ActionMenuContext.Provider>
)}
</>
);
};

View File

@@ -2,7 +2,7 @@ import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordSho
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordShowActionMenuBar = () => {
export const RecordShowRightDrawerActionMenuBar = () => {
const actionMenuEntries = useRecoilComponentValueV2(
actionMenuEntriesComponentSelector,
);

View File

@@ -10,15 +10,15 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui';
import { IconTrash, RouterDecorator } from 'twenty-ui';
const deleteMock = jest.fn();
const markAsDoneMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar,
decorators: [
RouterDecorator,
(Story) => (
<ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }}
@@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
[
'delete',
{
isPinned: true,
key: 'delete',
label: 'Delete',
position: 0,
@@ -55,16 +56,6 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
onClick: deleteMock,
},
],
[
'markAsDone',
{
key: 'markAsDone',
label: 'Mark as done',
position: 1,
Icon: IconCheckbox,
onClick: markAsDoneMock,
},
],
]),
);
set(
@@ -120,12 +111,8 @@ export const WithButtonClicks: Story = {
const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton);
const markAsDoneButton = await canvas.findByText('Mark as done');
await userEvent.click(markAsDoneButton);
await waitFor(() => {
expect(deleteMock).toHaveBeenCalled();
expect(markAsDoneMock).toHaveBeenCalled();
});
},
};

View File

@@ -2,7 +2,7 @@ import { expect, jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar';
import { RecordShowRightDrawerActionMenuBar } from '@/action-menu/components/RecordShowRightDrawerActionMenuBar';
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@@ -20,9 +20,9 @@ const deleteMock = jest.fn();
const addToFavoritesMock = jest.fn();
const exportMock = jest.fn();
const meta: Meta<typeof RecordShowActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowActionMenuBar',
component: RecordShowActionMenuBar,
const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
component: RecordShowRightDrawerActionMenuBar,
decorators: [
(Story) => (
<RecoilRoot
@@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowActionMenuBar> = {
export default meta;
type Story = StoryObj<typeof RecordShowActionMenuBar>;
type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
export const Default: Story = {
args: {

View File

@@ -0,0 +1,11 @@
import { createContext } from 'react';
type ActionMenuContextType = {
isInRightDrawer: boolean;
onActionExecutedCallback: () => void;
};
export const ActionMenuContext = createContext<ActionMenuContextType>({
isInRightDrawer: false,
onActionExecutedCallback: () => {},
});

View File

@@ -8,6 +8,7 @@ export type ActionMenuEntry = {
label: string;
position: number;
Icon: IconComponent;
isPinned?: boolean;
accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode;

View File

@@ -1 +0,0 @@
export type ActionMenuType = 'recordIndex' | 'recordShow';

View File

@@ -8,6 +8,7 @@ import {
AnimatedPlaceholderEmptyTitle,
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
H3Title,
Section,
} from 'twenty-ui';
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
@@ -21,7 +22,6 @@ import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { Section } from '@/ui/layout/section/components/Section';
import { TimelineCalendarEventsWithTotal } from '~/generated/graphql';
const StyledContainer = styled.div`

View File

@@ -8,6 +8,7 @@ import {
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
H1Title,
H1TitleFontColor,
Section,
} from 'twenty-ui';
import { ActivityList } from '@/activities/components/ActivityList';
@@ -20,7 +21,6 @@ import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/quer
import { useCustomResolver } from '@/activities/hooks/useCustomResolver';
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { Section } from '@/ui/layout/section/components/Section';
import { TimelineThread, TimelineThreadsWithTotal } from '~/generated/graphql';
const StyledContainer = styled.div`

View File

@@ -5,7 +5,6 @@ export type Attachment = {
type: AttachmentType;
companyId: string;
personId: string;
activityId: string;
authorId: string;
createdAt: string;
__typename: string;

View File

@@ -19,8 +19,6 @@ export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSig
__typename: true,
createdAt: true,
updatedAt: true,
activity: true,
activityId: true,
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
},
});

View File

@@ -18,7 +18,6 @@ const mockActivityTarget = {
updatedAt: '2021-08-03T19:20:06.000Z',
createdAt: '2021-08-03T19:20:06.000Z',
personId: '1',
activityId: '234',
companyId: '1',
id: '123',
};

View File

@@ -37,7 +37,6 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
activityId
authorId
companyId
createdAt

View File

@@ -1,11 +1,15 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui';
import {
Checkbox,
CheckboxShape,
IconCalendar,
OverflowingTextWithTooltip,
} from 'twenty-ui';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { ActivityRow } from '@/activities/components/ActivityRow';

View File

@@ -51,7 +51,6 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
activityId
authorId
companyId
createdAt
@@ -95,6 +94,8 @@ const mocks: MockedResponse[] = [
updatedAt
viewId
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}
@@ -138,6 +139,9 @@ const mocks: MockedResponse[] = [
rocketId
taskId
updatedAt
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}

View File

@@ -115,7 +115,7 @@ export const PageChangeEffect = () => {
break;
}
case isMatchingLocation(AppPath.CreateWorkspace): {
setHotkeyScope(PageHotkeyScope.CreateWokspace);
setHotkeyScope(PageHotkeyScope.CreateWorkspace);
break;
}
case isMatchingLocation(AppPath.SyncEmails): {

View File

@@ -27,6 +27,7 @@ import {
IconMicrosoft,
Loader,
MainButton,
StyledText,
} from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
@@ -187,7 +188,9 @@ export const SignInUpForm = () => {
</>
)}
<HorizontalSeparator visible={true} />
{(authProviders.google ||
authProviders.microsoft ||
authProviders.sso) && <HorizontalSeparator visible />}
{authProviders.password &&
(signInUpStep === SignInUpStep.Password ||
@@ -267,6 +270,12 @@ export const SignInUpForm = () => {
disableHotkeys
onKeyDown={handleKeyDown}
/>
{signInUpMode === SignInUpMode.SignUp && (
<StyledText
text={'At least 8 characters long.'}
color={theme.font.color.secondary}
/>
)}
</StyledInputContainer>
)}
/>

View File

@@ -14,6 +14,7 @@ export type CurrentWorkspace = Pick<
| 'currentBillingSubscription'
| 'workspaceMembersCount'
| 'isPublicInviteLinkEnabled'
| 'hasValidEntrepriseKey'
| 'metadataVersion'
>;

View File

@@ -10,6 +10,7 @@ import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchS
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { Command, CommandType } from '@/command-menu/types/Command';
import { Company } from '@/companies/types/Company';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName';
@@ -287,6 +288,14 @@ export const CommandMenu = () => {
: true) && cmd.type === CommandType.Create,
);
const matchingActionCommands = commandMenuCommands.filter(
(cmd) =>
(deferredCommandMenuSearch.length > 0
? checkInShortcuts(cmd, deferredCommandMenuSearch) ||
checkInLabels(cmd, deferredCommandMenuSearch)
: true) && cmd.type === CommandType.Action,
);
useListenClickOutside({
refs: [commandMenuRef],
callback: closeCommandMenu,
@@ -312,6 +321,7 @@ export const CommandMenu = () => {
const selectableItemIds = copilotCommands
.map((cmd) => cmd.id)
.concat(matchingActionCommands.map((cmd) => cmd.id))
.concat(matchingCreateCommand.map((cmd) => cmd.id))
.concat(matchingNavigateCommand.map((cmd) => cmd.id))
.concat(people?.map((person) => person.id))
@@ -320,22 +330,28 @@ export const CommandMenu = () => {
.concat(notes?.map((note) => note.id));
const isNoResults =
!matchingActionCommands.length &&
!matchingCreateCommand.length &&
!matchingNavigateCommand.length &&
!people?.length &&
!companies?.length &&
!notes?.length &&
!opportunities?.length;
const isLoading =
isPeopleLoading ||
isNotesLoading ||
isOpportunitiesLoading ||
isCompaniesLoading;
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
return (
<>
{isCommandMenuOpened && (
<StyledCommandMenu ref={commandMenuRef}>
<StyledCommandMenu ref={commandMenuRef} className="command-menu">
<StyledInputContainer>
<StyledInput
autoFocus
@@ -393,6 +409,23 @@ export const CommandMenu = () => {
</SelectableItem>
</CommandGroup>
)}
{mainContextStoreComponentInstanceId && (
<CommandGroup heading="Actions">
{matchingActionCommands?.map((actionCommand) => (
<SelectableItem
itemId={actionCommand.id}
key={actionCommand.id}
>
<CommandMenuItem
id={actionCommand.id}
label={actionCommand.label}
Icon={actionCommand.Icon}
onClick={actionCommand.onCommandClick}
/>
</SelectableItem>
))}
</CommandGroup>
)}
<CommandGroup heading="Create">
{matchingCreateCommand.map((cmd) => (
<SelectableItem itemId={cmd.id} key={cmd.id}>

View File

@@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { useRecoilCallback, useRecoilValue, useSetRecoilState } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
@@ -9,7 +9,9 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons';
import { sortByProperty } from '~/utils/array/sortByProperty';
@@ -27,10 +29,43 @@ export const useCommandMenu = () => {
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const openCommandMenu = useCallback(() => {
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
}, [setHotkeyScopeAndMemorizePreviousScope, setIsCommandMenuOpened]);
const mainContextStoreComponentInstanceId = useRecoilValue(
mainContextStoreComponentInstanceIdState,
);
const openCommandMenu = useRecoilCallback(
({ snapshot }) =>
() => {
if (isDefined(mainContextStoreComponentInstanceId)) {
const actionMenuEntries = snapshot.getLoadable(
actionMenuEntriesComponentSelector.selectorFamily({
instanceId: mainContextStoreComponentInstanceId,
}),
);
const actionCommands = actionMenuEntries
.getValue()
?.map((actionMenuEntry) => ({
id: actionMenuEntry.key,
label: actionMenuEntry.label,
Icon: actionMenuEntry.Icon,
onCommandClick: actionMenuEntry.onClick,
type: CommandType.Action,
}));
setCommands(actionCommands);
}
setIsCommandMenuOpened(true);
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen);
},
[
mainContextStoreComponentInstanceId,
setCommands,
setHotkeyScopeAndMemorizePreviousScope,
setIsCommandMenuOpened,
],
);
const closeCommandMenu = useRecoilCallback(
({ snapshot }) =>

View File

@@ -3,13 +3,14 @@ import { IconComponent } from 'twenty-ui';
export enum CommandType {
Navigate = 'Navigate',
Create = 'Create',
Action = 'Action',
}
export type Command = {
id: string;
to: string;
to?: string;
label: string;
type: CommandType.Navigate | CommandType.Create;
type: CommandType.Navigate | CommandType.Create | CommandType.Action;
Icon?: IconComponent;
firstHotKey?: string;
secondHotKey?: string;

View File

@@ -3,7 +3,7 @@ import { mainContextStoreComponentInstanceIdState } from '@/context-store/states
import { useContext, useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
export const SetMainContextStoreComponentInstanceIdEffect = () => {
export const MainContextStoreComponentInstanceIdSetterEffect = () => {
const setMainContextStoreComponentInstanceId = useSetRecoilState(
mainContextStoreComponentInstanceIdState,
);

View File

@@ -84,7 +84,7 @@ export const mocks = [
query: gql`
mutation CreateOneFavorite($input: FavoriteCreateInput!) {
createFavorite(data: $input) {
__typename
__typename
company {
__typename
accountOwnerId
@@ -295,6 +295,41 @@ export const mocks = [
updatedAt
}
workflowId
workflowRun {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
endedAt
id
name
output
position
startedAt
status
updatedAt
workflowId
workflowVersionId
}
workflowRunId
workflowVersion {
__typename
createdAt
deletedAt
id
name
position
status
steps
trigger
updatedAt
workflowId
}
workflowVersionId
workspaceMember {
__typename
avatarUrl
@@ -341,8 +376,8 @@ export const mocks = [
mutation DeleteOneFavorite($idToDelete: ID!) {
deleteFavorite(id: $idToDelete) {
__typename
id
deletedAt
id
}
}
`,
@@ -575,6 +610,41 @@ export const mocks = [
updatedAt
}
workflowId
workflowRun {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
endedAt
id
name
output
position
startedAt
status
updatedAt
workflowId
workflowVersionId
}
workflowRunId
workflowVersion {
__typename
createdAt
deletedAt
id
name
position
status
steps
trigger
updatedAt
workflowId
}
workflowVersionId
workspaceMember {
__typename
avatarUrl

View File

@@ -1,5 +1,5 @@
import { DropResult, ResponderProvided } from '@hello-pangea/dnd';
import { act, renderHook, waitFor } from '@testing-library/react';
import { renderHook, waitFor } from '@testing-library/react';
import { useSetRecoilState } from 'recoil';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
@@ -7,6 +7,7 @@ import { useFavorites } from '@/favorites/hooks/useFavorites';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { act } from 'react';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
import {

View File

@@ -3,13 +3,18 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableT
describe('findAvailableTimeZoneOption', () => {
it('should find the matching available IANA time zone select option from a given IANA time zone', () => {
const ianaTimeZone = 'Europe/Paris';
const expectedOption = {
label: '(GMT+02:00) Central European Summer Time - Paris',
value: 'Europe/Paris',
};
const expectedValue = 'Europe/Paris';
const expectedLabelWinter =
'(GMT+01:00) Central European Standard Time - Paris';
const expectedLabelSummer =
'(GMT+02:00) Central European Summer Time - Paris';
const option = findAvailableTimeZoneOption(ianaTimeZone);
expect(option).toEqual(expectedOption);
expect(option.value).toEqual(expectedValue);
expect(
expectedLabelWinter === option.label ||
expectedLabelSummer === option.label,
).toBeTruthy();
});
});

View File

@@ -3,11 +3,17 @@ import { formatTimeZoneLabel } from '@/localization/utils/formatTimeZoneLabel';
describe('formatTimeZoneLabel', () => {
it('should format the time zone label correctly when location is included in the label', () => {
const ianaTimeZone = 'Europe/Paris';
const expectedLabel = '(GMT+02:00) Central European Summer Time - Paris';
const expectedLabelSummer =
'(GMT+02:00) Central European Summer Time - Paris';
const expectedLabelWinter =
'(GMT+01:00) Central European Standard Time - Paris';
const formattedLabel = formatTimeZoneLabel(ianaTimeZone);
expect(formattedLabel).toEqual(expectedLabel);
expect(
expectedLabelSummer === formattedLabel ||
expectedLabelWinter === formattedLabel,
).toBeTruthy();
});
it('should format the time zone label correctly when location is not included in the label', () => {

View File

@@ -1,5 +1,6 @@
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilState } from 'recoil';
import {
@@ -23,6 +24,8 @@ export const MobileNavigationBar = () => {
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
useRecoilState(currentMobileNavigationDrawerState);
const { openSettingsMenu } = useOpenSettingsMenu();
const activeItemName = isNavigationDrawerExpanded
? currentMobileNavigationDrawer
: isCommandMenuOpened
@@ -62,10 +65,7 @@ export const MobileNavigationBar = () => {
Icon: IconSettings,
onClick: () => {
closeCommandMenu();
setIsNavigationDrawerExpanded(
(previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen,
);
setCurrentMobileNavigationDrawer('settings');
openSettingsMenu();
},
},
];

View File

@@ -0,0 +1,19 @@
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilState } from 'recoil';
export const useOpenSettingsMenu = () => {
const [, setIsNavigationDrawerExpanded] = useRecoilState(
isNavigationDrawerExpandedState,
);
const [, setCurrentMobileNavigationDrawer] = useRecoilState(
currentMobileNavigationDrawerState,
);
const openSettingsMenu = () => {
setIsNavigationDrawerExpanded(true);
setCurrentMobileNavigationDrawer('settings');
};
return { openSettingsMenu };
};

View File

@@ -43,8 +43,7 @@ export const NavigationDrawerItemForObjectMetadataItem = ({
const shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) =>
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
(viewA, viewB) => viewA.position - viewB.position,
);
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(

View File

@@ -11,5 +11,5 @@ export const query = gql`
export const variables = { idToDelete: 'idToDelete' };
export const responseData = {
id: 'idToDelete'
id: 'idToDelete',
};

View File

@@ -2,7 +2,8 @@ import { gql } from '@apollo/client';
import { FieldMetadataType } from '~/generated/graphql';
export const FIELD_METADATA_ID = '2c43466a-fe9e-4005-8d08-c5836067aa6c';
export const FIELD_RELATION_METADATA_ID = '4da0302d-358a-45cd-9973-9f92723ed3c1';
export const FIELD_RELATION_METADATA_ID =
'4da0302d-358a-45cd-9973-9f92723ed3c1';
export const RELATION_METADATA_ID = 'f81d4fae-7dec-11d0-a765-00a0c91e6bf6';
const baseFields = `
@@ -29,12 +30,12 @@ export const queries = {
}
`,
deleteMetadataFieldRelation: gql`
mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {
deleteOneRelation(input: { id: $idToDelete }) {
id
mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {
deleteOneRelation(input: { id: $idToDelete }) {
id
}
}
}
`,
`,
activateMetadataField: gql`
mutation UpdateOneFieldMetadataItem(
$idToUpdate: UUID!
@@ -94,7 +95,7 @@ export const variables = {
deactivateMetadataField: {
idToUpdate: FIELD_METADATA_ID,
updatePayload: { isActive: false, label: undefined },
}
},
};
const defaultResponseData = {
@@ -127,4 +128,3 @@ export const responseData = {
options: [],
},
};

View File

@@ -16,6 +16,7 @@ const Wrapper = getJestMetadataAndApolloMocksWrapper({
featureFlags: [],
allowImpersonation: false,
activationStatus: WorkspaceActivationStatus.Active,
hasValidEntrepriseKey: false,
metadataVersion: 1,
isPublicInviteLinkEnabled: false,
});

View File

@@ -3,11 +3,14 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
export const mapSoftDeleteFieldsToGraphQLQuery = (
objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>,
): string => {
const softDeleteFields = ['id', 'deletedAt'];
const softDeleteFields = ['deletedAt', 'id'];
const fieldsThatShouldBeQueried = objectMetadataItem.fields.filter(
(field) => field.isActive && softDeleteFields.includes(field.name),
);
const fieldsThatShouldBeQueried = objectMetadataItem.fields
.filter((field) => field.isActive && softDeleteFields.includes(field.name))
.sort(
(a, b) =>
softDeleteFields.indexOf(a.name) - softDeleteFields.indexOf(b.name),
);
return `{
__typename

View File

@@ -17,7 +17,7 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
id
intro
jobTitle
linkedinLink{
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
@@ -45,31 +45,14 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = `
primaryLinkLabel
secondaryLinks
}
`
`;
export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
__typename
activityTargets {
edges {
node {
__typename
activityId
companyId
createdAt
deletedAt
id
opportunityId
personId
rocketId
updatedAt
}
}
}
attachments {
edges {
node {
__typename
activityId
authorId
companyId
createdAt
@@ -190,6 +173,8 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
updatedAt
viewId
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}
@@ -308,6 +293,9 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
rocketId
taskId
updatedAt
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}
@@ -324,4 +312,4 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
primaryLinkLabel
secondaryLinks
}
`
`;

View File

@@ -15,5 +15,7 @@ export const variables = {
};
export const responseData = {
__typename: 'Person',
deletedAt: '2024-02-14T09:45:00Z',
id: 'a7286b9a-c039-4a89-9567-2dfa7953cda9',
};

View File

@@ -3,10 +3,19 @@ import { gql } from '@apollo/client';
import { peopleQueryResult } from '~/testing/mock-data/people';
export const query = gql`
query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) {
people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){
query FindManyPeople(
$filter: PersonFilterInput
$orderBy: [PersonOrderByInput]
$lastCursor: String
$limit: Int
) {
people(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges {
node {
__typename
@@ -27,38 +36,51 @@ export const query = gql`
export const mockPageSize = 2;
export const peopleMockWithIdsOnly: RecordGqlConnection = { ...peopleQueryResult.people,edges: peopleQueryResult.people.edges.map((edge) => ({ ...edge, node: { __typename: 'Person', id: edge.node.id } })) };
export const peopleMockWithIdsOnly: RecordGqlConnection = {
...peopleQueryResult.people,
edges: peopleQueryResult.people.edges.map((edge) => ({
...edge,
node: { __typename: 'Person', id: edge.node.id },
})),
};
export const firstRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize].cursor;
export const secondRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor;
export const thirdRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor;
export const firstRequestLastCursor =
peopleMockWithIdsOnly.edges[mockPageSize].cursor;
export const secondRequestLastCursor =
peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor;
export const thirdRequestLastCursor =
peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor;
export const variablesFirstRequest = {
filter: undefined,
limit: mockPageSize,
orderBy: undefined
orderBy: undefined,
};
export const variablesSecondRequest = {
filter: undefined,
limit: mockPageSize,
orderBy: undefined,
lastCursor: firstRequestLastCursor
lastCursor: firstRequestLastCursor,
};
export const variablesThirdRequest = {
filter: undefined,
limit: mockPageSize,
orderBy: undefined,
lastCursor: secondRequestLastCursor
}
lastCursor: secondRequestLastCursor,
};
const paginateRequestResponse = (response: RecordGqlConnection, start: number, end: number, hasNextPage: boolean, totalCount: number) => {
const paginateRequestResponse = (
response: RecordGqlConnection,
start: number,
end: number,
hasNextPage: boolean,
totalCount: number,
) => {
return {
...response,
edges: [
...response.edges.slice(start, end)
],
edges: [...response.edges.slice(start, end)],
pageInfo: {
...response.pageInfo,
startCursor: response.edges[start].cursor,
@@ -66,17 +88,35 @@ const paginateRequestResponse = (response: RecordGqlConnection, start: number, e
hasNextPage,
} satisfies RecordGqlConnection['pageInfo'],
totalCount,
}
}
};
};
export const responseFirstRequest = {
people: paginateRequestResponse(peopleMockWithIdsOnly, 0, mockPageSize, true, 6),
people: paginateRequestResponse(
peopleMockWithIdsOnly,
0,
mockPageSize,
true,
6,
),
};
export const responseSecondRequest = {
people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize, mockPageSize * 2, true, 6),
people: paginateRequestResponse(
peopleMockWithIdsOnly,
mockPageSize,
mockPageSize * 2,
true,
6,
),
};
export const responseThirdRequest = {
people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize * 2, mockPageSize * 3, false, 6),
people: paginateRequestResponse(
peopleMockWithIdsOnly,
mockPageSize * 2,
mockPageSize * 3,
false,
6,
),
};

View File

@@ -99,20 +99,6 @@ export const query = gql`
}
city
email
activityTargets {
edges {
node {
__typename
id
updatedAt
createdAt
personId
activityId
companyId
id
}
}
}
jobTitle
favorites {
edges {
@@ -137,7 +123,6 @@ export const query = gql`
createdAt
name
personId
activityId
companyId
id
authorId

View File

@@ -5,15 +5,10 @@ import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
const StyledContainer = styled.div`
position: relative;
`;
type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string;
};
@@ -46,7 +41,7 @@ export const MultipleFiltersDropdownContent = ({
const shoudShowFilterInput = objectFilterDropdownFilterIsSelected;
return (
<StyledContainer>
<>
{shoudShowFilterInput ? (
<ObjectFilterOperandSelectAndInput
filterDropdownId={filterDropdownId}
@@ -61,6 +56,6 @@ export const MultipleFiltersDropdownContent = ({
filterDefinitionUsedInDropdown?.type
}
/>
</StyledContainer>
</>
);
};

View File

@@ -28,8 +28,13 @@ export const ObjectFilterDropdownFilterInput = ({
const {
filterDefinitionUsedInDropdownState,
selectedOperandInDropdownState,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown({ filterDropdownId });
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const filterDefinitionUsedInDropdown = useRecoilValue(
filterDefinitionUsedInDropdownState,
);
@@ -53,7 +58,9 @@ export const ObjectFilterDropdownFilterInput = ({
ViewFilterOperand.IsRelative,
].includes(selectedOperandInDropdown);
if (!isDefined(filterDefinitionUsedInDropdown)) {
const shouldHide = isObjectFilterDropdownOperandSelectUnfolded;
if (shouldHide || !isDefined(filterDefinitionUsedInDropdown)) {
return null;
}

View File

@@ -8,9 +8,7 @@ const StyledOperandSelectContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
width: 100%;
z-index: 1000;
`;

View File

@@ -16,9 +16,11 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM
import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem';
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
@@ -34,7 +36,7 @@ export const StyledInput = styled.input`
margin: 0;
outline: none;
padding: ${({ theme }) => theme.spacing(2)};
height: 19px;
min-height: 19px;
font-family: inherit;
font-size: ${({ theme }) => theme.font.size.sm};
@@ -139,10 +141,15 @@ export const ObjectFilterDropdownFilterSelect = ({
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView();
const isAdvancedFiltersEnabled = useIsFeatureEnabled(
'IS_ADVANCED_FILTERS_ENABLED',
);
const shouldShowAdvancedFilterButton =
isDefined(currentViewId) &&
isDefined(currentViewWithCombinedFiltersAndSorts?.objectMetadataId) &&
isAdvancedFilterButtonVisible;
isAdvancedFilterButtonVisible &&
isAdvancedFiltersEnabled;
return (
<>
@@ -154,43 +161,45 @@ export const ObjectFilterDropdownFilterSelect = ({
setObjectFilterDropdownSearchInput(event.target.value)
}
/>
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
{shoudShowSeparator && <DropdownMenuSeparator />}
<DropdownMenuItemsContainer>
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<SelectableList
hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton}
selectableItemIdArray={selectableListItemIds}
selectableListId={OBJECT_FILTER_DROPDOWN_ID}
onEnter={handleEnter}
>
<DropdownMenuItemsContainer>
{visibleColumnsFilterDefinitions.map(
(visibleFilterDefinition, index) => (
<SelectableItem
itemId={visibleFilterDefinition.fieldMetadataId}
key={`visible-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={visibleFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
{shoudShowSeparator && <DropdownMenuSeparator />}
<DropdownMenuItemsContainer>
{hiddenColumnsFilterDefinitions.map(
(hiddenFilterDefinition, index) => (
<SelectableItem
itemId={hiddenFilterDefinition.fieldMetadataId}
key={`hidden-select-filter-${index}`}
>
<ObjectFilterDropdownFilterSelectMenuItem
filterDefinition={hiddenFilterDefinition}
/>
</SelectableItem>
),
)}
</DropdownMenuItemsContainer>
</SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</ScrollWrapper>
</>
);
};

View File

@@ -10,17 +10,28 @@ export const ObjectFilterDropdownOperandButton = () => {
const {
selectedOperandInDropdownState,
setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown();
const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState,
);
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const handleButtonClick = () => {
setIsObjectFilterDropdownOperandSelectUnfolded(
!isObjectFilterDropdownOperandSelectUnfolded,
);
};
return (
<DropdownMenuHeader
key={'selected-filter-operand'}
EndIcon={IconChevronDown}
onClick={() => setIsObjectFilterDropdownOperandSelectUnfolded(true)}
onClick={handleButtonClick}
>
{getOperandLabel(selectedOperandInDropdown)}
</DropdownMenuHeader>

View File

@@ -16,6 +16,7 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { isDefined } from '~/utils/isDefined';
export const EMPTY_FILTER_VALUE = '';
@@ -162,22 +163,24 @@ export const ObjectFilterDropdownOptionSelect = () => {
}
}}
>
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer hasMaxHeight>
{optionsInDropdown?.map((option) => (
<MenuItemMultiSelect
key={option.id}
selected={option.isSelected}
isKeySelected={option.id === selectedItemId}
onSelectChange={(selected) =>
handleMultipleOptionSelectChange(option, selected)
}
text={option.label}
color={option.color}
className=""
/>
))}
</DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
</ScrollWrapper>
</SelectableList>
);
};

View File

@@ -15,6 +15,7 @@ import { StyledHeaderDropdownButton } from '@/ui/layout/dropdown/components/Styl
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useContext } from 'react';
import { SORT_DIRECTIONS } from '../types/SortDirection';
@@ -42,17 +43,13 @@ export const StyledInput = styled.input`
}
`;
const StyledContainer = styled.div`
position: relative;
`;
const StyledSelectedSortDirectionContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
top: 32px;
width: 100%;
z-index: 1000;
`;
@@ -166,21 +163,23 @@ export const ObjectSortDropdownButton = ({
</DropdownMenuItemsContainer>
</StyledSelectedSortDirectionContainer>
)}
<StyledContainer>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() => setIsSortDirectionMenuUnfolded(true)}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder="Search fields"
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<DropdownMenuHeader
EndIcon={IconChevronDown}
onClick={() =>
setIsSortDirectionMenuUnfolded(!isSortDirectionMenuUnfolded)
}
>
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader>
<StyledInput
autoFocus
value={objectSortDropdownSearchInput}
placeholder="Search fields"
onChange={(event) =>
setObjectSortDropdownSearchInput(event.target.value)
}
/>
<ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
<DropdownMenuItemsContainer>
{visibleColumnsSortDefinitions.map(
(visibleSortDefinition, index) => (
@@ -214,7 +213,7 @@ export const ObjectSortDropdownButton = ({
),
)}
</DropdownMenuItemsContainer>
</StyledContainer>
</ScrollWrapper>
</>
}
onClose={handleDropdownButtonClose}

View File

@@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
import { RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-board/constants/RecordBoardClickOutsideListenerId';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
@@ -16,7 +17,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useListenClickOutsideV2 } from '@/ui/utilities/pointer-event/hooks/useListenClickOutsideV2';
import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
@@ -69,9 +70,15 @@ export const RecordBoard = () => {
const { resetRecordSelection, setRecordAsSelected } =
useRecordBoardSelection(recordBoardId);
useListenClickOutsideByClassName({
classNames: ['record-board-card'],
excludeClassNames: ['bottom-bar', 'action-menu-dropdown'],
useListenClickOutsideV2({
excludeClassNames: [
'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
refs: [boardRef],
callback: resetRecordSelection,
});

View File

@@ -0,0 +1 @@
export const RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID = 'record-board';

View File

@@ -17,7 +17,6 @@ import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { TextInput } from '@/ui/input/components/TextInput';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
@@ -29,6 +28,8 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import {
AnimatedEaseInOut,
AvatarChipVariant,
Checkbox,
CheckboxVariant,
ChipSize,
IconEye,
IconEyeOff,

View File

@@ -7,7 +7,9 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useCallback, useContext } from 'react';
import { RecoilState, useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
import { v4 as uuidv4 } from 'uuid';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SetFunction = <T>(
recoilVal: RecoilState<T>,
@@ -16,7 +18,7 @@ type SetFunction = <T>(
export const useAddNewCard = () => {
const columnContext = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem } =
const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
useContext(RecordBoardContext);
const { resetSearchFilter } = useEntitySelectSearch({
relationPickerScopeId: 'relation-picker',
@@ -75,16 +77,47 @@ export const useAddNewCard = () => {
(isOpportunity && company !== null) ||
(!isOpportunity && labelValue !== '')
) {
// TODO: Refactor this whole section (Add new card): this should be:
// - simpler
// - piloted by metadata,
// - avoid drill down props, especially internal stuff
// - and follow record table pending record creation logic
let computedLabelIdentifierValue: any = labelValue;
const labelIdentifierField = objectMetadataItem?.fields.find(
(field) =>
field.id === objectMetadataItem.labelIdentifierFieldMetadataId,
);
if (!isDefined(labelIdentifierField)) {
throw new Error('Label identifier field not found');
}
if (labelIdentifierField.type === FieldMetadataType.FullName) {
computedLabelIdentifierValue = {
firstName: labelValue,
lastName: '',
};
}
createOneRecord({
[selectFieldMetadataItem.name]: columnContext?.columnDefinition.value,
position,
...(isOpportunity
? { companyId: company?.id, name: company?.name }
: { [labelIdentifier.toLowerCase()]: labelValue }),
: {
[labelIdentifier.toLowerCase()]: computedLabelIdentifierValue,
}),
});
}
},
[createOneRecord, columnContext, selectFieldMetadataItem],
[
objectMetadataItem?.fields,
objectMetadataItem?.labelIdentifierFieldMetadataId,
createOneRecord,
selectFieldMetadataItem?.name,
columnContext?.columnDefinition?.value,
],
);
const handleAddNewCardClick = useRecoilCallback(

View File

@@ -5,7 +5,7 @@ import {
FieldFullNameMetadata,
FieldRatingMetadata,
FieldSelectMetadata,
FieldTextMetadata
FieldTextMetadata,
} from '@/object-record/record-field/types/FieldMetadata';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
@@ -29,7 +29,6 @@ if (!mockedPersonObjectMetadataItem) {
throw new Error('Person object metadata item not found');
}
const relationFieldMetadataItem = mockedPersonObjectMetadataItem?.fields?.find(
({ name }) => name === 'company',
);
@@ -110,4 +109,4 @@ export const actorFieldDefinition: FieldDefinition<FieldActorMetadata> = {
metadata: {
fieldName: 'actor',
},
};
};

View File

@@ -47,22 +47,6 @@ const mocks: MockedResponse[] = [
userId
}
accountOwnerId
activityTargets {
edges {
node {
__typename
activityId
companyId
createdAt
deletedAt
id
opportunityId
personId
rocketId
updatedAt
}
}
}
address {
addressStreet1
addressStreet2
@@ -81,7 +65,6 @@ const mocks: MockedResponse[] = [
edges {
node {
__typename
activityId
authorId
companyId
createdAt
@@ -129,6 +112,8 @@ const mocks: MockedResponse[] = [
updatedAt
viewId
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}
@@ -278,6 +263,9 @@ const mocks: MockedResponse[] = [
rocketId
taskId
updatedAt
workflowId
workflowRunId
workflowVersionId
workspaceMemberId
}
}

View File

@@ -21,7 +21,6 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px;
position: absolute;
top: -1px;
`;
@@ -46,6 +45,7 @@ type MultiItemFieldInputProps<T> = {
};
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ...
// This should be refactored with a hook instead that exposes those events in a context around this component and its children.
export const MultiItemFieldInput = <T,>({
items,
onPersist,
@@ -84,9 +84,9 @@ export const MultiItemFieldInput = <T,>({
setInputValue(value);
if (!validateInput) return;
if (errorData.isValid) {
setErrorData(errorData);
}
setErrorData(
errorData.isValid ? errorData : { isValid: true, errorMessage: '' },
);
};
const handleAddButtonClick = () => {

View File

@@ -1,14 +1,11 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import styled from '@emotion/styled';
import { useEffect, useState } from 'react';
import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useState } from 'react';
import {
IconBookmark,
IconBookmarkPlus,
IconComponent,
IconDotsVertical,
IconPencil,
IconTrash,
} from 'twenty-ui';
@@ -24,12 +21,6 @@ type MultiItemFieldMenuItemProps<T> = {
hasPrimaryButton?: boolean;
};
const StyledIconBookmark = styled(IconBookmark)`
color: ${({ theme }) => theme.font.color.light};
height: ${({ theme }) => theme.icon.size.sm}px;
width: ${({ theme }) => theme.icon.size.sm}px;
`;
export const MultiItemFieldMenuItem = <T,>({
dropdownId,
isPrimary,
@@ -47,66 +38,51 @@ export const MultiItemFieldMenuItem = <T,>({
const handleMouseLeave = () => setIsHovered(false);
const handleDeleteClick = () => {
closeDropdown();
setIsHovered(false);
onDelete?.();
};
useEffect(() => {
if (isDropdownOpen) {
return () => closeDropdown();
}
}, [closeDropdown, isDropdownOpen]);
const handleSetAsPrimaryClick = () => {
closeDropdown();
onSetAsPrimary?.();
};
const handleEditClick = () => {
closeDropdown();
onEdit?.();
};
return (
<MenuItem
<MenuItemWithOptionDropdown
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
text={<DisplayComponent value={value} />}
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
iconButtons={[
{
Wrapper: isHovered
? ({ iconButton }) => (
<Dropdown
dropdownId={dropdownId}
dropdownHotkeyScope={{ scope: dropdownId }}
dropdownPlacement="right-start"
dropdownStrategy="fixed"
disableBlur
clickableComponent={iconButton}
dropdownComponents={
<DropdownMenuItemsContainer>
{hasPrimaryButton && !isPrimary && (
<MenuItem
LeftIcon={IconBookmarkPlus}
text="Set as Primary"
onClick={onSetAsPrimary}
/>
)}
<MenuItem
LeftIcon={IconPencil}
text="Edit"
onClick={onEdit}
/>
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Delete"
onClick={handleDeleteClick}
/>
</DropdownMenuItemsContainer>
}
/>
)
: undefined,
Icon:
isPrimary && !isHovered
? (StyledIconBookmark as IconComponent)
: IconDotsVertical,
accent: 'tertiary',
onClick: isHovered ? () => {} : undefined,
},
]}
RightIcon={isHovered ? null : IconBookmark}
dropdownId={dropdownId}
dropdownContent={
<DropdownMenuItemsContainer>
{hasPrimaryButton && !isPrimary && (
<MenuItem
LeftIcon={IconBookmarkPlus}
text="Set as Primary"
onClick={handleSetAsPrimaryClick}
/>
)}
<MenuItem
LeftIcon={IconPencil}
text="Edit"
onClick={handleEditClick}
/>
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Delete"
onClick={handleDeleteClick}
/>
</DropdownMenuItemsContainer>
}
/>
);
};

View File

@@ -206,6 +206,7 @@ export const RecordIndexContainer = () => {
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
{recordIndexViewType === ViewType.Table && (
<>
<RecordIndexTableContainer
@@ -232,7 +233,7 @@ export const RecordIndexContainer = () => {
/>
</StyledContainerWithPadding>
)}
<RecordIndexActionMenu actionMenuId={recordIndexId} />
<RecordIndexActionMenu />
</RecordFieldValueSelectorContextProvider>
</StyledContainer>
);

Some files were not shown because too many files have changed in this diff Show More