Support junction relations in Field widget (#19518)
## Resolved relations in picker https://github.com/user-attachments/assets/bf2281a4-825b-4638-9cb9-7f1be987b8b6
This commit is contained in:
committed by
GitHub
parent
ec7d19a3af
commit
001b7285e6
+12
-7
@@ -79,13 +79,17 @@ export const useFieldListFieldMetadataItems = ({
|
||||
: 'inlineFieldMetadataItems',
|
||||
);
|
||||
|
||||
const { activityTargetFields, inlineRelationFields, boxedRelationFields } =
|
||||
categorizeRelationFields({
|
||||
relationFields: relationFieldMetadataItems ?? [],
|
||||
objectNameSingular,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
isJunctionRelationsEnabled,
|
||||
});
|
||||
const {
|
||||
activityTargetFields,
|
||||
inlineRelationFields,
|
||||
junctionRelationFields,
|
||||
boxedRelationFields,
|
||||
} = categorizeRelationFields({
|
||||
relationFields: relationFieldMetadataItems ?? [],
|
||||
objectNameSingular,
|
||||
objectPermissionsByObjectMetadataId,
|
||||
isJunctionRelationsEnabled,
|
||||
});
|
||||
|
||||
const allInlineFieldMetadataItems = [
|
||||
...(inlineFieldMetadataItems ?? []),
|
||||
@@ -95,6 +99,7 @@ export const useFieldListFieldMetadataItems = ({
|
||||
return {
|
||||
inlineFieldMetadataItems: allInlineFieldMetadataItems,
|
||||
legacyActivityTargetFieldMetadataItems: activityTargetFields,
|
||||
junctionRelationFieldMetadataItems: junctionRelationFields,
|
||||
boxedRelationFieldMetadataItems: boxedRelationFields,
|
||||
};
|
||||
};
|
||||
|
||||
+242
-28
@@ -1,13 +1,23 @@
|
||||
import { useStore } from 'jotai';
|
||||
import { type ReactNode, useCallback, useContext } from 'react';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { getFieldMetadataItemById } from '@/object-metadata/utils/getFieldMetadataItemById';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useRecordFieldsScopeContextOrThrow } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { useUpdateJunctionRelationFromCell } from '@/object-record/record-field/ui/hooks/useUpdateJunctionRelationFromCell';
|
||||
import { useAddNewRecordAndOpenSidePanel } from '@/object-record/record-field/ui/meta-types/input/hooks/useAddNewRecordAndOpenSidePanel';
|
||||
import { useUpdateRelationOneToManyFieldInput } from '@/object-record/record-field/ui/meta-types/input/hooks/useUpdateRelationOneToManyFieldInput';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
|
||||
import { getJoinColumnName } from '@/object-record/record-field/ui/utils/junction/getJoinColumnName';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { getSourceJoinColumnName } from '@/object-record/record-field/ui/utils/junction/getSourceJoinColumnName';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import { MultipleRecordPicker } from '@/object-record/record-picker/multiple-record-picker/components/MultipleRecordPicker';
|
||||
import { useMultipleRecordPickerOpen } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerOpen';
|
||||
import { useMultipleRecordPickerPerformSearch } from '@/object-record/record-picker/multiple-record-picker/hooks/useMultipleRecordPickerPerformSearch';
|
||||
@@ -15,13 +25,16 @@ import { multipleRecordPickerPickableMorphItemsComponentState } from '@/object-r
|
||||
import { multipleRecordPickerSearchFilterComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchFilterComponentState';
|
||||
import { multipleRecordPickerSearchableObjectMetadataItemsComponentState } from '@/object-record/record-picker/multiple-record-picker/states/multipleRecordPickerSearchableObjectMetadataItemsComponentState';
|
||||
import { getRecordFieldCardRelationPickerDropdownId } from '@/object-record/record-show/utils/getRecordFieldCardRelationPickerDropdownId';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { buildRecordLabelPayload } from '@/object-record/utils/buildRecordLabelPayload';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { dropdownPlacementComponentState } from '@/ui/layout/dropdown/states/dropdownPlacementComponentState';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
|
||||
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
|
||||
import { CustomError, isDefined } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
@@ -34,6 +47,7 @@ type RecordDetailRelationSectionDropdownToManyProps = {
|
||||
export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
dropdownTriggerClickableComponent,
|
||||
}: RecordDetailRelationSectionDropdownToManyProps) => {
|
||||
const store = useStore();
|
||||
const { scopeInstanceId } = useRecordFieldsScopeContextOrThrow();
|
||||
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||
const { fieldMetadataId } = fieldDefinition;
|
||||
@@ -56,11 +70,51 @@ export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isJunctionRelation = hasJunctionConfig(fieldMetadataItem.settings);
|
||||
|
||||
const relationFieldDefinition =
|
||||
fieldDefinition as FieldDefinition<FieldRelationMetadata>;
|
||||
|
||||
const { updateJunctionRelationFromCell, isJunctionConfigValid } =
|
||||
useUpdateJunctionRelationFromCell({
|
||||
fieldMetadataItem,
|
||||
fieldDefinition: relationFieldDefinition,
|
||||
recordId,
|
||||
});
|
||||
|
||||
const junctionConfig =
|
||||
isJunctionRelation && isJunctionConfigValid
|
||||
? getJunctionConfig({
|
||||
settings: fieldMetadataItem.settings,
|
||||
relationObjectMetadataId:
|
||||
relationFieldDefinition.metadata.relationObjectMetadataId,
|
||||
sourceObjectMetadataId: objectMetadataItem.id,
|
||||
objectMetadataItems,
|
||||
})
|
||||
: null;
|
||||
|
||||
const firstJunctionTargetField =
|
||||
junctionConfig && !junctionConfig.isMorphRelation
|
||||
? junctionConfig.targetFields[0]
|
||||
: undefined;
|
||||
|
||||
const junctionTargetObjectMetadata = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.id === firstJunctionTargetField?.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
|
||||
const isMorphJunction = junctionConfig?.isMorphRelation ?? false;
|
||||
|
||||
const { objectMetadataItem: relationObjectMetadataItem } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const pickerObjectMetadataItem =
|
||||
isJunctionRelation && isDefined(junctionTargetObjectMetadata)
|
||||
? junctionTargetObjectMetadata
|
||||
: relationObjectMetadataItem;
|
||||
|
||||
const relationFieldMetadataItem = relationObjectMetadataItem.fields.find(
|
||||
({ id }) => id === relationFieldMetadataId,
|
||||
);
|
||||
@@ -78,6 +132,23 @@ export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
|
||||
const relationRecords: ObjectRecord[] = (fieldValue as ObjectRecord[]) ?? [];
|
||||
|
||||
const pickerRecords =
|
||||
isJunctionRelation &&
|
||||
isDefined(junctionConfig) &&
|
||||
isDefined(junctionTargetObjectMetadata)
|
||||
? extractTargetRecordsFromJunction({
|
||||
junctionRecords: relationRecords,
|
||||
targetFields: junctionConfig.targetFields,
|
||||
objectMetadataItems,
|
||||
}).map((extracted) => ({
|
||||
recordId: extracted.recordId,
|
||||
objectMetadataId: extracted.objectMetadataId,
|
||||
}))
|
||||
: relationRecords.map((record) => ({
|
||||
recordId: record.id,
|
||||
objectMetadataId: pickerObjectMetadataItem.id,
|
||||
}));
|
||||
|
||||
const dropdownId = getRecordFieldCardRelationPickerDropdownId({
|
||||
fieldDefinition,
|
||||
recordId,
|
||||
@@ -107,6 +178,12 @@ export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const multipleRecordPickerPickableMorphItemsCallbackState =
|
||||
useAtomComponentStateCallbackState(
|
||||
multipleRecordPickerPickableMorphItemsComponentState,
|
||||
dropdownId,
|
||||
);
|
||||
|
||||
const { performSearch: multipleRecordPickerPerformSearch } =
|
||||
useMultipleRecordPickerPerformSearch();
|
||||
|
||||
@@ -127,40 +204,181 @@ export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
recordId,
|
||||
});
|
||||
|
||||
const { createOneRecord: createTargetRecord } = useCreateOneRecord({
|
||||
objectNameSingular:
|
||||
junctionTargetObjectMetadata?.nameSingular ??
|
||||
relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const { createOneRecord: createJunctionRecord } = useCreateOneRecord({
|
||||
objectNameSingular:
|
||||
junctionConfig?.junctionObjectMetadata?.nameSingular ??
|
||||
relationObjectMetadataNameSingular,
|
||||
});
|
||||
|
||||
const handleOpenRelationPickerDropdown = () => {
|
||||
const pickableMorphItems = pickerRecords.map((item) => ({
|
||||
recordId: item.recordId,
|
||||
objectMetadataId: item.objectMetadataId,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
}));
|
||||
|
||||
setMultipleRecordPickerSearchableObjectMetadataItems([
|
||||
relationObjectMetadataItem,
|
||||
pickerObjectMetadataItem,
|
||||
]);
|
||||
setMultipleRecordPickerSearchFilter('');
|
||||
setMultipleRecordPickerPickableMorphItems(
|
||||
relationRecords.map((record) => ({
|
||||
recordId: record.id,
|
||||
objectMetadataId: relationObjectMetadataItem.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
})),
|
||||
);
|
||||
setMultipleRecordPickerPickableMorphItems(pickableMorphItems);
|
||||
|
||||
openMultipleRecordPicker(dropdownId);
|
||||
|
||||
multipleRecordPickerPerformSearch({
|
||||
multipleRecordPickerInstanceId: dropdownId,
|
||||
forceSearchFilter: '',
|
||||
forceSearchableObjectMetadataItems: [relationObjectMetadataItem],
|
||||
forcePickableMorphItems: relationRecords.map((record) => ({
|
||||
recordId: record.id,
|
||||
objectMetadataId: relationObjectMetadataItem.id,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
})),
|
||||
forceSearchableObjectMetadataItems: [pickerObjectMetadataItem],
|
||||
forcePickableMorphItems: pickableMorphItems,
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNew = (searchString?: string) => {
|
||||
closeDropdown(dropdownId);
|
||||
const handleCreateNew = useCallback(
|
||||
async (searchString?: string) => {
|
||||
const updatePickerState = (
|
||||
newRecordId: string,
|
||||
targetObjectMetadataId: string,
|
||||
) => {
|
||||
const currentMorphItems = store.get(
|
||||
multipleRecordPickerPickableMorphItemsCallbackState,
|
||||
);
|
||||
|
||||
createNewRecordAndOpenSidePanel?.(searchString);
|
||||
};
|
||||
const newMorphItems = currentMorphItems.concat({
|
||||
recordId: newRecordId,
|
||||
objectMetadataId: targetObjectMetadataId,
|
||||
isSelected: true,
|
||||
isMatchingSearchFilter: true,
|
||||
});
|
||||
|
||||
store.set(
|
||||
multipleRecordPickerPickableMorphItemsCallbackState,
|
||||
newMorphItems,
|
||||
);
|
||||
|
||||
multipleRecordPickerPerformSearch({
|
||||
multipleRecordPickerInstanceId: dropdownId,
|
||||
forceSearchFilter: searchString,
|
||||
forceSearchableObjectMetadataItems: [pickerObjectMetadataItem],
|
||||
forcePickableMorphItems: newMorphItems,
|
||||
});
|
||||
};
|
||||
|
||||
if (
|
||||
isJunctionRelation &&
|
||||
isDefined(junctionConfig) &&
|
||||
!isMorphJunction &&
|
||||
isDefined(junctionTargetObjectMetadata)
|
||||
) {
|
||||
const { targetFields, sourceField } = junctionConfig;
|
||||
const targetField = targetFields[0];
|
||||
|
||||
if (!isDefined(targetField) || !isDefined(sourceField)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceJoinColumnName = getSourceJoinColumnName({
|
||||
sourceField,
|
||||
sourceObjectMetadata: objectMetadataItem,
|
||||
});
|
||||
|
||||
const targetJoinColumnName = getJoinColumnName(targetField.settings);
|
||||
|
||||
if (!sourceJoinColumnName || !targetJoinColumnName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTargetId = v4();
|
||||
const targetPayload = buildRecordLabelPayload({
|
||||
id: newTargetId,
|
||||
searchInput: searchString,
|
||||
objectMetadataItem: junctionTargetObjectMetadata,
|
||||
});
|
||||
|
||||
await createTargetRecord(targetPayload);
|
||||
|
||||
const newJunctionId = v4();
|
||||
const createdJunction = await createJunctionRecord({
|
||||
id: newJunctionId,
|
||||
[sourceJoinColumnName]: recordId,
|
||||
[targetJoinColumnName]: newTargetId,
|
||||
});
|
||||
|
||||
if (isDefined(createdJunction)) {
|
||||
store.set(
|
||||
recordStoreFamilyState.atomFamily(recordId),
|
||||
(currentRecord: ObjectRecord | null | undefined) => {
|
||||
if (!isDefined(currentRecord)) {
|
||||
return currentRecord;
|
||||
}
|
||||
const currentFieldValue = currentRecord[fieldName];
|
||||
const updatedJunctionRecords = Array.isArray(currentFieldValue)
|
||||
? [...currentFieldValue, createdJunction]
|
||||
: [createdJunction];
|
||||
|
||||
return {
|
||||
...currentRecord,
|
||||
[fieldName]: updatedJunctionRecords,
|
||||
} as ObjectRecord;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
updatePickerState(newTargetId, junctionTargetObjectMetadata.id);
|
||||
return;
|
||||
}
|
||||
|
||||
closeDropdown(dropdownId);
|
||||
createNewRecordAndOpenSidePanel?.(searchString);
|
||||
},
|
||||
[
|
||||
closeDropdown,
|
||||
createJunctionRecord,
|
||||
createNewRecordAndOpenSidePanel,
|
||||
createTargetRecord,
|
||||
dropdownId,
|
||||
fieldName,
|
||||
isMorphJunction,
|
||||
isJunctionRelation,
|
||||
junctionConfig,
|
||||
junctionTargetObjectMetadata,
|
||||
multipleRecordPickerPickableMorphItemsCallbackState,
|
||||
multipleRecordPickerPerformSearch,
|
||||
objectMetadataItem,
|
||||
pickerObjectMetadataItem,
|
||||
recordId,
|
||||
store,
|
||||
],
|
||||
);
|
||||
|
||||
const canCreateNew = !isMorphJunction;
|
||||
|
||||
const objectMetadataItemIdForCreate =
|
||||
isJunctionRelation && isDefined(junctionTargetObjectMetadata)
|
||||
? junctionTargetObjectMetadata.id
|
||||
: relationObjectMetadataItem.id;
|
||||
|
||||
const handleChange = useCallback(
|
||||
(morphItem: Parameters<typeof updateRelation>[0]) => {
|
||||
if (isJunctionRelation && isJunctionConfigValid) {
|
||||
updateJunctionRelationFromCell({ morphItem });
|
||||
} else {
|
||||
updateRelation(morphItem);
|
||||
}
|
||||
},
|
||||
[
|
||||
isJunctionRelation,
|
||||
isJunctionConfigValid,
|
||||
updateJunctionRelationFromCell,
|
||||
updateRelation,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -181,13 +399,9 @@ export const RecordDetailRelationSectionDropdownToMany = ({
|
||||
<MultipleRecordPicker
|
||||
focusId={dropdownId}
|
||||
componentInstanceId={dropdownId}
|
||||
onCreate={
|
||||
isDefined(createNewRecordAndOpenSidePanel)
|
||||
? handleCreateNew
|
||||
: undefined
|
||||
}
|
||||
objectMetadataItemIdForCreate={relationObjectMetadataItem.id}
|
||||
onChange={updateRelation}
|
||||
onCreate={canCreateNew ? handleCreateNew : undefined}
|
||||
objectMetadataItemIdForCreate={objectMetadataItemIdForCreate}
|
||||
onChange={handleChange}
|
||||
onSubmit={() => {
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
|
||||
+4
@@ -22,6 +22,7 @@ type CategorizeRelationFieldsArgs = {
|
||||
type CategorizedRelationFields = {
|
||||
activityTargetFields: FieldMetadataItem[];
|
||||
inlineRelationFields: FieldMetadataItem[];
|
||||
junctionRelationFields: FieldMetadataItem[];
|
||||
boxedRelationFields: FieldMetadataItem[];
|
||||
};
|
||||
|
||||
@@ -70,6 +71,7 @@ export const categorizeRelationFields = ({
|
||||
}: CategorizeRelationFieldsArgs): CategorizedRelationFields => {
|
||||
const activityTargetFields: FieldMetadataItem[] = [];
|
||||
const inlineRelationFields: FieldMetadataItem[] = [];
|
||||
const junctionRelationFields: FieldMetadataItem[] = [];
|
||||
const boxedRelationFields: FieldMetadataItem[] = [];
|
||||
|
||||
for (const field of relationFields) {
|
||||
@@ -82,6 +84,7 @@ export const categorizeRelationFields = ({
|
||||
// Junction relations (when feature enabled) are rendered inline with other fields
|
||||
if (isJunctionRelationsEnabled && isJunctionRelationField(field)) {
|
||||
inlineRelationFields.push(field);
|
||||
junctionRelationFields.push(field);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -94,6 +97,7 @@ export const categorizeRelationFields = ({
|
||||
return {
|
||||
activityTargetFields,
|
||||
inlineRelationFields,
|
||||
junctionRelationFields,
|
||||
boxedRelationFields,
|
||||
};
|
||||
};
|
||||
|
||||
+15
-6
@@ -4,6 +4,7 @@ import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadat
|
||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
|
||||
import { isFieldWidget } from '@/page-layout/widgets/field/utils/isFieldWidget';
|
||||
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
|
||||
@@ -23,8 +24,8 @@ import {
|
||||
TooltipPosition,
|
||||
} from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { RelationType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -83,6 +84,8 @@ export const WidgetActionFieldSeeAll = () => {
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const isJunction = hasJunctionConfig(relationMetadata?.settings);
|
||||
|
||||
const relationObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) =>
|
||||
item.nameSingular ===
|
||||
@@ -93,20 +96,26 @@ export const WidgetActionFieldSeeAll = () => {
|
||||
({ id }) => id === relationMetadata?.relationFieldMetadataId,
|
||||
);
|
||||
|
||||
const targetObjectMetadataItem = relationObjectMetadataItem;
|
||||
|
||||
const indexViewId = useAtomFamilySelectorValue(
|
||||
indexViewIdFromObjectMetadataItemFamilySelector,
|
||||
{ objectMetadataItemId: relationObjectMetadataItem?.id ?? '' },
|
||||
{ objectMetadataItemId: targetObjectMetadataItem?.id ?? '' },
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(relationMetadata) ||
|
||||
relationMetadata.relationType !== RelationType.ONE_TO_MANY ||
|
||||
!isDefined(relationFieldMetadataItem) ||
|
||||
!isDefined(relationObjectMetadataItem)
|
||||
!isDefined(targetObjectMetadataItem) ||
|
||||
isJunction
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isDefined(relationFieldMetadataItem)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterQueryParams = {
|
||||
filter: {
|
||||
[relationFieldMetadataItem.name]: {
|
||||
@@ -121,14 +130,14 @@ export const WidgetActionFieldSeeAll = () => {
|
||||
const filterLinkHref = getAppPath(
|
||||
AppPath.RecordIndexPage,
|
||||
{
|
||||
objectNamePlural: relationObjectMetadataItem.namePlural,
|
||||
objectNamePlural: targetObjectMetadataItem.namePlural,
|
||||
},
|
||||
filterQueryParams,
|
||||
);
|
||||
|
||||
const tooltipId = `widget-see-all-${widget.id}`;
|
||||
const relationLabelPlural =
|
||||
relationObjectMetadataItem.labelPlural.toLowerCase();
|
||||
targetObjectMetadataItem.labelPlural.toLowerCase();
|
||||
const tooltipContent = t`See all ${relationLabelPlural} linked to this record`;
|
||||
|
||||
return (
|
||||
|
||||
+29
@@ -4,10 +4,13 @@ import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/uti
|
||||
import { isFieldMorphRelation } from '@/object-record/record-field/ui/types/guards/isFieldMorphRelation';
|
||||
import { isFieldRelation } from '@/object-record/record-field/ui/types/guards/isFieldRelation';
|
||||
import { isFieldRichText } from '@/object-record/record-field/ui/types/guards/isFieldRichText';
|
||||
import { hasJunctionConfig } from '@/object-record/record-field/ui/utils/junction/hasJunctionConfig';
|
||||
import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector';
|
||||
import { useResolveFieldMetadataIdFromNameOrId } from '@/page-layout/hooks/useResolveFieldMetadataIdFromNameOrId';
|
||||
import { type PageLayoutWidget } from '@/page-layout/types/PageLayoutWidget';
|
||||
import { FieldWidgetDisplay } from '@/page-layout/widgets/field/components/FieldWidgetDisplay';
|
||||
import { FieldWidgetJunctionRelationCard } from '@/page-layout/widgets/field/components/FieldWidgetJunctionRelationCard';
|
||||
import { FieldWidgetJunctionRelationField } from '@/page-layout/widgets/field/components/FieldWidgetJunctionRelationField';
|
||||
import { FieldWidgetRichTextEditor } from '@/page-layout/widgets/field/components/FieldWidgetRichTextEditor';
|
||||
import { FieldWidgetMorphRelationCard } from '@/page-layout/widgets/field/components/FieldWidgetMorphRelationCard';
|
||||
import { FieldWidgetMorphRelationField } from '@/page-layout/widgets/field/components/FieldWidgetMorphRelationField';
|
||||
@@ -118,6 +121,32 @@ export const FieldWidget = ({ widget }: FieldWidgetProps) => {
|
||||
}
|
||||
|
||||
if (isFieldRelation(fieldDefinition)) {
|
||||
const isJunctionRelation = hasJunctionConfig(
|
||||
fieldDefinition.metadata.settings,
|
||||
);
|
||||
|
||||
if (isJunctionRelation) {
|
||||
if (fieldDisplayMode === FieldDisplayMode.CARD) {
|
||||
return (
|
||||
<FieldWidgetJunctionRelationCard
|
||||
fieldDefinition={fieldDefinition}
|
||||
relationValue={record}
|
||||
isInSidePanel={isInSidePanel}
|
||||
sourceObjectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FieldWidgetJunctionRelationField
|
||||
fieldDefinition={fieldDefinition}
|
||||
relationValue={record}
|
||||
isInSidePanel={isInSidePanel}
|
||||
sourceObjectMetadataId={objectMetadataItem.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (fieldDisplayMode === FieldDisplayMode.CARD) {
|
||||
return (
|
||||
<FieldWidgetRelationCard
|
||||
|
||||
+203
@@ -0,0 +1,203 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Fragment, useState } from 'react';
|
||||
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { RecordFieldsScopeContextProvider } from '@/object-record/record-field-list/contexts/RecordFieldsScopeContext';
|
||||
import { RecordDetailRecordsListContainer } from '@/object-record/record-field-list/record-detail-section/components/RecordDetailRecordsListContainer';
|
||||
import { RecordDetailRelationRecordsListItem } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem';
|
||||
import { RecordDetailRelationRecordsListItemEffect } from '@/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItemEffect';
|
||||
import { FieldContext } from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import {
|
||||
FieldInputEventContext,
|
||||
type FieldInputEvent,
|
||||
} from '@/object-record/record-field/ui/contexts/FieldInputEventContext';
|
||||
import { usePersistField } from '@/object-record/record-field/ui/hooks/usePersistField';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { FieldWidgetShowMoreButton } from '@/page-layout/widgets/field/components/FieldWidgetShowMoreButton';
|
||||
import { FIELD_WIDGET_RELATION_CARD_INITIAL_VISIBLE_ITEMS } from '@/page-layout/widgets/field/constants/FieldWidgetRelationCardInitialVisibleItems';
|
||||
import { FIELD_WIDGET_RELATION_CARD_LOAD_MORE_INCREMENT } from '@/page-layout/widgets/field/constants/FieldWidgetRelationCardLoadMoreIncrement';
|
||||
import { generateFieldWidgetInstanceId } from '@/page-layout/widgets/field/utils/generateFieldWidgetInstanceId';
|
||||
import { useCurrentWidget } from '@/page-layout/widgets/hooks/useCurrentWidget';
|
||||
import { useTargetRecord } from '@/ui/layout/contexts/useTargetRecord';
|
||||
import { SidePanelProvider } from '@/ui/layout/side-panel/contexts/SidePanelContext';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledShowMoreButtonContainer = styled.div`
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type FieldWidgetJunctionRelationCardProps = {
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
|
||||
relationValue: any;
|
||||
isInSidePanel: boolean;
|
||||
sourceObjectMetadataId: string;
|
||||
};
|
||||
|
||||
export const FieldWidgetJunctionRelationCard = ({
|
||||
fieldDefinition,
|
||||
relationValue,
|
||||
isInSidePanel,
|
||||
sourceObjectMetadataId,
|
||||
}: FieldWidgetJunctionRelationCardProps) => {
|
||||
const widget = useCurrentWidget();
|
||||
|
||||
const [expandedItem, setExpandedItem] = useState('');
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState(
|
||||
FIELD_WIDGET_RELATION_CARD_INITIAL_VISIBLE_ITEMS,
|
||||
);
|
||||
const targetRecord = useTargetRecord();
|
||||
|
||||
const instanceId = generateFieldWidgetInstanceId({
|
||||
widgetId: widget.id,
|
||||
recordId: targetRecord.id,
|
||||
fieldName: fieldDefinition.metadata.fieldName,
|
||||
isInSidePanel,
|
||||
});
|
||||
|
||||
const handleItemClick = (id: string) =>
|
||||
setExpandedItem(id === expandedItem ? '' : id);
|
||||
|
||||
const handleShowMore = () => {
|
||||
setVisibleItemsCount(
|
||||
(prevCount) => prevCount + FIELD_WIDGET_RELATION_CARD_LOAD_MORE_INCREMENT,
|
||||
);
|
||||
};
|
||||
|
||||
const fieldMetadata = fieldDefinition.metadata;
|
||||
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: targetRecord.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
|
||||
const useUpdateOneObjectRecordMutation = () => {
|
||||
const updateEntity = ({
|
||||
variables,
|
||||
}: {
|
||||
variables: {
|
||||
where: { id: string };
|
||||
updateOneRecordInput: Record<string, unknown>;
|
||||
};
|
||||
}) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: targetRecord.targetObjectNameSingular,
|
||||
idToUpdate: variables.where.id,
|
||||
updateOneRecordInput: variables.updateOneRecordInput,
|
||||
});
|
||||
};
|
||||
return [updateEntity, { loading: false }] as [
|
||||
(params: any) => void,
|
||||
{ loading: boolean },
|
||||
];
|
||||
};
|
||||
|
||||
const persistField = usePersistField({
|
||||
objectMetadataItemId: objectMetadataItem.id,
|
||||
});
|
||||
|
||||
const handleSubmit: FieldInputEvent = ({ newValue }) => {
|
||||
persistField({
|
||||
recordId: targetRecord.id,
|
||||
fieldDefinition,
|
||||
valueToPersist: newValue,
|
||||
});
|
||||
};
|
||||
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldMetadata.settings,
|
||||
relationObjectMetadataId: fieldMetadata.relationObjectMetadataId,
|
||||
sourceObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (!isDefined(junctionConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const junctionRecords = Array.isArray(relationValue) ? relationValue : [];
|
||||
|
||||
const extractedRecords = extractTargetRecordsFromJunction({
|
||||
junctionRecords,
|
||||
targetFields: junctionConfig.targetFields,
|
||||
objectMetadataItems,
|
||||
includeRecord: true,
|
||||
});
|
||||
|
||||
const targetRecordsWithMetadata = extractedRecords
|
||||
.map((extracted) => {
|
||||
const objectMetadata = objectMetadataItems.find(
|
||||
(item) => item.id === extracted.objectMetadataId,
|
||||
);
|
||||
if (!objectMetadata || !extracted.record) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
record: extracted.record,
|
||||
objectNameSingular: objectMetadata.nameSingular,
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (targetRecordsWithMetadata.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const visibleRecords = targetRecordsWithMetadata.slice(0, visibleItemsCount);
|
||||
const remainingCount = targetRecordsWithMetadata.length - visibleItemsCount;
|
||||
const hasMoreRecords = remainingCount > 0;
|
||||
|
||||
return (
|
||||
<SidePanelProvider value={{ isInSidePanel }}>
|
||||
<RecordFieldsScopeContextProvider value={{ scopeInstanceId: instanceId }}>
|
||||
<FieldContext.Provider
|
||||
value={{
|
||||
recordId: targetRecord.id,
|
||||
isLabelIdentifier: false,
|
||||
fieldDefinition,
|
||||
useUpdateRecord: useUpdateOneObjectRecordMutation,
|
||||
// Junction card detach/delete logic in RecordDetailRelationRecordsListItem
|
||||
// assumes direct relations. Force read-only to prevent data corruption.
|
||||
isRecordFieldReadOnly: true,
|
||||
}}
|
||||
>
|
||||
<FieldInputEventContext.Provider value={{ onSubmit: handleSubmit }}>
|
||||
<RecordDetailRecordsListContainer>
|
||||
{visibleRecords.map((item) => (
|
||||
<Fragment key={item.record.id}>
|
||||
<RecordDetailRelationRecordsListItemEffect
|
||||
relationRecordId={item.record.id}
|
||||
relationObjectMetadataNameSingular={item.objectNameSingular}
|
||||
/>
|
||||
<RecordDetailRelationRecordsListItem
|
||||
isExpanded={expandedItem === item.record.id}
|
||||
onClick={handleItemClick}
|
||||
relationRecord={item.record}
|
||||
relationObjectMetadataNameSingular={item.objectNameSingular}
|
||||
relationFieldMetadataId=""
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
{hasMoreRecords && (
|
||||
<StyledShowMoreButtonContainer>
|
||||
<FieldWidgetShowMoreButton
|
||||
remainingCount={remainingCount}
|
||||
onClick={handleShowMore}
|
||||
/>
|
||||
</StyledShowMoreButtonContainer>
|
||||
)}
|
||||
</RecordDetailRecordsListContainer>
|
||||
</FieldInputEventContext.Provider>
|
||||
</FieldContext.Provider>
|
||||
</RecordFieldsScopeContextProvider>
|
||||
</SidePanelProvider>
|
||||
);
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { RecordChip } from '@/object-record/components/RecordChip';
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldRelationMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { extractTargetRecordsFromJunction } from '@/object-record/record-field/ui/utils/junction/extractTargetRecordsFromJunction';
|
||||
import { getJunctionConfig } from '@/object-record/record-field/ui/utils/junction/getJunctionConfig';
|
||||
import { SidePanelProvider } from '@/ui/layout/side-panel/contexts/SidePanelContext';
|
||||
import { styled } from '@linaria/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
padding: ${themeCssVariables.spacing[1]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledRelationChipsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
type FieldWidgetJunctionRelationFieldProps = {
|
||||
fieldDefinition: FieldDefinition<FieldRelationMetadata>;
|
||||
relationValue: any;
|
||||
isInSidePanel: boolean;
|
||||
sourceObjectMetadataId: string;
|
||||
};
|
||||
|
||||
export const FieldWidgetJunctionRelationField = ({
|
||||
fieldDefinition,
|
||||
relationValue,
|
||||
isInSidePanel,
|
||||
sourceObjectMetadataId,
|
||||
}: FieldWidgetJunctionRelationFieldProps) => {
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
const fieldMetadata = fieldDefinition.metadata;
|
||||
|
||||
const junctionConfig = getJunctionConfig({
|
||||
settings: fieldMetadata.settings,
|
||||
relationObjectMetadataId: fieldMetadata.relationObjectMetadataId,
|
||||
sourceObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
if (!isDefined(junctionConfig)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const junctionRecords = Array.isArray(relationValue) ? relationValue : [];
|
||||
|
||||
const extractedRecords = extractTargetRecordsFromJunction({
|
||||
junctionRecords,
|
||||
targetFields: junctionConfig.targetFields,
|
||||
objectMetadataItems,
|
||||
includeRecord: true,
|
||||
});
|
||||
|
||||
const targetRecordsWithMetadata = extractedRecords
|
||||
.map((extracted) => {
|
||||
const objectMetadata = objectMetadataItems.find(
|
||||
(item) => item.id === extracted.objectMetadataId,
|
||||
);
|
||||
if (!objectMetadata || !extracted.record) {
|
||||
return null;
|
||||
}
|
||||
return { record: extracted.record, objectMetadata };
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
if (targetRecordsWithMetadata.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SidePanelProvider value={{ isInSidePanel }}>
|
||||
<StyledContainer>
|
||||
<StyledRelationChipsContainer>
|
||||
{targetRecordsWithMetadata.map(({ record, objectMetadata }) => (
|
||||
<RecordChip
|
||||
key={record.id}
|
||||
objectNameSingular={objectMetadata.nameSingular}
|
||||
record={record}
|
||||
/>
|
||||
))}
|
||||
</StyledRelationChipsContainer>
|
||||
</StyledContainer>
|
||||
</SidePanelProvider>
|
||||
);
|
||||
};
|
||||
+20
-6
@@ -1,16 +1,30 @@
|
||||
import { useFieldListFieldMetadataItems } from '@/object-record/record-field-list/hooks/useFieldListFieldMetadataItems';
|
||||
import { isJunctionRelationField } from '@/object-record/record-field/ui/utils/junction/isJunctionRelationField';
|
||||
import { FIELD_WIDGET_SUPPORTED_FIELD_TYPES } from '@/page-layout/widgets/field/constants/fieldWidgetSupportedFieldTypes';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export const useFieldWidgetEligibleFields = (objectNameSingular: string) => {
|
||||
const { boxedRelationFieldMetadataItems, inlineFieldMetadataItems } =
|
||||
useFieldListFieldMetadataItems({ objectNameSingular });
|
||||
const {
|
||||
boxedRelationFieldMetadataItems,
|
||||
junctionRelationFieldMetadataItems,
|
||||
inlineFieldMetadataItems,
|
||||
} = useFieldListFieldMetadataItems({ objectNameSingular });
|
||||
|
||||
return useMemo(() => {
|
||||
const eligibleInlineFields = inlineFieldMetadataItems.filter((field) =>
|
||||
FIELD_WIDGET_SUPPORTED_FIELD_TYPES.includes(field.type),
|
||||
const eligibleInlineFields = inlineFieldMetadataItems.filter(
|
||||
(field) =>
|
||||
FIELD_WIDGET_SUPPORTED_FIELD_TYPES.includes(field.type) &&
|
||||
!isJunctionRelationField(field),
|
||||
);
|
||||
|
||||
return [...boxedRelationFieldMetadataItems, ...eligibleInlineFields];
|
||||
}, [boxedRelationFieldMetadataItems, inlineFieldMetadataItems]);
|
||||
return [
|
||||
...boxedRelationFieldMetadataItems,
|
||||
...junctionRelationFieldMetadataItems,
|
||||
...eligibleInlineFields,
|
||||
];
|
||||
}, [
|
||||
boxedRelationFieldMetadataItems,
|
||||
junctionRelationFieldMetadataItems,
|
||||
inlineFieldMetadataItems,
|
||||
]);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user