Merge branch 'main' into c--build-twenty-query-builder-for-event-emitter

This commit is contained in:
Weiko 2024-10-29 18:45:38 +01:00
commit 09d6a64e53
587 changed files with 33978 additions and 26192 deletions

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

@ -0,0 +1,21 @@
name: CI Tinybird
on:
push:
branches:
- main
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
ci:
uses: tinybirdco/ci/.github/workflows/ci.yml@main
with:
data_project_dir: packages/twenty-tinybird
secrets:
tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }}
tb_host: https://api.eu-central-1.aws.tinybird.co

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

@ -20,4 +20,8 @@ Your turn 👇
» 19-October-2024 by [Thefool76](https://oss.gg/thefool76) YouTube Link: [YouTube](https://youtu.be/KuAycGuW698?si=q-YxcukbbYuO8BWf) » 19-October-2024 by [Thefool76](https://oss.gg/thefool76) YouTube Link: [YouTube](https://youtu.be/KuAycGuW698?si=q-YxcukbbYuO8BWf)
» 26-October-2024 by [Khaan25](https://oss.gg/Khaan25) YouTube Link: [YouTube](https://www.youtube.com/watch?v=1ruo4tTWNIg)
» 28-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Youtube Link: [Youtube](https://youtu.be/qfZyhrhCeyo)
--- ---

View File

@ -25,3 +25,11 @@ Your turn 👇
» 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 [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) » 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/twenty-crm-modern-solution-for-modern-problems-a0b65fec9d6c)
» 27-October-2024 by [Karan0207](https://oss.gg/karan0207) blog Link: [blog](https://medium.com/@karansingh0201k/my-journey-with-twenty-the-open-source-crm-that-really-gets-it-133879af6280)
» 27-October-2024 by [Vardhaman619](https://oss.gg/vardhaman619) blog Link: [blog](https://dev.to/vardhaman619/my-experience-with-modern-open-source-crm-twenty-crm-2hen)
» 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-crm)
» 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/is-twenty-crm-the-right-tool-for-your-business-heres-my-honest-review-0d41e9d8a7eb)

View File

@ -20,8 +20,14 @@ Your turn 👇
» 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) » 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) » 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) » 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)
» 26-October-2024 by [Yash-1511](https://oss.gg/Yash-1511) blog Link: [blog](https://medium.com/@yashp3020/a-comprehensive-guide-to-self-hosting-twenty-crm-with-docker-compose-40ea3fb4afdc)
--- ---
» 28-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) blog Link: [blog](https://www.harshbhat.me/blog/twenty-self-host)
» 28-October-2024 by [AshishViradiya153](https://oss.gg/AshishViradiya153) blog Link: [blog](https://medium.com/@ashishviradiya153/comprehensive-guide-to-self-hosting-twenty-crm-26e7fa36c846)

View File

@ -18,4 +18,12 @@ Your turn 👇
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) » 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)
» 27-October-2024 by [Khaan25](https://oss.gg/Khaan25) video Link: [video](https://x.com/zia_webdev/status/1850409233663115529)
» 27-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) video link: [video](https://youtube.com/shorts/OK52eaq0pAQ?feature=share)
» 28-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) video link: [video](https://youtu.be/65sOHce1gjw)
--- ---

View File

@ -18,7 +18,11 @@ Your turn 👇
//////////////////////////// ////////////////////////////
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) » 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) » 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)
» 24-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) Figma Link: [Figma](https://www.figma.com/design/2qlAPS3llwf8jrWKGHEf6O/Twenty-(sateshcharan)?node-id=1633-94880&t=GIceWxqyY0ajWXnZ-1)
» 28-October-2024 by [Vanshdeep Singh](https://oss.gg/Vanshdeepsingh-2232) Figma Link:[Figma](https://www.figma.com/design/akgDOb37YLUW9iWLB155EV/Twenty-(Copy)?node-id=478-19796&t=8Gz1yqls2Q3dsN9h-1)
---

View File

@ -19,4 +19,5 @@ Your turn 👇
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) » 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) » 22-October-2024 by [FaheemOnHub](https://oss.gg/FaheemOnHub) video Link: [video](https://drive.google.com/file/d/1bR59Q5gqoqHjzgdrF6K68U2hloexkQYM/view)
---
» 27-October-2024 by [Khaan25](https://oss.gg/Khaan25) video Link: [video](https://drive.google.com/file/d/1-wgzofJaWmnMcFgZZV5uYNNgtbJKJ_1G/view?usp=sharing/)

View File

@ -18,4 +18,6 @@ Your turn 👇
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) template Link: [template](https://twenty.com/) » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) template Link: [template](https://twenty.com/)
» 25-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) template Link: [template]()
--- ---

View File

@ -18,4 +18,6 @@ Your turn 👇
» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) guide Link: [guide](https://twenty.com/) » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) guide Link: [guide](https://twenty.com/)
» 26-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) guide Link: [guide](https://dev.to/sateshcharan/supercharge-your-marketing-with-twentycrm-n8n-1hfd)
--- ---

View File

@ -19,3 +19,7 @@ Your turn 👇
» 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan)
» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) » 22-October-2024 by [Khaan25](https://oss.gg/Khaan25)
» 26-October-2024 by [Naprila](https://oss.gg/Naprila)
» 28-October-2024 by [Aditya Deshlahre](https://oss.gg/adityadeshlahre)

View File

@ -49,6 +49,7 @@
"@stoplight/elements": "^8.0.5", "@stoplight/elements": "^8.0.5",
"@swc/jest": "^0.2.29", "@swc/jest": "^0.2.29",
"@tabler/icons-react": "^2.44.0", "@tabler/icons-react": "^2.44.0",
"@tiptap/extension-hard-break": "^2.9.1",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/facepaint": "^1.2.5", "@types/facepaint": "^1.2.5",
"@types/lodash.camelcase": "^4.3.7", "@types/lodash.camelcase": "^4.3.7",
@ -295,7 +296,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

@ -1,6 +1,5 @@
import styled from '@emotion/styled';
import { Loader } from '@/ui/display/loader/components/Loader'; import { Loader } from '@/ui/display/loader/components/Loader';
import styled from '@emotion/styled';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;

View File

@ -42,3 +42,6 @@ dist-ssr
.vite/ .vite/
.nyc_output/ .nyc_output/
# eslint-plugin-project-structure
projectStructure.cache.json

View File

@ -57,5 +57,6 @@ const config: StorybookConfig = {
}, },
}); });
}, },
logLevel: 'error',
}; };
export default config; export default config;

View File

