Migrate from ESLint to OxLint (#18443)

## Summary

Fully replaces ESLint with OxLint across the entire monorepo:

- **Replaced all ESLint configs** (`eslint.config.mjs`) with OxLint
configs (`.oxlintrc.json`) for every package: `twenty-front`,
`twenty-server`, `twenty-emails`, `twenty-ui`, `twenty-shared`,
`twenty-sdk`, `twenty-zapier`, `twenty-docs`, `twenty-website`,
`twenty-apps/*`, `create-twenty-app`
- **Migrated custom lint rules** from ESLint plugin format to OxLint JS
plugin system (`@oxlint/plugins`), including
`styled-components-prefixed-with-styled`, `no-hardcoded-colors`,
`sort-css-properties-alphabetically`,
`graphql-resolvers-should-be-guarded`,
`rest-api-methods-should-be-guarded`, `max-consts-per-file`, and
Jotai-related rules
- **Migrated custom rule tests** from ESLint `RuleTester` + Jest to
`oxlint/plugins-dev` `RuleTester` + Vitest
- **Removed all ESLint dependencies** from `package.json` files and
regenerated lockfiles
- **Updated Nx targets** (`lint`, `lint:diff-with-main`, `fmt`) in
`nx.json` and per-project `project.json` to use `oxlint` commands with
proper `dependsOn` for plugin builds
- **Updated CI workflows** (`.github/workflows/ci-*.yaml`) — no more
ESLint executor
- **Updated IDE setup**: replaced `dbaeumer.vscode-eslint` with
`oxc.oxc-vscode` extension, configured `source.fixAll.oxc` and
format-on-save with Prettier
- **Replaced all `eslint-disable` comments** with `oxlint-disable`
equivalents across the codebase
- **Updated docs** (`twenty-docs`) to reference OxLint instead of ESLint
- **Renamed** `twenty-eslint-rules` package to `twenty-oxlint-rules`

### Temporarily disabled rules (tracked in `OXLINT_MIGRATION_TODO.md`)

| Rule | Package | Violations | Auto-fixable |
|------|---------|-----------|-------------|
| `twenty/sort-css-properties-alphabetically` | twenty-front | 578 | Yes
|
| `typescript/consistent-type-imports` | twenty-server | 3814 | Yes |
| `twenty/max-consts-per-file` | twenty-server | 94 | No |

### Dropped plugins (no OxLint equivalent)

`eslint-plugin-project-structure`, `lingui/*`, `@stylistic/*`,
`import/order`, `prefer-arrow/prefer-arrow-functions`,
`eslint-plugin-mdx`, `@next/eslint-plugin-next`,
`eslint-plugin-storybook`, `eslint-plugin-react-refresh`. Partial
coverage for `jsx-a11y` and `unused-imports`.

### Additional fixes (pre-existing issues exposed by merge)

- Fixed `EmailThreadPreview.tsx` broken import from main rename
(`useOpenEmailThreadInSidePanel`)
- Restored truthiness guard in `getActivityTargetObjectRecords.ts`
- Fixed `AgentTurnResolver` return types to match entity (virtual
`fileMediaType`/`fileUrl` are resolved via `@ResolveField()`)

## Test plan

- [x] `npx nx lint twenty-front` passes
- [x] `npx nx lint twenty-server` passes
- [x] `npx nx lint twenty-docs` passes
- [x] Custom oxlint rules validated with Vitest: `npx nx test
twenty-oxlint-rules`
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx typecheck twenty-server` passes
- [x] CI workflows trigger correctly with `dependsOn:
["twenty-oxlint-rules:build"]`
- [x] IDE linting works with `oxc.oxc-vscode` extension
This commit is contained in:
Charles Bochet
2026-03-06 01:03:50 +01:00
committed by GitHub
parent b421efbff7
commit 9d57bc39e5
880 changed files with 4711 additions and 9687 deletions
@@ -29,7 +29,7 @@ export const ActionButton = ({
return (
<>
{action.shortLabel ? (
{isDefined(action.shortLabel) ? (
<Button
Icon={action.Icon}
size="small"
@@ -34,7 +34,7 @@ export const ExportMultipleRecordsAction = () => {
const { closeActionMenu } = useCloseActionMenu({});
const exportProgress = progress
const exportProgress = isDefined(progress)
? {
processedRecordCount: progress.processedRecordCount,
totalRecordCount: progress.totalRecordCount,
@@ -24,11 +24,11 @@ export const ExportNoteActionSingleRecordAction = () => {
try {
parsedBody = JSON.parse(initialBody);
} catch {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.warn(
`Failed to parse body for record ${recordId}, for rich text version 'v2'`,
);
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.warn(initialBody);
}
@@ -1,6 +1,7 @@
import { type CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore';
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared/utils';
type CalendarEventDetailsEffectProps = {
record: CalendarEvent;
@@ -12,7 +13,7 @@ export const CalendarEventDetailsEffect = ({
const { upsertRecordsInStore } = useUpsertRecordsInStore();
useEffect(() => {
if (!record) {
if (!isDefined(record)) {
return;
}
@@ -34,7 +34,7 @@ export const CalendarEventParticipantsResponseStatus = ({
<CalendarEventParticipantsResponseStatusField
key={responseStatus}
responseStatus={responseStatus}
participants={groupedParticipants[responseStatus] || []}
participants={groupedParticipants[responseStatus] ?? []}
/>
))}
</>
@@ -108,7 +108,7 @@ export const CalendarEventsCard = () => {
// TODO: change animated placeholder
return (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noMatchRecord" />
@@ -8,6 +8,7 @@ import { EmailThreadMessageSender } from '@/activities/emails/components/EmailTh
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { type EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
import { FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MessageParticipantRole } from 'twenty-shared/types';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { MessageChannelVisibility } from '~/generated/graphql';
@@ -53,7 +54,7 @@ export const EmailThreadMessage = ({
(participant) => participant.role !== MessageParticipantRole.FROM,
);
if (!sender || receivers.length === 0) {
if (!isDefined(sender) || receivers.length === 0) {
return null;
}
@@ -4,6 +4,8 @@ import { ActivityRow } from '@/activities/components/ActivityRow';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { useOpenEmailThreadInSidePanel } from '@/side-panel/hooks/useOpenEmailThreadInSidePanel';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { Avatar } from 'twenty-ui/display';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import {
@@ -74,8 +76,8 @@ type EmailThreadPreviewProps = {
};
export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
const { openEmailThreadInSidePanel } = useOpenEmailThreadInSidePanel();
const { theme } = useContext(ThemeContext);
const { openEmailThreadInSidePanel } = useOpenEmailThreadInSidePanel();
const visibility = thread.visibility;
@@ -120,7 +122,7 @@ export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => {
}
type="rounded"
/>
{thread?.lastTwoParticipants?.[0] && (
{isDefined(thread?.lastTwoParticipants?.[0]) && (
<StyledAvatarWrapper>
<Avatar
avatarUrl={thread.lastTwoParticipants[0].avatarUrl}
@@ -90,7 +90,7 @@ export const EmailsCard = () => {
if (!firstQueryLoading && !timelineThreads?.length) {
return (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="emptyInbox" />
@@ -170,7 +170,7 @@ export const AttachmentList = ({
return (
<>
{attachmentsWithFile && attachmentsWithFile.length > 0 && (
{attachmentsWithFile.length > 0 && (
<StyledContainer>
<StyledTitleBar>
<StyledTitle>
@@ -75,13 +75,13 @@ export const DropZone = ({
return (
<StyledContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...getRootProps()}
>
{isDragActive && (
<>
<input
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...getInputProps()}
/>
<StyledUploadIconContainer>
@@ -70,7 +70,7 @@ export const FilesCard = () => {
inputFileRef?.current?.click?.();
};
const isAttachmentsEmpty = !attachments || attachments.length === 0;
const isAttachmentsEmpty = attachments.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
@@ -104,7 +104,7 @@ export const FilesCard = () => {
/>
) : (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noFile" />
@@ -24,7 +24,7 @@ describe.skip('downloadFile', () => {
);
expect(link).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-ignore
expect(link?.style?.display).toBe('none');
@@ -113,18 +113,18 @@ export const useUpdateActivityTargetFromCell = ({
);
if (isDefined(existingActivityTarget)) {
activityTargetsAfterUpdate = activityTargetWithTargetRecords
.map((activityTarget) => {
activityTargetsAfterUpdate = activityTargetWithTargetRecords.flatMap(
(activityTarget) => {
if (
activityTarget.targetObject.id === morphItem.recordId &&
!morphItem.isSelected
) {
return undefined;
return [];
}
return activityTarget.activityTarget;
})
.filter(isDefined);
return [activityTarget.activityTarget];
},
);
if (!morphItem.isSelected) {
await deleteOneActivityTarget(
@@ -56,7 +56,7 @@ export const NoteList = ({
button,
}: NoteListProps) => (
<>
{notes && notes.length > 0 && (
{notes.length > 0 && (
<StyledContainer>
<StyledTitleBar>
<StyledTitle>
@@ -43,7 +43,7 @@ export const NotesCard = () => {
activityObjectNameSingular: CoreObjectNameSingular.Note,
});
const isNotesEmpty = !notes || notes.length === 0;
const isNotesEmpty = notes.length === 0;
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: targetRecord.targetObjectNameSingular,
@@ -62,7 +62,7 @@ export const NotesCard = () => {
if (isNotesEmpty) {
return (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noNote" />
@@ -72,7 +72,7 @@ export const TaskGroups = ({ targetableObject }: TaskGroupsProps) => {
if (isTasksEmpty) {
return (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="noTask" />
@@ -45,7 +45,7 @@ const StyledCount = styled.span`
export const TaskList = ({ title, tasks, button }: TaskListProps) => (
<>
{tasks && tasks.length > 0 && (
{tasks.length > 0 && (
<StyledContainer>
<StyledTitleBar>
{title && (
@@ -52,7 +52,7 @@ export const TimelineCard = () => {
useTimelineActivities(targetRecord);
const isTimelineActivitiesEmpty =
!timelineActivities || timelineActivities.length === 0;
timelineActivities.length === 0;
if (loading === true) {
return <SkeletonLoader withSubSections />;
@@ -61,7 +61,7 @@ export const TimelineCard = () => {
if (isTimelineActivitiesEmpty) {
const placeholderContent = (
<AnimatedPlaceholderEmptyContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
>
<AnimatedPlaceholder type="emptyTimeline" />
@@ -1,4 +1,5 @@
import { EventFieldDiff } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiff';
import { isDefined } from 'twenty-shared/utils';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
@@ -19,7 +20,7 @@ export const EventFieldDiffContainer = ({
}: EventFieldDiffContainerProps) => {
const fieldMetadataItem = fieldMetadataItemMap[diffKey];
if (!fieldMetadataItem) {
if (!isDefined(fieldMetadataItem)) {
throw new Error(
`Cannot find field metadata item for field name ${diffKey} on object ${mainObjectMetadataItem.nameSingular}`,
);
@@ -31,13 +31,11 @@ export const getActivityTargetObjectRecords = ({
const targets = activityTargets
? activityTargets
: activityRecord &&
'noteTargets' in activityRecord &&
activityRecord.noteTargets
: 'noteTargets' in activityRecord &&
isDefined(activityRecord.noteTargets)
? activityRecord.noteTargets
: activityRecord &&
'taskTargets' in activityRecord &&
activityRecord.taskTargets
: 'taskTargets' in activityRecord &&
isDefined(activityRecord.taskTargets)
? activityRecord.taskTargets
: [];
@@ -3,6 +3,7 @@ import { ShimmeringText } from '@/ai/components/ShimmeringText';
import { styled } from '@linaria/react';
import { useContext, useState } from 'react';
import { type DataMessagePart } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown, IconChevronUp, IconCpu } from 'twenty-ui/display';
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
@@ -65,7 +66,7 @@ export const RoutingStatusDisplay = ({
const [isExpanded, setIsExpanded] = useState(false);
const isLoading = data.state === 'loading';
const isDebugMode = process.env.IS_DEBUG_MODE === 'true';
const isExpandable = isDebugMode && data.state === 'routed' && data.debug;
const isExpandable = isDebugMode && data.state === 'routed' && isDefined(data.debug);
if (data.state === 'error') {
return null;
@@ -22,7 +22,7 @@ import { useUpdateEffect } from '~/hooks/useUpdateEffect';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
// eslint-disable-next-line twenty/no-state-useref
// oxlint-disable-next-line twenty/no-state-useref
const apolloRef = useRef<ApolloFactory<NormalizedCacheObject> | null>(null);
const navigate = useNavigate();
@@ -108,7 +108,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
});
return apolloRef.current.getClient();
// eslint-disable-next-line react-hooks/exhaustive-deps
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [
setTokenPair,
setCurrentUser,
@@ -56,10 +56,10 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({
const updatedGroupByConnections = cachedGroupByConnections.map(
(groupConnection) => {
const groupByDimensionValues =
readField('groupByDimensionValues', groupConnection) || [];
readField('groupByDimensionValues', groupConnection) ?? [];
const cachedEdges =
readField<RecordGqlRefEdge[]>('edges', groupConnection) || [];
readField<RecordGqlRefEdge[]>('edges', groupConnection) ?? [];
const cachedTotalCount = readField<number | undefined>(
'totalCount',
@@ -166,7 +166,7 @@ export const triggerUpdateGroupByQueriesOptimisticEffect = ({
const dimensionKey = recordDimensionValues.join('|');
const dimensionExists = updatedGroupByConnections.some((conn) => {
const connDimensionValues =
readField('groupByDimensionValues', conn) || [];
readField('groupByDimensionValues', conn) ?? [];
return (
Array.isArray(connDimensionValues) &&
connDimensionValues.join('|') === dimensionKey
@@ -165,7 +165,7 @@ export const triggerCreateRecordsOptimisticEffect = ({
},
);
if (recordToCreateReference && !recordAlreadyInCache) {
if (isDefined(recordToCreateReference) && !recordAlreadyInCache) {
const cursor = encodeCursor(recordToCreate);
const edge = {
@@ -94,7 +94,7 @@ export const triggerDestroyRecordsOptimisticEffect = ({
cachedEdges?.filter((cachedEdge) => {
const nodeId = readField<string>('id', cachedEdge.node);
return nodeId && !recordIdsToDestroy.includes(nodeId);
return isDefined(nodeId) && !recordIdsToDestroy.includes(nodeId);
}) || [];
if (nextCachedEdges.length === cachedEdges?.length)
@@ -139,7 +139,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
attempts: {
max: 2,
retryIf: (error) => {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log('retryIf error from retryLink', error);
if (this.isAuthenticationError(error)) {
return false;
@@ -163,14 +163,14 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
renewalPromise = renewToken(graphqlUri, getTokenPair())
.then((tokens) => {
if (isDefined(tokens)) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log('setTokenPair from handleTokenRenewal');
onTokenPairChange?.(tokens);
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
}
})
.catch(() => {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log(
'Failed to renew token, triggering unauthenticated error from handleTokenRenewal',
);
@@ -234,7 +234,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
});
})
.catch((sentryError) => {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(
'Failed to capture GraphQL error with Sentry:',
sentryError,
@@ -248,7 +248,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
onErrorCb?.(graphQLErrors);
for (const graphQLError of graphQLErrors) {
if (graphQLError.message === 'Unauthorized') {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log('Unauthorized, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
@@ -262,7 +262,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
return;
}
case 'UNAUTHENTICATED': {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log('UNAUTHENTICATED, triggering token renewal');
return handleTokenRenewal(operation, forward);
}
@@ -294,7 +294,7 @@ export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
this.isRestOperation(operation) &&
this.isAuthenticationError(networkError as ServerError)
) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log(
'Authentication error, triggering token renewal from errorLink',
);
@@ -6,7 +6,7 @@ export const createCaptchaRefreshLink = (
return new ApolloLink((operation, forward) => {
const { variables } = operation;
const hasCaptchaToken = variables && 'captchaToken' in variables;
const hasCaptchaToken = variables != null && 'captchaToken' in variables;
return forward(operation).map((response) => {
if (hasCaptchaToken) {
@@ -1,15 +1,15 @@
import { OperationType } from '@/apollo/types/operation-type';
const operationTypeColors = {
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
query: '#03A9F4',
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
mutation: '#61A600',
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
subscription: '#61A600',
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
error: '#F51818',
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
default: '#61A600',
};
@@ -7,7 +7,7 @@ export const getTokenPair = (): AuthTokenPair | undefined => {
const stringTokenPair = cookieStorage.getItem('tokenPair');
if (!isDefined(stringTokenPair)) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.log('tokenPair is undefined');
return undefined;
@@ -40,7 +40,7 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
console.groupCollapsed(...titleArgs);
if (variables && Object.keys(variables).length !== 0) {
if (Object.keys(variables).length !== 0) {
logDebug('VARIABLES', variables);
}
@@ -70,7 +70,7 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
errors.forEach((err: any) => {
logDebug(
`%c${err.message}`,
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
'color: #F51818; font-weight: lighter',
);
});
@@ -78,7 +78,7 @@ export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
logDebug('HEADERS: ', headers);
if (variables && Object.keys(variables).length !== 0) {
if (Object.keys(variables).length !== 0) {
logDebug('VARIABLES', variables);
}
@@ -107,13 +107,13 @@ export class StreamingRestLink extends ApolloLink {
try {
const definition = operation.query.definitions[0];
if (!definition || definition.kind !== 'OperationDefinition') {
if (!isDefined(definition) || definition.kind !== 'OperationDefinition') {
return null;
}
if (
!definition.selectionSet ||
!definition.selectionSet.selections ||
!isDefined(definition.selectionSet) ||
!isDefined(definition.selectionSet.selections) ||
definition.selectionSet.selections.length === 0
) {
return null;
@@ -121,7 +121,7 @@ export class StreamingRestLink extends ApolloLink {
const selection = definition.selectionSet.selections[0];
if (!selection || !isDefined(selection.directives)) {
if (!isDefined(selection) || !isDefined(selection.directives)) {
return null;
}
@@ -34,7 +34,7 @@ export const useInitializeQueryParamState = () => {
);
}
} catch (error) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(
'Failed to parse billingCheckoutSession from URL',
error,
@@ -69,7 +69,7 @@ export const TwoFactorAuthenticationSetupEffect = () => {
handleTwoFactorAuthenticationProvisioningInitiation();
// Two factor authentication provisioning only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>;
@@ -121,7 +121,7 @@ export const VerifyEmailEffect = () => {
verifyEmailToken();
// Verify email only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [clientConfigApiStatus.isLoadedOnce]);
if (isError) {
@@ -32,7 +32,7 @@ export const VerifyLoginTokenEffect = () => {
navigate(AppPath.SignInUp);
}
// Verify only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
// oxlint-disable-next-line react-hooks/exhaustive-deps
}, [clientConfigLoaded]);
return <></>;
@@ -55,7 +55,7 @@ const StyledSeparator = styled.span`
`;
export const FooterNote = () => {
const isOnAWorkspace = useIsCurrentLocationOnAWorkspace();
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const { shouldOfferBypass, shouldUseBypass, enableBypass } =
useWorkspaceBypass();
@@ -244,7 +244,7 @@ export const SignInUpGlobalScopeForm = () => {
{(authProviders.google || authProviders.microsoft) && (
<HorizontalSeparator />
)}
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
{/* oxlint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>
<SignInUpWithCredentials isGlobalScope />
</FormProvider>
@@ -66,7 +66,7 @@ export const SignInUpWorkspaceScopeForm = () => {
<HorizontalSeparator />
) : null}
{providers.password && (
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
<FormProvider {...form}>
<SignInUpWithCredentials />
</FormProvider>
@@ -231,7 +231,7 @@ export const SignInUpTOTPVerification = () => {
<Trans>Paste the code below</Trans>
</StyledTextContainer>
<StyledMainContentContainer>
{/* // eslint-disable-next-line react/jsx-props-no-spreading */}
{/* // oxlint-disable-next-line react/jsx-props-no-spreading */}
<Controller
name="otp"
control={form.control}
@@ -247,7 +247,7 @@ export const SignInUpTOTPVerification = () => {
{slots.slice(0, 3).map((slot, idx) => (
<Slot
key={idx}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...slot}
/>
))}
@@ -259,7 +259,7 @@ export const SignInUpTOTPVerification = () => {
{slots.slice(3).map((slot, idx) => (
<Slot
key={idx}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...slot}
/>
))}
@@ -85,7 +85,7 @@ export const SignInUpWorkspaceScopeFormEffect = () => {
signInUpStep === SignInUpStep.Init &&
!workspaceAuthProviders.google &&
!workspaceAuthProviders.microsoft &&
!workspaceAuthProviders.sso
workspaceAuthProviders.sso.length === 0
) {
continueWithEmail();
return;
@@ -30,7 +30,7 @@ export const useWorkspaceFromInviteHash = () => {
onCompleted: (data) => {
if (
isDefined(currentWorkspace) &&
data?.findWorkspaceFromInviteHash &&
isDefined(data?.findWorkspaceFromInviteHash) &&
currentWorkspace.id === data.findWorkspaceFromInviteHash.id
) {
const workspaceDisplayName =
@@ -93,10 +93,11 @@ export const MeteredPriceSelector = ({
);
const isChanged =
selectedPriceId && selectedPriceId !== currentMeteredPrice?.stripePriceId;
isDefined(selectedPriceId) &&
selectedPriceId !== currentMeteredPrice?.stripePriceId;
const isUpgrade = () => {
if (!isChanged || !selectedPrice || !currentMeteredPrice) return false;
if (!isChanged || !isDefined(selectedPrice) || !isDefined(currentMeteredPrice)) return false;
return (
(selectedPrice.tiers as BillingPriceTiers)[0].flatAmount >
(currentMeteredPrice.tiers as BillingPriceTiers)[0].flatAmount
@@ -56,7 +56,7 @@ export const FileBlock = createReactBlockSpec(
},
{
render: ({ block, editor }) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
// oxlint-disable-next-line react-hooks/rules-of-hooks
const inputFileRef = useRef<HTMLInputElement>(null);
const handleUploadAttachment = async (file: File) => {
@@ -23,7 +23,7 @@ interface BlockEditorProps {
readonly?: boolean;
}
// eslint-disable-next-line twenty/no-hardcoded-colors
// oxlint-disable-next-line twenty/no-hardcoded-colors
const StyledEditor = styled.div`
width: 100%;
@@ -12,9 +12,9 @@ export const parseInitialBlocknote = (
try {
parsedBody = JSON.parse(blocknote);
} catch {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.warn(logContext ?? `Failed to parse blocknote body`);
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.warn(blocknote);
}
@@ -151,7 +151,7 @@ export const useClientConfig = (): UseClientConfigResult => {
magicLink: false,
sso: clientConfig.authProviders.sso,
});
setAiModels(clientConfig.aiModels || []);
setAiModels(clientConfig.aiModels ?? []);
setIsAnalyticsEnabled(clientConfig.analyticsEnabled);
setIsDeveloperDefaultSignInPrefilled(clientConfig.signInPrefilled);
setIsMultiWorkspaceEnabled(clientConfig.isMultiWorkspaceEnabled);
@@ -4,6 +4,7 @@ import { IconArrowUpRight, type IconComponent } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { useCommandMenuOnItemClick } from '@/command-menu/hooks/useCommandMenuOnItemClick';
import { isDefined } from 'twenty-shared/utils';
import { isSelectedItemIdComponentFamilyState } from '@/ui/layout/selectable-list/states/isSelectedItemIdComponentFamilyState';
import { useAtomComponentFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateValue';
@@ -51,8 +52,8 @@ export const CommandMenuItem = ({
return (
<MenuItem
withIconContainer={!LeftComponent}
LeftIcon={LeftComponent ? undefined : Icon}
withIconContainer={!isDefined(LeftComponent)}
LeftIcon={isDefined(LeftComponent) ? undefined : Icon}
LeftComponent={LeftComponent}
text={label}
contextualText={description}
@@ -14,7 +14,7 @@ export const CommandMenuItemToggle = (props: CommandMenuItemToggleProps) => {
return (
<MenuItemToggle
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...props}
focused={isSelectedItemId}
withIconContainer
@@ -158,13 +158,13 @@ describe('useSetGlobalCommandMenuContext', () => {
const { setGlobalCommandMenuContext } =
useSetGlobalCommandMenuContext();
// eslint-disable-next-line twenty/matching-state-variable
// oxlint-disable-next-line twenty/matching-state-variable
const previousTargetedRecordsRule = useAtomComponentStateValue(
contextStoreTargetedRecordsRuleComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
);
// eslint-disable-next-line twenty/matching-state-variable
// oxlint-disable-next-line twenty/matching-state-variable
const previousNumberOfSelectedRecords = useAtomComponentStateValue(
contextStoreNumberOfSelectedRecordsComponentState,
SIDE_PANEL_PREVIOUS_COMPONENT_INSTANCE_ID,
@@ -1,4 +1,5 @@
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { isDefined } from 'twenty-shared/utils';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
@@ -14,7 +15,7 @@ export const useContextStoreObjectMetadataItemOrThrow = (
objectId: contextStoreCurrentObjectMetadataItemId ?? '',
});
if (!objectMetadataItem) {
if (!isDefined(objectMetadataItem)) {
throw new Error('Object metadata item is not set in context store');
}
@@ -54,7 +54,7 @@ export const useGetPublicWorkspaceDataByDomain = () => {
if (isWorkspaceNotFoundError) {
redirectToDefaultDomain();
} else {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error(error);
}
},
@@ -1,3 +1,4 @@
import { isDefined } from 'twenty-shared/utils';
import { type ObjectOptionsDropdownContextValue } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { type RecordBoardColumnHeaderAggregateDropdownContextValue } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderAggregateDropdownContext';
import { type RecordTableColumnAggregateFooterDropdownContextValue } from '@/object-record/record-table/record-table-footer/components/RecordTableColumnAggregateFooterDropdownContext';
@@ -22,7 +23,7 @@ export const useDropdownContextStateManagement = <
}) => {
const dropdownContext = useContext(context);
if (!dropdownContext) {
if (!isDefined(dropdownContext)) {
throw new Error(
`useDropdownContextStateManagement must be used within a context provider (${context.Provider.name})`,
);
@@ -33,7 +33,7 @@ export const AppErrorBoundary = ({
return scope;
});
} catch (sentryError) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error('Failed to capture exception with Sentry:', sentryError);
}
@@ -42,7 +42,7 @@ export const PromiseRejectionEffect = () => {
return scope;
});
} catch (sentryError) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error('Failed to capture exception with Sentry:', sentryError);
}
},
@@ -58,7 +58,7 @@ export const SentryInitEffect = () => {
setIsSentryInitialized(true);
} catch (error) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error('Failed to initialize Sentry:', error);
} finally {
setIsSentryInitializing(false);
@@ -82,7 +82,7 @@ export const SentryInitEffect = () => {
});
setIsSentryUserDefined(true);
} catch (error) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error('Failed to set Sentry user:', error);
}
} else if (!isDefined(currentUser) && isSentryInitialized) {
@@ -90,7 +90,7 @@ export const SentryInitEffect = () => {
const { setUser } = await import('@sentry/react');
setUser(null);
} catch (error) {
// eslint-disable-next-line no-console
// oxlint-disable-next-line no-console
console.error('Failed to clear Sentry user:', error);
}
}
@@ -209,7 +209,7 @@ export const CurrentWorkspaceMemberFavorites = ({
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
// TODO: (Drag Drop Bug) Adding bottom margin to ensure drag-to-last-position works. Need to find better solution that doesn't affect spacing.
// Issue: Without margin, dragging to last position triggers next folder drop area
@@ -49,9 +49,9 @@ export const CurrentWorkspaceMemberFavoritesFolders = () => {
}
if (
(!favorites || favorites.length === 0) &&
favorites.length === 0 &&
!isFavoriteFolderCreating &&
(!favoritesByFolder || favoritesByFolder.length === 0)
favoritesByFolder.length === 0
) {
return null;
}
@@ -59,7 +59,7 @@ export const FavoritesDroppable = ({
>
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{children}
@@ -34,7 +34,7 @@ export const FavoritesFolderContent = ({
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{favorites.map((favorite, index) => (
@@ -71,9 +71,10 @@ export const FavoriteFolderPickerEffect = ({
const checkedFolderIds = favorites
.filter(
(favorite) =>
favorite.recordId === targetId && favorite.forWorkspaceMemberId,
favorite.recordId === targetId &&
isDefined(favorite.forWorkspaceMemberId),
)
.map((favorite) => favorite.favoriteFolderId || 'no-folder');
.map((favorite) => favorite.favoriteFolderId ?? 'no-folder');
setFavoriteFolderPickerChecked(checkedFolderIds);
}, [favorites, setFavoriteFolderPickerChecked, record?.id]);
@@ -33,7 +33,9 @@ export const useCreateFavorite = () => {
const relevantFavorites = favoriteFolderId
? favorites.filter((fav) => fav.favoriteFolderId === favoriteFolderId)
: favorites.filter(
(fav) => !fav.favoriteFolderId && fav.forWorkspaceMemberId,
(fav) =>
!isDefined(fav.favoriteFolderId) &&
isDefined(fav.forWorkspaceMemberId),
);
const maxPosition = Math.max(
@@ -1,4 +1,5 @@
import { type AttachmentFileCategory } from '@/activities/files/types/AttachmentFileCategory';
import { isDefined } from 'twenty-shared/utils';
import { useFileIconColors } from '@/file/hooks/useFileIconColors';
import { IconMapping } from '@/file/utils/fileIconMappings';
import { styled } from '@linaria/react';
@@ -46,7 +47,9 @@ export const FileIcon = ({
<StyledIconContainer
background={iconColors[fileCategory] ?? theme.color.gray}
>
{Icon && <Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />}
{isDefined(Icon) && (
<Icon size={theme.icon.size.sm} stroke={theme.icon.stroke.sm} />
)}
</StyledIconContainer>
);
};
@@ -50,7 +50,8 @@ export const useMentionSearch = () => {
},
});
const searchRecords = data?.search.edges.map((edge) => edge.node) || [];
const searchRecords =
data?.search.edges.map((edge) => edge.node) ?? [];
return searchRecords.map((searchRecord) => ({
recordId: searchRecord.recordId,
@@ -18,7 +18,7 @@ export const IsAppMetadataReadyEffect = () => {
metadataStoreState,
'objectMetadataItems',
);
// eslint-disable-next-line twenty/matching-state-variable
// oxlint-disable-next-line twenty/matching-state-variable
const metadataStoreViews = useAtomFamilyStateValue(
metadataStoreState,
'views',
@@ -50,10 +50,9 @@ export const CurrentWorkspaceMemberNavigationMenuItemFolders = () => {
}
if (
(!navigationMenuItemsSorted || navigationMenuItemsSorted.length === 0) &&
navigationMenuItemsSorted.length === 0 &&
!isNavigationMenuItemFolderCreating &&
(!userNavigationMenuItemsByFolder ||
userNavigationMenuItemsByFolder.length === 0)
userNavigationMenuItemsByFolder.length === 0
) {
return null;
}
@@ -258,7 +258,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{folder.navigationMenuItems.map((navigationMenuItem, index) => (
@@ -65,7 +65,7 @@ export const NavigationMenuItemDroppable = ({
>
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{children}
@@ -33,7 +33,7 @@ export const NavigationMenuItemFolderContent = ({
{(provided) => (
<div
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => (
@@ -44,9 +44,9 @@ export const WorkspaceNavigationMenuItemFolderDragClone = ({
return (
<div
ref={draggableProvided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.dragHandleProps}
style={{
...draggableProvided.draggableProps.style,
@@ -242,7 +242,7 @@ export const WorkspaceNavigationMenuItemsFolder = ({
<StyledFolderDroppableContent
ref={provided.innerRef}
$compact={isEditMode || navigationMenuItems.length === 0}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{navigationMenuItems.map((navigationMenuItem, index) => {
@@ -27,7 +27,8 @@ export const useCreateNavigationMenuItem = () => {
const relevantItems = folderId
? navigationMenuItems.filter((item) => item.folderId === folderId)
: navigationMenuItems.filter(
(item) => !item.folderId && item.userWorkspaceId,
(item) =>
!isDefined(item.folderId) && isDefined(item.userWorkspaceId),
);
const maxPosition = Math.max(
@@ -59,7 +60,8 @@ export const useCreateNavigationMenuItem = () => {
const relevantItems = folderId
? navigationMenuItems.filter((item) => item.folderId === folderId)
: navigationMenuItems.filter(
(item) => !item.folderId && item.userWorkspaceId,
(item) =>
!isDefined(item.folderId) && isDefined(item.userWorkspaceId),
);
const maxPosition = Math.max(
@@ -13,9 +13,9 @@ export const computeInsertIndexAndPosition = (
);
const insertRef = itemsInFolder[targetIndex];
const lastInFolder = itemsInFolder[itemsInFolder.length - 1];
const flatIndex = insertRef
const flatIndex = isDefined(insertRef)
? currentDraft.indexOf(insertRef)
: lastInFolder
: isDefined(lastInFolder)
? currentDraft.indexOf(lastInFolder) + 1
: currentDraft.length;
const prevPosition = itemsInFolder[targetIndex - 1]?.position ?? 0;
@@ -178,7 +178,7 @@ export const NavigationDrawerSectionForWorkspaceItems = ({
{(provided) => (
<StyledWorkspaceDroppableList
ref={provided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...provided.droppableProps}
>
{filteredItems.map((item, index) => {
@@ -1,4 +1,5 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { isDefined } from 'twenty-shared/utils';
import { isActiveFieldMetadataItem } from '@/object-metadata/utils/isActiveFieldMetadataItem';
import { useMemo } from 'react';
@@ -9,7 +10,7 @@ export const useActiveFieldMetadataItems = ({
}) => {
const activeFieldMetadataItems = useMemo(
() =>
objectMetadataItem
isDefined(objectMetadataItem)
? objectMetadataItem.readableFields.filter(
({ id, isActive, isSystem, name }) =>
isActiveFieldMetadataItem({
@@ -1,7 +1,6 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { isDefined } from 'twenty-shared/utils';
export const objectMetadataItemsBySingularNameSelector =
createAtomFamilySelector<ObjectMetadataItem[], string[]>({
@@ -11,14 +10,13 @@ export const objectMetadataItemsBySingularNameSelector =
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
return objectNameSingulars
.map(
(objectNameSingular) =>
objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
) ?? null,
)
.filter(isDefined);
return objectNameSingulars.flatMap((objectNameSingular) => {
const found = objectMetadataItems.find(
(objectMetadataItem) =>
objectMetadataItem.nameSingular === objectNameSingular,
);
return found !== undefined ? [found] : [];
});
},
});
@@ -48,7 +48,7 @@ export const useAggregateRecords = <T extends AggregateRecordsData>({
const { data, loading, error } = useQuery<RecordGqlOperationFindManyResult>(
aggregateQuery,
{
skip: skip || !objectMetadataItem || !hasReadPermission,
skip: skip || !isDefined(objectMetadataItem) || !hasReadPermission,
variables: {
filter,
},
@@ -142,26 +142,30 @@ export const useCreateManyRecords = <
}
});
const recordsCreatedInCache = recordOptimisticRecordsInput
.map((recordToCreate) =>
createOneRecordInCache({
const recordsCreatedInCache = recordOptimisticRecordsInput.flatMap(
(recordToCreate) => {
const created = createOneRecordInCache({
...recordToCreate,
__typename: getObjectTypename(objectMetadataItem.nameSingular),
}),
)
.filter(isDefined);
});
return created !== undefined && created !== null ? [created] : [];
},
);
if (recordsCreatedInCache.length > 0) {
const recordNodeCreatedInCache = recordsCreatedInCache
.map((record) =>
getRecordNodeFromRecord({
const recordNodeCreatedInCache = recordsCreatedInCache.flatMap(
(record) => {
const node = getRecordNodeFromRecord({
objectMetadataItem,
objectMetadataItems,
record: record,
computeReferences: false,
}),
)
.filter(isDefined);
});
return node !== undefined && node !== null ? [node] : [];
},
);
triggerCreateRecordsOptimisticEffect({
cache: apolloCoreClient.cache,
@@ -162,7 +162,7 @@ export const useCreateOneRecord = <
},
})
.catch((error: Error) => {
if (!recordCreatedInCache) {
if (!isDefined(recordCreatedInCache)) {
throw error;
}
@@ -1,4 +1,5 @@
import { useQuery } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { useMemo } from 'react';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
@@ -70,7 +71,7 @@ export const useFindDuplicateRecords = <T extends ObjectRecord = ObjectRecord>({
const results = useMemo(
() =>
objectResults?.map((result: RecordGqlConnectionEdgesRequired) => {
return result
return isDefined(result)
? (getRecordsFromRecordConnection({
recordConnection: result,
}) as T[])
@@ -1,4 +1,5 @@
import { useQuery, type WatchQueryFetchPolicy } from '@apollo/client';
import { isDefined } from 'twenty-shared/utils';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
@@ -89,7 +90,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({
const { data, loading, error, fetchMore, refetch } =
useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, {
skip: skip || !objectMetadataItem || !hasReadPermission,
skip: skip || !isDefined(objectMetadataItem) || !hasReadPermission,
variables: {
filter: withSoftDeleteFilter,
orderBy,
@@ -56,7 +56,7 @@ export const useFindOneRecord = <T extends ObjectRecord = ObjectRecord>({
const { data, loading, error, refetch } = useQuery<{
[nameSingular: string]: RecordGqlNode;
}>(findOneRecordQuery, {
skip: !objectMetadataItem || !objectRecordId || skip || !hasReadPermission,
skip: !isDefined(objectMetadataItem) || !objectRecordId || skip || !hasReadPermission,
variables: { objectRecordId },
client: apolloCoreClient,
onCompleted: (data) => {
@@ -102,12 +102,12 @@ export const ObjectFilterDropdownDateInput = () => {
: null;
const relativeDate =
resolvedValue && typeof resolvedValue === 'object'
isDefined(resolvedValue) && typeof resolvedValue === 'object'
? resolvedValue
: undefined;
const safePlainDateValue: string | undefined =
resolvedValue && typeof resolvedValue === 'string'
isDefined(resolvedValue) && typeof resolvedValue === 'string'
? resolvedValue
: undefined;
@@ -11,7 +11,7 @@ export const getRelativeDateDisplayValue = (
relativeDate: RelativeDateFilter,
shouldDisplayTimeZoneAbbreviation?: boolean,
) => {
if (!relativeDate) return '';
if (!isDefined(relativeDate)) return '';
const { direction, amount, unit } = relativeDate;
const directionFormatted = capitalize(direction.toLowerCase());
@@ -1,11 +1,12 @@
import { useDropdownContextStateManagement } from '@/dropdown-context-state-management/hooks/useDropdownContextStateManagement';
import { ObjectOptionsDropdownContext } from '@/object-record/object-options-dropdown/states/contexts/ObjectOptionsDropdownContext';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
export const useObjectOptionsDropdown = () => {
const context = useContext(ObjectOptionsDropdownContext);
if (!context) {
if (!isDefined(context)) {
throw new Error('useObjectOptionsDropdown must be used within a context');
}
@@ -85,7 +85,7 @@ export const useObjectOptionsForBoard = ({
recordIndexFieldDefinitionsByKey[fieldMetadataId];
return {
...(existingBoardField || availableColumnDefinition),
...(existingBoardField ?? availableColumnDefinition),
isVisible: false,
};
}),
@@ -53,9 +53,9 @@ export const RecordBoardCardDraggableContainer = ({
<StyledDraggableContainer
id={`record-board-card-${columnIndex}-${rowIndex}`}
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
data-selectable-id={recordId}
data-select-disable
@@ -47,7 +47,7 @@ export const RecordBoardColumnCardsContainer = ({
return (
<StyledColumnCardsContainer
ref={droppableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided?.droppableProps}
>
{recordIndexRecordIdsByGroup.map((recordId, index) => (
@@ -68,7 +68,7 @@ export const RecordBoardColumnCardsContainer = ({
{(draggableProvided) => (
<div
ref={draggableProvided.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided.draggableProps}
></div>
)}
@@ -3,6 +3,7 @@ import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDrop
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { styled } from '@linaria/react';
import { type Nullable } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/components';
import { AppTooltip, TooltipDelay } from 'twenty-ui/display';
@@ -36,7 +37,7 @@ export const RecordBoardColumnHeaderAggregateDropdownButton = ({
<>
<StyledTagContainer>
<Tag
text={value ? value.toString() : '-'}
text={isDefined(value) ? value.toString() : '-'}
color="transparent"
weight="regular"
/>
@@ -146,7 +146,7 @@ export const RecordCalendarMonthBodyDay = ({
<Droppable droppableId={dayKey}>
{(droppableProvided, droppableSnapshot) => (
<StyledCardsContainer
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...droppableProvided.droppableProps}
ref={droppableProvided.innerRef}
isDraggedOver={droppableSnapshot.isDraggingOver}
@@ -77,9 +77,9 @@ export const RecordCalendarCardDraggableContainer = ({
<StyledDraggableContainer
id={`record-calendar-card-${recordId}`}
ref={draggableProvided?.innerRef}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.dragHandleProps}
// eslint-disable-next-line react/jsx-props-no-spreading
// oxlint-disable-next-line react/jsx-props-no-spreading
{...draggableProvided?.draggableProps}
data-selectable-id={recordId}
>
@@ -1,6 +1,7 @@
import { type DropResult } from '@hello-pangea/dnd';
import { useStore } from 'jotai';
import { useCallback, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { processGroupDrop } from '@/object-record/record-drag/utils/processGroupDrop';
@@ -22,7 +23,7 @@ export const useProcessBoardCardDrop = () => {
const processBoardCardDrop = useCallback(
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
if (!selectFieldMetadataItem) return;
if (!isDefined(selectFieldMetadataItem)) return;
processGroupDrop({
groupDropResult: boardCardDropResult,
@@ -40,7 +40,11 @@ export const RecordDetailDuplicatesSection = ({
objectRecordIds: duplicateRecordIds,
});
if (!queryResults || !queryResults[0] || queryResults[0].length === 0)
if (
!isDefined(queryResults) ||
!isDefined(queryResults[0]) ||
queryResults[0].length === 0
)
return null;
return (
@@ -49,7 +49,7 @@ export const RecordDetailRelationSectionDropdownToMany = ({
objectMetadataItems,
});
if (!fieldMetadataItem || !objectMetadataItem) {
if (!isDefined(fieldMetadataItem) || !isDefined(objectMetadataItem)) {
throw new CustomError(
'Field metadata item or object metadata item not found',
'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND',
@@ -25,7 +25,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
import { CustomError } from 'twenty-shared/utils';
import { CustomError, isDefined } from 'twenty-shared/utils';
import { IconForbid, IconPencil } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
@@ -56,7 +56,7 @@ export const RecordDetailRelationSectionDropdownToOne = ({
objectMetadataItems,
});
if (!fieldMetadataItem || !objectMetadataItem) {
if (!isDefined(fieldMetadataItem) || !isDefined(objectMetadataItem)) {
throw new CustomError(
'Field metadata item or object metadata item not found',
'FIELD_METADATA_ITEM_OR_OBJECT_METADATA_ITEM_NOT_FOUND',
@@ -1,7 +1,7 @@
import { type FieldMetadataItemRelation } from '@/object-metadata/types/FieldMetadataItemRelation';
import { recordStoreMorphOneToManyValueWithObjectNameFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreMorphOneToManyValueWithObjectNameFamilySelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { CustomError, isNonEmptyArray } from 'twenty-shared/utils';
import { CustomError, isDefined, isNonEmptyArray } from 'twenty-shared/utils';
export const useGetMorphRelationRelatedRecordsWithObjectNameSingular = ({
recordId,
@@ -28,7 +28,7 @@ export const useGetMorphRelationRelatedRecordsWithObjectNameSingular = ({
...recordWithObjectNameSingular,
value: Array.isArray(recordWithObjectNameSingular.value)
? recordWithObjectNameSingular.value
: recordWithObjectNameSingular.value
: isDefined(recordWithObjectNameSingular.value)
? [recordWithObjectNameSingular.value]
: [],
}))
@@ -9,6 +9,7 @@ import { type VariablePickerComponent } from '@/object-record/record-field/ui/fo
import { type FieldPhonesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
type FormPhoneFieldInputProps = {
label?: string;
@@ -39,7 +40,7 @@ export const FormPhoneFieldInput = ({
onChange({
primaryPhoneCountryCode: defaultValue?.primaryPhoneCountryCode ?? '',
primaryPhoneCallingCode: defaultValue?.primaryPhoneCallingCode ?? '',
primaryPhoneNumber: number ? `${number}` : '',
primaryPhoneNumber: isDefined(number) ? `${number}` : '',
});
};
@@ -8,6 +8,7 @@ import { VariableChipStandalone } from '@/object-record/record-field/ui/form-typ
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import { t } from '@lingui/core/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -43,7 +44,6 @@ export const FormSingleRecordFieldChip = ({
disabled,
}: FormSingleRecordFieldChipProps) => {
if (
!!draftValue &&
draftValue.type === 'variable' &&
isStandaloneVariableString(draftValue.value)
) {
@@ -56,7 +56,7 @@ export const FormSingleRecordFieldChip = ({
);
}
if (!!draftValue && draftValue.type === 'static' && !!selectedRecord) {
if (draftValue.type === 'static' && isDefined(selectedRecord)) {
return (
<StyledRecordChipContainer>
<RecordChip
@@ -88,7 +88,7 @@ export const FormSingleRecordPicker = ({
}
: {
type: 'static',
value: defaultValue || '',
value: (defaultValue as string | undefined) ?? '',
};
if (objectNameSingulars.length === 0) {
@@ -3,6 +3,7 @@ import { useMultiSelectFieldDisplay } from '@/object-record/record-field/ui/meta
import { MultiSelectDisplay } from '@/ui/field/display/components/MultiSelectDisplay';
import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList';
import { Tag } from 'twenty-ui/components';
import { isDefined } from 'twenty-shared/utils';
export const MultiSelectFieldDisplay = () => {
const { fieldValue, fieldDefinition } = useMultiSelectFieldDisplay();
@@ -15,7 +16,7 @@ export const MultiSelectFieldDisplay = () => {
)
: [];
if (!selectedOptions) return null;
if (!isDefined(selectedOptions)) return null;
return isFocused ? (
<ExpandableList isChipCountDisplayed={isFocused}>

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