Compare commits

..
Author SHA1 Message Date
prastoin 60a203b371 test(server): integ test sync agent 2026-02-20 12:20:13 +01:00
prastoin 79529a7a26 splti test 2026-02-20 12:16:34 +01:00
prastoin 3c3a4daeb2 test(server): integration 2026-02-20 11:57:11 +01:00
prastoin 6926aa3c25 fixup 2026-02-20 11:56:50 +01:00
prastoin ad15eb371b feat(server): integ 2026-02-20 11:56:48 +01:00
prastoin 0d46d98943 feat(sdK): agent manifest implem 2026-02-20 11:56:25 +01:00
prastoin c206e2eee0 feat(shared): manifest types 2026-02-20 11:55:07 +01:00
3bc887e12e Add default relation to standard object on custom object in manifest (#18033)
add default relation fields when creating an object from an application

---------

Co-authored-by: prastoin <[email protected]>
2026-02-20 11:49:06 +01:00
Thomas TrompetteandGitHub d060c843d1 Remove sse feature flag (#18114)
As title
2026-02-20 11:46:03 +01:00
Paul RastoinandGitHub 175df59c21 Support Skill in manifest (#18092)
# Introduction
Support skill in manifest, pre-requisite for the twenty standard app
migration
2026-02-20 11:45:42 +01:00
6ad581d178 Fix global search for CJK and non-tokenizable text (#18030)
## Summary

Fixes #12962

- Adds a two-pass ILIKE fallback to the global search service
(`search.service.ts`)
- **Fast path**: runs the tsvector query first (uses GIN index,
sub-millisecond)
- **Fallback**: only if tsvector returns fewer results than the limit,
runs an ILIKE query on `searchVector::text` to catch cases where
PostgreSQL's `simple` text search config fails to tokenize (continuous
CJK text, etc.)
- Zero performance impact for the common case (Latin text where tsvector
works)
- Also adds `escapeForIlike` utility to properly escape `%`, `_`, `\` in
user input

### Why tsvector fails for CJK

PostgreSQL's `simple` config treats continuous CJK text as a single
lexeme:
- `to_tsvector('simple', '示例商业线索')` → `'示例商业线索':1`
- Searching `商业:*` only prefix-matches from the start, so it misses `商业`
in the middle

The ILIKE fallback catches these substring matches when the tsvector
path can't.

### What this fixes

- Global search (command menu / sidebar)
- Relation picker (single and multi-object)
- Morph relation picker

All three use `search.service.ts` under the hood.

Co-authored-by: mykh-hailo (original direction in #18021)

## Test plan

- [ ] Search `示例` with records `示例商业线索` and `示例-商业-线索` → both should
appear
- [ ] Search `商业` → both should appear (previously only the hyphenated
one did)
- [ ] Search for Latin text (e.g. `john`) → same performance, results
unchanged
- [ ] Relation picker search with CJK text → results appear
- [ ] Search input with special chars like `%` or `_` → no SQL
injection, results correct


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: mykh-hailo <[email protected]>
Co-authored-by: Cursor <[email protected]>
2026-02-20 11:02:09 +01:00
411 changed files with 4010 additions and 2501 deletions
+3 -1
View File
@@ -89,6 +89,7 @@ In interactive mode, you can pick from:
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
- **Example view** — a saved view for the example object (`views/example-view.ts`)
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
## What gets scaffolded
@@ -106,11 +107,12 @@ In interactive mode, you can pick from:
- `front-components/hello-world.tsx` — Example front component
- `views/example-view.ts` — Example saved view for the example object
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
- `skills/example-skill.ts` — Example AI agent skill definition
## Next steps
- Run `yarn twenty help` to see all available commands.
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items).
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
- Types are autogenerated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
@@ -114,6 +114,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: false,
includeExampleView: false,
includeExampleNavigationMenuItem: false,
includeExampleSkill: false,
};
}
@@ -125,6 +126,7 @@ export class CreateAppCommand {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
}
@@ -164,6 +166,11 @@ export class CreateAppCommand {
value: 'navigationMenuItem',
checked: true,
},
{
name: 'Example skill (AI agent skill definition)',
value: 'skill',
checked: true,
},
],
},
]);
@@ -189,6 +196,7 @@ export class CreateAppCommand {
includeExampleView: includeView,
includeExampleNavigationMenuItem:
selectedExamples.includes('navigationMenuItem'),
includeExampleSkill: selectedExamples.includes('skill'),
};
}
@@ -7,4 +7,5 @@ export type ExampleOptions = {
includeExampleFrontComponent: boolean;
includeExampleView: boolean;
includeExampleNavigationMenuItem: boolean;
includeExampleSkill: boolean;
};
@@ -1,8 +1,9 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { tmpdir } from 'os';
import { copyBaseApplicationProject } from '@/utils/app-template';
import { type ExampleOptions } from '@/types/scaffolding-options';
import { GENERATED_DIR } from 'twenty-shared/application';
import { copyBaseApplicationProject } from '@/utils/app-template';
import * as fs from 'fs-extra';
import { tmpdir } from 'os';
import { join } from 'path';
// Mock fs-extra's copy function to skip copying base template (not available during tests)
jest.mock('fs-extra', () => {
@@ -23,11 +24,13 @@ const ALL_EXAMPLES: ExampleOptions = {
includeExampleFrontComponent: true,
includeExampleView: true,
includeExampleNavigationMenuItem: true,
includeExampleSkill: true,
};
const NO_EXAMPLES: ExampleOptions = {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: false,
includeExampleView: false,
@@ -109,7 +112,7 @@ describe('copyBaseApplicationProject', () => {
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
expect(gitignoreContent).toContain('/node_modules');
expect(gitignoreContent).toContain('generated');
expect(gitignoreContent).toContain(GENERATED_DIR);
});
it('should create yarn.lock file', async () => {
@@ -437,6 +440,7 @@ describe('copyBaseApplicationProject', () => {
exampleOptions: {
includeExampleObject: false,
includeExampleField: false,
includeExampleSkill: false,
includeExampleLogicFunction: false,
includeExampleFrontComponent: true,
includeExampleView: false,
@@ -472,6 +476,7 @@ describe('copyBaseApplicationProject', () => {
appDirectory: testAppDirectory,
exampleOptions: {
includeExampleObject: false,
includeExampleSkill: false,
includeExampleField: false,
includeExampleLogicFunction: true,
includeExampleFrontComponent: false,
@@ -89,6 +89,14 @@ export const copyBaseApplicationProject = async ({
});
}
if (exampleOptions.includeExampleSkill) {
await createExampleSkill({
appDirectory: sourceFolderPath,
fileFolder: 'skills',
fileName: 'example-skill.ts',
});
}
await createDefaultPostInstallFunction({
appDirectory: sourceFolderPath,
fileFolder: 'logic-functions',
@@ -424,6 +432,36 @@ export default defineNavigationMenuItem({
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createExampleSkill = async ({
appDirectory,
fileFolder,
fileName,
}: {
appDirectory: string;
fileFolder?: string;
fileName: string;
}) => {
const universalIdentifier = v4();
const content = `import { defineSkill } from 'twenty-sdk';
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
'${universalIdentifier}';
export default defineSkill({
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
name: 'example-skill',
label: 'Example Skill',
description: 'A sample skill for your application',
icon: 'IconBrain',
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
});
`;
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
};
const createApplicationConfig = async ({
displayName,
description,
@@ -14,6 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
**What you can do today:**
- Define custom objects and fields as code (managed data model)
- Build logic functions with custom triggers
- Define skills for AI agents
- Deploy the same app across multiple workspaces
## Prerequisites
@@ -44,7 +45,7 @@ yarn twenty app:dev
The scaffolder supports three modes for controlling which example files are included:
```bash filename="Terminal"
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item)
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
npx create-twenty-app@latest my-app
# Minimal: only core files (application-config.ts and default-role.ts)
@@ -117,8 +118,10 @@ my-twenty-app/
│ └── hello-world.tsx # Example front component
├── views/
│ └── example-view.ts # Example saved view definition
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
── navigation-menu-items/
└── example-navigation-menu-item.ts # Example sidebar navigation link
└── skills/
└── example-skill.ts # Example AI agent skill definition
```
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
@@ -147,6 +150,7 @@ The SDK detects entities by parsing your TypeScript files for **`export default
| `defineField()` | Field extensions for existing objects |
| `defineView()` | Saved view definitions |
| `defineNavigationMenuItem()` | Navigation menu item definitions |
| `defineSkill()` | AI agent skill definitions |
<Note>
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
@@ -167,7 +171,7 @@ export default defineObject({
Later commands will add more files and folders:
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
## Authentication
@@ -220,6 +224,7 @@ The SDK provides helper functions for defining your app entities. As described i
| `defineField()` | Extend existing objects with additional fields |
| `defineView()` | Define saved views for objects |
| `defineNavigationMenuItem()` | Define sidebar navigation links |
| `defineSkill()` | Define AI agent skills |
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
@@ -729,6 +734,40 @@ You can create new front components in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
### Skills
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
```typescript
// src/skills/example-skill.ts
import { defineSkill } from 'twenty-sdk';
export default defineSkill({
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
name: 'sales-outreach',
label: 'Sales Outreach',
description: 'Guides the AI agent through a structured sales outreach process',
icon: 'IconBrain',
content: `You are a sales outreach assistant. When reaching out to a prospect:
1. Research the company and recent news
2. Identify the prospect's role and likely pain points
3. Draft a personalized message referencing specific details
4. Keep the tone professional but conversational`,
});
```
Key points:
- `name` is a unique identifier string for the skill (kebab-case recommended).
- `label` is the human-readable display name shown in the UI.
- `content` contains the skill instructions — this is the text the AI agent uses.
- `icon` (optional) sets the icon displayed in the UI.
- `description` (optional) provides additional context about the skill's purpose.
You can create new skills in two ways:
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
### Generated typed client
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
@@ -1494,7 +1494,6 @@ export enum FeatureFlagKey {
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED = 'IS_ROW_LEVEL_PERMISSION_PREDICATES_ENABLED',
IS_SSE_DB_EVENTS_ENABLED = 'IS_SSE_DB_EVENTS_ENABLED',
IS_TASK_TARGET_MIGRATED = 'IS_TASK_TARGET_MIGRATED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
}
@@ -5,7 +5,6 @@ import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCu
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useIsWorkspaceActivationStatusEqualsTo } from '@/workspace/hooks/useIsWorkspaceActivationStatusEqualsTo';
import { useLocation, useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
@@ -24,7 +23,7 @@ export const usePageChangeEffectNavigateLocation = () => {
);
const { defaultHomePagePath } = useDefaultHomePagePath();
const location = useLocation();
const calendarBookingPageId = useRecoilValueV2(calendarBookingPageIdState);
const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState);
const someMatchingLocationOf = (appPaths: AppPath[]): boolean =>
appPaths.some((appPath) => isMatchingLocation(location, appPath));
@@ -50,9 +49,7 @@ export const usePageChangeEffectNavigateLocation = () => {
const objectMetadataItem = objectMetadataItems?.find(
(objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural,
);
const verifyEmailRedirectPath = useRecoilValueV2(
verifyEmailRedirectPathState,
);
const verifyEmailRedirectPath = useRecoilValue(verifyEmailRedirectPathState);
if (
(!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) &&
@@ -17,7 +17,6 @@ import { hasAnySoftDeleteFilterOnViewComponentSelector } from '@/object-record/r
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useContext, useMemo } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
@@ -137,7 +136,7 @@ export const useShouldActionBeRegisteredParams = ({
forceRegisteredActionsByKeyState,
);
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isFeatureFlagEnabled = (featureFlagKey: FeatureFlagKey) => {
const featureFlag = currentWorkspace?.featureFlags?.find(
@@ -2,7 +2,7 @@ import { css, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { format } from 'date-fns';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import {
CalendarChannelVisibility,
type TimelineCalendarEvent,
@@ -85,7 +85,7 @@ export const CalendarEventRow = ({
className,
}: CalendarEventRowProps) => {
const theme = useTheme();
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { openCalendarEventInCommandMenu } =
useOpenCalendarEventInCommandMenu();
@@ -11,7 +11,7 @@ import { type ActivityTargetableObject } from '@/activities/types/ActivityTarget
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
import { Modal } from '@/ui/layout/modal/components/Modal';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { ActivityList } from '@/activities/components/ActivityList';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
@@ -135,7 +135,7 @@ export const AttachmentList = ({
const [previewedAttachment, setPreviewedAttachment] =
useState<Attachment | null>(null);
const isAttachmentPreviewEnabled = useRecoilValueV2(
const isAttachmentPreviewEnabled = useRecoilValue(
isAttachmentPreviewEnabledState,
);
@@ -2,14 +2,12 @@ import { gql, InMemoryCache } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SnackBarComponentInstanceContext } from '@/ui/feedback/snack-bar-manager/contexts/SnackBarComponentInstanceContext';
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
@@ -113,27 +111,26 @@ const task = {
};
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot>
<MockedProvider cache={cache}>
<JestObjectMetadataItemSetter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</MockedProvider>
</RecoilRoot>
</JotaiProvider>
<RecoilRoot>
<MockedProvider cache={cache}>
<JestObjectMetadataItemSetter>
<SnackBarComponentInstanceContext.Provider
value={{ instanceId: 'snack-bar-manager' }}
>
{children}
</SnackBarComponentInstanceContext.Provider>
</JestObjectMetadataItemSetter>
</MockedProvider>
</RecoilRoot>
);
describe('useActivityTargetObjectRecords', () => {
it('return targetObjects', async () => {
jotaiStore.set(currentWorkspaceMemberState.atom, mockWorkspaceMembers[0]);
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState,
);
@@ -148,6 +145,7 @@ describe('useActivityTargetObjectRecords', () => {
return {
activityTargetObjectRecords,
setCurrentWorkspaceMember,
setObjectMetadataItems,
setRecordFromStore,
};
@@ -156,6 +154,7 @@ describe('useActivityTargetObjectRecords', () => {
);
act(() => {
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
result.current.setObjectMetadataItems(generatedMockObjectMetadataItems);
result.current.setRecordFromStore(task);
});
@@ -1,5 +1,4 @@
import styled from '@emotion/styled';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
@@ -93,9 +92,9 @@ export const EventRow = ({
event,
mainObjectMetadataItem,
}: EventRowProps) => {
const currentWorkspaceMember = useRecoilValueV2(currentWorkspaceMemberState);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const allowRequestsToTwentyIcons = useRecoilValueV2(
const allowRequestsToTwentyIcons = useRecoilValue(
allowRequestsToTwentyIconsState,
);
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { EditorContent } from '@tiptap/react';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { LightButton } from 'twenty-ui/input';
import { DropZone } from '@/activities/files/components/DropZone';
@@ -22,7 +23,6 @@ import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
@@ -144,7 +144,7 @@ export const AIChatTab = () => {
const hasMessages = messages.length > 0;
const { uploadFiles } = useAIChatFileUpload();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const smartModelLabel = useAiModelLabel(currentWorkspace?.smartModel, false);
const { editor, handleSendAndClear } = useAIChatEditor({
@@ -1,3 +1,5 @@
import { useSetRecoilState } from 'recoil';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
import { agentChatSelectedFilesStateV2 } from '@/ai/states/agentChatSelectedFilesStateV2';
import { agentChatUploadedFilesStateV2 } from '@/ai/states/agentChatUploadedFilesStateV2';
@@ -21,7 +23,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { cookieStorage } from '~/utils/cookie-storage';
export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const setAgentChatUsage = useSetRecoilStateV2(agentChatUsageStateV2);
const { getBrowsingContext } = useGetBrowsingContext();
@@ -1,42 +1,41 @@
import { InMemoryCache, type NormalizedCacheObject } from '@apollo/client';
import { useMemo, useRef } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { previousUrlState } from '@/auth/states/previousUrlState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { appVersionState } from '@/client-config/states/appVersionState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { ApolloFactory, type Options } from '@/apollo/services/apollo.factory';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { appVersionState } from '@/client-config/states/appVersionState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
// eslint-disable-next-line twenty/no-state-useref
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
const navigate = useNavigate();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const setTokenPair = useSetRecoilState(tokenPairState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const appVersion = useRecoilValueV2(appVersionState);
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilStateV2(
const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState(
currentWorkspaceMemberState,
);
const setCurrentUser = useSetRecoilStateV2(currentUserState);
const setCurrentUserWorkspace = useSetRecoilStateV2(
currentUserWorkspaceState,
);
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setPreviousUrl = useSetRecoilStateV2(previousUrlState);
const location = useLocation();
@@ -1,13 +1,13 @@
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
import { currentUserState } from '@/auth/states/currentUserState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { RouterProvider } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
export const AppRouter = () => {
// We want to disable logic function settings but keep the code for now
const isFunctionSettingsEnabled = false;
const currentUser = useRecoilValueV2(currentUserState);
const currentUser = useRecoilValue(currentUserState);
const isAdminPageEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
@@ -1,3 +1,12 @@
import { useEffect, useState } from 'react';
import {
matchPath,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import {
setSessionId,
useEventTracker,
@@ -6,15 +15,6 @@ import { useExecuteTasksOnAnyLocationChange } from '@/app/hooks/useExecuteTasksO
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useEffect, useState } from 'react';
import {
matchPath,
useLocation,
useNavigate,
useParams,
} from 'react-router-dom';
import { useRecoilCallback } from 'recoil';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
@@ -93,7 +93,7 @@ export const PageChangeEffect = () => {
const { executeTasksOnAnyLocationChange } =
useExecuteTasksOnAnyLocationChange();
const isAppEffectRedirectEnabled = useRecoilValueV2(
const isAppEffectRedirectEnabled = useRecoilValue(
isAppEffectRedirectEnabledState,
);
@@ -366,7 +366,7 @@ export const PageChangeEffect = () => {
}, [eventTracker, location.pathname]);
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
useEffect(() => {
if (isCaptchaScriptLoaded && isCaptchaRequiredForPath(location.pathname)) {
@@ -1,60 +1,63 @@
import { useCallback } from 'react';
import { useRecoilCallback } from 'recoil';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import deepEqual from 'deep-equal';
// Initialize state that are hydrated from query parameters
// We used to use recoil-sync to do this, but it was causing issues with Firefox
export const useInitializeQueryParamState = () => {
const initializeQueryParamState = useCallback(() => {
const handlers = {
billingCheckoutSession: (value: string) => {
const billingCheckoutSession = jotaiStore.get(
billingCheckoutSessionState.atom,
);
const initializeQueryParamState = useRecoilCallback(
({ set, snapshot }) =>
() => {
const handlers = {
billingCheckoutSession: (value: string) => {
const billingCheckoutSession = snapshot
.getLoadable(billingCheckoutSessionState)
.getValue();
try {
const parsedValue = JSON.parse(decodeURIComponent(value));
try {
const parsedValue = JSON.parse(decodeURIComponent(value));
if (
typeof parsedValue === 'object' &&
parsedValue !== null &&
'plan' in parsedValue &&
'interval' in parsedValue &&
'requirePaymentMethod' in parsedValue &&
!deepEqual(billingCheckoutSession, parsedValue)
) {
jotaiStore.set(
billingCheckoutSessionState.atom,
parsedValue as BillingCheckoutSession,
);
if (
typeof parsedValue === 'object' &&
parsedValue !== null &&
'plan' in parsedValue &&
'interval' in parsedValue &&
'requirePaymentMethod' in parsedValue &&
!deepEqual(billingCheckoutSession, parsedValue)
) {
set(
billingCheckoutSessionState,
parsedValue as BillingCheckoutSession,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
);
set(
billingCheckoutSessionState,
BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
);
}
},
};
const queryParams = new URLSearchParams(window.location.search);
for (const [paramName, handler] of Object.entries(handlers)) {
const value = queryParams.get(paramName);
if (value !== null) {
handler(value);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
);
jotaiStore.set(
billingCheckoutSessionState.atom,
BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
);
}
},
};
const queryParams = new URLSearchParams(window.location.search);
for (const [paramName, handler] of Object.entries(handlers)) {
const value = queryParams.get(paramName);
if (value !== null) {
handler(value);
}
}
}, []);
[],
);
return { initializeQueryParamState };
};
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isAppEffectRedirectEnabledState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isAppEffectRedirectEnabledState = createState<boolean>({
key: 'isAppEffectRedirectEnabledState',
defaultValue: true,
});
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const verifyEmailRedirectPathState = createStateV2<string | undefined>({
export const verifyEmailRedirectPathState = createState<string | undefined>({
key: 'verifyEmailRedirectPathState',
defaultValue: undefined,
});
@@ -1,15 +1,13 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import { AuthContext } from '@/auth/contexts/AuthContext';
import { currentWorkspaceDeletedMembersState } from '@/auth/states/currentWorkspaceDeletedMembersState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
const currentWorkspaceMembers = useRecoilValueV2(
currentWorkspaceMembersState,
);
const currentWorkspaceDeletedMembers = useRecoilValueV2(
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
const currentWorkspaceDeletedMembers = useRecoilValue(
currentWorkspaceDeletedMembersState,
);
@@ -5,6 +5,7 @@ import { useCurrentUserWorkspaceTwoFactorAuthentication } from '@/settings/two-f
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useLingui } from '@lingui/react/macro';
import { useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -18,7 +19,7 @@ export const TwoFactorAuthenticationSetupEffect = () => {
const navigate = useNavigateApp();
const { origin } = useOrigin();
const loginToken = useRecoilValueV2(loginTokenState);
const loginToken = useRecoilValue(loginTokenState);
const qrCode = useRecoilValueV2(qrCodeState);
const setQrCodeState = useSetRecoilStateV2(qrCodeState);
@@ -12,12 +12,11 @@ import { Modal } from '@/ui/layout/modal/components/Modal';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export const VerifyEmailEffect = () => {
const {
@@ -30,7 +29,7 @@ export const VerifyEmailEffect = () => {
const [searchParams] = useSearchParams();
const [isError, setIsError] = useState(false);
const setVerifyEmailRedirectPath = useSetRecoilStateV2(
const setVerifyEmailRedirectPath = useSetRecoilState(
verifyEmailRedirectPathState,
);
@@ -42,7 +41,7 @@ export const VerifyEmailEffect = () => {
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { verifyLoginToken } = useVerifyLogin();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const clientConfigApiStatus = useRecoilValueV2(clientConfigApiStatusState);
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
const { t } = useLingui();
useEffect(() => {
@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useNavigateApp } from '~/hooks/useNavigateApp';
@@ -17,7 +17,7 @@ export const VerifyLoginTokenEffect = () => {
const navigate = useNavigateApp();
const { verifyLoginToken } = useVerifyLogin();
const { isSaved: clientConfigLoaded } = useRecoilValueV2(
const { isSaved: clientConfigLoaded } = useRecoilValue(
clientConfigApiStatusState,
);
@@ -4,12 +4,11 @@ import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/i
import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useApolloClient } from '@apollo/client';
import { MockedProvider } from '@apollo/client/testing';
import { type ReactNode, act } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { RecoilRoot } from 'recoil';
import { RecoilRoot, useRecoilValue } from 'recoil';
import {
email,
@@ -159,15 +158,15 @@ describe('useAuth', () => {
const { result } = renderHook(
() => {
const client = useApolloClient();
const workspaceAuthProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(
workspaceAuthProvidersState,
);
const billing = useRecoilValueV2(billingState);
const isDeveloperDefaultSignInPrefilled = useRecoilValueV2(
const billing = useRecoilValue(billingState);
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
isDeveloperDefaultSignInPrefilledState,
);
const supportChat = useRecoilValueV2(supportChatState);
const isMultiWorkspaceEnabled = useRecoilValueV2(
const supportChat = useRecoilValue(supportChatState);
const isMultiWorkspaceEnabled = useRecoilValue(
isMultiWorkspaceEnabledState,
);
return {
@@ -1,15 +1,14 @@
import { act, renderHook } from '@testing-library/react';
import { RecoilRoot } from 'recoil';
import { RecoilRoot, useSetRecoilState } from 'recoil';
import { useIsLogged } from '@/auth/hooks/useIsLogged';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
const renderHooks = () => {
const { result } = renderHook(
() => {
const isLogged = useIsLogged();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
return {
isLogged,
@@ -4,9 +4,14 @@ import {
snapshot_UNSTABLE,
useGotoRecoilSnapshot,
useRecoilCallback,
useRecoilValue,
useSetRecoilState,
} from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { billingState } from '@/client-config/states/billingState';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useCheckUserExistsLazyQuery,
@@ -18,14 +23,12 @@ import {
useSignUpMutation,
useVerifyEmailAndGetLoginTokenMutation,
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation,
type AuthTokenPair,
type AuthToken,
type AuthTokenPair,
} from '~/generated-metadata/graphql';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
@@ -44,8 +47,11 @@ import {
} from '@/auth/utils/availableWorkspacesUtils';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { captchaState } from '@/client-config/states/captchaState';
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { useOrigin } from '@/domain-manager/hooks/useOrigin';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
@@ -65,19 +71,17 @@ import { cookieStorage } from '~/utils/cookie-storage';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
export const useAuth = () => {
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setLoginToken = useSetRecoilStateV2(loginTokenState);
const setIsAppEffectRedirectEnabled = useSetRecoilStateV2(
const setTokenPair = useSetRecoilState(tokenPairState);
const setLoginToken = useSetRecoilState(loginTokenState);
const setIsAppEffectRedirectEnabled = useSetRecoilState(
isAppEffectRedirectEnabledState,
);
const { origin } = useOrigin();
const { requestFreshCaptchaToken } = useRequestFreshCaptchaToken();
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const isMultiWorkspaceEnabled = useRecoilValueV2(
isMultiWorkspaceEnabledState,
);
const isEmailVerificationRequired = useRecoilValueV2(
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const isEmailVerificationRequired = useRecoilValue(
isEmailVerificationRequiredState,
);
const { loadCurrentUser } = useLoadCurrentUser();
@@ -85,7 +89,7 @@ export const useAuth = () => {
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
const { createWorkspace } = useSignUpInNewWorkspace();
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { redirect } = useRedirect();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
@@ -102,7 +106,7 @@ export const useAuth = () => {
useVerifyEmailAndGetWorkspaceAgnosticTokenMutation();
const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation();
const workspacePublicData = useRecoilValueV2(workspacePublicDataState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
@@ -119,7 +123,7 @@ export const useAuth = () => {
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
const clearSession = useRecoilCallback(
({ snapshot }) =>
({ snapshot, set }) =>
async () => {
const sseClient = getSnapshotValue(snapshot, sseClientState);
@@ -127,26 +131,53 @@ export const useAuth = () => {
const emptySnapshot = snapshot_UNSTABLE();
const authProvidersValue = jotaiStore.get(
workspaceAuthProvidersState.atom,
);
const isCurrentUserLoadedValue = jotaiStore.get(
isCurrentUserLoadedState.atom,
);
const domainConfigurationValue = jotaiStore.get(
domainConfigurationState.atom,
);
const workspacePublicDataValue = jotaiStore.get(
workspacePublicDataState.atom,
);
const lastAuthenticatedMethod = jotaiStore.get(
lastAuthenticatedMethodState.atom,
);
const isCaptchaScriptLoadedValue = jotaiStore.get(
isCaptchaScriptLoadedState.atom,
);
const authProvidersValue = snapshot
.getLoadable(workspaceAuthProvidersState)
.getValue();
const billing = snapshot.getLoadable(billingState).getValue();
const isDeveloperDefaultSignInPrefilled = snapshot
.getLoadable(isDeveloperDefaultSignInPrefilledState)
.getValue();
const supportChat = snapshot.getLoadable(supportChatState).getValue();
const captcha = snapshot.getLoadable(captchaState).getValue();
const clientConfigApiStatus = snapshot
.getLoadable(clientConfigApiStatusState)
.getValue();
const isCurrentUserLoaded = snapshot
.getLoadable(isCurrentUserLoadedState)
.getValue();
const isMultiWorkspaceEnabled = snapshot
.getLoadable(isMultiWorkspaceEnabledState)
.getValue();
const domainConfiguration = snapshot
.getLoadable(domainConfigurationState)
.getValue();
const apiConfig = snapshot.getLoadable(apiConfigState).getValue();
const sentryConfig = snapshot.getLoadable(sentryConfigState).getValue();
const workspacePublicData = snapshot
.getLoadable(workspacePublicDataState)
.getValue();
const lastAuthenticatedMethod = snapshot
.getLoadable(lastAuthenticatedMethodState)
.getValue();
const initialSnapshot = emptySnapshot.map(() => {
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(workspaceAuthProvidersState, authProvidersValue);
set(billingState, billing);
set(
isDeveloperDefaultSignInPrefilledState,
isDeveloperDefaultSignInPrefilled,
);
set(supportChatState, supportChat);
set(captchaState, captcha);
set(apiConfigState, apiConfig);
set(sentryConfigState, sentryConfig);
set(workspacePublicDataState, workspacePublicData);
set(clientConfigApiStatusState, clientConfigApiStatus);
set(isCurrentUserLoadedState, isCurrentUserLoaded);
set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled);
set(domainConfigurationState, domainConfiguration);
set(isCaptchaScriptLoadedState, isCaptchaScriptLoaded);
return undefined;
});
@@ -155,18 +186,7 @@ export const useAuth = () => {
goToRecoilSnapshot(initialSnapshot);
jotaiStore.set(workspaceAuthProvidersState.atom, authProvidersValue);
jotaiStore.set(workspacePublicDataState.atom, workspacePublicDataValue);
jotaiStore.set(isCurrentUserLoadedState.atom, isCurrentUserLoadedValue);
jotaiStore.set(domainConfigurationState.atom, domainConfigurationValue);
jotaiStore.set(
isCaptchaScriptLoadedState.atom,
isCaptchaScriptLoadedValue,
);
jotaiStore.set(
lastAuthenticatedMethodState.atom,
lastAuthenticatedMethod,
);
set(lastAuthenticatedMethodState, lastAuthenticatedMethod);
await client.clearStore();
setLastAuthenticateWorkspaceDomain(null);
@@ -179,6 +199,7 @@ export const useAuth = () => {
setLastAuthenticateWorkspaceDomain,
loadMockedObjectMetadataItems,
navigate,
isCaptchaScriptLoaded,
],
);
@@ -1,7 +1,8 @@
import { useRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
export const useIsLogged = (): boolean => {
const [tokenPair] = useRecoilStateV2(tokenPairState);
const [tokenPair] = useRecoilState(tokenPairState);
return !!tokenPair;
};
@@ -9,7 +9,7 @@ import {
} from '@/auth/states/signInUpStepState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
import { t } from '@lingui/core/macro';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import {
IconGmail,
IconMail,
@@ -89,7 +89,7 @@ export const EmailVerificationSent = ({
email: string | null;
isError?: boolean;
}) => {
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { handleResendEmailVerificationToken, loading: isLoading } =
useHandleResendEmailVerificationToken();
@@ -5,6 +5,7 @@ import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import { motion } from 'framer-motion';
import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { ClickToActionLink, UndecoratedLink } from 'twenty-ui/navigation';
import { useAuth } from '@/auth/hooks/useAuth';
@@ -125,12 +126,12 @@ const StyledActionLinkContainer = styled.div`
export const SignInUpGlobalScopeForm = () => {
const authProviders = useRecoilValueV2(authProvidersState);
const signInUpStep = useRecoilValueV2(signInUpStepState);
const signInUpStep = useRecoilValue(signInUpStepState);
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { signOut } = useAuth();
const { createWorkspace } = useSignUpInNewWorkspace();
const availableWorkspaces = useRecoilValueV2(availableWorkspacesState);
const availableWorkspaces = useRecoilValue(availableWorkspacesState);
const theme = useTheme();
const { t } = useLingui();
@@ -12,9 +12,9 @@ import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthPro
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
import { FormProvider } from 'react-hook-form';
import { useRecoilValue } from 'recoil';
import { HorizontalSeparator } from 'twenty-ui/display';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -23,8 +23,8 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpWorkspaceScopeForm = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValue(
workspaceAuthBypassProvidersState,
);
const { shouldOfferBypass, shouldUseBypass } = useWorkspaceBypass();
@@ -6,11 +6,11 @@ import {
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { useEffect } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const SignInUpGlobalScopeFormEffect = () => {
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const [searchParams, setSearchParams] = useSearchParams();
const { setAuthTokens } = useAuth();
const { loadCurrentUser } = useLoadCurrentUser();
@@ -3,13 +3,13 @@
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -17,7 +17,7 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpSSOIdentityProviderSelection = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { redirectToSSOLoginPage } = useSSO();
@@ -9,7 +9,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Trans, useLingui } from '@lingui/react/macro';
import QRCode from 'react-qr-code';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useSetRecoilState } from 'recoil';
import { IconCopy } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
@@ -71,7 +71,7 @@ export const SignInUpTwoFactorAuthenticationProvision = () => {
const theme = useTheme();
const { copyToClipboard } = useCopyToClipboard();
const qrCode = useRecoilValueV2(qrCodeState);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const handleClick = () => {
setSignInUpStep(SignInUpStep.TwoFactorAuthenticationVerification);
@@ -18,12 +18,11 @@ import { Trans, useLingui } from '@lingui/react/macro';
import { OTPInput, type SlotProps } from 'input-otp';
import { useState } from 'react';
import { Controller } from 'react-hook-form';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { MainButton } from 'twenty-ui/input';
import { ClickToActionLink } from 'twenty-ui/navigation';
import { useNavigateApp } from '~/hooks/useNavigateApp';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
const StyledMainContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@@ -187,8 +186,8 @@ export const SignInUpTOTPVerification = () => {
const navigate = useNavigateApp();
const { readCaptchaToken } = useReadCaptchaToken();
const { isCaptchaReady } = useCaptcha();
const loginToken = useRecoilValueV2(loginTokenState);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const loginToken = useRecoilValue(loginTokenState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const { t } = useLingui();
const { form } = useTwoFactorAuthenticationForm();
@@ -19,11 +19,10 @@ import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const StyledForm = styled.form`
align-items: center;
@@ -40,15 +39,13 @@ export const SignInUpWithCredentials = ({
const { t } = useLingui();
const form = useFormContext<Form>();
const [signInUpStep, setSignInUpStep] = useRecoilStateV2(signInUpStepState);
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [showErrors, setShowErrors] = useState(false);
const captcha = useRecoilValueV2(captchaState);
const isRequestingCaptchaToken = useRecoilValueV2(
const captcha = useRecoilValue(captchaState);
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const lastAuthenticatedMethod = useRecoilValueV2(
lastAuthenticatedMethodState,
);
const lastAuthenticatedMethod = useRecoilValue(lastAuthenticatedMethodState);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const {
@@ -10,10 +10,9 @@ import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { memo } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconGoogle } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
@@ -30,9 +29,10 @@ export const SignInUpWithGoogle = ({
isGlobalScope?: boolean;
}) => {
const { t } = useLingui();
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithGoogle } = useSignInWithGoogle();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
@@ -9,10 +9,9 @@ import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue } from 'recoil';
import { HorizontalSeparator, IconMicrosoft } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { LastUsedPill } from './LastUsedPill';
import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
@@ -26,9 +25,10 @@ export const SignInUpWithMicrosoft = ({
const theme = useTheme();
const { t } = useLingui();
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
@@ -7,11 +7,9 @@ import {
} from '@/auth/states/signInUpStepState';
import { AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useTheme } from '@emotion/react';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, IconLock } from 'twenty-ui/display';
import { MainButton } from 'twenty-ui/input';
@@ -21,11 +19,12 @@ import { StyledSSOButtonContainer } from './SignInUpSSOButtonStyles';
export const SignInUpWithSSO = () => {
const theme = useTheme();
const { t } = useLingui();
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const signInUpStep = useRecoilValueV2(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] =
useRecoilStateV2(lastAuthenticatedMethodState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const [lastAuthenticatedMethod, setLastAuthenticatedMethod] = useRecoilState(
lastAuthenticatedMethodState,
);
const hasMultipleAuthMethods = useHasMultipleAuthMethods();
const { redirectToSSOLoginPage } = useSSO();
@@ -7,9 +7,8 @@ import {
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { captchaState } from '@/client-config/states/captchaState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect, useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
const searchParams = new URLSearchParams(window.location.search);
@@ -22,13 +21,13 @@ enum LoadingStatus {
}
export const SignInUpWorkspaceScopeFormEffect = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const isRequestingCaptchaToken = useRecoilValueV2(
const isRequestingCaptchaToken = useRecoilValue(
isRequestingCaptchaTokenState,
);
const captcha = useRecoilValueV2(captchaState);
const captcha = useRecoilValue(captchaState);
const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>(
LoadingStatus.Loading,
@@ -39,7 +38,7 @@ export const SignInUpWorkspaceScopeFormEffect = () => {
const { signInUpStep, continueWithEmail, continueWithCredentials } =
useSignInUp(form);
const setSignInUpStep = useSetRecoilStateV2(signInUpStepState);
const setSignInUpStep = useSetRecoilState(signInUpStepState);
useEffect(() => {
if (!workspaceAuthProviders) {
@@ -1,14 +1,11 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { act, renderHook } from '@testing-library/react';
import { type ReactNode, createElement } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import {
type PublicWorkspaceDataOutput,
@@ -23,21 +20,16 @@ jest.mock('~/generated-metadata/graphql');
dynamicActivate(SOURCE_LOCALE);
const renderHooks = () => {
jotaiStore.set(workspacePublicDataState.atom, {
id: 'workspace-id',
} as PublicWorkspaceDataOutput);
const { result } = renderHook(() => useHandleResetPassword(), {
wrapper: ({ children }: { children: ReactNode }) =>
createElement(
JotaiProvider,
{ store: jotaiStore },
createElement(
RecoilRoot,
null as any,
createElement(I18nProvider, { i18n }, children),
),
),
wrapper: ({ children }) =>
RecoilRoot({
initializeState: ({ set }) => {
set(workspacePublicDataState, {
id: 'workspace-id',
} as PublicWorkspaceDataOutput);
},
children: I18nProvider({ i18n, children }),
}),
});
return { result };
};
@@ -1,13 +1,8 @@
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import {
jotaiStore,
resetJotaiStore,
} from '@/ui/utilities/state/jotai/jotaiStore';
import { renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
const TestWrapper = ({
@@ -18,24 +13,24 @@ const TestWrapper = ({
children: ReactNode;
initialEntry?: string;
isDeveloperDefaultSignInPrefilled?: boolean;
}) => {
jotaiStore.set(
isDeveloperDefaultSignInPrefilledState.atom,
isDeveloperDefaultSignInPrefilled,
);
return (
<MemoryRouter initialEntries={[initialEntry]}>
<JotaiProvider store={jotaiStore}>
<RecoilRoot>{children}</RecoilRoot>
</JotaiProvider>
</MemoryRouter>
);
};
}) => (
<MemoryRouter initialEntries={[initialEntry]}>
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(
isDeveloperDefaultSignInPrefilledState,
isDeveloperDefaultSignInPrefilled,
);
}}
>
{children}
</RecoilRoot>
</MemoryRouter>
);
describe('useSignInUpForm', () => {
beforeEach(() => {
jest.clearAllMocks();
resetJotaiStore();
});
it('should initialize the form with default values', async () => {
@@ -5,14 +5,14 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { useEmailPasswordResetLinkMutation } from '~/generated-metadata/graphql';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useHandleResetPassword = () => {
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
const workspacePublicData = useRecoilValueV2(workspacePublicDataState);
const currentUser = useRecoilValueV2(currentUserState);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const currentUser = useRecoilValue(currentUserState);
const { t } = useLingui();
@@ -1,8 +1,8 @@
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useHasMultipleAuthMethods = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
if (!workspaceAuthProviders) {
return false;
@@ -18,23 +18,23 @@ import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCu
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { ApolloError } from '@apollo/client';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { buildAppPathWithQueryParams } from '~/utils/buildAppPathWithQueryParams';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
import { useAuth } from '@/auth/hooks/useAuth';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
export const useSignInUp = (form: UseFormReturn<Form>) => {
const { enqueueErrorSnackBar } = useSnackBar();
const { t } = useLingui();
const [signInUpStep, setSignInUpStep] = useRecoilStateV2(signInUpStepState);
const [signInUpStep, setSignInUpStep] = useRecoilState(signInUpStepState);
const [signInUpMode, setSignInUpMode] = useRecoilStateV2(signInUpModeState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const { isCaptchaReady } = useCaptcha();
const setLastAuthenticatedMethod = useSetRecoilStateV2(
const setLastAuthenticatedMethod = useSetRecoilState(
lastAuthenticatedMethodState,
);
@@ -3,6 +3,7 @@ import { t } from '@lingui/core/macro';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
import { useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { z } from 'zod';
import {
@@ -12,7 +13,6 @@ import {
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isDefined } from 'twenty-shared/utils';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
const makeValidationSchema = (signInUpStep: SignInUpStep) =>
z
@@ -34,11 +34,11 @@ const makeValidationSchema = (signInUpStep: SignInUpStep) =>
export type Form = z.infer<ReturnType<typeof makeValidationSchema>>;
export const useSignInUpForm = () => {
const signInUpStep = useRecoilValueV2(signInUpStepState);
const signInUpStep = useRecoilValue(signInUpStepState);
const validationSchema = makeValidationSchema(signInUpStep); // Create schema based on the current step
const isDeveloperDefaultSignInPrefilled = useRecoilValueV2(
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
isDeveloperDefaultSignInPrefilledState,
);
const [searchParams] = useSearchParams();
@@ -2,15 +2,15 @@ import { useParams, useSearchParams } from 'react-router-dom';
import { useAuth } from '@/auth/hooks/useAuth';
import { billingCheckoutSessionState } from '@/auth/states/billingCheckoutSessionState';
import { useRecoilValue } from 'recoil';
import { type SocialSSOSignInUpActionType } from '@/auth/types/socialSSOSignInUp.type';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useSignInWithMicrosoft = () => {
const workspaceInviteHash = useParams().workspaceInviteHash;
const [searchParams] = useSearchParams();
const workspacePersonalInviteToken =
searchParams.get('inviteToken') ?? undefined;
const billingCheckoutSession = useRecoilValueV2(billingCheckoutSessionState);
const billingCheckoutSession = useRecoilValue(billingCheckoutSessionState);
const { signInWithMicrosoft } = useAuth();
return {
@@ -1,16 +1,16 @@
import { useRecoilState, useRecoilValue } from 'recoil';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { workspaceBypassModeState } from '@/workspace/states/workspaceBypassModeState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useWorkspaceBypass = () => {
const workspaceAuthProviders = useRecoilValueV2(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValueV2(
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const workspaceAuthBypassProviders = useRecoilValue(
workspaceAuthBypassProvidersState,
);
const [workspaceBypassMode, setWorkspaceBypassMode] = useRecoilStateV2(
const [workspaceBypassMode, setWorkspaceBypassMode] = useRecoilState(
workspaceBypassModeState,
);
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
@@ -16,7 +16,7 @@ export const useWorkspaceFromInviteHash = () => {
const { enqueueErrorSnackBar, enqueueInfoSnackBar } = useSnackBar();
const navigate = useNavigateApp();
const workspaceInviteHash = useParams().workspaceInviteHash;
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const [initiallyLoggedIn] = useState(isDefined(currentWorkspace));
const { data: workspaceFromInviteHash, loading } =
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AvailableWorkspaces } from '~/generated-metadata/graphql';
export const availableWorkspacesState = createStateV2<AvailableWorkspaces>({
export const availableWorkspacesState = createState<AvailableWorkspaces>({
key: 'availableWorkspacesState',
defaultValue: {
availableWorkspacesForSignIn: [],
@@ -1,9 +1,8 @@
import { type BillingCheckoutSession } from '@/auth/types/billingCheckoutSession.type';
import { BILLING_CHECKOUT_SESSION_DEFAULT_VALUE } from '@/billing/constants/BillingCheckoutSessionDefaultValue';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const billingCheckoutSessionState =
createStateV2<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
});
export const billingCheckoutSessionState = createState<BillingCheckoutSession>({
key: 'billingCheckoutSessionState',
defaultValue: BILLING_CHECKOUT_SESSION_DEFAULT_VALUE,
});
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type User } from '~/generated-metadata/graphql';
export type CurrentUser = Pick<
@@ -15,7 +15,7 @@ export type CurrentUser = Pick<
| 'hasPassword'
>;
export const currentUserState = createStateV2<CurrentUser | null>({
export const currentUserState = createState<CurrentUser | null>({
key: 'currentUserState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { type ObjectPermissions } from 'twenty-shared/types';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type UserWorkspace } from '~/generated-metadata/graphql';
export type CurrentUserWorkspace = Pick<
@@ -10,7 +10,7 @@ export type CurrentUserWorkspace = Pick<
};
export const currentUserWorkspaceState =
createStateV2<CurrentUserWorkspace | null>({
createState<CurrentUserWorkspace | null>({
key: 'currentUserWorkspaceState',
defaultValue: null,
});
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type DeletedWorkspaceMember } from '~/generated-metadata/graphql';
export const currentWorkspaceDeletedMembersState = createStateV2<
export const currentWorkspaceDeletedMembersState = createState<
DeletedWorkspaceMember[]
>({
key: 'currentWorkspaceDeletedMembersState',
@@ -1,5 +1,5 @@
import { type WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export type CurrentWorkspaceMember = Omit<
WorkspaceMember,
@@ -7,7 +7,7 @@ export type CurrentWorkspaceMember = Omit<
>;
export const currentWorkspaceMemberState =
createStateV2<CurrentWorkspaceMember | null>({
createState<CurrentWorkspaceMember | null>({
key: 'currentWorkspaceMemberState',
defaultValue: null,
});
@@ -1,7 +1,7 @@
import { type PartialWorkspaceMember } from '@/settings/roles/types/RoleWithPartialMembers';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const currentWorkspaceMembersState = createStateV2<
export const currentWorkspaceMembersState = createState<
PartialWorkspaceMember[]
>({
key: 'currentWorkspaceMembersState',
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import {
type Application,
type Role,
@@ -43,7 +43,7 @@ export type CurrentWorkspace = Pick<
workspaceCustomApplication: Pick<Application, 'id'> | null;
};
export const currentWorkspaceState = createStateV2<CurrentWorkspace | null>({
export const currentWorkspaceState = createState<CurrentWorkspace | null>({
key: 'currentWorkspaceState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isCurrentUserLoadedState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCurrentUserLoadedState = createState<boolean>({
key: 'isCurrentUserLoadedState',
defaultValue: false,
});
@@ -1,10 +1,9 @@
import { jwtDecode } from 'jwt-decode';
import { selector } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { tokenPairState } from './tokenPairState';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { createSelectorV2 } from '@/ui/utilities/state/jotai/utils/createSelectorV2';
export const isImpersonatingState = createSelectorV2<boolean>({
export const isImpersonatingState = selector<boolean>({
key: 'isImpersonatingState',
get: ({ get }) => {
const tokenPair = get(tokenPairState);
@@ -1,9 +1,12 @@
import { type AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { atom } from 'recoil';
export const lastAuthenticatedMethodState =
createStateV2<AuthenticatedMethod | null>({
key: 'lastAuthenticatedMethodState',
defaultValue: null,
useLocalStorage: true,
});
import { type AuthenticatedMethod } from '@/auth/types/AuthenticatedMethod.enum';
import { localStorageEffect } from '~/utils/recoil/localStorageEffect';
const LAST_AUTHENTICATED_METHOD_STORAGE_KEY = 'lastAuthenticatedMethodState';
export const lastAuthenticatedMethodState = atom<AuthenticatedMethod | null>({
key: LAST_AUTHENTICATED_METHOD_STORAGE_KEY,
default: null,
effects: [localStorageEffect()],
});
@@ -1,7 +1,7 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthToken } from '~/generated-metadata/graphql';
export const loginTokenState = createStateV2<AuthToken['token'] | null>({
export const loginTokenState = createState<AuthToken['token'] | null>({
key: 'loginTokenState',
defaultValue: null,
});
@@ -1,10 +1,7 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { selectorFamily } from 'recoil';
// Temporary bridge: reads from Jotai store for migrated state.
// Will be fully migrated to Jotai in PR 6.
export const objectPermissionsFamilySelector = selectorFamily<
{
canRead: boolean;
@@ -16,9 +13,7 @@ export const objectPermissionsFamilySelector = selectorFamily<
get:
({ objectNameSingular }) =>
({ get }) => {
const currentUserWorkspace = jotaiStore.get(
currentUserWorkspaceState.atom,
);
const currentUserWorkspace = get(currentUserWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItem = objectMetadataItems.find(
@@ -1,4 +1,4 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export enum SignInUpStep {
Init = 'init',
Email = 'email',
@@ -10,7 +10,7 @@ export enum SignInUpStep {
TwoFactorAuthenticationProvision = 'TwoFactorAuthenticationProvision',
}
export const signInUpStepState = createStateV2<SignInUpStep>({
export const signInUpStepState = createState<SignInUpStep>({
key: 'signInUpStepState',
defaultValue: SignInUpStep.Init,
});
@@ -1,12 +1,18 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
import { type AuthTokenPair } from '~/generated-metadata/graphql';
import { cookieStorageEffect } from '~/utils/recoil/cookieStorageEffect';
export const tokenPairState = createStateV2<AuthTokenPair | null>({
export const tokenPairState = createState<AuthTokenPair | null>({
key: 'tokenPairState',
defaultValue: null,
useCookieStorage: {
cookieKey: 'tokenPair',
validateInitFn: (payload: AuthTokenPair) =>
Boolean(payload['accessOrWorkspaceAgnosticToken']),
},
effects: [
cookieStorageEffect(
'tokenPair',
{},
{
validateInitFn: (payload: AuthTokenPair) =>
Boolean(payload['accessOrWorkspaceAgnosticToken']),
},
),
],
});
@@ -1,8 +1,8 @@
import { type PublicWorkspaceDataOutput } from '~/generated-metadata/graphql';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const workspacePublicDataState =
createStateV2<PublicWorkspaceDataOutput | null>({
createState<PublicWorkspaceDataOutput | null>({
key: 'workspacePublicDataState',
defaultValue: null,
});
@@ -1,4 +1,5 @@
import { useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsBillingCreditsSection } from '@/billing/components/SettingsBillingCreditsSection';
@@ -15,14 +16,13 @@ import {
useBillingPortalSessionQuery,
} from '~/generated-metadata/graphql';
import { useGetWorkflowNodeExecutionUsage } from '@/billing/hooks/useGetWorkflowNodeExecutionUsage';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const SettingsBillingContent = () => {
const { t } = useLingui();
const { redirect } = useRedirect();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const subscriptions = currentWorkspace?.billingSubscriptions;
@@ -29,8 +29,8 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useMemo, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
H2Title,
@@ -144,7 +144,7 @@ export const SettingsBillingSubscriptionInfo = ({
const [cancelSwitchMeteredPrice] = useCancelSwitchMeteredPriceMutation();
const setCurrentWorkspace = useSetRecoilStateV2(currentWorkspaceState);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const isTrialPeriod = subscriptionStatus === SubscriptionStatus.Trialing;
@@ -7,7 +7,6 @@ import {
type MeteredBillingPrice,
} from '@/billing/types/billing-price-tiers.type';
import { useNumberFormat } from '@/localization/hooks/useNumberFormat';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
@@ -15,6 +14,7 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { findOrThrow, isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -48,7 +48,7 @@ export const MeteredPriceSelector = ({
const { currentMeteredBillingPrice } = useCurrentMetered();
const { formatNumber } = useNumberFormat();
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
@@ -12,12 +12,12 @@ import { beautifyExactDate } from '~/utils/date-utils';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import { useCurrentMetered } from '@/billing/hooks/useCurrentMetered';
import { useCurrentBillingFlags } from '@/billing/hooks/useCurrentBillingFlags';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useBillingWording = () => {
const { t } = useLingui();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -3,11 +3,11 @@ import {
BillingPlanKey,
} from '~/generated-metadata/graphql';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
export const useCurrentBillingFlags = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -1,7 +1,7 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useCurrentPlan } from '@/billing/hooks/useCurrentPlan';
import type { MeteredBillingPrice } from '@/billing/types/billing-price-tiers.type';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { assertIsDefinedOrThrow, findOrThrow } from 'twenty-shared/utils';
import {
BillingProductKey,
@@ -11,7 +11,7 @@ import {
export const useCurrentMetered = () => {
const { currentPlan } = useCurrentPlan();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -2,12 +2,12 @@ import { BillingPlanKey } from '~/generated-metadata/graphql';
import { assertIsDefinedOrThrow, findOrThrow } from 'twenty-shared/utils';
import { usePlans } from './usePlans';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
export const useCurrentPlan = () => {
const { listPlans } = usePlans();
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
assertIsDefinedOrThrow(currentWorkspace);
@@ -1,16 +1,16 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { useEndSubscriptionTrialPeriodMutation } from '~/generated-metadata/graphql';
export const useEndSubscriptionTrialPeriod = () => {
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const [endSubscriptionTrialPeriod] = useEndSubscriptionTrialPeriodMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilStateV2(
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const [isLoading, setIsLoading] = useState(false);
@@ -1,8 +1,8 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useHasNextBillingPhase = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const hasNextBillingPhase =
currentWorkspace?.currentBillingSubscription?.phases.length === 2;
@@ -1,8 +1,8 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const useNextBillingPhase = () => {
const currentWorkspace = useRecoilValueV2(currentWorkspaceState);
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const nextBillingPhase =
currentWorkspace?.currentBillingSubscription?.phases[1];
@@ -1,19 +1,19 @@
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { getCaptchaUrlByProvider } from '@/captcha/utils/getCaptchaUrlByProvider';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { useCaptcha } from '@/client-config/hooks/useCaptcha';
import { captchaState } from '@/client-config/states/captchaState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
export const CaptchaProviderScriptLoaderEffect = () => {
const captcha = useRecoilValueV2(captchaState);
const setIsCaptchaScriptLoaded = useSetRecoilStateV2(
const captcha = useRecoilValue(captchaState);
const setIsCaptchaScriptLoaded = useSetRecoilState(
isCaptchaScriptLoadedState,
);
const { isCaptchaScriptLoaded, isCaptchaConfigured } = useCaptcha();
@@ -1,26 +1,13 @@
import { act, renderHook } from '@testing-library/react';
import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { RecoilRoot } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
const Wrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot>{children}</RecoilRoot>
</JotaiProvider>
);
describe('useReadCaptchaToken', () => {
beforeEach(() => {
jotaiStore.set(captchaTokenState.atom, undefined);
});
it('should return undefined when no token exists', async () => {
const { result } = renderHook(() => useReadCaptchaToken(), {
wrapper: Wrapper,
wrapper: RecoilRoot,
});
await act(async () => {
@@ -30,11 +17,23 @@ describe('useReadCaptchaToken', () => {
});
it('should return the token when it exists', async () => {
jotaiStore.set(captchaTokenState.atom, 'test-token');
const { result } = renderHook(() => useReadCaptchaToken(), {
wrapper: Wrapper,
});
const { result } = renderHook(
() => {
const hook = useReadCaptchaToken();
return hook;
},
{
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaTokenState, 'test-token');
}}
>
{children}
</RecoilRoot>
),
},
);
await act(async () => {
const token = await result.current.readCaptchaToken();
@@ -1,23 +1,14 @@
import { act, renderHook, waitFor } from '@testing-library/react';
import { type ReactNode } from 'react';
import { RecoilRoot } from 'recoil';
import { Provider as JotaiProvider } from 'jotai';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { captchaState } from '@/client-config/states/captchaState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { type Captcha, CaptchaDriverType } from '~/generated-metadata/graphql';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
jest.mock('@/captcha/utils/isCaptchaRequiredForPath');
const createWrapper = ({ children }: { children: ReactNode }) => (
<JotaiProvider store={jotaiStore}>
<RecoilRoot>{children}</RecoilRoot>
</JotaiProvider>
);
describe('useRequestFreshCaptchaToken', () => {
const mockGrecaptchaExecute = jest.fn();
const mockTurnstileRender = jest.fn();
@@ -56,7 +47,7 @@ describe('useRequestFreshCaptchaToken', () => {
(isCaptchaRequiredForPath as jest.Mock).mockReturnValue(false);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper,
wrapper: RecoilRoot,
});
await act(async () => {
@@ -70,7 +61,7 @@ describe('useRequestFreshCaptchaToken', () => {
it('should not request a token if captcha provider is undefined', async () => {
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper,
wrapper: RecoilRoot,
});
await act(async () => {
@@ -83,14 +74,20 @@ describe('useRequestFreshCaptchaToken', () => {
});
it('should request a token from Google reCAPTCHA when provider is GOOGLE_RECAPTCHA', async () => {
jotaiStore.set(isRequestingCaptchaTokenState.atom, false);
jotaiStore.set(captchaState.atom, {
provider: CaptchaDriverType.GOOGLE_RECAPTCHA,
siteKey: 'google-site-key',
} as Captcha);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper,
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.GOOGLE_RECAPTCHA,
siteKey: 'google-site-key',
});
set(isRequestingCaptchaTokenState, false);
}}
>
{children}
</RecoilRoot>
),
});
await act(async () => {
@@ -107,14 +104,20 @@ describe('useRequestFreshCaptchaToken', () => {
});
it('should request a token from Turnstile when provider is TURNSTILE', async () => {
jotaiStore.set(isRequestingCaptchaTokenState.atom, false);
jotaiStore.set(captchaState.atom, {
provider: CaptchaDriverType.TURNSTILE,
siteKey: 'turnstile-site-key',
} as Captcha);
const { result } = renderHook(() => useRequestFreshCaptchaToken(), {
wrapper: createWrapper,
wrapper: ({ children }) => (
<RecoilRoot
initializeState={({ set }) => {
set(captchaState, {
provider: CaptchaDriverType.TURNSTILE,
siteKey: 'turnstile-site-key',
});
set(isRequestingCaptchaTokenState, false);
}}
>
{children}
</RecoilRoot>
),
});
await act(async () => {
@@ -1,16 +1,22 @@
import { useRecoilCallback } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useCallback } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useReadCaptchaToken = () => {
const readCaptchaToken = useCallback(() => {
const existingCaptchaToken = jotaiStore.get(captchaTokenState.atom);
const readCaptchaToken = useRecoilCallback(
({ snapshot }) =>
() => {
const existingCaptchaToken = snapshot
.getLoadable(captchaTokenState)
.getValue();
if (isDefined(existingCaptchaToken)) {
return existingCaptchaToken;
}
}, []);
if (isDefined(existingCaptchaToken)) {
return existingCaptchaToken;
}
},
[],
);
return { readCaptchaToken };
};
@@ -1,58 +1,61 @@
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState';
import { isCaptchaRequiredForPath } from '@/captcha/utils/isCaptchaRequiredForPath';
import { captchaState } from '@/client-config/states/captchaState';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useCallback } from 'react';
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
import { CaptchaDriverType } from '~/generated-metadata/graphql';
export const useRequestFreshCaptchaToken = () => {
const setCaptchaToken = useSetRecoilStateV2(captchaTokenState);
const setIsRequestingCaptchaToken = useSetRecoilStateV2(
const setCaptchaToken = useSetRecoilState(captchaTokenState);
const setIsRequestingCaptchaToken = useSetRecoilState(
isRequestingCaptchaTokenState,
);
const requestFreshCaptchaToken = useCallback(async () => {
if (!isCaptchaRequiredForPath(window.location.pathname)) {
return;
}
const requestFreshCaptchaToken = useRecoilCallback(
({ snapshot }) =>
async () => {
if (!isCaptchaRequiredForPath(window.location.pathname)) {
return;
}
const captcha = jotaiStore.get(captchaState.atom);
const captcha = snapshot.getLoadable(captchaState).getValue();
if (!isDefined(captcha)) {
return;
}
if (!isDefined(captcha)) {
return;
}
assertIsDefinedOrThrow(captcha);
assertIsDefinedOrThrow(captcha);
setIsRequestingCaptchaToken(true);
setIsRequestingCaptchaToken(true);
let captchaWidget: any;
switch (captcha.provider) {
case CaptchaDriverType.GOOGLE_RECAPTCHA:
window.grecaptcha
.execute(captcha.siteKey, {
action: 'submit',
})
.then((token: string) => {
setCaptchaToken(token);
setIsRequestingCaptchaToken(false);
});
break;
case CaptchaDriverType.TURNSTILE:
captchaWidget = window.turnstile.render('#captcha-widget', {
sitekey: captcha.siteKey,
});
window.turnstile.execute(captchaWidget, {
callback: (token: string) => {
setCaptchaToken(token);
setIsRequestingCaptchaToken(false);
},
});
}
}, [setCaptchaToken, setIsRequestingCaptchaToken]);
let captchaWidget: any;
switch (captcha.provider) {
case CaptchaDriverType.GOOGLE_RECAPTCHA:
window.grecaptcha
.execute(captcha.siteKey, {
action: 'submit',
})
.then((token: string) => {
setCaptchaToken(token);
setIsRequestingCaptchaToken(false);
});
break;
case CaptchaDriverType.TURNSTILE:
captchaWidget = window.turnstile.render('#captcha-widget', {
sitekey: captcha.siteKey,
});
window.turnstile.execute(captchaWidget, {
callback: (token: string) => {
setCaptchaToken(token);
setIsRequestingCaptchaToken(false);
},
});
}
},
[setCaptchaToken, setIsRequestingCaptchaToken],
);
return { requestFreshCaptchaToken };
};
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const captchaTokenState = createStateV2<string | undefined>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const captchaTokenState = createState<string | undefined>({
key: 'captchaTokenState',
defaultValue: undefined,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isCaptchaScriptLoadedState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCaptchaScriptLoadedState = createState<boolean>({
key: 'isCaptchaScriptLoadedState',
defaultValue: false,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isRequestingCaptchaTokenState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isRequestingCaptchaTokenState = createState<boolean>({
key: 'isRequestingCaptchaTokenState',
defaultValue: false,
});
@@ -1,18 +1,19 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
import { isInFrame } from '~/utils/isInIframe';
import { isDefined } from 'twenty-shared/utils';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const ChromeExtensionSidecarEffect = () => {
const navigate = useNavigate();
const setTokenPair = useSetRecoilStateV2(tokenPairState);
const setTokenPair = useSetRecoilState(tokenPairState);
const chromeExtensionId = useRecoilValueV2(chromeExtensionIdState);
const setIsLoadingTokensFromExtension = useSetRecoilStateV2(
const setIsLoadingTokensFromExtension = useSetRecoilState(
isLoadingTokensFromExtensionState,
);
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isLoadingTokensFromExtensionState = createStateV2<boolean | null>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isLoadingTokensFromExtensionState = createState<boolean | null>({
key: 'isLoadingTokensFromExtensionState',
defaultValue: null,
});
@@ -1,12 +1,13 @@
import { useRecoilValue } from 'recoil';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { AppFullScreenErrorFallback } from '@/error-handler/components/AppFullScreenErrorFallback';
import { useLingui } from '@lingui/react/macro';
export const ClientConfigProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { isErrored, error } = useRecoilValueV2(clientConfigApiStatusState);
const { isErrored, error } = useRecoilValue(clientConfigApiStatusState);
const { t } = useLingui();
return isErrored && error instanceof Error ? (
@@ -1,11 +1,11 @@
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
export const ClientConfigProviderEffect = () => {
const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilStateV2(
const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilState(
clientConfigApiStatusState,
);
@@ -2,15 +2,15 @@ import { captchaTokenState } from '@/captcha/states/captchaTokenState';
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
import { captchaState } from '@/client-config/states/captchaState';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
export const useCaptcha = () => {
const captcha = useRecoilValueV2(captchaState);
const captchaToken = useRecoilValueV2(captchaTokenState);
const clientConfigApiStatus = useRecoilValueV2(clientConfigApiStatusState);
const isCaptchaScriptLoaded = useRecoilValueV2(isCaptchaScriptLoadedState);
const captcha = useRecoilValue(captchaState);
const captchaToken = useRecoilValue(captchaTokenState);
const clientConfigApiStatus = useRecoilValue(clientConfigApiStatusState);
const isCaptchaScriptLoaded = useRecoilValue(isCaptchaScriptLoadedState);
const isClientConfigLoaded = clientConfigApiStatus.isLoadedOnce;
const isSiteKeyDefined = isDefined(captcha?.siteKey);
@@ -26,14 +26,14 @@ import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { type ClientConfig } from '@/client-config/types/ClientConfig';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { isAttachmentPreviewEnabledStateV2 } from '@/client-config/states/isAttachmentPreviewEnabledStateV2';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { getClientConfig } from '@/client-config/utils/getClientConfig';
import { allowRequestsToTwentyIconsState } from '@/client-config/states/allowRequestsToTwentyIcons';
import { useSetRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useSetRecoilStateV2';
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
type UseClientConfigResult = {
data: { clientConfig: ClientConfig } | undefined;
@@ -45,35 +45,35 @@ type UseClientConfigResult = {
export const useClientConfig = (): UseClientConfigResult => {
const setIsAnalyticsEnabled = useSetRecoilStateV2(isAnalyticsEnabledState);
const setDomainConfiguration = useSetRecoilStateV2(domainConfigurationState);
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
const setAuthProviders = useSetRecoilStateV2(authProvidersState);
const setAiModels = useSetRecoilStateV2(aiModelsState);
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilStateV2(
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
isDeveloperDefaultSignInPrefilledState,
);
const setIsMultiWorkspaceEnabled = useSetRecoilStateV2(
const setIsMultiWorkspaceEnabled = useSetRecoilState(
isMultiWorkspaceEnabledState,
);
const setIsEmailVerificationRequired = useSetRecoilStateV2(
const setIsEmailVerificationRequired = useSetRecoilState(
isEmailVerificationRequiredState,
);
const setBilling = useSetRecoilStateV2(billingState);
const setSupportChat = useSetRecoilStateV2(supportChatState);
const setBilling = useSetRecoilState(billingState);
const setSupportChat = useSetRecoilState(supportChatState);
const setSentryConfig = useSetRecoilStateV2(sentryConfigState);
const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilStateV2(
const setSentryConfig = useSetRecoilState(sentryConfigState);
const [clientConfigApiStatus, setClientConfigApiStatus] = useRecoilState(
clientConfigApiStatusState,
);
const setCaptcha = useSetRecoilStateV2(captchaState);
const setCaptcha = useSetRecoilState(captchaState);
const setChromeExtensionId = useSetRecoilStateV2(chromeExtensionIdState);
const setApiConfig = useSetRecoilStateV2(apiConfigState);
const setApiConfig = useSetRecoilState(apiConfigState);
const setCanManageFeatureFlags = useSetRecoilStateV2(
const setCanManageFeatureFlags = useSetRecoilState(
canManageFeatureFlagsState,
);
@@ -81,55 +81,57 @@ export const useClientConfig = (): UseClientConfigResult => {
labPublicFeatureFlagsStateV2,
);
const setMicrosoftMessagingEnabled = useSetRecoilStateV2(
const setMicrosoftMessagingEnabled = useSetRecoilState(
isMicrosoftMessagingEnabledState,
);
const setMicrosoftCalendarEnabled = useSetRecoilStateV2(
const setMicrosoftCalendarEnabled = useSetRecoilState(
isMicrosoftCalendarEnabledState,
);
const setGoogleMessagingEnabled = useSetRecoilStateV2(
const setGoogleMessagingEnabled = useSetRecoilState(
isGoogleMessagingEnabledState,
);
const setGoogleCalendarEnabled = useSetRecoilStateV2(
const setGoogleCalendarEnabled = useSetRecoilState(
isGoogleCalendarEnabledState,
);
const setIsAttachmentPreviewEnabled = useSetRecoilStateV2(
const setIsAttachmentPreviewEnabled = useSetRecoilState(
isAttachmentPreviewEnabledState,
);
const setIsConfigVariablesInDbEnabled = useSetRecoilStateV2(
const setIsConfigVariablesInDbEnabled = useSetRecoilState(
isConfigVariablesInDbEnabledState,
);
const setCalendarBookingPageId = useSetRecoilStateV2(
const setCalendarBookingPageId = useSetRecoilState(
calendarBookingPageIdState,
);
const setIsImapSmtpCaldavEnabled = useSetRecoilStateV2(
const setIsImapSmtpCaldavEnabled = useSetRecoilState(
isImapSmtpCaldavEnabledState,
);
const setIsEmailingDomainsEnabled = useSetRecoilStateV2(
const setIsEmailingDomainsEnabled = useSetRecoilState(
isEmailingDomainsEnabledState,
);
const setAllowRequestsToTwentyIcons = useSetRecoilStateV2(
const setAllowRequestsToTwentyIcons = useSetRecoilState(
allowRequestsToTwentyIconsState,
);
const setIsCloudflareIntegrationEnabled = useSetRecoilStateV2(
const setIsCloudflareIntegrationEnabled = useSetRecoilState(
isCloudflareIntegrationEnabledState,
);
const setIsClickHouseConfigured = useSetRecoilStateV2(
const setIsClickHouseConfigured = useSetRecoilState(
isClickHouseConfiguredState,
);
const setAppVersion = useSetRecoilStateV2(appVersionState);
const store = useStore();
const fetchClientConfig = useCallback(async () => {
setClientConfigApiStatus((prev) => ({
...prev,
@@ -191,7 +193,7 @@ export const useClientConfig = (): UseClientConfigResult => {
setGoogleMessagingEnabled(clientConfig?.isGoogleMessagingEnabled);
setGoogleCalendarEnabled(clientConfig?.isGoogleCalendarEnabled);
setIsAttachmentPreviewEnabled(clientConfig?.isAttachmentPreviewEnabled);
jotaiStore.set(
store.set(
isAttachmentPreviewEnabledStateV2.atom,
clientConfig?.isAttachmentPreviewEnabled,
);
@@ -252,6 +254,7 @@ export const useClientConfig = (): UseClientConfigResult => {
setSentryConfig,
setSupportChat,
setAllowRequestsToTwentyIcons,
store,
]);
return {
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const allowRequestsToTwentyIconsState = createStateV2<boolean>({
export const allowRequestsToTwentyIconsState = createState<boolean>({
key: 'allowRequestsToTwentyIcons',
defaultValue: true,
});
@@ -1,7 +1,7 @@
import { type ApiConfig } from '~/generated-metadata/graphql';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const apiConfigState = createStateV2<ApiConfig | null>({
export const apiConfigState = createState<ApiConfig | null>({
key: 'apiConfigState',
defaultValue: null,
});
@@ -1,7 +1,7 @@
import { type Billing } from '~/generated-metadata/graphql';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const billingState = createStateV2<Billing | null>({
export const billingState = createState<Billing | null>({
key: 'billingState',
defaultValue: null,
});
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const calendarBookingPageIdState = createStateV2<string | null>({
export const calendarBookingPageIdState = createState<string | null>({
key: 'calendarBookingPageIdState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const canManageFeatureFlagsState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const canManageFeatureFlagsState = createState<boolean>({
key: 'canManageFeatureFlagsState',
defaultValue: false,
});
@@ -1,7 +1,7 @@
import { type Captcha } from '~/generated-metadata/graphql';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const captchaState = createStateV2<Captcha | null>({
export const captchaState = createState<Captcha | null>({
key: 'captchaState',
defaultValue: null,
});
@@ -1,5 +1,5 @@
import { type ClientConfig } from '@/client-config/types/ClientConfig';
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
type ClientConfigApiStatus = {
isLoadedOnce: boolean;
@@ -10,7 +10,7 @@ type ClientConfigApiStatus = {
data?: { clientConfig: ClientConfig };
};
export const clientConfigApiStatusState = createStateV2<ClientConfigApiStatus>({
export const clientConfigApiStatusState = createState<ClientConfigApiStatus>({
key: 'clientConfigApiStatus',
defaultValue: {
isLoadedOnce: false,
@@ -1,5 +1,5 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
export const isAttachmentPreviewEnabledState = createStateV2<boolean>({
import { createState } from '@/ui/utilities/state/utils/createState';
export const isAttachmentPreviewEnabledState = createState<boolean>({
key: 'isAttachmentPreviewEnabled',
defaultValue: false,
});
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const isClickHouseConfiguredState = createStateV2<boolean>({
export const isClickHouseConfiguredState = createState<boolean>({
key: 'isClickHouseConfigured',
defaultValue: false,
});
@@ -1,6 +1,6 @@
import { createStateV2 } from '@/ui/utilities/state/jotai/utils/createStateV2';
import { createState } from '@/ui/utilities/state/utils/createState';
export const isCloudflareIntegrationEnabledState = createStateV2<boolean>({
export const isCloudflareIntegrationEnabledState = createState<boolean>({
key: 'isCloudflareIntegrationEnabled',
defaultValue: false,
});

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