@ -29,6 +29,7 @@ initialize({
with payload ${JSON.stringify(requestBody)}\n with payload ${JSON.stringify(requestBody)}\n
This request should be mocked with MSW`); This request should be mocked with MSW`);
}, },
quiet: true,
}); });
const preview: Preview = { const preview: Preview = {

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

@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = {
extensionsToTreatAsEsm: ['.ts', '.tsx'], extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: { coverageThreshold: {
global: { global: {
statements: 59, statements: 58,
lines: 55, lines: 55,
functions: 49, functions: 47,
}, },
}, },
collectCoverageFrom: ['<rootDir>/src/**/*.ts'], collectCoverageFrom: ['<rootDir>/src/**/*.ts'],

View File

@ -1,18 +1,18 @@
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import { jest } from '@storybook/jest'; import { jest } from '@storybook/jest';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { graphql, HttpResponse } from 'msw'; import { HttpResponse, graphql } from 'msw';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { IconsProvider } from 'twenty-ui';
import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary';
import indexAppPath from '@/navigation/utils/indexAppPath'; import indexAppPath from '@/navigation/utils/indexAppPath';
import { AppPath } from '@/types/AppPath';
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { AppRouter } from '@/app/components/AppRouter'; import { AppRouter } from '@/app/components/AppRouter';
import { AppPath } from '@/types/AppPath';
import { IconsProvider } from 'twenty-ui';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedUserData } from '~/testing/mock-data/users'; import { mockedUserData } from '~/testing/mock-data/users';

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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;
}; };
@ -1155,12 +1166,13 @@ export type UpdateObjectPayload = {
icon?: InputMaybe<Scalars['String']>; icon?: InputMaybe<Scalars['String']>;
imageIdentifierFieldMetadataId?: InputMaybe<Scalars['String']>; imageIdentifierFieldMetadataId?: InputMaybe<Scalars['String']>;
isActive?: InputMaybe<Scalars['Boolean']>; isActive?: InputMaybe<Scalars['Boolean']>;
isLabelSyncedWithName?: InputMaybe<Scalars['Boolean']>;
labelIdentifierFieldMetadataId?: InputMaybe<Scalars['String']>; labelIdentifierFieldMetadataId?: InputMaybe<Scalars['String']>;
labelPlural?: InputMaybe<Scalars['String']>; labelPlural?: InputMaybe<Scalars['String']>;
labelSingular?: InputMaybe<Scalars['String']>; labelSingular?: InputMaybe<Scalars['String']>;
namePlural?: InputMaybe<Scalars['String']>; namePlural?: InputMaybe<Scalars['String']>;
nameSingular?: InputMaybe<Scalars['String']>; nameSingular?: InputMaybe<Scalars['String']>;
shouldSyncLabelAndName?: InputMaybe<Scalars['Boolean']>; shortcut?: InputMaybe<Scalars['String']>;
}; };
export type UpdateOneObjectInput = { export type UpdateOneObjectInput = {
@ -1271,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'];
@ -1470,6 +1483,7 @@ export type Object = {
indexMetadatas: ObjectIndexMetadatasConnection; indexMetadatas: ObjectIndexMetadatasConnection;
isActive: Scalars['Boolean']; isActive: Scalars['Boolean'];
isCustom: Scalars['Boolean']; isCustom: Scalars['Boolean'];
isLabelSyncedWithName: Scalars['Boolean'];
isRemote: Scalars['Boolean']; isRemote: Scalars['Boolean'];
isSystem: Scalars['Boolean']; isSystem: Scalars['Boolean'];
labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>; labelIdentifierFieldMetadataId?: Maybe<Scalars['String']>;
@ -1477,7 +1491,7 @@ export type Object = {
labelSingular: Scalars['String']; labelSingular: Scalars['String'];
namePlural: Scalars['String']; namePlural: Scalars['String'];
nameSingular: Scalars['String']; nameSingular: Scalars['String'];
shouldSyncLabelAndName: Scalars['Boolean']; shortcut?: Maybe<Scalars['String']>;
updatedAt: Scalars['DateTime']; updatedAt: Scalars['DateTime'];
}; };
@ -1675,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'];
@ -1708,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'];
@ -1795,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; }>;
@ -1812,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'];
@ -1821,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'];
}>; }>;
@ -2042,6 +2063,7 @@ export const UserQueryFragmentFragmentDoc = gql`
allowImpersonation allowImpersonation
activationStatus activationStatus
isPublicInviteLinkEnabled isPublicInviteLinkEnabled
hasValidEntrepriseKey
featureFlags { featureFlags {
id id
key key
@ -3441,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

@ -2,6 +2,7 @@ import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql'; import { OnboardingStatus, SubscriptionStatus } from '~/generated/graphql';

View File

@ -1,8 +1,9 @@
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { SettingsPath } from '@/types/SettingsPath';
import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState'; import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import { useRecoilValue, useResetRecoilState } from 'recoil';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
export const useCleanRecoilState = () => { export const useCleanRecoilState = () => {

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({
@ -120,17 +121,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 +140,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,7 +1,9 @@
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 { 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 { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@ -25,6 +27,9 @@ export const RecordIndexActionMenuEffect = () => {
`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 +48,11 @@ export const RecordIndexActionMenuEffect = () => {
isDropdownOpen, isDropdownOpen,
]); ]);
useEffect(() => {
if (isRightDrawerOpen || isCommandMenuOpened) {
closeActionBar();
}
}, [closeActionBar, isRightDrawerOpen, isCommandMenuOpened]);
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

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

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

