Compare commits

...
Author SHA1 Message Date
Claude 18dc06fd37 Replace as-casts with value guards in field hooks and utility functions
- useRatingField: isFieldRatingValue guard instead of as FieldRatingValue
- useUuidField: remove redundant as FieldUUidValue casts
- useDateField: isFieldDateValue guard instead of as string | null
- useBooleanField: isFieldBooleanValue guard instead of as boolean
- getLabelIdentifierFieldValue: isFieldFullNameValue guard before accessing
  .firstName/.lastName on record value typed as any
- getImageIdentifierFieldValue: isString guard instead of as string

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
2026-04-09 21:03:48 +00:00
Claude c9b230e067 Add Zod guards to isRecordMatchingFilter and getFieldLinkDefinedLinks
Validate composite field values (FULL_NAME, ADDRESS, LINKS, ACTOR,
EMAILS, PHONES) with isField*Value guards before accessing properties
on record[filterKey] which is typed as any from ObjectRecord. Also
replace isDefined with isFieldLinksValue in getFieldLinkDefinedLinks.

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
2026-04-09 20:58:42 +00:00
Claude 9e7ff0a6c1 Remove unsafe type assertions and manual typeof checks across frontend and server
Frontend:
- useExportProcessRecordsForCSV: add isFieldCurrencyValue guard for
  currency record values instead of unvalidated property access
- PhonesDisplay: replace typeof === 'object' with Array.isArray in
  parseAdditionalPhones, use unknown instead of any

Server:
- Remove redundant `as string[]` casts in calendar and messaging
  person listeners (6 instances) - type already narrowed by ?? []
- compute-changed-additional-emails: remove as string[] casts,
  let Array.isArray narrow the type
- transform-emails-value: remove as string[] after JSON.parse,
  rely on isNonEmptyArray for runtime validation
- find-person-by-primary-or-additional-email: use isNonEmptyArray
  from twenty-shared instead of Array.isArray
- create-company-and-contact.service: same isNonEmptyArray fix

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
2026-04-09 20:50:19 +00:00
Claude 26a8fc15e2 Merge remote-tracking branch 'origin/main' into claude/add-primary-email-utility-PSSBG 2026-04-09 20:39:59 +00:00
Claude 305cec6996 Replace unsafe as casts with Zod-based isField*Value guards
Replace manual type assertions (as FieldXValue) with proper Zod
validation guards across 6 frontend files:

- useGetSecondaryRecordTableCellButton: validate phones/emails/links
  values before accessing properties
- EventFieldDiffValueEffect: use isFieldFilesValue instead of
  Array.isArray + as cast
- CurrencyFieldInput: validate defaultValue with isFieldCurrencyValue
- FilesFieldInput: validate draftValue with isFieldFilesValue
- useOpenFilesFieldInput: validate store value with isFieldFilesValue
- FormFieldInput: validate all composite field defaults (fullName,
  address, links, emails, phones, richText) with their respective
  Zod guards

