0f2af6a6cb
## Recoil to Jotai Migration — PR 13: Workflow, Page Layout, AI, Activities, Settings, SSE & Remaining Modules ### Summary This is the 13th PR in the [14-PR migration series](packages/twenty-front/src/modules/ui/utilities/state/jotai/MIGRATION_PLAN.md) to replace Recoil with Jotai as the state management library in `twenty-front`. This PR migrates the remaining consumer code across all modules — updating ~1,076 files with ~10k insertions/deletions. ### What changed **New Jotai infrastructure (13 new files):** - `createComponentSelectorV2` and `createComponentFamilySelectorV2` — instance-scoped derived atoms, replacing Recoil's component selectors - 6 new hooks: `useRecoilComponentSelectorValueV2`, `useRecoilComponentFamilySelectorValueV2`, `useRecoilComponentSelectorCallbackStateV2`, `useRecoilComponentFamilySelectorCallbackStateV2`, `useRecoilComponentStateCallbackStateV2`, `useRecoilComponentFamilyStateCallbackStateV2` - New types: `ComponentSelectorV2`, `ComponentFamilySelectorV2`, `SelectorCallbacksV2` - Extended `buildGetHelper` to support the new selector patterns **State definition migrations:** - `createState()` → `createStateV2()` (Jotai `atom()`) - `createFamilyState()` → `createFamilyStateV2()` (Jotai `atom()` with family cache) - Recoil `selector`/`selectorFamily` → Jotai derived `atom()` via V2 utilities **Hook migrations (~2,400 removed, ~1,545 added V2 equivalents):** - `useRecoilValue` → `useRecoilValueV2` - `useRecoilState` → `useRecoilStateV2` - `useSetRecoilState` → `useSetRecoilStateV2` - `useRecoilCallback` → `useCallback` + `jotaiStore.get/set` - `useRecoilComponentValue` → `useRecoilComponentValueV2` - `useSetRecoilComponentState` → `useSetRecoilComponentStateV2` - `useRecoilComponentFamilyValue` → `useRecoilComponentFamilyValueV2` / `useFamilySelectorValueV2` - ~397 direct `import from 'recoil'` removed **Deleted files (9 files):** - `dateLocaleState.ts` — consolidated - `currentAIChatThreadTitleStateV2.ts` — renamed to replace the original - `isCommandMenuOpenedState.ts` — removed - `filesFieldUploadState.ts` — replaced by V2 - `recordStoreFamilySelector.ts`, `recordStoreFieldValueSelector.ts`, `recordStoreRecordsSelector.ts` — replaced by V2 equivalents - `settingsAllRolesSelector.ts` — replaced by `useSettingsAllRoles` hook - `settingsRoleIdsStateV2.ts` — consolidated **Modules migrated:** - Workflow (diagram, steps, variables, filters) - Page Layout (widgets, fields, graph, tabs) - AI (chat, agents, browsing context) - Activities (rich text editor, targets, timeline) - Action Menu (single/multiple record actions) - Command Menu (navigation, history, pages, workflow) - Context Store - Record Board, Record Table, Record Calendar, Record Index - Record Field, Record Filter, Record Sort, Record Group - Record Drag, Record Picker, Record Store - Views (view bar, view picker, filters, sorts) - Settings (roles, permissions, SSO, security, data model, developers, domains) - SSE (event streams, subscriptions) - Spreadsheet Import - Auth, Favorites, Front Components, Information Banner
187 lines
5.5 KiB
TypeScript
187 lines
5.5 KiB
TypeScript
import { SETTINGS_PLAYGROUND_FORM_SCHEMA_SELECT_OPTIONS } from '@/settings/playground/constants/SettingsPlaygroundFormSchemaSelectOptions';
|
|
import { playgroundApiKeyState } from '@/settings/playground/states/playgroundApiKeyState';
|
|
import { PlaygroundSchemas } from '@/settings/playground/types/PlaygroundSchemas';
|
|
import { PlaygroundTypes } from '@/settings/playground/types/PlaygroundTypes';
|
|
import { Select } from '@/ui/input/components/Select';
|
|
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
|
import { useRecoilStateV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilStateV2';
|
|
import styled from '@emotion/styled';
|
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
import { useLingui } from '@lingui/react/macro';
|
|
import { Controller, useForm } from 'react-hook-form';
|
|
import { SettingsPath } from 'twenty-shared/types';
|
|
import { CustomError } from 'twenty-shared/utils';
|
|
import { IconApi, IconBrandGraphql } from 'twenty-ui/display';
|
|
import { Button } from 'twenty-ui/input';
|
|
import { z } from 'zod';
|
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
|
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
|
|
|
const playgroundSetupFormSchema = z.object({
|
|
apiKeyForPlayground: z.string(),
|
|
schema: z.enum(PlaygroundSchemas),
|
|
playgroundType: z.enum(PlaygroundTypes),
|
|
});
|
|
|
|
type PlaygroundSetupFormValues = z.infer<typeof playgroundSetupFormSchema>;
|
|
|
|
const StyledForm = styled.form`
|
|
display: grid;
|
|
grid-template-columns: 1.5fr 1fr 1fr 0.5fr;
|
|
align-items: end;
|
|
gap: ${({ theme }) => theme.spacing(2)};
|
|
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
|
width: 100%;
|
|
`;
|
|
|
|
export const PlaygroundSetupForm = () => {
|
|
const { t } = useLingui();
|
|
const navigateSettings = useNavigateSettings();
|
|
const [playgroundApiKey, setPlaygroundApiKey] = useRecoilStateV2(
|
|
playgroundApiKeyState,
|
|
);
|
|
|
|
const {
|
|
control,
|
|
handleSubmit,
|
|
formState: { isSubmitting },
|
|
setError,
|
|
} = useForm<PlaygroundSetupFormValues>({
|
|
mode: 'onTouched',
|
|
resolver: zodResolver(playgroundSetupFormSchema),
|
|
defaultValues: {
|
|
schema: PlaygroundSchemas.CORE,
|
|
playgroundType: PlaygroundTypes.REST,
|
|
apiKeyForPlayground: playgroundApiKey || '',
|
|
},
|
|
});
|
|
|
|
const validateApiKey = async (values: PlaygroundSetupFormValues) => {
|
|
try {
|
|
// Validate by fetching the schema (but not storing it)
|
|
const response = await fetch(
|
|
`${REACT_APP_SERVER_BASE_URL}/rest/open-api/${values.schema}`,
|
|
{
|
|
headers: { Authorization: `Bearer ${values.apiKeyForPlayground}` },
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new CustomError(
|
|
`HTTP error! status: ${response.status}`,
|
|
'HTTP_ERROR',
|
|
);
|
|
}
|
|
|
|
const openAPIReference = await response.json();
|
|
|
|
if (!openAPIReference.tags) {
|
|
throw new Error('Invalid API Key');
|
|
}
|
|
|
|
return true;
|
|
} catch {
|
|
throw new Error(t`Invalid API key`);
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (values: PlaygroundSetupFormValues) => {
|
|
try {
|
|
await validateApiKey(values);
|
|
|
|
setPlaygroundApiKey(values.apiKeyForPlayground);
|
|
|
|
const path =
|
|
values.playgroundType === PlaygroundTypes.GRAPHQL
|
|
? SettingsPath.GraphQLPlayground
|
|
: SettingsPath.RestPlayground;
|
|
|
|
navigateSettings(path, {
|
|
schema: values.schema.toLowerCase(),
|
|
});
|
|
} catch (error) {
|
|
setError('apiKeyForPlayground', {
|
|
type: 'manual',
|
|
message:
|
|
error instanceof Error
|
|
? error.message
|
|
: t`An unexpected error occurred`,
|
|
});
|
|
}
|
|
};
|
|
|
|
return (
|
|
<StyledForm onSubmit={handleSubmit(onSubmit)}>
|
|
<Controller
|
|
name="apiKeyForPlayground"
|
|
control={control}
|
|
render={({ field: { onChange, value }, fieldState: { error } }) => (
|
|
<SettingsTextInput
|
|
instanceId="playground-api-key"
|
|
label={t`API Key`}
|
|
placeholder={t`Enter your API key`}
|
|
value={value}
|
|
onChange={(newValue) => {
|
|
onChange(newValue);
|
|
setPlaygroundApiKey(newValue);
|
|
}}
|
|
error={error?.message}
|
|
required
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="schema"
|
|
control={control}
|
|
defaultValue={PlaygroundSchemas.CORE}
|
|
render={({ field: { onChange, value } }) => (
|
|
<Select
|
|
dropdownId="schema"
|
|
label={t`Schema`}
|
|
options={SETTINGS_PLAYGROUND_FORM_SCHEMA_SELECT_OPTIONS.map(
|
|
(option) => ({
|
|
...option,
|
|
label: t(option.label),
|
|
}),
|
|
)}
|
|
value={value}
|
|
onChange={onChange}
|
|
/>
|
|
)}
|
|
/>
|
|
<Controller
|
|
name="playgroundType"
|
|
control={control}
|
|
defaultValue={PlaygroundTypes.REST}
|
|
render={({ field: { onChange, value } }) => (
|
|
<Select
|
|
dropdownId="apiPlaygroundType"
|
|
label={t`API`}
|
|
options={[
|
|
{
|
|
value: PlaygroundTypes.REST,
|
|
label: t`REST`,
|
|
Icon: IconApi,
|
|
},
|
|
{
|
|
value: PlaygroundTypes.GRAPHQL,
|
|
label: t`GraphQL`,
|
|
Icon: IconBrandGraphql,
|
|
},
|
|
]}
|
|
value={value}
|
|
onChange={onChange}
|
|
/>
|
|
)}
|
|
/>
|
|
<Button
|
|
title={t`Launch`}
|
|
variant="primary"
|
|
accent="blue"
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
/>
|
|
</StyledForm>
|
|
);
|
|
};
|