Fix search record relations (#12553)

Bunch of fixes:
- fix relations in search records
- allow to update relations in update record action
- fix multi select

<img width="503" alt="Capture d’écran 2025-06-11 à 18 30 40"
src="https://github.com/user-attachments/assets/ab652405-ec18-4454-9a60-c0db4c5df823"
/>
<img width="503" alt="Capture d’écran 2025-06-11 à 18 31 04"
src="https://github.com/user-attachments/assets/70b55e49-58ba-4cc2-b38b-13842714fc28"
/>
This commit is contained in:
Thomas Trompette
2025-06-12 16:33:25 +02:00
committed by GitHub
parent cf01faf276
commit 7e6d3295d6
11 changed files with 81 additions and 36 deletions

View File

@ -173,7 +173,9 @@ export const AdvancedFilterFieldSelectMenu = ({
{shouldShowSeparator && <DropdownMenuSeparator />} {shouldShowSeparator && <DropdownMenuSeparator />}
{shouldShowHiddenFields && ( {shouldShowHiddenFields && (
<> <>
<DropdownMenuSectionLabel label={t`Hidden fields`} /> {visibleColumnsFieldMetadataItems.length > 0 && (
<DropdownMenuSectionLabel label={t`Hidden fields`} />
)}
<DropdownMenuItemsContainer> <DropdownMenuItemsContainer>
{hiddenColumnsFieldMetadataItems.map( {hiddenColumnsFieldMetadataItems.map(
(hiddenFieldMetadataItem, index) => ( (hiddenFieldMetadataItem, index) => (

View File

@ -9,6 +9,7 @@ import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'
import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext'; import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { isObject } from '@sniptt/guards';
import { FieldMetadataType } from 'twenty-shared/types'; import { FieldMetadataType } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
@ -40,7 +41,13 @@ export const AdvancedFilterValueFormInput = ({
useApplyObjectFilterDropdownFilterValue(); useApplyObjectFilterDropdownFilterValue();
const handleChange = (newValue: JsonValue) => { const handleChange = (newValue: JsonValue) => {
applyObjectFilterDropdownFilterValue(String(newValue)); if (typeof newValue === 'string') {
applyObjectFilterDropdownFilterValue(newValue);
} else if (Array.isArray(newValue) || isObject(newValue)) {
applyObjectFilterDropdownFilterValue(JSON.stringify(newValue));
} else {
applyObjectFilterDropdownFilterValue(String(newValue));
}
}; };
const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2( const fieldMetadataItemUsedInDropdown = useRecoilComponentValueV2(
@ -68,13 +75,15 @@ export const AdvancedFilterValueFormInput = ({
); );
} }
const field = {
type: recordFilter.type as FieldMetadataType,
label: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
};
return ( return (
<FormFieldInput <FormFieldInput
field={{ field={field}
type: recordFilter.type as FieldMetadataType,
label: '',
metadata: fieldDefinition?.metadata as FieldMetadata,
}}
defaultValue={recordFilter.value} defaultValue={recordFilter.value}
onChange={handleChange} onChange={handleChange}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}

View File

@ -104,7 +104,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as string | undefined} defaultValue={defaultValue as string | undefined}
onChange={onChange} onChange={onChange}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
options={field.metadata.options} options={field.metadata?.options}
readonly={readonly} readonly={readonly}
/> />
) : isFieldFullName(field) ? ( ) : isFieldFullName(field) ? (
@ -170,7 +170,7 @@ export const FormFieldInput = ({
defaultValue={defaultValue as FieldMultiSelectValue | string | undefined} defaultValue={defaultValue as FieldMultiSelectValue | string | undefined}
onChange={onChange} onChange={onChange}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
options={field.metadata.options} options={field.metadata?.options}
readonly={readonly} readonly={readonly}
placeholder={placeholder} placeholder={placeholder}
/> />
@ -213,7 +213,9 @@ export const FormFieldInput = ({
<FormRelationToOneFieldInput <FormRelationToOneFieldInput
label={field.label} label={field.label}
objectNameSingular={field.metadata.relationObjectMetadataNameSingular} objectNameSingular={field.metadata.relationObjectMetadataNameSingular}
defaultValue={defaultValue as FieldRelationValue<FieldRelationToOneValue>} defaultValue={
defaultValue as FieldRelationValue<FieldRelationToOneValue> | string
}
onChange={onChange} onChange={onChange}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}
readonly={readonly} readonly={readonly}

View File

@ -83,6 +83,7 @@ export const FormAddressFieldInput = ({
placeholder="Post Code" placeholder="Post Code"
/> />
<FormCountrySelectInput <FormCountrySelectInput
label="Country"
selectedCountryName={defaultValue?.addressCountry ?? ''} selectedCountryName={defaultValue?.addressCountry ?? ''}
onChange={handleChange('addressCountry')} onChange={handleChange('addressCountry')}
readonly={readonly} readonly={readonly}

View File

@ -9,11 +9,13 @@ import { SelectOption } from 'twenty-ui/input';
export const FormCountrySelectInput = ({ export const FormCountrySelectInput = ({
selectedCountryName, selectedCountryName,
onChange, onChange,
label,
readonly = false, readonly = false,
VariablePicker, VariablePicker,
}: { }: {
selectedCountryName: string; selectedCountryName: string;
onChange: (country: string) => void; onChange: (country: string) => void;
label?: string;
readonly?: boolean; readonly?: boolean;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
}) => { }) => {
@ -52,7 +54,7 @@ export const FormCountrySelectInput = ({
return ( return (
<FormSelectFieldInput <FormSelectFieldInput
label="Country" label={label}
onChange={onCountryChange} onChange={onCountryChange}
options={options} options={options}
defaultValue={selectedCountryName} defaultValue={selectedCountryName}

View File

@ -15,6 +15,7 @@ import { OverlayContainer } from '@/ui/layout/overlay/components/OverlayContaine
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString'; import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariableString';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { isArray } from '@sniptt/guards';
import { useId, useState } from 'react'; import { useId, useState } from 'react';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { VisibilityHidden } from 'twenty-ui/accessibility'; import { VisibilityHidden } from 'twenty-ui/accessibility';
@ -63,6 +64,14 @@ const StyledPlaceholder = styled.div`
width: 100%; width: 100%;
`; `;
const safeParsedValue = (value: string) => {
try {
return JSON.parse(value);
} catch (error) {
return value;
}
};
export const FormMultiSelectFieldInput = ({ export const FormMultiSelectFieldInput = ({
label, label,
defaultValue, defaultValue,
@ -87,7 +96,7 @@ export const FormMultiSelectFieldInput = ({
const [draftValue, setDraftValue] = useState< const [draftValue, setDraftValue] = useState<
| { | {
type: 'static'; type: 'static';
value: FieldMultiSelectValue; value: FieldMultiSelectValue | string;
editingMode: 'view' | 'edit'; editingMode: 'view' | 'edit';
} }
| { | {
@ -171,10 +180,14 @@ export const FormMultiSelectFieldInput = ({
}; };
const selectedNames = const selectedNames =
draftValue.type === 'static' ? draftValue.value : undefined; draftValue.type === 'static' && isDefined(draftValue.value)
? isArray(draftValue.value)
? draftValue.value
: safeParsedValue(draftValue.value)
: undefined;
const selectedOptions = const selectedOptions =
isDefined(selectedNames) && isDefined(options) isDefined(selectedNames) && isDefined(options) && isArray(selectedNames)
? options.filter((option) => ? options.filter((option) =>
selectedNames.some((name) => option.value === name), selectedNames.some((name) => option.value === name),
) )
@ -246,7 +259,7 @@ export const FormMultiSelectFieldInput = ({
options={options} options={options}
onCancel={onCancel} onCancel={onCancel}
onOptionSelected={onOptionSelected} onOptionSelected={onOptionSelected}
values={draftValue.value} values={selectedNames}
/> />
</OverlayContainer> </OverlayContainer>
)} )}

View File

@ -1,21 +1,28 @@
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker'; import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
import { isDefined } from 'twenty-shared/utils'; import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { JsonValue } from 'type-fest';
import { import {
FieldRelationToOneValue, FieldRelationToOneValue,
FieldRelationValue, FieldRelationValue,
} from '@/object-record/record-field/types/FieldMetadata'; } from '@/object-record/record-field/types/FieldMetadata';
import { isObject } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { JsonValue } from 'type-fest';
export type FormRelationToOneFieldInputProps = { export type FormRelationToOneFieldInputProps = {
label?: string; label?: string;
objectNameSingular?: string; objectNameSingular?: string;
defaultValue?: FieldRelationValue<FieldRelationToOneValue>; defaultValue?: FieldRelationValue<FieldRelationToOneValue> | string;
onChange: (value: JsonValue) => void; onChange: (value: JsonValue) => void;
readonly?: boolean; readonly?: boolean;
VariablePicker?: VariablePickerComponent; VariablePicker?: VariablePickerComponent;
}; };
const isFieldRelationToOneValue = (
value: FormRelationToOneFieldInputProps['defaultValue'],
): value is FieldRelationValue<FieldRelationToOneValue> => {
return isObject(value) && isDefined(value?.id);
};
export const FormRelationToOneFieldInput = ({ export const FormRelationToOneFieldInput = ({
label, label,
objectNameSingular, objectNameSingular,
@ -28,12 +35,12 @@ export const FormRelationToOneFieldInput = ({
isDefined(objectNameSingular) && ( isDefined(objectNameSingular) && (
<FormSingleRecordPicker <FormSingleRecordPicker
label={label} label={label}
defaultValue={defaultValue?.id} defaultValue={
onChange={(recordId) => { isFieldRelationToOneValue(defaultValue)
onChange({ ? defaultValue?.id
id: recordId, : defaultValue
}); }
}} onChange={onChange}
objectNameSingular={objectNameSingular} objectNameSingular={objectNameSingular}
disabled={readonly} disabled={readonly}
VariablePicker={VariablePicker} VariablePicker={VariablePicker}

View File

@ -15,6 +15,7 @@ type Story = StoryObj<typeof FormCountrySelectInput>;
export const Default: Story = { export const Default: Story = {
args: { args: {
label: 'Country',
selectedCountryName: 'Canada', selectedCountryName: 'Canada',
}, },
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {

View File

@ -8,4 +8,4 @@ export const isFieldRelationToOneObject = (
field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>, field: Pick<FieldDefinition<FieldMetadata>, 'type' | 'metadata'>,
): field is FieldDefinition<FieldRelationMetadata> => ): field is FieldDefinition<FieldRelationMetadata> =>
isFieldRelation(field) && isFieldRelation(field) &&
field.metadata.relationType === RelationType.MANY_TO_ONE; field.metadata?.relationType === RelationType.MANY_TO_ONE;

View File

@ -6,19 +6,21 @@ import { useEffect, useState } from 'react';
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition'; import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput'; import { FormFieldInput } from '@/object-record/record-field/components/FormFieldInput';
import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker'; import { FormSingleRecordPicker } from '@/object-record/record-field/form-types/components/FormSingleRecordPicker';
import { isFieldRelation } from '@/object-record/record-field/types/guards/isFieldRelation';
import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect'; import { WorkflowFieldsMultiSelect } from '@/workflow/components/WorkflowEditUpdateEventFieldsMultiSelect';
import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody'; import { WorkflowStepBody } from '@/workflow/workflow-steps/components/WorkflowStepBody';
import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader'; import { WorkflowStepHeader } from '@/workflow/workflow-steps/components/WorkflowStepHeader';
import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow'; import { useActionHeaderTypeOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionHeaderTypeOrThrow';
import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow'; import { useActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/hooks/useActionIconColorOrThrow';
import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon'; import { getActionIcon } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIcon';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField';
import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker'; import { WorkflowVariablePicker } from '@/workflow/workflow-variables/components/WorkflowVariablePicker';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { HorizontalSeparator, useIcons } from 'twenty-ui/display'; import { HorizontalSeparator, useIcons } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input'; import { SelectOption } from 'twenty-ui/input';
import { JsonValue } from 'type-fest'; import { JsonValue } from 'type-fest';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { shouldDisplayFormField } from '@/workflow/workflow-steps/workflow-actions/utils/shouldDisplayFormField'; import { RelationType } from '~/generated-metadata/graphql';
type WorkflowEditActionUpdateRecordProps = { type WorkflowEditActionUpdateRecordProps = {
action: WorkflowUpdateRecordAction; action: WorkflowUpdateRecordAction;
@ -217,25 +219,31 @@ export const WorkflowEditActionUpdateRecord = ({
<HorizontalSeparator noMargin /> <HorizontalSeparator noMargin />
{formData.fieldsToUpdate.map((fieldName) => { {formData.fieldsToUpdate.map((fieldName) => {
const fieldDefinition = inlineFieldDefinitions?.find( const fieldDefinition = inlineFieldDefinitions?.find((definition) => {
(definition) => definition.metadata.fieldName === fieldName, const isFieldRelationManyToOne =
); isFieldRelation(definition) &&
definition.metadata.relationType === RelationType.MANY_TO_ONE;
const value = isFieldRelationManyToOne
? `${definition.metadata.fieldName}Id`
: definition.metadata.fieldName;
return value === fieldName;
});
if (!isDefined(fieldDefinition)) { if (!isDefined(fieldDefinition)) {
return null; return null;
} }
const currentValue = formData[ const currentValue = formData[fieldName] as JsonValue;
fieldDefinition.metadata.fieldName
] as JsonValue;
return ( return (
<FormFieldInput <FormFieldInput
key={fieldDefinition.metadata.fieldName} key={fieldName}
defaultValue={currentValue} defaultValue={currentValue}
field={fieldDefinition} field={fieldDefinition}
onChange={(value) => { onChange={(value) => {
handleFieldChange(fieldDefinition.metadata.fieldName, value); handleFieldChange(fieldName, value);
}} }}
VariablePicker={WorkflowVariablePicker} VariablePicker={WorkflowVariablePicker}
readonly={isFormDisabled} readonly={isFormDisabled}

View File

@ -68,7 +68,7 @@ export const WorkflowFindRecordsFiltersEffect = ({
isDefined(defaultValue?.recordFilterGroups) && isDefined(defaultValue?.recordFilterGroups) &&
defaultValue.recordFilterGroups.length > 0 defaultValue.recordFilterGroups.length > 0
) { ) {
setCurrentRecordFilterGroups(defaultValue.recordFilterGroups); setCurrentRecordFilterGroups(defaultValue.recordFilterGroups ?? []);
setHasInitializedCurrentRecordFilterGroups(true); setHasInitializedCurrentRecordFilterGroups(true);
} }
}, [ }, [