Also removes the not-yet-merged getPrimaryEmailFromRecord utility.

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
2026-04-09 20:36:11 +00:00
Claude ee902781cf Add getPrimaryEmailFromRecord utility with Zod guard and CLAUDE.md rule
Use isFieldEmailsValue Zod guard instead of manual typeof/null checks
for validating email field values. Add a CLAUDE.md rule to enforce
using existing Zod-based isField*Value guards for all composite field
type validation across the codebase.

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
2026-04-09 20:11:16 +00:00
23 changed files with 155 additions and 86 deletions
+6
View File
@@ -171,6 +171,12 @@ packages/
Use existing helpers from `twenty-shared` instead of manual type guards:
- `isDefined()`, `isNonEmptyString()`, `isNonEmptyArray()`
### Field Value Validation
- **Use Zod-based guards** (`isField*Value`) to validate composite field values — never use manual `typeof` / `instanceof` / null checks
- Existing Zod guards live in `packages/twenty-front/src/modules/object-record/record-field/ui/types/guards/` (e.g. `isFieldEmailsValue`, `isFieldPhonesValue`, `isFieldLinksValue`, `isFieldAddressValue`, `isFieldFullNameValue`, etc.)
- Avoid `as FieldXValue` type assertions — validate with the guard first, then access properties safely
- When writing new code that handles emails, phones, links, addresses, or other composite field types, always import and use the corresponding `isField*Value` guard instead of writing manual checks like `typeof x === 'object'` or `x !== null`
## Development Workflow
IMPORTANT: Use Context7 for code generation, setup or configuration steps, or library/API documentation. Automatically use the Context7 MCP tools to resolve library IDs and get library docs without waiting for explicit requests.
@@ -3,7 +3,7 @@ import { useContext, useEffect } from 'react';
import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldFilesValue } from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
@@ -37,16 +37,15 @@ export const EventFieldDiffValueEffect = ({
if (
fieldMetadataItem.type === FieldMetadataType.FILES &&
isDefined(recordStore) &&
Array.isArray(diffRecord)
isFieldFilesValue(diffRecord)
) {
const currentFiles = Array.isArray(recordStore[fieldMetadataItem.name])
? (recordStore[fieldMetadataItem.name] as FieldFilesValue[])
: [];
const storeValue = recordStore[fieldMetadataItem.name];
const currentFiles = isFieldFilesValue(storeValue) ? storeValue : [];
const currentFileMap = new Map(
currentFiles.map((file) => [file.fileId, file]),
);
fieldValue = (diffRecord as FieldFilesValue[]).map((file) => {
fieldValue = diffRecord.map((file) => {
const currentFile = currentFileMap.get(file.fileId);
if (isDefined(currentFile)) {
return { ...file, url: currentFile.url };
@@ -1,3 +1,5 @@
import { isString } from '@sniptt/guards';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
@@ -5,9 +7,10 @@ import { isDefined } from 'twenty-shared/utils';
export const getImageIdentifierFieldValue = (
record: ObjectRecord,
imageIdentifierFieldMetadataItem: FieldMetadataItem | undefined,
) => {
): string | null => {
if (isDefined(imageIdentifierFieldMetadataItem?.name)) {
return record[imageIdentifierFieldMetadataItem.name] as string;
const value = record[imageIdentifierFieldMetadataItem.name];
return isString(value) ? value : null;
}
return null;
@@ -1,5 +1,6 @@
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isFieldFullNameValue } from '@/object-record/record-field/ui/types/guards/isFieldFullNameValue';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -13,7 +14,10 @@ export const getLabelIdentifierFieldValue = (
const recordIdentifierValue = record[labelIdentifierFieldMetadataItem.name];
if (labelIdentifierFieldMetadataItem.type === FieldMetadataType.FULL_NAME) {
return `${recordIdentifierValue?.firstName ?? ''} ${recordIdentifierValue?.lastName ?? ''}`;
if (!isFieldFullNameValue(recordIdentifierValue)) {
return '';
}
return `${recordIdentifierValue.firstName ?? ''} ${recordIdentifierValue.lastName ?? ''}`;
}
return isDefined(recordIdentifierValue) ? `${recordIdentifierValue}` : '';
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldCurrencyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldCurrencyValue } from '@/object-record/record-field/ui/types/guards/isFieldCurrencyValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -19,16 +20,24 @@ export const useExportProcessRecordsForCSV = (objectNameSingular: string) => {
}
switch (field.type) {
case FieldMetadataType.CURRENCY:
case FieldMetadataType.CURRENCY: {
const currencyValue = record[field.name];
if (!isFieldCurrencyValue(currencyValue)) {
return processedRecord;
}
return {
...processedRecord,
[field.name]: {
amountMicros: convertCurrencyMicrosToCurrencyAmount(
record[field.name].amountMicros,
),
currencyCode: record[field.name].currencyCode,
amountMicros:
currencyValue.amountMicros !== null
? convertCurrencyMicrosToCurrencyAmount(
currencyValue.amountMicros,
)
: null,
currencyCode: currencyValue.currencyCode,
} satisfies FieldCurrencyValue,
};
}
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.ARRAY:
case FieldMetadataType.RAW_JSON:
@@ -20,35 +20,35 @@ import { FormUuidFieldInput } from '@/object-record/record-field/ui/form-types/c
import { type VariablePickerComponent } from '@/object-record/record-field/ui/form-types/types/VariablePickerComponent';
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
import {
type FieldAddressValue,
type FieldArrayValue,
type FieldEmailsValue,
type FieldFullNameValue,
type FieldLinksValue,
type FieldMetadata,
type FieldMultiSelectValue,
type FieldPhonesValue,
type FieldRelationToOneValue,
type FieldRelationValue,
type FieldRichTextValue,
type FormFieldCurrencyValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
import { isFieldAddressValue } from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
import { isFieldArray } from '@/object-record/record-field/ui/types/guards/isFieldArray';
import { isFieldBoolean } from '@/object-record/record-field/ui/types/guards/isFieldBoolean';
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
import { isFieldEmailsValue } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
import { isFieldFiles } from '@/object-record/record-field/ui/types/guards/isFieldFiles';
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
import { isFieldFullNameValue } from '@/object-record/record-field/ui/types/guards/isFieldFullNameValue';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
import { isFieldMultiSelect } from '@/object-record/record-field/ui/types/guards/isFieldMultiSelect';
import { isFieldNumber } from '@/object-record/record-field/ui/types/guards/isFieldNumber';
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
import { isFieldPhonesValue } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
import { isFieldRawJson } from '@/object-record/record-field/ui/types/guards/isFieldRawJson';
import { isFieldRelationManyToOne } from '@/object-record/record-field/ui/types/guards/isFieldRelationManyToOne';
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
import { isFieldRichTextValue } from '@/object-record/record-field/ui/types/guards/isFieldRichTextValue';
import { isFieldSelect } from '@/object-record/record-field/ui/types/guards/isFieldSelect';
import { isFieldText } from '@/object-record/record-field/ui/types/guards/isFieldText';
import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid';
@@ -120,7 +120,9 @@ export const FormFieldInput = ({
) : isFieldFullName(field) ? (
<FormFullNameFieldInput
label={field.label}
defaultValue={defaultValue as FieldFullNameValue | undefined}
defaultValue={
isFieldFullNameValue(defaultValue) ? defaultValue : undefined
}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -128,7 +130,9 @@ export const FormFieldInput = ({
) : isFieldAddress(field) ? (
<FormAddressFieldInput
label={field.label}
defaultValue={defaultValue as FieldAddressValue | undefined}
defaultValue={
isFieldAddressValue(defaultValue) ? defaultValue : undefined
}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -136,7 +140,7 @@ export const FormFieldInput = ({
) : isFieldLinks(field) ? (
<FormLinksFieldInput
label={field.label}
defaultValue={defaultValue as FieldLinksValue | undefined}
defaultValue={isFieldLinksValue(defaultValue) ? defaultValue : undefined}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -144,7 +148,7 @@ export const FormFieldInput = ({
) : isFieldEmails(field) ? (
<FormEmailsFieldInput
label={field.label}
defaultValue={defaultValue as FieldEmailsValue | undefined}
defaultValue={isFieldEmailsValue(defaultValue) ? defaultValue : undefined}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -161,7 +165,7 @@ export const FormFieldInput = ({
) : isFieldPhones(field) ? (
<FormPhoneFieldInput
label={field.label}
defaultValue={defaultValue as FieldPhonesValue | undefined}
defaultValue={isFieldPhonesValue(defaultValue) ? defaultValue : undefined}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -223,7 +227,9 @@ export const FormFieldInput = ({
) : isFieldRichText(field) ? (
<FormRichTextFieldInput
label={field.label}
defaultValue={defaultValue as FieldRichTextValue | undefined}
defaultValue={
isFieldRichTextValue(defaultValue) ? defaultValue : undefined
}
onChange={onChange}
VariablePicker={VariablePicker}
readonly={readonly}
@@ -6,6 +6,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldBoolean } from '@/object-record/record-field/ui/types/guards/isFieldBoolean';
import { isFieldBooleanValue } from '@/object-record/record-field/ui/types/guards/isFieldBooleanValue';
import { useAtomFamilySelectorState } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorState';
export const useBooleanField = () => {
@@ -24,7 +25,7 @@ export const useBooleanField = () => {
{ recordId, fieldName },
);
const typedFieldValue = fieldValue as boolean;
const typedFieldValue = isFieldBooleanValue(fieldValue) ? fieldValue : false;
return {
fieldDefinition,
@@ -3,6 +3,7 @@ import { useContext } from 'react';
import { useRecordFieldInput } from '@/object-record/record-field/ui/hooks/useRecordFieldInput';
import { type FieldDateValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
import { isFieldDateValue } from '@/object-record/record-field/ui/types/guards/isFieldDateValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -22,7 +23,7 @@ export const useDateField = () => {
{ recordId, fieldName },
);
const typedFieldValue = fieldValue as string | null | undefined;
const typedFieldValue = isFieldDateValue(fieldValue) ? fieldValue : undefined;
const { setDraftValue } = useRecordFieldInput<FieldDateValue>();
@@ -3,10 +3,10 @@ import { useContext } from 'react';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { type FieldRatingValue } from 'twenty-shared/types';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldRating } from '@/object-record/record-field/ui/types/guards/isFieldRating';
import { isFieldRatingValue } from '@/object-record/record-field/ui/types/guards/isFieldRatingValue';
import { useAtomFamilySelectorState } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorState';
export const useRatingField = () => {
@@ -21,7 +21,7 @@ export const useRatingField = () => {
{ recordId, fieldName },
);
const rating = (fieldValue ?? null) as FieldRatingValue | null;
const rating = isFieldRatingValue(fieldValue) ? fieldValue : null;
return {
fieldDefinition,
@@ -1,7 +1,6 @@
import { useContext } from 'react';
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
import { type FieldUUidValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { assertFieldMetadata } from '@/object-record/record-field/ui/types/guards/assertFieldMetadata';
import { isFieldTextValue } from '@/object-record/record-field/ui/types/guards/isFieldTextValue';
import { isFieldUuid } from '@/object-record/record-field/ui/types/guards/isFieldUuid';
@@ -21,9 +20,7 @@ export const useUuidField = () => {
{ recordId, fieldName },
);
const fieldTextValue = isFieldTextValue(fieldValue as FieldUUidValue)
? (fieldValue as FieldUUidValue)
: '';
const fieldTextValue = isFieldTextValue(fieldValue) ? fieldValue : '';
return {
fieldDefinition,
@@ -1,7 +1,6 @@
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { type FieldCurrencyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput';
import { CurrencyCode } from 'twenty-shared/constants';
@@ -28,9 +27,9 @@ export const CurrencyFieldInput = () => {
RecordFieldComponentInstanceContext,
);
const defaultCurrencyCodeWithoutSQLQuotes = (
defaultValue as FieldCurrencyValue
)?.currencyCode?.replace(/'/g, '') as CurrencyCode;
const defaultCurrencyCodeWithoutSQLQuotes = isFieldCurrencyValue(defaultValue)
? (defaultValue.currencyCode?.replace(/'/g, '') as CurrencyCode)
: undefined;
const defaultCurrencyCodeIsNotEmpty = isNonEmptyString(
defaultCurrencyCodeWithoutSQLQuotes,
@@ -9,7 +9,10 @@ import { MULTI_ITEM_FIELD_INPUT_DROPDOWN_ID_PREFIX } from '@/object-record/recor
import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/utils/uploadMultipleFiles';
import { recordFieldInputIsFieldInErrorComponentState } from '@/object-record/record-field/ui/states/recordFieldInputIsFieldInErrorComponentState';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { filesSchema } from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
import {
filesSchema,
isFieldFilesValue,
} from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { filePreviewState } from '@/ui/field/display/states/filePreviewState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -49,7 +52,7 @@ export const FilesFieldInput = () => {
);
const files = useMemo(
() => (draftValue ?? []) as FieldFilesValue[],
() => (isFieldFilesValue(draftValue) ? draftValue : []),
[draftValue],
);
@@ -2,7 +2,7 @@ import { useFileUpload } from '@/file-upload/hooks/useFileUpload';
import { useUploadFilesFieldFile } from '@/object-record/record-field/ui/meta-types/hooks/useUploadFilesFieldFile';
import { uploadMultipleFiles } from '@/object-record/record-field/ui/meta-types/utils/uploadMultipleFiles';
import { filesFieldUploadState } from '@/object-record/record-field/ui/states/filesFieldUploadState';
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldFilesValue } from '@/object-record/record-field/ui/types/guards/isFieldFilesValue';
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
import { RECORD_TABLE_CELL_INPUT_ID_PREFIX } from '@/object-record/record-table/constants/RecordTableCellInputIdPrefix';
import { RecordTableComponentInstanceContext } from '@/object-record/record-table/states/context/RecordTableComponentInstanceContext';
@@ -59,12 +59,15 @@ export const useOpenFilesFieldInput = () => {
};
};
}) => {
const fieldValue = store.get(
const storeFieldValue = store.get(
recordStoreFamilySelector.selectorFamily({
recordId,
fieldName,
}),
) as FieldFilesValue[];
);
const fieldValue = isFieldFilesValue(storeFieldValue)
? storeFieldValue
: undefined;
const instanceId = getRecordFieldInputInstanceId({
recordId,
@@ -1,9 +1,11 @@
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined, isValidUrl } from 'twenty-shared/utils';
import { type FieldLinksValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldLinksValue } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
export const getFieldLinkDefinedLinks = (fieldValue: FieldLinksValue) => {
if (!isDefined(fieldValue)) {
if (!isFieldLinksValue(fieldValue)) {
return [];
}
@@ -50,6 +50,12 @@ import {
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/EnrichedObjectMetadataItem';
import { computePossibleMorphGqlFieldForFieldName } from '@/object-record/cache/utils/computePossibleMorphGqlFieldForFieldName';
import { isFieldActorValue } from '@/object-record/record-field/ui/types/guards/isFieldActorValue';
import { isFieldAddressValue } from '@/object-record/record-field/ui/types/guards/isFieldAddressValue';
import { isFieldEmailsValue } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
import { isFieldFullNameValue } from '@/object-record/record-field/ui/types/guards/isFieldFullNameValue';
import { isFieldLinksValue } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
import { isFieldPhonesValue } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
const isLeafFilter = (
filter: RecordGqlOperationFilter,
@@ -271,22 +277,32 @@ export const isRecordMatchingFilter = ({
}
case FieldMetadataType.FULL_NAME: {
const fullNameFilter = filterValue as FullNameFilter;
const fullNameValue = record[filterKey];
if (!isFieldFullNameValue(fullNameValue)) {
return false;
}
return (
(fullNameFilter.firstName === undefined ||
isMatchingStringFilter({
stringFilter: fullNameFilter.firstName,
value: record[filterKey]?.firstName,
value: fullNameValue.firstName,
})) &&
(fullNameFilter.lastName === undefined ||
isMatchingStringFilter({
stringFilter: fullNameFilter.lastName,
value: record[filterKey]?.lastName,
value: fullNameValue.lastName,
}))
);
}
case FieldMetadataType.ADDRESS: {
const addressFilter = filterValue as AddressFilter;
const addressValue = record[filterKey];
if (!isFieldAddressValue(addressValue)) {
return false;
}
const keys = [
'addressStreet1',
@@ -305,12 +321,17 @@ export const isRecordMatchingFilter = ({
return isMatchingStringFilter({
stringFilter: value,
value: record[filterKey]?.[key],
value: addressValue[key],
});
});
}
case FieldMetadataType.LINKS: {
const linksFilter = filterValue as LinksFilter;
const linksValue = record[filterKey];
if (!isFieldLinksValue(linksValue)) {
return false;
}
const keys = ['primaryLinkLabel', 'primaryLinkUrl'] as const;
@@ -322,7 +343,7 @@ export const isRecordMatchingFilter = ({
return isMatchingStringFilter({
stringFilter: value,
value: record[filterKey]?.[key],
value: linksValue[key],
});
});
}
@@ -360,18 +381,23 @@ export const isRecordMatchingFilter = ({
}
case FieldMetadataType.ACTOR: {
const actorFilter = filterValue as ActorFilter;
const actorValue = record[filterKey];
if (!isFieldActorValue(actorValue)) {
return false;
}
if (isDefined(actorFilter.workspaceMemberId)) {
return isMatchingUUIDFilter({
uuidFilter: actorFilter.workspaceMemberId,
value: record[filterKey]?.workspaceMemberId,
value: actorValue.workspaceMemberId,
});
}
if (isDefined(actorFilter.source)) {
return isMatchingSelectFilter({
selectFilter: actorFilter.source,
value: record[filterKey].source,
value: actorValue.source,
});
}
@@ -379,12 +405,17 @@ export const isRecordMatchingFilter = ({
actorFilter.name === undefined ||
isMatchingStringFilter({
stringFilter: actorFilter.name,
value: record[filterKey]?.name,
value: actorValue.name,
})
);
}
case FieldMetadataType.EMAILS: {
const emailsFilter = filterValue as EmailsFilter;
const emailsValue = record[filterKey];
if (!isFieldEmailsValue(emailsValue)) {
return false;
}
if (emailsFilter.primaryEmail === undefined) {
return false;
@@ -392,11 +423,16 @@ export const isRecordMatchingFilter = ({
return isMatchingStringFilter({
stringFilter: emailsFilter.primaryEmail,
value: record[filterKey]?.primaryEmail,
value: emailsValue.primaryEmail,
});
}
case FieldMetadataType.PHONES: {
const phonesFilter = filterValue as PhonesFilter;
const phonesValue = record[filterKey];
if (!isFieldPhonesValue(phonesValue)) {
return false;
}
const keys: (keyof PhonesFilter)[] = ['primaryPhoneNumber'];
@@ -408,7 +444,7 @@ export const isRecordMatchingFilter = ({
return isMatchingStringFilter({
stringFilter: value,
value: record[filterKey]?.[key],
value: phonesValue[key],
});
});
}
@@ -5,8 +5,11 @@ import {
type FieldPhonesValue,
} from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
import { isFieldEmailsValue } from '@/object-record/record-field/ui/types/guards/isFieldEmailsValue';
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
import { isFieldLinksValue } from '@/object-record/record-field/ui/types/guards/isFieldLinksValue';
import { isFieldPhones } from '@/object-record/record-field/ui/types/guards/isFieldPhones';
import { isFieldPhonesValue } from '@/object-record/record-field/ui/types/guards/isFieldPhonesValue';
import { useRecordFieldValue } from '@/object-record/record-store/hooks/useRecordFieldValue';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
@@ -44,9 +47,9 @@ export const useGetSecondaryRecordTableCellButton = () => {
let openLinkOnClick: () => void = () => {};
let copyOnClick: () => void = () => {};
if (isFieldPhones(fieldDefinition)) {
if (isFieldPhones(fieldDefinition) && isFieldPhonesValue(fieldValue)) {
const { primaryPhoneCallingCode = '', primaryPhoneNumber = '' } =
fieldValue as FieldPhonesValue;
fieldValue;
const phoneNumber = `${primaryPhoneCallingCode}${primaryPhoneNumber}`;
openLinkOnClick = () => {
window.open(`tel:${phoneNumber}`, '_blank');
@@ -56,8 +59,8 @@ export const useGetSecondaryRecordTableCellButton = () => {
};
}
if (isFieldEmails(fieldDefinition)) {
const email = (fieldValue as FieldEmailsValue).primaryEmail ?? '';
if (isFieldEmails(fieldDefinition) && isFieldEmailsValue(fieldValue)) {
const email = fieldValue.primaryEmail ?? '';
openLinkOnClick = () => {
window.open(`mailto:${email}`, '_blank');
};
@@ -66,8 +69,8 @@ export const useGetSecondaryRecordTableCellButton = () => {
};
}
if (isFieldLinks(fieldDefinition)) {
const url = (fieldValue as FieldLinksValue).primaryLinkUrl ?? '';
if (isFieldLinks(fieldDefinition) && isFieldLinksValue(fieldValue)) {
const url = fieldValue.primaryLinkUrl ?? '';
openLinkOnClick = () => {
window.open(ensureAbsoluteUrl(url), '_blank');
};
@@ -116,18 +116,20 @@ export const PhonesDisplay = ({
);
};
const parseAdditionalPhones = (additionalPhones?: any) => {
const parseAdditionalPhones = (additionalPhones?: unknown) => {
if (!additionalPhones) {
return [];
}
if (typeof additionalPhones === 'object') {
if (Array.isArray(additionalPhones)) {
return additionalPhones;
}
if (typeof additionalPhones === 'string') {
try {
return JSON.parse(additionalPhones);
const parsed = JSON.parse(additionalPhones);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
logError(t`Error parsing additional phones: ${error}`);
}
@@ -17,14 +17,12 @@ export const transformEmailsValue = (
if (additionalEmails) {
try {
const emailArray = (
isNonEmptyString(additionalEmails)
? JSON.parse(additionalEmails)
: additionalEmails
) as string[];
const emailArray = isNonEmptyString(additionalEmails)
? JSON.parse(additionalEmails)
: additionalEmails;
additionalEmails = isNonEmptyArray(emailArray)
? JSON.stringify(emailArray.map((email) => email.toLowerCase()))
? JSON.stringify(emailArray.map((email: string) => email.toLowerCase()))
: null;
} catch {
/* empty */
@@ -45,8 +45,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -82,8 +81,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -115,8 +113,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = peopleHavingEmails
.flatMap((eventPayload) => [
eventPayload.properties.before.emails.primaryEmail,
...((eventPayload.properties.before.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.before.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -8,7 +8,7 @@ import {
ConnectedAccountProvider,
type FieldActorSource,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { type DeepPartial, type Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -251,7 +251,7 @@ export class CreateCompanyAndPersonService {
const existingPersonOnAdditionalEmails = alreadyCreatedPeople.find(
(person) => {
return (
Array.isArray(person.emails?.additionalEmails) &&
isNonEmptyArray(person.emails?.additionalEmails) &&
person.emails.additionalEmails.some(
(email) => email.toLowerCase() === contact.handle.toLowerCase(),
)
@@ -5,8 +5,8 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/
export const computeChangedAdditionalEmails = (
diff: Partial<ObjectRecordDiff<PersonWorkspaceEntity>>,
) => {
const before = diff.emails?.before?.additionalEmails as string[];
const after = diff.emails?.after?.additionalEmails as string[];
const before = diff.emails?.before?.additionalEmails;
const after = diff.emails?.after?.additionalEmails;
if (!Array.isArray(before) || !Array.isArray(after)) {
return {
@@ -1,3 +1,5 @@
import { isNonEmptyArray } from 'twenty-shared/utils';
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
export const findPersonByPrimaryOrAdditionalEmail = ({
@@ -20,7 +22,7 @@ export const findPersonByPrimaryOrAdditionalEmail = ({
const personWithAdditionalEmail = people.find((person) => {
const additionalEmails = person.emails?.additionalEmails;
if (!Array.isArray(additionalEmails)) {
if (!isNonEmptyArray(additionalEmails)) {
return false;
}
@@ -45,8 +45,7 @@ export class MessageParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -82,8 +81,7 @@ export class MessageParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -121,8 +119,8 @@ export class MessageParticipantPersonListener {
personEmails: personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.before.emails.primaryEmail,
...((eventPayload.properties.before.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.before.emails?.additionalEmails ??
[]),
])
.filter(isDefined),
workspaceMemberIds: [],