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:
@ -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) => (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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 }) => {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
Reference in New Issue
Block a user