Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18dc06fd37 | ||
|
|
c9b230e067 | ||
|
|
9e7ff0a6c1 | ||
|
|
26a8fc15e2 | ||
|
|
305cec6996 | ||
|
|
ee902781cf |
@@ -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.
|
||||
|
||||
+5
-6
@@ -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 };
|
||||
|
||||
+5
-2
@@ -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;
|
||||
|
||||
+5
-1
@@ -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}` : '';
|
||||
|
||||
+14
-5
@@ -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:
|
||||
|
||||
+18
-12
@@ -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}
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+2
-1
@@ -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>();
|
||||
|
||||
|
||||
+2
-2
@@ -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
-4
@@ -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,
|
||||
|
||||
+3
-4
@@ -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,
|
||||
|
||||
+5
-2
@@ -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],
|
||||
);
|
||||
|
||||
|
||||
+6
-3
@@ -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,
|
||||
|
||||
+4
-2
@@ -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 [];
|
||||
}
|
||||
|
||||
|
||||
+45
-9
@@ -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],
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+9
-6
@@ -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}`);
|
||||
}
|
||||
|
||||
+4
-6
@@ -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 */
|
||||
|
||||
+3
-6
@@ -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);
|
||||
|
||||
|
||||
+2
-2
@@ -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(),
|
||||
)
|
||||
|
||||
+2
-2
@@ -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 {
|
||||
|
||||
+3
-1
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+4
-6
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user