@ -4,7 +4,7 @@ import { differenceInSeconds, endOfDay, format } from 'date-fns';
import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow'; import { CalendarEventRow } from '@/activities/calendar/components/CalendarEventRow';
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from 'twenty-ui';
import { TimelineCalendarEvent } from '~/generated/graphql'; import { TimelineCalendarEvent } from '~/generated/graphql';
type CalendarDayCardContentProps = { type CalendarDayCardContentProps = {

View File

@ -9,6 +9,8 @@ import {
IconArrowRight, IconArrowRight,
IconLock, IconLock,
isDefined, isDefined,
Card,
CardContent,
} from 'twenty-ui'; } from 'twenty-ui';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor'; import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
@ -18,8 +20,6 @@ import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendar
import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate'; import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded'; import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { Card } from '@/ui/layout/card/components/Card';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { import {
CalendarChannelVisibility, CalendarChannelVisibility,
TimelineCalendarEvent, TimelineCalendarEvent,

View File

@ -2,7 +2,7 @@ import { useContext } from 'react';
import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent'; import { CalendarDayCardContent } from '@/activities/calendar/components/CalendarDayCardContent';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from 'twenty-ui';
type CalendarMonthCardProps = { type CalendarMonthCardProps = {
dayTimes: number[]; dayTimes: number[];

View File

@ -1,5 +1,5 @@
import { Card } from '@/ui/layout/card/components/Card';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Card } from 'twenty-ui';
const StyledList = styled(Card)` const StyledList = styled(Card)`
& > :not(:last-child) { & > :not(:last-child) {

View File

@ -1,5 +1,5 @@
import { CardContent } from '@/ui/layout/card/components/CardContent';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { CardContent } from 'twenty-ui';
import React from 'react'; import React from 'react';
const StyledRowContent = styled(CardContent)<{ const StyledRowContent = styled(CardContent)<{

View File

@ -1,9 +1,9 @@
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { import {
AnimatedPlaceholder, AnimatedPlaceholder,
AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyContainer,
AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTextContainer,
AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderEmptyTitle,
Loader,
} from 'twenty-ui'; } from 'twenty-ui';
export const EmailLoader = ({ loadingText }: { loadingText?: string }) => ( export const EmailLoader = ({ loadingText }: { loadingText?: string }) => (

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

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

@ -1,6 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { Card } from '@/ui/layout/card/components/Card'; import { Card } from 'twenty-ui';
type EventCardProps = { type EventCardProps = {
children: React.ReactNode; children: React.ReactNode;

View File

@ -10,12 +10,12 @@ import { previousUrlState } from '@/auth/states/previousUrlState';
import { tokenPairState } from '@/auth/states/tokenPairState'; import { tokenPairState } from '@/auth/states/tokenPairState';
import { workspacesState } from '@/auth/states/workspaces'; import { workspacesState } from '@/auth/states/workspaces';
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { AppPath } from '@/types/AppPath';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { AppPath } from '@/types/AppPath';
import { ApolloFactory, Options } from '../services/apollo.factory'; import { ApolloFactory, Options } from '../services/apollo.factory';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => { export const useApolloFactory = (options: Partial<Options<any>> = {}) => {

View File

@ -27,11 +27,17 @@ export const GotoHotkeysEffectsProvider = () => {
), ),
}); });
return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => {
<GoToHotkeyItemEffect if (!objectMetadataItem.shortcut) {
key={`go-to-hokey-item-${objectMetadataItem.id}`} return null;
hotkey={objectMetadataItem.namePlural[0]} }
pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`}
/> return (
)); <GoToHotkeyItemEffect
key={`go-to-hokey-item-${objectMetadataItem.id}`}
hotkey={objectMetadataItem.shortcut}
pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`}
/>
);
});
}; };

View File

@ -1,7 +1,6 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconCheckbox } from 'twenty-ui';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { import {
@ -21,6 +20,7 @@ import { AppPath } from '@/types/AppPath';
import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { PageHotkeyScope } from '@/types/PageHotkeyScope';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { IconCheckbox } from 'twenty-ui';
import { useCleanRecoilState } from '~/hooks/useCleanRecoilState'; import { useCleanRecoilState } from '~/hooks/useCleanRecoilState';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation';
@ -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

@ -2,14 +2,16 @@ import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import {
useSignInUpForm,
validationSchema,
} from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle'; import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft'; import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { authProvidersState } from '@/client-config/states/authProvidersState'; import { authProvidersState } from '@/client-config/states/authProvidersState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { Loader } from '@/ui/feedback/loader/components/Loader';
import { TextInput } from '@/ui/input/components/TextInput'; import { TextInput } from '@/ui/input/components/TextInput';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
@ -23,7 +25,9 @@ import {
IconGoogle, IconGoogle,
IconKey, IconKey,
IconMicrosoft, IconMicrosoft,
Loader,
MainButton, MainButton,
StyledText,
} from 'twenty-ui'; } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -127,7 +131,8 @@ export const SignInUpForm = () => {
const isEmailStepSubmitButtonDisabledCondition = const isEmailStepSubmitButtonDisabledCondition =
signInUpStep === SignInUpStep.Email && signInUpStep === SignInUpStep.Email &&
(form.watch('email')?.length === 0 || shouldWaitForCaptchaToken); (!validationSchema.shape.email.safeParse(form.watch('email')).success ||
shouldWaitForCaptchaToken);
// TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders // TODO: isValid is actually a proxy function. If it is not rendered the first time, react might not trigger re-renders
// We make the isValid check synchronous and update a reactState to make sure this does not happen // We make the isValid check synchronous and update a reactState to make sure this does not happen
@ -183,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 ||
@ -263,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

@ -5,20 +5,20 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken'; import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { useRecoilState, useSetRecoilState } from 'recoil'; import { useRecoilState, useSetRecoilState } from 'recoil';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
import { useAuth } from '../../hooks/useAuth'; import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO';
import { import {
SignInUpStep, SignInUpStep,
signInUpStepState, signInUpStepState,
} from '@/auth/states/signInUpStepState'; } from '@/auth/states/signInUpStepState';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; import { AppPath } from '@/types/AppPath';
import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO'; import { useAuth } from '../../hooks/useAuth';
export enum SignInUpMode { export enum SignInUpMode {
SignIn = 'sign-in', SignIn = 'sign-in',

View File

@ -1,13 +1,13 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { z } from 'zod'; import { z } from 'zod';
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex'; import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
const validationSchema = z export const validationSchema = z
.object({ .object({
exist: z.boolean(), exist: z.boolean(),
email: z.string().trim().email('Email must be a valid email'), email: z.string().trim().email('Email must be a valid email'),

View File

@ -3,11 +3,12 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useRecoilValue, useSetRecoilState } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { AppPath } from '@/types/AppPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState';
import { AppPath } from '@/types/AppPath';
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql'; import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';

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

@ -124,7 +124,8 @@ describe('useCommandMenu', () => {
namePlural: 'tasks', namePlural: 'tasks',
labelSingular: 'Task', labelSingular: 'Task',
labelPlural: 'Tasks', labelPlural: 'Tasks',
shouldSyncLabelAndName: true, isLabelSyncedWithName: true,
shortcut: 'T',
description: 'A task', description: 'A task',
icon: 'IconCheckbox', icon: 'IconCheckbox',
isCustom: false, isCustom: false,

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 }) =>
@ -83,8 +118,8 @@ export const useCommandMenu = () => {
to: `/objects/${item.namePlural}`, to: `/objects/${item.namePlural}`,
label: `Go to ${item.labelPlural}`, label: `Go to ${item.labelPlural}`,
type: CommandType.Navigate, type: CommandType.Navigate,
firstHotKey: 'G', firstHotKey: item.shortcut ? 'G' : undefined,
secondHotKey: item.labelPlural[0], secondHotKey: item.shortcut,
Icon: ALL_ICONS[ Icon: ALL_ICONS[
(item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight' (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight'
], ],

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

@ -1,7 +1,7 @@
import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { computeViewRecordGqlOperationFilter } from '@/object-record/record-filter/utils/computeViewRecordGqlOperationFilter';
import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
export const computeContextStoreFilters = ( export const computeContextStoreFilters = (
@ -12,9 +12,10 @@ export const computeContextStoreFilters = (
if (contextStoreTargetedRecordsRule.mode === 'exclusion') { if (contextStoreTargetedRecordsRule.mode === 'exclusion') {
queryFilter = makeAndFilterVariables([ queryFilter = makeAndFilterVariables([
turnFiltersIntoQueryFilter( computeViewRecordGqlOperationFilter(
contextStoreTargetedRecordsRule.filters, contextStoreTargetedRecordsRule.filters,
objectMetadataItem?.fields ?? [], objectMetadataItem?.fields ?? [],
[],
), ),
contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 contextStoreTargetedRecordsRule.excludedRecordIds.length > 0
? { ? {

View File

@ -85,238 +85,238 @@ export const mocks = [
mutation CreateOneFavorite($input: FavoriteCreateInput!) { mutation CreateOneFavorite($input: FavoriteCreateInput!) {
createFavorite(data: $input) { createFavorite(data: $input) {
__typename __typename
company { company {
__typename __typename
accountOwnerId accountOwnerId
address { address {
addressStreet1 addressStreet1
addressStreet2 addressStreet2
addressCity addressCity
addressState addressState
addressCountry addressCountry
addressPostcode addressPostcode
addressLat addressLat
addressLng addressLng
} }
annualRecurringRevenue { annualRecurringRevenue {
amountMicros amountMicros
currencyCode currencyCode
} }
createdAt createdAt
createdBy { createdBy {
source source
workspaceMemberId
name
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
position
tagline
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
companyId
createdAt
deletedAt
id
note {
__typename
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
position
title
updatedAt
}
noteId
opportunity {
__typename
amount {
amountMicros
currencyCode
}
closeDate
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
pointOfContactId
position
stage
updatedAt
}
opportunityId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
personId
position
rocket {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
position
updatedAt
}
rocketId
task {
__typename
assigneeId
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
dueAt
id
position
status
title
updatedAt
}
taskId
updatedAt
view {
__typename
createdAt
deletedAt
icon
id
isCompact
kanbanFieldMetadataId
key
name
objectMetadataId
position
type
updatedAt
}
viewId
workflow {
__typename
createdAt
deletedAt
id
lastPublishedVersionId
name
position
statuses
updatedAt
}
workflowId
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
workspaceMemberId workspaceMemberId
name
}
deletedAt
domainName {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
employees
id
idealCustomerProfile
introVideo {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name
position
tagline
updatedAt
visaSponsorship
workPolicy
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
} }
} }
companyId
createdAt
deletedAt
id
note {
__typename
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
position
title
updatedAt
}
noteId
opportunity {
__typename
amount {
amountMicros
currencyCode
}
closeDate
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
pointOfContactId
position
stage
updatedAt
}
opportunityId
person {
__typename
avatarUrl
city
companyId
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
emails {
primaryEmail
additionalEmails
}
id
intro
jobTitle
linkedinLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
name {
firstName
lastName
}
performanceRating
phones {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
position
updatedAt
whatsapp {
primaryPhoneNumber
primaryPhoneCountryCode
additionalPhones
}
workPreference
xLink {
primaryLinkUrl
primaryLinkLabel
secondaryLinks
}
}
personId
position
rocket {
__typename
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
id
name
position
updatedAt
}
rocketId
task {
__typename
assigneeId
body
createdAt
createdBy {
source
workspaceMemberId
name
}
deletedAt
dueAt
id
position
status
title
updatedAt
}
taskId
updatedAt
view {
__typename
createdAt
deletedAt
icon
id
isCompact
kanbanFieldMetadataId
key
name
objectMetadataId
position
type
updatedAt
}
viewId
workflow {
__typename
createdAt
deletedAt
id
lastPublishedVersionId
name
position
statuses
updatedAt
}
workflowId
workspaceMember {
__typename
avatarUrl
colorScheme
createdAt
dateFormat
deletedAt
id
locale
name {
firstName
lastName
}
timeFormat
timeZone
updatedAt
userEmail
userId
}
workspaceMemberId
}
}
`, `,
variables: { variables: {
input: { input: {

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

@ -4,13 +4,12 @@ import { MemoryRouter } from 'react-router-dom';
import { useSetRecoilState } from 'recoil'; import { useSetRecoilState } from 'recoil';
import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState'; import { currentMobileNavigationDrawerState } from '@/navigation/states/currentMobileNavigationDrawerState';
import { AppPath } from '@/types/AppPath';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { AppPath } from '@/types/AppPath';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { import {
AppNavigationDrawer, AppNavigationDrawer,

View File

@ -1,5 +1,4 @@
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import indexAppPath from '../indexAppPath'; import indexAppPath from '../indexAppPath';
describe('getIndexAppPath', () => { describe('getIndexAppPath', () => {

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

@ -24,7 +24,8 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql`
updatedAt updatedAt
labelIdentifierFieldMetadataId labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId imageIdentifierFieldMetadataId
shouldSyncLabelAndName shortcut
isLabelSyncedWithName
indexMetadatas(paging: { first: 100 }) { indexMetadatas(paging: { first: 100 }) {
edges { edges {
node { node {

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

@ -24,6 +24,8 @@ export const query = gql`
updatedAt updatedAt
labelIdentifierFieldMetadataId labelIdentifierFieldMetadataId
imageIdentifierFieldMetadataId imageIdentifierFieldMetadataId
shortcut
isLabelSyncedWithName
fields(paging: { first: 1000 }, filter: $fieldFilter) { fields(paging: { first: 1000 }, filter: $fieldFilter) {
edges { edges {
node { node {

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

@ -24,7 +24,9 @@ export enum CoreObjectNameSingular {
View = 'view', View = 'view',
ViewField = 'viewField', ViewField = 'viewField',
ViewFilter = 'viewFilter', ViewFilter = 'viewFilter',
ViewFilterGroup = 'viewFilterGroup',
ViewSort = 'viewSort', ViewSort = 'viewSort',
ViewGroup = 'viewGroup',
Webhook = 'webhook', Webhook = 'webhook',
WorkspaceMember = 'workspaceMember', WorkspaceMember = 'workspaceMember',
MessageThreadSubscriber = 'messageThreadSubscriber', MessageThreadSubscriber = 'messageThreadSubscriber',

View File

@ -30,7 +30,7 @@ describe('objectMetadataItemSchema', () => {
namePlural: 'notCamelCase', namePlural: 'notCamelCase',
nameSingular: 'notCamelCase', nameSingular: 'notCamelCase',
updatedAt: 'invalid date', updatedAt: 'invalid date',
shouldSyncLabelAndName: 'not a boolean', isLabelSyncedWithName: 'not a boolean',
}; };
// When // When

View File

@ -26,5 +26,6 @@ export const objectMetadataItemSchema = z.object({
namePlural: camelCaseStringSchema, namePlural: camelCaseStringSchema,
nameSingular: camelCaseStringSchema, nameSingular: camelCaseStringSchema,
updatedAt: z.string().datetime(), updatedAt: z.string().datetime(),
shouldSyncLabelAndName: z.boolean(), shortcut: z.string().nullable().optional(),
isLabelSyncedWithName: z.boolean(),
}) satisfies z.ZodType<ObjectMetadataItem>; }) satisfies z.ZodType<ObjectMetadataItem>;

View File

@ -0,0 +1,161 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
import { useCallback } from 'react';
import { IconLibraryPlus, IconPlus, isDefined, LightButton } from 'twenty-ui';
import { v4 } from 'uuid';
type AdvancedFilterAddFilterRuleSelectProps = {
viewFilterGroup: ViewFilterGroup;
lastChildPosition?: number;
};
export const AdvancedFilterAddFilterRuleSelect = ({
viewFilterGroup,
lastChildPosition = 0,
}: AdvancedFilterAddFilterRuleSelectProps) => {
const dropdownId = `advanced-filter-add-filter-rule-${viewFilterGroup.id}`;
const { currentViewId } = useGetCurrentView();
const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const newPositionInViewFilterGroup = lastChildPosition + 1;
const { closeDropdown } = useDropdown(dropdownId);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const objectMetadataId =
currentViewWithCombinedFiltersAndSorts?.objectMetadataId;
if (!objectMetadataId) {
throw new Error('Object metadata id is missing from current view');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId,
});
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const getDefaultFilterDefinition = useCallback(() => {
const defaultFilterDefinition =
availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
) ?? availableFilterDefinitions?.[0];
if (!defaultFilterDefinition) {
throw new Error('Missing default filter definition');
}
return defaultFilterDefinition;
}, [availableFilterDefinitions, objectMetadataItem]);
const handleAddFilter = () => {
closeDropdown();
const defaultFilterDefinition = getDefaultFilterDefinition();
upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
viewFilterGroupId: viewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
});
};
const handleAddFilterGroup = () => {
closeDropdown();
if (!currentViewId) {
throw new Error('Missing view id');
}
const newViewFilterGroup = {
id: v4(),
viewId: currentViewId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
parentViewFilterGroupId: viewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
};
upsertCombinedViewFilterGroup(newViewFilterGroup);
const defaultFilterDefinition = getDefaultFilterDefinition();
upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
viewFilterGroupId: newViewFilterGroup.id,
positionInViewFilterGroup: newPositionInViewFilterGroup,
});
};
const isFilterRuleGroupOptionVisible = !isDefined(
viewFilterGroup.parentViewFilterGroupId,
);
if (!isFilterRuleGroupOptionVisible) {
return (
<LightButton
Icon={IconPlus}
title="Add filter rule"
onClick={handleAddFilter}
/>
);
}
return (
<Dropdown
disableBlur
dropdownId={dropdownId}
clickableComponent={
<LightButton Icon={IconPlus} title="Add filter rule" />
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
LeftIcon={IconPlus}
text="Add rule"
onClick={handleAddFilter}
/>
{isFilterRuleGroupOptionVisible && (
<MenuItem
LeftIcon={IconLibraryPlus}
text="Add rule group"
onClick={handleAddFilterGroup}
/>
)}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -0,0 +1,41 @@
import { AdvancedFilterLogicalOperatorDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorDropdown';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import styled from '@emotion/styled';
import { capitalize } from '~/utils/string/capitalize';
const StyledText = styled.div`
height: ${({ theme }) => theme.spacing(8)};
display: flex;
align-items: center;
`;
const StyledContainer = styled.div`
align-items: start;
display: flex;
min-width: ${({ theme }) => theme.spacing(20)};
color: ${({ theme }) => theme.font.color.tertiary};
`;
type AdvancedFilterLogicalOperatorCellProps = {
index: number;
viewFilterGroup: ViewFilterGroup;
};
export const AdvancedFilterLogicalOperatorCell = ({
index,
viewFilterGroup,
}: AdvancedFilterLogicalOperatorCellProps) => (
<StyledContainer>
{index === 0 ? (
<StyledText>Where</StyledText>
) : index === 1 ? (
<AdvancedFilterLogicalOperatorDropdown
viewFilterGroup={viewFilterGroup}
/>
) : (
<StyledText>
{capitalize(viewFilterGroup.logicalOperator.toLowerCase())}
</StyledText>
)}
</StyledContainer>
);

View File

@ -0,0 +1,33 @@
import { ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS } from '@/object-record/advanced-filter/constants/AdvancedFilterLogicalOperatorOptions';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { Select } from '@/ui/input/components/Select';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
type AdvancedFilterLogicalOperatorDropdownProps = {
viewFilterGroup: ViewFilterGroup;
};
export const AdvancedFilterLogicalOperatorDropdown = ({
viewFilterGroup,
}: AdvancedFilterLogicalOperatorDropdownProps) => {
const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();
const handleChange = (value: ViewFilterGroupLogicalOperator) => {
upsertCombinedViewFilterGroup({
...viewFilterGroup,
logicalOperator: value,
});
};
return (
<Select
disableBlur
fullWidth
dropdownId={`advanced-filter-logical-operator-${viewFilterGroup.id}`}
value={viewFilterGroup.logicalOperator}
onChange={handleChange}
options={ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS}
/>
);
};

View File

@ -0,0 +1,78 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell';
import { AdvancedFilterRuleOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown';
import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter';
import { AdvancedFilterViewFilterGroup } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterGroup';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-ui';
const StyledRow = styled.div`
display: flex;
width: 100%;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
align-items: start;
background-color: ${({ theme, isGrayBackground }) =>
isGrayBackground ? theme.background.transparent.lighter : 'transparent'};
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
`;
type AdvancedFilterRootLevelViewFilterGroupProps = {
rootLevelViewFilterGroupId: string;
};
export const AdvancedFilterRootLevelViewFilterGroup = ({
rootLevelViewFilterGroupId,
}: AdvancedFilterRootLevelViewFilterGroupProps) => {
const {
currentViewFilterGroup: rootLevelViewFilterGroup,
childViewFiltersAndViewFilterGroups,
lastChildPosition,
} = useCurrentViewViewFilterGroup({
viewFilterGroupId: rootLevelViewFilterGroupId,
});
if (!isDefined(rootLevelViewFilterGroup)) {
return null;
}
return (
<StyledContainer>
{childViewFiltersAndViewFilterGroups.map((child, i) =>
child.__typename === 'ViewFilterGroup' ? (
<StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell
index={i}
viewFilterGroup={rootLevelViewFilterGroup}
/>
<AdvancedFilterViewFilterGroup viewFilterGroupId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterGroupId={child.id} />
</StyledRow>
) : (
<StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell
index={i}
viewFilterGroup={rootLevelViewFilterGroup}
/>
<AdvancedFilterViewFilter viewFilterId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} />
</StyledRow>
),
)}
<AdvancedFilterAddFilterRuleSelect
viewFilterGroup={rootLevelViewFilterGroup}
lastChildPosition={lastChildPosition}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,87 @@
import { AdvancedFilterRuleOptionsDropdownButton } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdownButton';
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
import { useDeleteCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useDeleteCombinedViewFilterGroup';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters';
import { isDefined } from 'twenty-ui';
type AdvancedFilterRuleOptionsDropdownProps =
| {
viewFilterId: string;
viewFilterGroupId?: never;
}
| {
viewFilterId?: never;
viewFilterGroupId: string;
};
export const AdvancedFilterRuleOptionsDropdown = ({
viewFilterId,
viewFilterGroupId,
}: AdvancedFilterRuleOptionsDropdownProps) => {
const dropdownId = `advanced-filter-rule-options-${viewFilterId ?? viewFilterGroupId}`;
const { deleteCombinedViewFilter } = useDeleteCombinedViewFilters();
const { deleteCombinedViewFilterGroup } = useDeleteCombinedViewFilterGroup();
const { currentViewFilterGroup, childViewFiltersAndViewFilterGroups } =
useCurrentViewViewFilterGroup({
viewFilterGroupId,
});
const currentViewFilter = useCurrentViewFilter({
viewFilterId,
});
const handleRemove = async () => {
if (isDefined(viewFilterId)) {
deleteCombinedViewFilter(viewFilterId);
const isOnlyViewFilterInGroup =
childViewFiltersAndViewFilterGroups.length === 1;
if (
isOnlyViewFilterInGroup &&
isDefined(currentViewFilter?.viewFilterGroupId)
) {
deleteCombinedViewFilterGroup(currentViewFilter.viewFilterGroupId);
}
} else if (isDefined(currentViewFilterGroup)) {
deleteCombinedViewFilterGroup(currentViewFilterGroup.id);
const childViewFilters = childViewFiltersAndViewFilterGroups.filter(
(child) => child.__typename === 'ViewFilter',
);
for (const childViewFilter of childViewFilters) {
await deleteCombinedViewFilter(childViewFilter.id);
}
} else {
throw new Error('No view filter or view filter group to remove');
}
};
const removeButtonLabel = viewFilterId ? 'Remove rule' : 'Remove rule group';
return (
<Dropdown
disableBlur
dropdownId={dropdownId}
clickableComponent={
<AdvancedFilterRuleOptionsDropdownButton dropdownId={dropdownId} />
}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem text={removeButtonLabel} onClick={handleRemove} />
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
);
};

View File

@ -0,0 +1,25 @@
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { IconButton, IconDotsVertical } from 'twenty-ui';
type AdvancedFilterRuleOptionsDropdownButtonProps = {
dropdownId: string;
};
export const AdvancedFilterRuleOptionsDropdownButton = ({
dropdownId,
}: AdvancedFilterRuleOptionsDropdownButtonProps) => {
const { toggleDropdown } = useDropdown(dropdownId);
const handleClick = () => {
toggleDropdown();
};
return (
<IconButton
aria-label="Filter rule options"
variant="tertiary"
Icon={IconDotsVertical}
onClick={handleClick}
/>
);
};

View File

@ -0,0 +1,47 @@
import { AdvancedFilterViewFilterFieldSelect } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterFieldSelect';
import { AdvancedFilterViewFilterOperandSelect } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterOperandSelect';
import { AdvancedFilterViewFilterValueInput } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilterValueInput';
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
import { configurableViewFilterOperands } from '@/object-record/object-filter-dropdown/utils/configurableViewFilterOperands';
import styled from '@emotion/styled';
const StyledValueDropdownContainer = styled.div`
flex: 3;
`;
const StyledRow = styled.div`
flex: 1;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
white-space: nowrap;
overflow: hidden;
`;
type AdvancedFilterViewFilterProps = {
viewFilterId: string;
};
export const AdvancedFilterViewFilter = ({
viewFilterId,
}: AdvancedFilterViewFilterProps) => {
const filter = useCurrentViewFilter({ viewFilterId });
if (!filter) {
return null;
}
return (
<ObjectFilterDropdownScope filterScopeId={filter.id}>
<StyledRow>
<AdvancedFilterViewFilterFieldSelect viewFilterId={filter.id} />
<AdvancedFilterViewFilterOperandSelect viewFilterId={filter.id} />
<StyledValueDropdownContainer>
{configurableViewFilterOperands.has(filter.operand) && (
<AdvancedFilterViewFilterValueInput viewFilterId={filter.id} />
)}
</StyledValueDropdownContainer>
</StyledRow>
</ObjectFilterDropdownScope>
);
};

View File

@ -0,0 +1,71 @@
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
import { ObjectFilterDropdownFilterSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect';
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { objectFilterDropdownIsSelectingCompositeFieldComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownIsSelectingCompositeFieldComponentState';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
flex: 2;
`;
type AdvancedFilterViewFilterFieldSelectProps = {
viewFilterId: string;
};
export const AdvancedFilterViewFilterFieldSelect = ({
viewFilterId,
}: AdvancedFilterViewFilterFieldSelectProps) => {
const { advancedFilterDropdownId } = useAdvancedFilterDropdown(viewFilterId);
const filter = useCurrentViewFilter({ viewFilterId });
const selectedFieldLabel = filter?.definition.label ?? '';
const { setAdvancedFilterViewFilterGroupId, setAdvancedFilterViewFilterId } =
useFilterDropdown();
const [objectFilterDropdownIsSelectingCompositeField] =
useRecoilComponentStateV2(
objectFilterDropdownIsSelectingCompositeFieldComponentState,
);
const shouldShowCompositeSelectionSubMenu =
objectFilterDropdownIsSelectingCompositeField;
return (
<StyledContainer>
<Dropdown
disableBlur
dropdownId={advancedFilterDropdownId}
clickableComponent={
<SelectControl
selectedOption={{
label: selectedFieldLabel,
value: null,
}}
/>
}
onOpen={() => {
setAdvancedFilterViewFilterId(filter?.id);
setAdvancedFilterViewFilterGroupId(filter?.viewFilterGroupId);
}}
dropdownComponents={
shouldShowCompositeSelectionSubMenu ? (
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
) : (
<ObjectFilterDropdownFilterSelect />
)
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,67 @@
import { AdvancedFilterAddFilterRuleSelect } from '@/object-record/advanced-filter/components/AdvancedFilterAddFilterRuleSelect';
import { AdvancedFilterLogicalOperatorCell } from '@/object-record/advanced-filter/components/AdvancedFilterLogicalOperatorCell';
import { AdvancedFilterRuleOptionsDropdown } from '@/object-record/advanced-filter/components/AdvancedFilterRuleOptionsDropdown';
import { AdvancedFilterViewFilter } from '@/object-record/advanced-filter/components/AdvancedFilterViewFilter';
import { useCurrentViewViewFilterGroup } from '@/object-record/advanced-filter/hooks/useCurrentViewViewFilterGroup';
import styled from '@emotion/styled';
const StyledRow = styled.div`
display: flex;
width: 100%;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledContainer = styled.div<{ isGrayBackground?: boolean }>`
align-items: start;
background-color: ${({ theme, isGrayBackground }) =>
isGrayBackground ? theme.background.transparent.lighter : 'transparent'};
border: ${({ theme }) => `1px solid ${theme.border.color.medium}`};
border-radius: ${({ theme }) => theme.border.radius.md};
display: flex;
flex: 1;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
overflow: hidden;
`;
type AdvancedFilterViewFilterGroupProps = {
viewFilterGroupId: string;
};
export const AdvancedFilterViewFilterGroup = ({
viewFilterGroupId,
}: AdvancedFilterViewFilterGroupProps) => {
const {
currentViewFilterGroup,
childViewFiltersAndViewFilterGroups,
lastChildPosition,
} = useCurrentViewViewFilterGroup({
viewFilterGroupId,
});
if (!currentViewFilterGroup) {
return null;
}
return (
<StyledContainer
isGrayBackground={!!currentViewFilterGroup.parentViewFilterGroupId}
>
{childViewFiltersAndViewFilterGroups.map((child, i) => (
<StyledRow key={child.id}>
<AdvancedFilterLogicalOperatorCell
index={i}
viewFilterGroup={currentViewFilterGroup}
/>
<AdvancedFilterViewFilter viewFilterId={child.id} />
<AdvancedFilterRuleOptionsDropdown viewFilterId={child.id} />
</StyledRow>
))}
<AdvancedFilterAddFilterRuleSelect
viewFilterGroup={currentViewFilterGroup}
lastChildPosition={lastChildPosition}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,111 @@
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandLabel } from '@/object-record/object-filter-dropdown/utils/getOperandLabel';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-ui';
const StyledContainer = styled.div`
flex: 1;
`;
type AdvancedFilterViewFilterOperandSelectProps = {
viewFilterId: string;
};
export const AdvancedFilterViewFilterOperandSelect = ({
viewFilterId,
}: AdvancedFilterViewFilterOperandSelectProps) => {
const dropdownId = `advanced-filter-view-filter-operand-${viewFilterId}`;
const filter = useCurrentViewFilter({ viewFilterId });
const isDisabled = !filter?.fieldMetadataId;
const { closeDropdown } = useDropdown(dropdownId);
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const handleOperandChange = (operand: ViewFilterOperand) => {
closeDropdown();
if (!filter) {
throw new Error('Filter is not defined');
}
const { value, displayValue } = getInitialFilterValue(
filter.definition.type,
operand,
filter.value,
filter.displayValue,
);
upsertCombinedViewFilter({
...filter,
operand,
value,
displayValue,
});
};
const operandsForFilterType = isDefined(filter?.definition)
? getOperandsForFilterDefinition(filter.definition)
: [];
if (isDisabled === true) {
return (
<SelectControl
selectedOption={{
label: filter?.operand
? getOperandLabel(filter.operand)
: 'Select operand',
value: null,
}}
isDisabled
/>
);
}
return (
<StyledContainer>
<Dropdown
disableBlur
dropdownId={dropdownId}
clickableComponent={
<SelectControl
selectedOption={{
label: filter.operand
? getOperandLabel(filter.operand)
: 'Select operand',
value: null,
}}
/>
}
dropdownComponents={
<DropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => (
<MenuItem
key={`select-filter-operand-${index}`}
onClick={() => {
handleOperandChange(filterOperand);
}}
text={getOperandLabel(filterOperand)}
/>
))}
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,70 @@
import { useCurrentViewFilter } from '@/object-record/advanced-filter/hooks/useCurrentViewFilter';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { SelectControl } from '@/ui/input/components/SelectControl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
type AdvancedFilterViewFilterValueInputProps = {
viewFilterId: string;
};
export const AdvancedFilterViewFilterValueInput = ({
viewFilterId,
}: AdvancedFilterViewFilterValueInputProps) => {
const dropdownId = `advanced-filter-view-filter-value-input-${viewFilterId}`;
const filter = useCurrentViewFilter({ viewFilterId });
const isDisabled = !filter?.fieldMetadataId || !filter.operand;
const {
setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown,
setIsObjectFilterDropdownOperandSelectUnfolded,
setSelectedFilter,
} = useFilterDropdown();
if (isDisabled) {
return (
<SelectControl
isDisabled
selectedOption={{
label: filter?.displayValue ?? '',
value: null,
}}
/>
);
}
return (
<Dropdown
disableBlur
dropdownId={dropdownId}
clickableComponent={
<SelectControl
selectedOption={{
label: filter?.displayValue ?? '',
value: null,
}}
/>
}
onOpen={() => {
setFilterDefinitionUsedInDropdown(filter.definition);
setSelectedOperandInDropdown(filter.operand);
setIsObjectFilterDropdownOperandSelectUnfolded(true);
setSelectedFilter(filter);
}}
dropdownComponents={
<DropdownMenuItemsContainer>
<ObjectFilterDropdownFilterInput />
</DropdownMenuItemsContainer>
}
dropdownHotkeyScope={{ scope: ADVANCED_FILTER_DROPDOWN_ID }}
dropdownOffset={{ y: 8, x: 0 }}
dropdownPlacement="bottom-start"
dropdownMenuWidth={280}
/>
);
};

View File

@ -0,0 +1,12 @@
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
export const ADVANCED_FILTER_LOGICAL_OPERATOR_OPTIONS = [
{
value: ViewFilterGroupLogicalOperator.AND,
label: 'And',
},
{
value: ViewFilterGroupLogicalOperator.OR,
label: 'Or',
},
];

View File

@ -0,0 +1,14 @@
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
export const useAdvancedFilterDropdown = (viewFilterId?: string) => {
const advancedFilterDropdownId = `advanced-filter-view-filter-field-${viewFilterId}`;
const { closeDropdown: closeAdvancedFilterDropdown } = useDropdown(
advancedFilterDropdownId,
);
return {
closeAdvancedFilterDropdown,
advancedFilterDropdownId,
};
};

View File

@ -0,0 +1,31 @@
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters';
export const useCurrentViewFilter = ({
viewFilterId,
}: {
viewFilterId?: string;
}) => {
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const viewFilter = currentViewWithCombinedFiltersAndSorts?.viewFilters.find(
(viewFilter) => viewFilter.id === viewFilterId,
);
if (!viewFilter) {
return undefined;
}
const [filter] = mapViewFiltersToFilters(
[viewFilter],
availableFilterDefinitions,
);
return filter;
};

View File

@ -0,0 +1,59 @@
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { ViewFilter } from '@/views/types/ViewFilter';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { isDefined } from 'twenty-ui';
export const useCurrentViewViewFilterGroup = ({
viewFilterGroupId,
}: {
viewFilterGroupId?: string;
}) => {
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const viewFilterGroup =
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.find(
(viewFilterGroup) => viewFilterGroup.id === viewFilterGroupId,
);
if (!isDefined(viewFilterGroup)) {
return {
currentViewFilterGroup: undefined,
childViewFiltersAndViewFilterGroups: [] as (
| ViewFilter
| ViewFilterGroup
)[],
};
}
const childViewFilters =
currentViewWithCombinedFiltersAndSorts?.viewFilters.filter(
(viewFilterToFilter) =>
viewFilterToFilter.viewFilterGroupId === viewFilterGroup.id,
);
const childViewFilterGroups =
currentViewWithCombinedFiltersAndSorts?.viewFilterGroups.filter(
(viewFilterGroupToFilter) =>
viewFilterGroupToFilter.parentViewFilterGroupId === viewFilterGroup.id,
);
const childViewFiltersAndViewFilterGroups = [
...(childViewFilterGroups ?? []),
...(childViewFilters ?? []),
].sort((a, b) => {
const positionA = a.positionInViewFilterGroup ?? 0;
const positionB = b.positionInViewFilterGroup ?? 0;
return positionA - positionB;
});
const lastChildPosition =
childViewFiltersAndViewFilterGroups[
childViewFiltersAndViewFilterGroups.length - 1
]?.positionInViewFilterGroup ?? 0;
return {
currentViewFilterGroup: viewFilterGroup,
childViewFiltersAndViewFilterGroups,
lastChildPosition,
};
};

View File

@ -0,0 +1,111 @@
import { useRecoilCallback } from 'recoil';
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { useGetViewFromCache } from '@/views/hooks/useGetViewFromCache';
import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState';
import { unsavedToDeleteViewFilterGroupIdsComponentFamilyState } from '@/views/states/unsavedToDeleteViewFilterGroupIdsComponentFamilyState';
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
import { isDefined } from '~/utils/isDefined';
export const useDeleteCombinedViewFilterGroup = (
viewBarComponentId?: string,
) => {
const unsavedToUpsertViewFilterGroupsCallbackState =
useRecoilComponentCallbackStateV2(
unsavedToUpsertViewFilterGroupsComponentFamilyState,
viewBarComponentId,
);
const unsavedToDeleteViewFilterGroupIdsCallbackState =
useRecoilComponentCallbackStateV2(
unsavedToDeleteViewFilterGroupIdsComponentFamilyState,
viewBarComponentId,
);
const currentViewIdCallbackState = useRecoilComponentCallbackStateV2(
currentViewIdComponentState,
viewBarComponentId,
);
const { getViewFromCache } = useGetViewFromCache();
const deleteCombinedViewFilterGroup = useRecoilCallback(
({ snapshot, set }) =>
async (filterGroupId: string) => {
const currentViewId = getSnapshotValue(
snapshot,
currentViewIdCallbackState,
);
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
snapshot,
unsavedToUpsertViewFilterGroupsCallbackState({
viewId: currentViewId,
}),
);
const unsavedToDeleteViewFilterGroupIds = getSnapshotValue(
snapshot,
unsavedToDeleteViewFilterGroupIdsCallbackState({
viewId: currentViewId,
}),
);
if (!currentViewId) {
return;
}
const currentView = await getViewFromCache(currentViewId);
if (!currentView) {
return;
}
const matchingFilterGroupInCurrentView =
currentView.viewFilterGroups?.find(
(viewFilterGroup) => viewFilterGroup.id === filterGroupId,
);
const matchingFilterGroupInUnsavedFilterGroups =
unsavedToUpsertViewFilterGroups.find(
(viewFilterGroup) => viewFilterGroup.id === filterGroupId,
);
if (isDefined(matchingFilterGroupInUnsavedFilterGroups)) {
set(
unsavedToUpsertViewFilterGroupsCallbackState({
viewId: currentViewId,
}),
unsavedToUpsertViewFilterGroups.filter(
(viewFilterGroup) => viewFilterGroup.id !== filterGroupId,
),
);
}
if (isDefined(matchingFilterGroupInCurrentView)) {
set(
unsavedToDeleteViewFilterGroupIdsCallbackState({
viewId: currentViewId,
}),
[
...new Set([
...unsavedToDeleteViewFilterGroupIds,
matchingFilterGroupInCurrentView.id,
]),
],
);
}
},
[
currentViewIdCallbackState,
getViewFromCache,
unsavedToDeleteViewFilterGroupIdsCallbackState,
unsavedToUpsertViewFilterGroupsCallbackState,
],
);
return {
deleteCombinedViewFilterGroup,
};
};

View File

@ -0,0 +1,54 @@
import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue';
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
import { useRecoilComponentCallbackStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentCallbackStateV2';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { unsavedToUpsertViewFilterGroupsComponentFamilyState } from '@/views/states/unsavedToUpsertViewFilterGroupsComponentFamilyState';
import { ViewFilterGroup } from '@/views/types/ViewFilterGroup';
import { useRecoilCallback } from 'recoil';
export const useUpsertCombinedViewFilterGroup = () => {
const instanceId = useAvailableComponentInstanceIdOrThrow(
ViewComponentInstanceContext,
);
const unsavedToUpsertViewFilterGroupsCallbackState =
useRecoilComponentCallbackStateV2(
unsavedToUpsertViewFilterGroupsComponentFamilyState,
instanceId,
);
const upsertCombinedViewFilterGroup = useRecoilCallback(
({ snapshot, set }) =>
(newViewFilterGroup: Omit<ViewFilterGroup, '__typename'>) => {
const currentViewUnsavedToUpsertViewFilterGroups =
unsavedToUpsertViewFilterGroupsCallbackState({
viewId: newViewFilterGroup.viewId,
});
const unsavedToUpsertViewFilterGroups = getSnapshotValue(
snapshot,
currentViewUnsavedToUpsertViewFilterGroups,
);
const newViewFilterWithTypename: ViewFilterGroup = {
...newViewFilterGroup,
__typename: 'ViewFilterGroup',
};
set(
unsavedToUpsertViewFilterGroupsCallbackState({
viewId: newViewFilterGroup.viewId,
}),
[
...unsavedToUpsertViewFilterGroups.filter(
(viewFilterGroup) => viewFilterGroup.id !== newViewFilterGroup.id,
),
newViewFilterWithTypename,
],
);
},
[unsavedToUpsertViewFilterGroupsCallbackState],
);
return { upsertCombinedViewFilterGroup };
};

View File

@ -45,7 +45,7 @@ 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
@ -324,4 +324,4 @@ export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = `
primaryLinkLabel primaryLinkLabel
secondaryLinks secondaryLinks
} }
` `;

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

@ -0,0 +1,124 @@
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useUpsertCombinedViewFilterGroup } from '@/object-record/advanced-filter/hooks/useUpsertCombinedViewFilterGroup';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent';
import { StyledMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { ADVANCED_FILTER_DROPDOWN_ID } from '@/views/constants/AdvancedFilterDropdownId';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters';
import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { ViewFilterGroupLogicalOperator } from '@/views/types/ViewFilterGroupLogicalOperator';
import styled from '@emotion/styled';
import { IconFilter, Pill } from 'twenty-ui';
import { v4 } from 'uuid';
export const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1)};
border-top: 1px solid ${({ theme }) => theme.border.color.light};
`;
export const StyledMenuItemSelect = styled(StyledMenuItemBase)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
`;
export const StyledPill = styled(Pill)`
background: ${({ theme }) => theme.color.blueAccent10};
color: ${({ theme }) => theme.color.blue};
`;
export const AdvancedFilterButton = () => {
const advancedFilterQuerySubFilterCount = 0; // TODO
const { openDropdown: openAdvancedFilterDropdown } = useDropdown(
ADVANCED_FILTER_DROPDOWN_ID,
);
const { closeDropdown: closeObjectFilterDropdown } = useDropdown(
OBJECT_FILTER_DROPDOWN_ID,
);
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView();
const { upsertCombinedViewFilterGroup } = useUpsertCombinedViewFilterGroup();
const { upsertCombinedViewFilter } = useUpsertCombinedViewFilters();
const objectMetadataId =
currentViewWithCombinedFiltersAndSorts?.objectMetadataId;
if (!objectMetadataId) {
throw new Error('Object metadata id is missing from current view');
}
const { objectMetadataItem } = useObjectMetadataItemById({
objectId: objectMetadataId ?? null,
});
const availableFilterDefinitions = useRecoilComponentValueV2(
availableFilterDefinitionsComponentState,
);
const handleClick = () => {
if (!currentViewId) {
throw new Error('Missing current view id');
}
const alreadyHasAdvancedFilterGroup =
(currentViewWithCombinedFiltersAndSorts?.viewFilterGroups?.length ?? 0) >
0;
if (!alreadyHasAdvancedFilterGroup) {
const newViewFilterGroup = {
id: v4(),
viewId: currentViewId,
logicalOperator: ViewFilterGroupLogicalOperator.AND,
};
upsertCombinedViewFilterGroup(newViewFilterGroup);
const defaultFilterDefinition =
availableFilterDefinitions.find(
(filterDefinition) =>
filterDefinition.fieldMetadataId ===
objectMetadataItem?.labelIdentifierFieldMetadataId,
) ?? availableFilterDefinitions?.[0];
if (!defaultFilterDefinition) {
throw new Error('Missing default filter definition');
}
upsertCombinedViewFilter({
id: v4(),
fieldMetadataId: defaultFilterDefinition.fieldMetadataId,
operand: getOperandsForFilterDefinition(defaultFilterDefinition)[0],
definition: defaultFilterDefinition,
value: '',
displayValue: '',
viewFilterGroupId: newViewFilterGroup.id,
});
}
openAdvancedFilterDropdown();
closeObjectFilterDropdown();
};
return (
<StyledContainer>
<StyledMenuItemSelect onClick={handleClick}>
<MenuItemLeftContent LeftIcon={IconFilter} text="Advanced filter" />
{advancedFilterQuerySubFilterCount > 0 && (
<StyledPill label={advancedFilterQuerySubFilterCount.toString()} />
)}
</StyledMenuItemSelect>
</StyledContainer>
);
};

View File

@ -1,7 +1,7 @@
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { ObjectFilterDropdownFilterInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterInput';
import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu';
import { ObjectFilterOperandSelectAndInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterOperandSelectAndInput';
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';
@ -48,11 +48,13 @@ export const MultipleFiltersDropdownContent = ({
return ( return (
<StyledContainer> <StyledContainer>
{shoudShowFilterInput ? ( {shoudShowFilterInput ? (
<ObjectFilterDropdownFilterInput filterDropdownId={filterDropdownId} /> <ObjectFilterOperandSelectAndInput
filterDropdownId={filterDropdownId}
/>
) : shouldShowCompositeSelectionSubMenu ? ( ) : shouldShowCompositeSelectionSubMenu ? (
<ObjectFilterDropdownFilterSelectCompositeFieldSubMenu /> <ObjectFilterDropdownFilterSelectCompositeFieldSubMenu />
) : ( ) : (
<ObjectFilterDropdownFilterSelect /> <ObjectFilterDropdownFilterSelect isAdvancedFilterButtonVisible />
)} )}
<MultipleFiltersDropdownFilterOnFilterChangedEffect <MultipleFiltersDropdownFilterOnFilterChangedEffect
filterDefinitionUsedInDropdownType={ filterDefinitionUsedInDropdownType={

View File

@ -63,6 +63,7 @@ export const ObjectFilterDropdownDateInput = () => {
: newDate.toLocaleDateString() : newDate.toLocaleDateString()
: '', : '',
definition: filterDefinitionUsedInDropdown, definition: filterDefinitionUsedInDropdown,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
}); });
setIsObjectFilterDropdownUnfolded(false); setIsObjectFilterDropdownUnfolded(false);
@ -92,6 +93,7 @@ export const ObjectFilterDropdownDateInput = () => {
operand: selectedOperandInDropdown, operand: selectedOperandInDropdown,
displayValue: getRelativeDateDisplayValue(relativeDate), displayValue: getRelativeDateDisplayValue(relativeDate),
definition: filterDefinitionUsedInDropdown, definition: filterDefinitionUsedInDropdown,
viewFilterGroupId: selectedFilter?.viewFilterGroupId,
}); });
setIsObjectFilterDropdownUnfolded(false); setIsObjectFilterDropdownUnfolded(false);

View File

@ -2,8 +2,6 @@ import { useRecoilValue } from 'recoil';
import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownDateInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput';
import { ObjectFilterDropdownNumberInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput'; import { ObjectFilterDropdownNumberInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput';
import { ObjectFilterDropdownOperandButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton';
import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect';
import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect'; import { ObjectFilterDropdownOptionSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect';
import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput';
import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect';
@ -14,19 +12,11 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/
import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter';
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
import styled from '@emotion/styled';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
const StyledOperandSelectContainer = styled.div` import { DATE_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/DateFilterTypes';
background: ${({ theme }) => theme.background.secondary}; import { NUMBER_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/NumberFilterTypes';
box-shadow: ${({ theme }) => theme.boxShadow.light}; import { TEXT_FILTER_TYPES } from '@/object-record/object-filter-dropdown/constants/TextFilterTypes';
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
width: 100%;
z-index: 1000;
`;
type ObjectFilterDropdownFilterInputProps = { type ObjectFilterDropdownFilterInputProps = {
filterDropdownId?: string; filterDropdownId?: string;
@ -38,13 +28,8 @@ 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,
); );
@ -74,40 +59,21 @@ export const ObjectFilterDropdownFilterInput = ({
return ( return (
<> <>
<ObjectFilterDropdownOperandButton />
{isObjectFilterDropdownOperandSelectUnfolded && (
<StyledOperandSelectContainer>
<ObjectFilterDropdownOperandSelect />
</StyledOperandSelectContainer>
)}
{isConfigurable && selectedOperandInDropdown && ( {isConfigurable && selectedOperandInDropdown && (
<> <>
{[ {TEXT_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) &&
'TEXT',
'EMAIL',
'EMAILS',
'PHONE',
'FULL_NAME',
'LINK',
'LINKS',
'ADDRESS',
'ACTOR',
'ARRAY',
'RAW_JSON',
'PHONES',
].includes(filterDefinitionUsedInDropdown.type) &&
!isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( !isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
<ObjectFilterDropdownTextSearchInput /> <ObjectFilterDropdownTextSearchInput />
)} )}
{['NUMBER', 'CURRENCY'].includes( {NUMBER_FILTER_TYPES.includes(
filterDefinitionUsedInDropdown.type, filterDefinitionUsedInDropdown.type,
) && <ObjectFilterDropdownNumberInput />} ) && <ObjectFilterDropdownNumberInput />}
{filterDefinitionUsedInDropdown.type === 'RATING' && ( {filterDefinitionUsedInDropdown.type === 'RATING' && (
<ObjectFilterDropdownRatingInput /> <ObjectFilterDropdownRatingInput />
)} )}
{['DATE_TIME', 'DATE'].includes( {DATE_FILTER_TYPES.includes(filterDefinitionUsedInDropdown.type) && (
filterDefinitionUsedInDropdown.type, <ObjectFilterDropdownDateInput />
) && <ObjectFilterDropdownDateInput />} )}
{filterDefinitionUsedInDropdown.type === 'RELATION' && ( {filterDefinitionUsedInDropdown.type === 'RELATION' && (
<> <>
<ObjectFilterDropdownSearchInput /> <ObjectFilterDropdownSearchInput />

View File

@ -0,0 +1,40 @@
import { ObjectFilterDropdownOperandButton } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton';
import { ObjectFilterDropdownOperandSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
const StyledOperandSelectContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
box-shadow: ${({ theme }) => theme.boxShadow.light};
border-radius: ${({ theme }) => theme.border.radius.md};
left: 10px;
position: absolute;
top: 10px;
width: 100%;
z-index: 1000;
`;
export const ObjectFilterDropdownFilterOperandSelect = ({
filterDropdownId,
}: {
filterDropdownId?: string;
}) => {
const { isObjectFilterDropdownOperandSelectUnfoldedState } =
useFilterDropdown({ filterDropdownId });
const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue(
isObjectFilterDropdownOperandSelectUnfoldedState,
);
return (
<>
<ObjectFilterDropdownOperandButton />
{isObjectFilterDropdownOperandSelectUnfolded && (
<StyledOperandSelectContainer>
<ObjectFilterDropdownOperandSelect />
</StyledOperandSelectContainer>
)}
</>
);
};

View File

@ -3,6 +3,8 @@ import { useContext } from 'react';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
import { AdvancedFilterButton } from '@/object-record/object-filter-dropdown/components/AdvancedFilterButton';
import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem';
import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
@ -15,6 +17,7 @@ import { SelectableItem } from '@/ui/layout/selectable-list/components/Selectabl
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 { 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 { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui'; import { isDefined } from 'twenty-ui';
@ -45,12 +48,27 @@ export const StyledInput = styled.input`
} }
`; `;
export const ObjectFilterDropdownFilterSelect = () => { type ObjectFilterDropdownFilterSelectProps = {
isAdvancedFilterButtonVisible?: boolean;
};
export const ObjectFilterDropdownFilterSelect = ({
isAdvancedFilterButtonVisible,
}: ObjectFilterDropdownFilterSelectProps) => {
const { const {
setObjectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput,
objectFilterDropdownSearchInputState, objectFilterDropdownSearchInputState,
advancedFilterViewFilterIdState,
} = useFilterDropdown(); } = useFilterDropdown();
const advancedFilterViewFilterId = useRecoilValue(
advancedFilterViewFilterIdState,
);
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
advancedFilterViewFilterId,
);
const objectFilterDropdownSearchInput = useRecoilValue( const objectFilterDropdownSearchInput = useRecoilValue(
objectFilterDropdownSearchInputState, objectFilterDropdownSearchInputState,
); );
@ -110,14 +128,22 @@ export const ObjectFilterDropdownFilterSelect = () => {
} }
resetSelectedItem(); resetSelectedItem();
selectFilter({ filterDefinition: selectedFilterDefinition }); selectFilter({ filterDefinition: selectedFilterDefinition });
closeAdvancedFilterDropdown();
}; };
const shoudShowSeparator = const shoudShowSeparator =
visibleColumnsFilterDefinitions.length > 0 && visibleColumnsFilterDefinitions.length > 0 &&
hiddenColumnsFilterDefinitions.length > 0; hiddenColumnsFilterDefinitions.length > 0;
const { currentViewId, currentViewWithCombinedFiltersAndSorts } =
useGetCurrentView();
const shouldShowAdvancedFilterButton =
isDefined(currentViewId) &&
isDefined(currentViewWithCombinedFiltersAndSorts?.objectMetadataId) &&
isAdvancedFilterButtonVisible;
return ( return (
<> <>
<StyledInput <StyledInput
@ -164,6 +190,7 @@ export const ObjectFilterDropdownFilterSelect = () => {
)} )}
</DropdownMenuItemsContainer> </DropdownMenuItemsContainer>
</SelectableList> </SelectableList>
{shouldShowAdvancedFilterButton && <AdvancedFilterButton />}
</> </>
); );
}; };

View File

@ -1,3 +1,4 @@
import { useAdvancedFilterDropdown } from '@/object-record/advanced-filter/hooks/useAdvancedFilterDropdown';
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState'; import { objectFilterDropdownFilterIsSelectedComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFilterIsSelectedComponentState';
import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState'; import { objectFilterDropdownFirstLevelFilterDefinitionComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownFirstLevelFilterDefinitionComponentState';
@ -6,6 +7,7 @@ import { objectFilterDropdownSubMenuFieldTypeComponentState } from '@/object-rec
import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition';
import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel';
import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel';
import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue';
import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType';
import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs';
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader';
@ -13,6 +15,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { useState } from 'react'; import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { IconApps, IconChevronLeft, isDefined, useIcons } from 'twenty-ui'; import { IconApps, IconChevronLeft, isDefined, useIcons } from 'twenty-ui';
export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => { export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
@ -47,10 +50,46 @@ export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = () => {
setFilterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown,
setSelectedOperandInDropdown, setSelectedOperandInDropdown,
setObjectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput,
selectFilter,
advancedFilterViewFilterIdState,
advancedFilterViewFilterGroupIdState,
} = useFilterDropdown(); } = useFilterDropdown();
const advancedFilterViewFilterId = useRecoilValue(
advancedFilterViewFilterIdState,
);
const advancedFilterViewFilterGroupId = useRecoilValue(
advancedFilterViewFilterGroupIdState,
);
const { closeAdvancedFilterDropdown } = useAdvancedFilterDropdown(
advancedFilterViewFilterId,
);
const handleSelectFilter = (definition: FilterDefinition | null) => { const handleSelectFilter = (definition: FilterDefinition | null) => {
if (definition !== null) { if (definition !== null) {
if (
isDefined(advancedFilterViewFilterId) &&
isDefined(advancedFilterViewFilterGroupId)
) {
closeAdvancedFilterDropdown();
const operand = getOperandsForFilterDefinition(definition)[0];
const { value, displayValue } = getInitialFilterValue(
definition.type,
operand,
);
selectFilter({
id: advancedFilterViewFilterId,
fieldMetadataId: definition.fieldMetadataId,
value,
operand,
displayValue,
definition,
viewFilterGroupId: advancedFilterViewFilterGroupId,
});
}
setFilterDefinitionUsedInDropdown(definition); setFilterDefinitionUsedInDropdown(definition);
setSelectedOperandInDropdown( setSelectedOperandInDropdown(

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