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 uses: ./.github/workflows/actions/nx-affected
with: with:
tag: scope:frontend tag: scope:frontend
tasks: ${{ matrix.task }} tasks: ${{ matrix.task }}

View File

@@ -41,10 +41,7 @@ jobs:
cp .env.example .env cp .env.example .env
echo "Generating secrets..." echo "Generating secrets..."
echo "# === Randomly generated secrets ===" >>.env echo "# === Randomly generated secrets ===" >>.env
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env echo "APP_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 "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.env
echo "Starting server..." 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' if: github.event.action != 'closed'
steps: steps:
- uses: actions/checkout@v4 - 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 - name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
uses: ./.github/workflows/actions/yarn-install uses: ./.github/workflows/actions/yarn-install
- name: Utils / Run Danger.js - name: Utils / Run Danger.js
if: steps.changed-files.outputs.changed == 'true'
run: cd packages/twenty-utils && npx nx danger:ci run: cd packages/twenty-utils && npx nx danger:ci
env: env:
DANGER_GITHUB_API_TOKEN: ${{ github.token }} DANGER_GITHUB_API_TOKEN: ${{ github.token }}
@@ -42,16 +35,9 @@ jobs:
if: github.event.action == 'closed' && github.event.pull_request.merged == true if: github.event.action == 'closed' && github.event.pull_request.merged == true
steps: steps:
- uses: actions/checkout@v4 - 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 - name: Install dependencies
if: steps.changed-files.outputs.changed == 'true'
uses: ./.github/workflows/actions/yarn-install uses: ./.github/workflows/actions/yarn-install
- name: Run congratulate-dangerfile.js - name: Run congratulate-dangerfile.js
if: steps.changed-files.outputs.changed == 'true'
run: cd packages/twenty-utils && npx nx danger:congratulate run: cd packages/twenty-utils && npx nx danger:congratulate
env: env:
DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}

2
.gitignore vendored
View File

@@ -9,7 +9,6 @@
.nx/installation .nx/installation
.nx/cache .nx/cache
projectStructure.cache.json
.pnp.* .pnp.*
.yarn/* .yarn/*
@@ -30,3 +29,4 @@ storybook-static
.nyc_output .nyc_output
test-results/ test-results/
dump.rdb 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> <br>
<p align="center"> <p align="center">

View File

@@ -91,10 +91,7 @@ fi
# Generate random strings for secrets # Generate random strings for secrets
echo "# === Randomly generated secrets ===" >>.env echo "# === Randomly generated secrets ===" >>.env
echo "ACCESS_TOKEN_SECRET=$(openssl rand -base64 32)" >>.env echo "APP_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 "" >>.env echo "" >>.env
echo "POSTGRES_ADMIN_PASSWORD=$(openssl rand -base64 32)" >>.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-jsx-a11y": "^6.8.0",
"eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prefer-arrow": "^1.2.3",
"eslint-plugin-prettier": "^5.1.2", "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": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.4", "eslint-plugin-react-refresh": "^0.4.4",

View File

@@ -8,10 +8,7 @@ REDIS_URL=redis://redis:6379
SERVER_URL=http://localhost:3000 SERVER_URL=http://localhost:3000
# Use openssl rand -base64 32 for each secret # Use openssl rand -base64 32 for each secret
# ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access # APP_SECRET=replace_me_with_a_random_string
# 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
SIGN_IN_PREFILLED=true SIGN_IN_PREFILLED=true

View File

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

View File

@@ -55,26 +55,11 @@ spec:
value: "7d" value: "7d"
- name: "LOGIN_TOKEN_EXPIRES_IN" - name: "LOGIN_TOKEN_EXPIRES_IN"
value: "1h" value: "1h"
- name: ACCESS_TOKEN_SECRET - name: APP_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: tokens name: tokens
key: accessToken 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: ports:
- containerPort: 3000 - containerPort: 3000
name: http-tcp name: http-tcp

View File

@@ -42,26 +42,11 @@ spec:
value: "redis" value: "redis"
- name: "REDIS_URL" - name: "REDIS_URL"
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379" value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
- name: ACCESS_TOKEN_SECRET - name: APP_SECRET
valueFrom: valueFrom:
secretKeyRef: secretKeyRef:
name: tokens name: tokens
key: accessToken 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: command:
- yarn - yarn
- worker:prod - worker:prod

View File

@@ -91,7 +91,7 @@ resource "kubernetes_deployment" "twentycrm_server" {
value = "1h" value = "1h"
} }
env { env {
name = "ACCESS_TOKEN_SECRET" name = "APP_SECRET"
value_from { value_from {
secret_key_ref { secret_key_ref {
name = "tokens" 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 { port {
container_port = 3000 container_port = 3000
protocol = "TCP" protocol = "TCP"

View File

@@ -78,7 +78,7 @@ resource "kubernetes_deployment" "twentycrm_worker" {
} }
env { env {
name = "ACCESS_TOKEN_SECRET" name = "APP_SECRET"
value_from { value_from {
secret_key_ref { secret_key_ref {
name = "tokens" 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 { resources {
requests = { requests = {
cpu = "250m" cpu = "250m"

View File

@@ -41,4 +41,7 @@ dist-ssr
*.sw? *.sw?
.vite/ .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", "$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json",
"projectRoot": "packages/twenty-front",
"structureRoot": "src",
"regexParameters": { "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": [ "structure": [
{ { "name": "*" },
"name": "packages", { "name": "*", "children": [] },
"children": [ { "name": "modules", "ruleId": "modulesFolderRule" }
{
"name": "twenty-front",
"children": [
{ "name": "*", "children": [] },
{ "name": "*" },
{
"name": "src",
"children": [
{ "name": "*", "children": [] },
{ "name": "*" },
{
"name": "modules",
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "types", "ruleId": "doNotCheckLeafFolderRule" }
]
}
]
}
]
}
]
}
], ],
"rules": { "rules": {
"modulesFolderRule": {
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "types", "children": [] }
]
},
"moduleFolderRule": { "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, "folderRecursionLimit": 6,
"children": [ "children": [
{ "ruleId": "moduleFolderRule" }, { "ruleId": "moduleFolderRule" },
{ "name": "hooks", "ruleId": "hooksLeafFolderRule" }, { "name": "hooks", "ruleId": "hooksLeafFolderRule" },
{ "name": "utils", "ruleId": "utilsLeafFolderRule" }, { "name": "utils", "ruleId": "utilsLeafFolderRule" },
{ "name": "states", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "states", "children": [] },
{ "name": "types", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "types", "children": [] },
{ "name": "graphql", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "graphql", "children": [] },
{ "name": "components", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "components", "children": [] },
{ "name": "effect-components", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "effect-components", "children": [] },
{ "name": "constants", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "constants", "children": [] },
{ "name": "validation-schemas", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "validation-schemas", "children": [] },
{ "name": "contexts", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "contexts", "children": [] },
{ "name": "scopes", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "scopes", "children": [] },
{ "name": "services", "ruleId": "doNotCheckLeafFolderRule" }, { "name": "services", "children": [] },
{ "name": "errors", "ruleId": "doNotCheckLeafFolderRule" } { "name": "errors", "children": [] }
] ]
}, },
"hooksLeafFolderRule": { "hooksLeafFolderRule": {
"folderRecursionLimit": 2, "folderRecursionLimit": 2,
"children": [ "children": [
@@ -63,12 +51,8 @@
{ "name": "internal", "ruleId": "hooksLeafFolderRule" } { "name": "internal", "ruleId": "hooksLeafFolderRule" }
] ]
}, },
"doNotCheckLeafFolderRule": {
"folderRecursionLimit": 1,
"children": [{ "name": "*" }, { "name": "*", "children": [] }]
},
"utilsLeafFolderRule": { "utilsLeafFolderRule": {
"folderRecursionLimit": 1,
"children": [ "children": [
{ "name": "{camelCase}.ts" }, { "name": "{camelCase}.ts" },
{ {

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client'; import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
export type Maybe<T> = T | null; export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>; export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] }; export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@@ -155,6 +155,11 @@ export type ClientConfig = {
support: Support; support: Support;
}; };
export type ComputeStepOutputSchemaInput = {
/** Step JSON format */
step: Scalars['JSON'];
};
export type CreateServerlessFunctionInput = { export type CreateServerlessFunctionInput = {
description?: InputMaybe<Scalars['String']>; description?: InputMaybe<Scalars['String']>;
name: Scalars['String']; name: Scalars['String'];
@@ -424,6 +429,7 @@ export type Mutation = {
authorizeApp: AuthorizeApp; authorizeApp: AuthorizeApp;
challenge: LoginToken; challenge: LoginToken;
checkoutSession: SessionEntity; checkoutSession: SessionEntity;
computeStepOutputSchema: Scalars['JSON'];
createOIDCIdentityProvider: SetupSsoOutput; createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken; createOneAppToken: AppToken;
createOneObject: Object; createOneObject: Object;
@@ -509,6 +515,11 @@ export type MutationCheckoutSessionArgs = {
}; };
export type MutationComputeStepOutputSchemaArgs = {
input: ComputeStepOutputSchemaInput;
};
export type MutationCreateOidcIdentityProviderArgs = { export type MutationCreateOidcIdentityProviderArgs = {
input: SetupOidcSsoInput; input: SetupOidcSsoInput;
}; };
@@ -1272,6 +1283,7 @@ export type Workspace = {
displayName?: Maybe<Scalars['String']>; displayName?: Maybe<Scalars['String']>;
domainName?: Maybe<Scalars['String']>; domainName?: Maybe<Scalars['String']>;
featureFlags?: Maybe<Array<FeatureFlag>>; featureFlags?: Maybe<Array<FeatureFlag>>;
hasValidEntrepriseKey: Scalars['Boolean'];
id: Scalars['UUID']; id: Scalars['UUID'];
inviteHash?: Maybe<Scalars['String']>; inviteHash?: Maybe<Scalars['String']>;
isPublicInviteLinkEnabled: Scalars['Boolean']; 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<{ export type RenewTokenMutationVariables = Exact<{
appToken: Scalars['String']; 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<{ export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String']; 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 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; }>; 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 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<{ export type ActivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];
@@ -1823,6 +1835,13 @@ export type ActivateWorkflowVersionMutationVariables = Exact<{
export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean }; export type ActivateWorkflowVersionMutation = { __typename?: 'Mutation', activateWorkflowVersion: boolean };
export type ComputeStepOutputSchemaMutationVariables = Exact<{
input: ComputeStepOutputSchemaInput;
}>;
export type ComputeStepOutputSchemaMutation = { __typename?: 'Mutation', computeStepOutputSchema: any };
export type DeactivateWorkflowVersionMutationVariables = Exact<{ export type DeactivateWorkflowVersionMutationVariables = Exact<{
workflowVersionId: Scalars['String']; workflowVersionId: Scalars['String'];
}>; }>;
@@ -2044,6 +2063,7 @@ export const UserQueryFragmentFragmentDoc = gql`
allowImpersonation allowImpersonation
activationStatus activationStatus
isPublicInviteLinkEnabled isPublicInviteLinkEnabled
hasValidEntrepriseKey
featureFlags { featureFlags {
id id
key key
@@ -3443,6 +3463,37 @@ export function useActivateWorkflowVersionMutation(baseOptions?: Apollo.Mutation
export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>; export type ActivateWorkflowVersionMutationHookResult = ReturnType<typeof useActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>; export type ActivateWorkflowVersionMutationResult = Apollo.MutationResult<ActivateWorkflowVersionMutation>;
export type ActivateWorkflowVersionMutationOptions = Apollo.BaseMutationOptions<ActivateWorkflowVersionMutation, ActivateWorkflowVersionMutationVariables>; 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` export const DeactivateWorkflowVersionDocument = gql`
mutation DeactivateWorkflowVersion($workflowVersionId: String!) { mutation DeactivateWorkflowVersion($workflowVersionId: String!) {
deactivateWorkflowVersion(workflowVersionId: $workflowVersionId) deactivateWorkflowVersion(workflowVersionId: $workflowVersionId)

View File

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

View File

@@ -1,5 +1,5 @@
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; 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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; 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'; import { IconTrash, isDefined } from 'twenty-ui';
export const DeleteRecordsActionEffect = ({ export const DeleteRecordsActionEffect = ({
position, position,
objectMetadataItem, objectMetadataItem,
actionMenuType,
}: { }: {
position: number; position: number;
objectMetadataItem: ObjectMetadataItem; objectMetadataItem: ObjectMetadataItem;
actionMenuType: ActionMenuType;
}) => { }) => {
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
@@ -93,6 +91,9 @@ export const DeleteRecordsActionEffect = ({
contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT && contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT &&
contextStoreNumberOfSelectedRecords > 0; contextStoreNumberOfSelectedRecords > 0;
const { isInRightDrawer, onActionExecutedCallback } =
useContext(ActionMenuContext);
useEffect(() => { useEffect(() => {
if (canDelete) { if (canDelete) {
addActionMenuEntry({ addActionMenuEntry({
@@ -101,6 +102,7 @@ export const DeleteRecordsActionEffect = ({
position, position,
Icon: IconTrash, Icon: IconTrash,
accent: 'danger', accent: 'danger',
isPinned: true,
onClick: () => { onClick: () => {
setIsDeleteRecordsModalOpen(true); setIsDeleteRecordsModalOpen(true);
}, },
@@ -120,17 +122,14 @@ export const DeleteRecordsActionEffect = ({
} can be recovered from the Options menu.`} } can be recovered from the Options menu.`}
onConfirmClick={() => { onConfirmClick={() => {
handleDeleteClick(); handleDeleteClick();
onActionExecutedCallback?.();
if (actionMenuType === 'recordShow') { if (isInRightDrawer) {
closeRightDrawer(); closeRightDrawer();
} }
}} }}
deleteButtonText={`Delete ${ deleteButtonText={`Delete ${
contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
}`} }`}
modalVariant={
actionMenuType === 'recordShow' ? 'tertiary' : 'primary'
}
/> />
), ),
}); });
@@ -142,13 +141,14 @@ export const DeleteRecordsActionEffect = ({
removeActionMenuEntry('delete'); removeActionMenuEntry('delete');
}; };
}, [ }, [
actionMenuType,
addActionMenuEntry, addActionMenuEntry,
canDelete, canDelete,
closeRightDrawer, closeRightDrawer,
contextStoreNumberOfSelectedRecords, contextStoreNumberOfSelectedRecords,
handleDeleteClick, handleDeleteClick,
isDeleteRecordsModalOpen, isDeleteRecordsModalOpen,
isInRightDrawer,
onActionExecutedCallback,
position, position,
removeActionMenuEntry, 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 { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
import { ActionMenuType } from '@/action-menu/types/ActionMenuType'; import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordActionMenuEntriesSetter = ({ const singleRecordActionEffects = [
actionMenuType, ManageFavoritesActionEffect,
}: { ExportRecordsActionEffect,
actionMenuType: ActionMenuType; DeleteRecordsActionEffect,
}) => { ];
const multipleRecordActionEffects = [
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
];
export const RecordActionMenuEntriesSetter = () => {
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState, contextStoreNumberOfSelectedRecordsComponentState,
); );
@@ -33,19 +40,20 @@ export const RecordActionMenuEntriesSetter = ({
return null; return null;
} }
if (contextStoreNumberOfSelectedRecords === 1) { const actions =
return ( contextStoreNumberOfSelectedRecords === 1
<SingleRecordActionMenuEntriesSetter ? singleRecordActionEffects
objectMetadataItem={objectMetadataItem} : multipleRecordActionEffects;
actionMenuType={actionMenuType}
/>
);
}
return ( return (
<MultipleRecordsActionMenuEntriesSetter <>
objectMetadataItem={objectMetadataItem} {actions.map((ActionEffect, index) => (
actionMenuType={actionMenuType} <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 { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect'; 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 { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
export const RecordIndexActionMenu = ({ export const RecordIndexActionMenu = () => {
actionMenuId,
}: {
actionMenuId: string;
}) => {
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataIdComponentState, contextStoreCurrentObjectMetadataIdComponentState,
); );
@@ -20,15 +16,18 @@ export const RecordIndexActionMenu = ({
return ( return (
<> <>
{contextStoreCurrentObjectMetadataId && ( {contextStoreCurrentObjectMetadataId && (
<ActionMenuComponentInstanceContext.Provider <ActionMenuContext.Provider
value={{ instanceId: actionMenuId }} value={{
isInRightDrawer: false,
onActionExecutedCallback: () => {},
}}
> >
<RecordIndexActionMenuBar /> <RecordIndexActionMenuBar />
<RecordIndexActionMenuDropdown /> <RecordIndexActionMenuDropdown />
<ActionMenuConfirmationModals /> <ActionMenuConfirmationModals />
<RecordIndexActionMenuEffect /> <RecordIndexActionMenuEffect />
<RecordActionMenuEntriesSetter actionMenuType="recordIndex" /> <RecordActionMenuEntriesSetter />
</ActionMenuComponentInstanceContext.Provider> </ActionMenuContext.Provider>
)} )}
</> </>
); );

View File

@@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
@@ -30,7 +31,9 @@ export const RecordIndexActionMenuBar = () => {
actionMenuEntriesComponentSelector, actionMenuEntriesComponentSelector,
); );
if (actionMenuEntries.length === 0) { const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
if (pinnedEntries.length === 0) {
return null; return null;
} }
@@ -42,9 +45,10 @@ export const RecordIndexActionMenuBar = () => {
}} }}
> >
<StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel> <StyledLabel>{contextStoreNumberOfSelectedRecords} selected:</StyledLabel>
{actionMenuEntries.map((entry, index) => ( {pinnedEntries.map((entry, index) => (
<RecordIndexActionMenuBarEntry key={index} entry={entry} /> <RecordIndexActionMenuBarEntry key={index} entry={entry} />
))} ))}
<RecordIndexActionMenuBarAllActionsButton />
</BottomBar> </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 styled from '@emotion/styled';
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent';
type RecordIndexActionMenuBarEntryProps = { type RecordIndexActionMenuBarEntryProps = {
entry: ActionMenuEntry; entry: ActionMenuEntry;
}; };
const StyledButton = styled.div<{ accent: MenuItemAccent }>` const StyledButton = styled.div`
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${(props) => color: ${({ theme }) => theme.font.color.secondary};
props.accent === 'danger'
? props.theme.color.red
: props.theme.font.color.secondary};
cursor: pointer; cursor: pointer;
display: flex; display: flex;
justify-content: center; justify-content: center;
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
transition: background 0.1s ease; transition: background ${({ theme }) => theme.animation.duration.fast} ease;
user-select: none; user-select: none;
&:hover { &:hover {
background: ${({ theme, accent }) => background: ${({ theme }) => theme.background.tertiary};
accent === 'danger'
? theme.background.danger
: theme.background.tertiary};
} }
`; `;
@@ -40,10 +33,7 @@ export const RecordIndexActionMenuBarEntry = ({
}: RecordIndexActionMenuBarEntryProps) => { }: RecordIndexActionMenuBarEntryProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<StyledButton <StyledButton onClick={() => entry.onClick?.()}>
accent={entry.accent ?? 'default'}
onClick={() => entry.onClick?.()}
>
{entry.Icon && <entry.Icon size={theme.icon.size.md} />} {entry.Icon && <entry.Icon size={theme.icon.size.md} />}
<StyledButtonLabel>{entry.label}</StyledButtonLabel> <StyledButtonLabel>{entry.label}</StyledButtonLabel>
</StyledButton> </StyledButton>

View File

@@ -1,9 +1,13 @@
import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; import { useActionMenu } from '@/action-menu/hooks/useActionMenu';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; 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 { 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 { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; 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 { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
@@ -19,12 +23,22 @@ export const RecordIndexActionMenuEffect = () => {
const { openActionBar, closeActionBar } = useActionMenu(actionMenuId); 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( const isDropdownOpen = useRecoilValue(
extractComponentState( extractComponentState(
isDropdownOpenComponentState, isDropdownOpenComponentState,
`action-menu-dropdown-${actionMenuId}`, `action-menu-dropdown-${actionMenuId}`,
), ),
); );
const { isRightDrawerOpen } = useRightDrawer();
const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState);
useEffect(() => { useEffect(() => {
if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) { if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) {
@@ -43,5 +57,11 @@ export const RecordIndexActionMenuEffect = () => {
isDropdownOpen, isDropdownOpen,
]); ]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
setIsBottomBarOpened(false);
}
}, [isRightDrawerOpen, isCommandMenuOpened, setIsBottomBarOpened]);
return null; return null;
}; };

View File

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

View File

@@ -10,15 +10,15 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
import { userEvent, waitFor, within } from '@storybook/test'; import { userEvent, waitFor, within } from '@storybook/test';
import { IconCheckbox, IconTrash } from 'twenty-ui'; import { IconTrash, RouterDecorator } from 'twenty-ui';
const deleteMock = jest.fn(); const deleteMock = jest.fn();
const markAsDoneMock = jest.fn();
const meta: Meta<typeof RecordIndexActionMenuBar> = { const meta: Meta<typeof RecordIndexActionMenuBar> = {
title: 'Modules/ActionMenu/RecordIndexActionMenuBar', title: 'Modules/ActionMenu/RecordIndexActionMenuBar',
component: RecordIndexActionMenuBar, component: RecordIndexActionMenuBar,
decorators: [ decorators: [
RouterDecorator,
(Story) => ( (Story) => (
<ContextStoreComponentInstanceContext.Provider <ContextStoreComponentInstanceContext.Provider
value={{ instanceId: 'story-action-menu' }} value={{ instanceId: 'story-action-menu' }}
@@ -48,6 +48,7 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
[ [
'delete', 'delete',
{ {
isPinned: true,
key: 'delete', key: 'delete',
label: 'Delete', label: 'Delete',
position: 0, position: 0,
@@ -55,16 +56,6 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
onClick: deleteMock, onClick: deleteMock,
}, },
], ],
[
'markAsDone',
{
key: 'markAsDone',
label: 'Mark as done',
position: 1,
Icon: IconCheckbox,
onClick: markAsDoneMock,
},
],
]), ]),
); );
set( set(
@@ -120,12 +111,8 @@ export const WithButtonClicks: Story = {
const deleteButton = await canvas.findByText('Delete'); const deleteButton = await canvas.findByText('Delete');
await userEvent.click(deleteButton); await userEvent.click(deleteButton);
const markAsDoneButton = await canvas.findByText('Mark as done');
await userEvent.click(markAsDoneButton);
await waitFor(() => { await waitFor(() => {
expect(deleteMock).toHaveBeenCalled(); 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 { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil'; 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 { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@@ -20,9 +20,9 @@ const deleteMock = jest.fn();
const addToFavoritesMock = jest.fn(); const addToFavoritesMock = jest.fn();
const exportMock = jest.fn(); const exportMock = jest.fn();
const meta: Meta<typeof RecordShowActionMenuBar> = { const meta: Meta<typeof RecordShowRightDrawerActionMenuBar> = {
title: 'Modules/ActionMenu/RecordShowActionMenuBar', title: 'Modules/ActionMenu/RecordShowRightDrawerActionMenuBar',
component: RecordShowActionMenuBar, component: RecordShowRightDrawerActionMenuBar,
decorators: [ decorators: [
(Story) => ( (Story) => (
<RecoilRoot <RecoilRoot
@@ -98,7 +98,7 @@ const meta: Meta<typeof RecordShowActionMenuBar> = {
export default meta; export default meta;
type Story = StoryObj<typeof RecordShowActionMenuBar>; type Story = StoryObj<typeof RecordShowRightDrawerActionMenuBar>;
export const Default: Story = { export const Default: Story = {
args: { 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; label: string;
position: number; position: number;
Icon: IconComponent; Icon: IconComponent;
isPinned?: boolean;
accent?: MenuItemAccent; accent?: MenuItemAccent;
onClick?: (event?: MouseEvent<HTMLElement>) => void; onClick?: (event?: MouseEvent<HTMLElement>) => void;
ConfirmationModal?: ReactNode; ConfirmationModal?: ReactNode;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,15 @@
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; 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 { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { getActivitySummary } from '@/activities/utils/getActivitySummary';
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils'; import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
import { ActivityRow } from '@/activities/components/ActivityRow'; import { ActivityRow } from '@/activities/components/ActivityRow';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import { isNonEmptyString } from '@sniptt/guards'; import { isNonEmptyString } from '@sniptt/guards';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; 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 { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; 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 { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands';
import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons'; import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons';
import { sortByProperty } from '~/utils/array/sortByProperty'; import { sortByProperty } from '~/utils/array/sortByProperty';
@@ -27,10 +29,43 @@ export const useCommandMenu = () => {
goBackToPreviousHotkeyScope, goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope(); } = usePreviousHotkeyScope();
const openCommandMenu = useCallback(() => { const mainContextStoreComponentInstanceId = useRecoilValue(
setIsCommandMenuOpened(true); mainContextStoreComponentInstanceIdState,
setHotkeyScopeAndMemorizePreviousScope(AppHotkeyScope.CommandMenuOpen); );
}, [setHotkeyScopeAndMemorizePreviousScope, setIsCommandMenuOpened]);
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( const closeCommandMenu = useRecoilCallback(
({ snapshot }) => ({ snapshot }) =>

View File

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

View File

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

View File

@@ -84,7 +84,7 @@ export const mocks = [
query: gql` query: gql`
mutation CreateOneFavorite($input: FavoriteCreateInput!) { mutation CreateOneFavorite($input: FavoriteCreateInput!) {
createFavorite(data: $input) { createFavorite(data: $input) {
__typename __typename
company { company {
__typename __typename
accountOwnerId accountOwnerId
@@ -295,6 +295,41 @@ export const mocks = [
updatedAt updatedAt
} }
workflowId 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 { workspaceMember {
__typename __typename
avatarUrl avatarUrl
@@ -341,8 +376,8 @@ export const mocks = [
mutation DeleteOneFavorite($idToDelete: ID!) { mutation DeleteOneFavorite($idToDelete: ID!) {
deleteFavorite(id: $idToDelete) { deleteFavorite(id: $idToDelete) {
__typename __typename
id
deletedAt deletedAt
id
} }
} }
`, `,
@@ -575,6 +610,41 @@ export const mocks = [
updatedAt updatedAt
} }
workflowId 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 { workspaceMember {
__typename __typename
avatarUrl avatarUrl

View File

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

View File

@@ -3,13 +3,18 @@ import { findAvailableTimeZoneOption } from '@/localization/utils/findAvailableT
describe('findAvailableTimeZoneOption', () => { describe('findAvailableTimeZoneOption', () => {
it('should find the matching available IANA time zone select option from a given IANA time zone', () => { it('should find the matching available IANA time zone select option from a given IANA time zone', () => {
const ianaTimeZone = 'Europe/Paris'; const ianaTimeZone = 'Europe/Paris';
const expectedOption = { const expectedValue = 'Europe/Paris';
label: '(GMT+02:00) Central European Summer Time - Paris', const expectedLabelWinter =
value: 'Europe/Paris', '(GMT+01:00) Central European Standard Time - Paris';
}; const expectedLabelSummer =
'(GMT+02:00) Central European Summer Time - Paris';
const option = findAvailableTimeZoneOption(ianaTimeZone); 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', () => { describe('formatTimeZoneLabel', () => {
it('should format the time zone label correctly when location is included in the label', () => { it('should format the time zone label correctly when location is included in the label', () => {
const ianaTimeZone = 'Europe/Paris'; 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); 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', () => { 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 { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { useOpenSettingsMenu } from '@/navigation/hooks/useOpenSettings';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { import {
@@ -23,6 +24,8 @@ export const MobileNavigationBar = () => {
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] = const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
useRecoilState(currentMobileNavigationDrawerState); useRecoilState(currentMobileNavigationDrawerState);
const { openSettingsMenu } = useOpenSettingsMenu();
const activeItemName = isNavigationDrawerExpanded const activeItemName = isNavigationDrawerExpanded
? currentMobileNavigationDrawer ? currentMobileNavigationDrawer
: isCommandMenuOpened : isCommandMenuOpened
@@ -62,10 +65,7 @@ export const MobileNavigationBar = () => {
Icon: IconSettings, Icon: IconSettings,
onClick: () => { onClick: () => {
closeCommandMenu(); closeCommandMenu();
setIsNavigationDrawerExpanded( openSettingsMenu();
(previousIsOpen) => activeItemName !== 'settings' || !previousIsOpen,
);
setCurrentMobileNavigationDrawer('settings');
}, },
}, },
]; ];

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 shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1;
const sortedObjectMetadataViews = [...objectMetadataViews].sort( const sortedObjectMetadataViews = [...objectMetadataViews].sort(
(viewA, viewB) => (viewA, viewB) => viewA.position - viewB.position,
viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
); );
const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,10 +3,19 @@ import { gql } from '@apollo/client';
import { peopleQueryResult } from '~/testing/mock-data/people'; import { peopleQueryResult } from '~/testing/mock-data/people';
export const query = gql` export const query = gql`
query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) { query FindManyPeople(
people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor){ $filter: PersonFilterInput
$orderBy: [PersonOrderByInput]
$lastCursor: String
$limit: Int
) {
people(
filter: $filter
orderBy: $orderBy
first: $limit
after: $lastCursor
) {
edges { edges {
node { node {
__typename __typename
@@ -27,38 +36,51 @@ export const query = gql`
export const mockPageSize = 2; 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 firstRequestLastCursor =
export const secondRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor; peopleMockWithIdsOnly.edges[mockPageSize].cursor;
export const thirdRequestLastCursor = peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor; export const secondRequestLastCursor =
peopleMockWithIdsOnly.edges[mockPageSize * 2].cursor;
export const thirdRequestLastCursor =
peopleMockWithIdsOnly.edges[mockPageSize * 3].cursor;
export const variablesFirstRequest = { export const variablesFirstRequest = {
filter: undefined, filter: undefined,
limit: mockPageSize, limit: mockPageSize,
orderBy: undefined orderBy: undefined,
}; };
export const variablesSecondRequest = { export const variablesSecondRequest = {
filter: undefined, filter: undefined,
limit: mockPageSize, limit: mockPageSize,
orderBy: undefined, orderBy: undefined,
lastCursor: firstRequestLastCursor lastCursor: firstRequestLastCursor,
}; };
export const variablesThirdRequest = { export const variablesThirdRequest = {
filter: undefined, filter: undefined,
limit: mockPageSize, limit: mockPageSize,
orderBy: undefined, 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 { return {
...response, ...response,
edges: [ edges: [...response.edges.slice(start, end)],
...response.edges.slice(start, end)
],
pageInfo: { pageInfo: {
...response.pageInfo, ...response.pageInfo,
startCursor: response.edges[start].cursor, startCursor: response.edges[start].cursor,
@@ -66,17 +88,35 @@ const paginateRequestResponse = (response: RecordGqlConnection, start: number, e
hasNextPage, hasNextPage,
} satisfies RecordGqlConnection['pageInfo'], } satisfies RecordGqlConnection['pageInfo'],
totalCount, totalCount,
} };
} };
export const responseFirstRequest = { export const responseFirstRequest = {
people: paginateRequestResponse(peopleMockWithIdsOnly, 0, mockPageSize, true, 6), people: paginateRequestResponse(
peopleMockWithIdsOnly,
0,
mockPageSize,
true,
6,
),
}; };
export const responseSecondRequest = { export const responseSecondRequest = {
people: paginateRequestResponse(peopleMockWithIdsOnly, mockPageSize, mockPageSize * 2, true, 6), people: paginateRequestResponse(
peopleMockWithIdsOnly,
mockPageSize,
mockPageSize * 2,
true,
6,
),
}; };
export const responseThirdRequest = { 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 city
email email
activityTargets {
edges {
node {
__typename
id
updatedAt
createdAt
personId
activityId
companyId
id
}
}
}
jobTitle jobTitle
favorites { favorites {
edges { edges {
@@ -137,7 +123,6 @@ export const query = gql`
createdAt createdAt
name name
personId personId
activityId
companyId companyId
id id
authorId 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 { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState'; import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect';
import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect';
const StyledContainer = styled.div`
position: relative;
`;
type MultipleFiltersDropdownContentProps = { type MultipleFiltersDropdownContentProps = {
filterDropdownId?: string; filterDropdownId?: string;
}; };
@@ -46,7 +41,7 @@ export const MultipleFiltersDropdownContent = ({
const shoudShowFilterInput = objectFilterDropdownFilterIsSelected; const shoudShowFilterInput = objectFilterDropdownFilterIsSelected;
return ( return (
<StyledContainer> <>
{shoudShowFilterInput ? ( {shoudShowFilterInput ? (
<ObjectFilterOperandSelectAndInput <ObjectFilterOperandSelectAndInput
filterDropdownId={filterDropdownId} filterDropdownId={filterDropdownId}
@@ -61,6 +56,6 @@ export const MultipleFiltersDropdownContent = ({
filterDefinitionUsedInDropdown?.type filterDefinitionUsedInDropdown?.type
} }
/> />
</StyledContainer> </>
); );
}; };

View File

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

View File

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

View File

@@ -10,17 +10,28 @@ export const ObjectFilterDropdownOperandButton = () => {
const { const {
selectedOperandInDropdownState, selectedOperandInDropdownState,
setIsObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded,
isObjectFilterDropdownOperandSelectUnfoldedState,
} = useFilterDropdown(); } = useFilterDropdown();
const selectedOperandInDropdown = useRecoilValue( const selectedOperandInDropdown = useRecoilValue(
selectedOperandInDropdownState, selectedOperandInDropdownState,
); );
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
const handleButtonClick = () => {
setIsObjectFilterDropdownOperandSelectUnfolded(
!isObjectFilterDropdownOperandSelectUnfolded,
);
};
return ( return (
<DropdownMenuHeader <DropdownMenuHeader
key={'selected-filter-operand'} key={'selected-filter-operand'}
EndIcon={IconChevronDown} EndIcon={IconChevronDown}
onClick={() => setIsObjectFilterDropdownOperandSelectUnfolded(true)} onClick={handleButtonClick}
> >
{getOperandLabel(selectedOperandInDropdown)} {getOperandLabel(selectedOperandInDropdown)}
</DropdownMenuHeader> </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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect'; import { MenuItemMultiSelect } from '@/ui/navigation/menu-item/components/MenuItemMultiSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const EMPTY_FILTER_VALUE = ''; export const EMPTY_FILTER_VALUE = '';
@@ -162,22 +163,24 @@ export const ObjectFilterDropdownOptionSelect = () => {
} }
}} }}
> >
<DropdownMenuItemsContainer hasMaxHeight> <ScrollWrapper contextProviderName="dropdownMenuItemsContainer">
{optionsInDropdown?.map((option) => ( <DropdownMenuItemsContainer hasMaxHeight>
<MenuItemMultiSelect {optionsInDropdown?.map((option) => (
key={option.id} <MenuItemMultiSelect
selected={option.isSelected} key={option.id}
isKeySelected={option.id === selectedItemId} selected={option.isSelected}
onSelectChange={(selected) => isKeySelected={option.id === selectedItemId}
handleMultipleOptionSelectChange(option, selected) onSelectChange={(selected) =>
} handleMultipleOptionSelectChange(option, selected)
text={option.label} }
color={option.color} text={option.label}
className="" color={option.color}
/> className=""
))} />
</DropdownMenuItemsContainer> ))}
{showNoResult && <MenuItem text="No result" />} </DropdownMenuItemsContainer>
{showNoResult && <MenuItem text="No result" />}
</ScrollWrapper>
</SelectableList> </SelectableList>
); );
}; };

View File

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

View File

@@ -6,6 +6,7 @@ import { Key } from 'ts-key-enum';
import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader'; import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect'; 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 { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; 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 { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; 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 { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useScrollRestoration } from '~/hooks/useScrollRestoration'; import { useScrollRestoration } from '~/hooks/useScrollRestoration';
@@ -69,9 +70,15 @@ export const RecordBoard = () => {
const { resetRecordSelection, setRecordAsSelected } = const { resetRecordSelection, setRecordAsSelected } =
useRecordBoardSelection(recordBoardId); useRecordBoardSelection(recordBoardId);
useListenClickOutsideByClassName({ useListenClickOutsideV2({
classNames: ['record-board-card'], excludeClassNames: [
excludeClassNames: ['bottom-bar', 'action-menu-dropdown'], 'bottom-bar',
'action-menu-dropdown',
'command-menu',
'modal-backdrop',
],
listenerId: RECORD_BOARD_CLICK_OUTSIDE_LISTENER_ID,
refs: [boardRef],
callback: resetRecordSelection, 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 { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts';
@@ -29,6 +28,8 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { import {
AnimatedEaseInOut, AnimatedEaseInOut,
AvatarChipVariant, AvatarChipVariant,
Checkbox,
CheckboxVariant,
ChipSize, ChipSize,
IconEye, IconEye,
IconEyeOff, IconEyeOff,

View File

@@ -7,7 +7,9 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useCallback, useContext } from 'react'; import { useCallback, useContext } from 'react';
import { RecoilState, useRecoilCallback } from 'recoil'; import { RecoilState, useRecoilCallback } from 'recoil';
import { isDefined } from 'twenty-ui';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { FieldMetadataType } from '~/generated-metadata/graphql';
type SetFunction = <T>( type SetFunction = <T>(
recoilVal: RecoilState<T>, recoilVal: RecoilState<T>,
@@ -16,7 +18,7 @@ type SetFunction = <T>(
export const useAddNewCard = () => { export const useAddNewCard = () => {
const columnContext = useContext(RecordBoardColumnContext); const columnContext = useContext(RecordBoardColumnContext);
const { createOneRecord, selectFieldMetadataItem } = const { createOneRecord, selectFieldMetadataItem, objectMetadataItem } =
useContext(RecordBoardContext); useContext(RecordBoardContext);
const { resetSearchFilter } = useEntitySelectSearch({ const { resetSearchFilter } = useEntitySelectSearch({
relationPickerScopeId: 'relation-picker', relationPickerScopeId: 'relation-picker',
@@ -75,16 +77,47 @@ export const useAddNewCard = () => {
(isOpportunity && company !== null) || (isOpportunity && company !== null) ||
(!isOpportunity && labelValue !== '') (!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({ createOneRecord({
[selectFieldMetadataItem.name]: columnContext?.columnDefinition.value, [selectFieldMetadataItem.name]: columnContext?.columnDefinition.value,
position, position,
...(isOpportunity ...(isOpportunity
? { companyId: company?.id, name: company?.name } ? { 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( const handleAddNewCardClick = useRecoilCallback(

View File

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

View File

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

View File

@@ -21,7 +21,6 @@ import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmp
const StyledDropdownMenu = styled(DropdownMenu)` const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px; left: -1px;
position: absolute;
top: -1px; top: -1px;
`; `;
@@ -46,6 +45,7 @@ type MultiItemFieldInputProps<T> = {
}; };
// Todo: the API of this component does not look healthy: we have renderInput, renderItem, formatInput, ... // 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,>({ export const MultiItemFieldInput = <T,>({
items, items,
onPersist, onPersist,
@@ -84,9 +84,9 @@ export const MultiItemFieldInput = <T,>({
setInputValue(value); setInputValue(value);
if (!validateInput) return; if (!validateInput) return;
if (errorData.isValid) { setErrorData(
setErrorData(errorData); errorData.isValid ? errorData : { isValid: true, errorMessage: '' },
} );
}; };
const handleAddButtonClick = () => { 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 { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import styled from '@emotion/styled'; import { MenuItemWithOptionDropdown } from '@/ui/navigation/menu-item/components/MenuItemWithOptionDropdown';
import { useEffect, useState } from 'react'; import { useState } from 'react';
import { import {
IconBookmark, IconBookmark,
IconBookmarkPlus, IconBookmarkPlus,
IconComponent,
IconDotsVertical,
IconPencil, IconPencil,
IconTrash, IconTrash,
} from 'twenty-ui'; } from 'twenty-ui';
@@ -24,12 +21,6 @@ type MultiItemFieldMenuItemProps<T> = {
hasPrimaryButton?: boolean; 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,>({ export const MultiItemFieldMenuItem = <T,>({
dropdownId, dropdownId,
isPrimary, isPrimary,
@@ -47,66 +38,51 @@ export const MultiItemFieldMenuItem = <T,>({
const handleMouseLeave = () => setIsHovered(false); const handleMouseLeave = () => setIsHovered(false);
const handleDeleteClick = () => { const handleDeleteClick = () => {
closeDropdown();
setIsHovered(false); setIsHovered(false);
onDelete?.(); onDelete?.();
}; };
useEffect(() => { const handleSetAsPrimaryClick = () => {
if (isDropdownOpen) { closeDropdown();
return () => closeDropdown(); onSetAsPrimary?.();
} };
}, [closeDropdown, isDropdownOpen]);
const handleEditClick = () => {
closeDropdown();
onEdit?.();
};
return ( return (
<MenuItem <MenuItemWithOptionDropdown
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
text={<DisplayComponent value={value} />} text={<DisplayComponent value={value} />}
isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen} isIconDisplayedOnHoverOnly={!isPrimary && !isDropdownOpen}
iconButtons={[ RightIcon={isHovered ? null : IconBookmark}
{ dropdownId={dropdownId}
Wrapper: isHovered dropdownContent={
? ({ iconButton }) => ( <DropdownMenuItemsContainer>
<Dropdown {hasPrimaryButton && !isPrimary && (
dropdownId={dropdownId} <MenuItem
dropdownHotkeyScope={{ scope: dropdownId }} LeftIcon={IconBookmarkPlus}
dropdownPlacement="right-start" text="Set as Primary"
dropdownStrategy="fixed" onClick={handleSetAsPrimaryClick}
disableBlur />
clickableComponent={iconButton} )}
dropdownComponents={ <MenuItem
<DropdownMenuItemsContainer> LeftIcon={IconPencil}
{hasPrimaryButton && !isPrimary && ( text="Edit"
<MenuItem onClick={handleEditClick}
LeftIcon={IconBookmarkPlus} />
text="Set as Primary" <MenuItem
onClick={onSetAsPrimary} accent="danger"
/> LeftIcon={IconTrash}
)} text="Delete"
<MenuItem onClick={handleDeleteClick}
LeftIcon={IconPencil} />
text="Edit" </DropdownMenuItemsContainer>
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,
},
]}
/> />
); );
}; };

View File

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

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