Improve opportunity behavior (#3487)
* Fix opportunity relation * Fix * Fix * Fix tests * Fix * Fix * Fix opportunities * Fix Opportunity standard object and apply maxWidth to text ellipsis * Update packages/twenty-front/src/modules/ui/field/display/components/EllipsisDisplay.tsx Co-authored-by: Thaïs <guigon.thais@gmail.com> * Fix --------- Co-authored-by: Thaïs <guigon.thais@gmail.com>
This commit is contained in:
@ -229,6 +229,7 @@ export const CompanyBoardCard = () => {
|
|||||||
<FieldContext.Provider
|
<FieldContext.Provider
|
||||||
value={{
|
value={{
|
||||||
entityId: boardCardId,
|
entityId: boardCardId,
|
||||||
|
maxWidth: 156,
|
||||||
recoilScopeId: boardCardId + viewField.fieldMetadataId,
|
recoilScopeId: boardCardId + viewField.fieldMetadataId,
|
||||||
isLabelIdentifier: false,
|
isLabelIdentifier: false,
|
||||||
fieldDefinition: {
|
fieldDefinition: {
|
||||||
|
|||||||
@ -260,6 +260,7 @@ export const RecordShowPage = () => {
|
|||||||
key={record.id + fieldMetadataItem.id}
|
key={record.id + fieldMetadataItem.id}
|
||||||
value={{
|
value={{
|
||||||
entityId: record.id,
|
entityId: record.id,
|
||||||
|
maxWidth: 272,
|
||||||
recoilScopeId: record.id + fieldMetadataItem.id,
|
recoilScopeId: record.id + fieldMetadataItem.id,
|
||||||
isLabelIdentifier: false,
|
isLabelIdentifier: false,
|
||||||
fieldDefinition:
|
fieldDefinition:
|
||||||
|
|||||||
@ -28,6 +28,7 @@ export type GenericFieldContextType = {
|
|||||||
isLabelIdentifier: boolean;
|
isLabelIdentifier: boolean;
|
||||||
basePathToShowPage?: string;
|
basePathToShowPage?: string;
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
|
maxWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FieldContext = createContext<GenericFieldContextType>(
|
export const FieldContext = createContext<GenericFieldContextType>(
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
|
|||||||
import { useTextField } from '../../hooks/useTextField';
|
import { useTextField } from '../../hooks/useTextField';
|
||||||
|
|
||||||
export const TextFieldDisplay = () => {
|
export const TextFieldDisplay = () => {
|
||||||
const { fieldValue } = useTextField();
|
const { fieldValue, maxWidth } = useTextField();
|
||||||
|
|
||||||
return <TextDisplay text={fieldValue} />;
|
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -9,7 +9,8 @@ import { isFieldText } from '../../types/guards/isFieldText';
|
|||||||
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
|
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
|
||||||
|
|
||||||
export const useTextField = () => {
|
export const useTextField = () => {
|
||||||
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
|
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
|
||||||
|
useContext(FieldContext);
|
||||||
|
|
||||||
assertFieldMetadata('TEXT', isFieldText, fieldDefinition);
|
assertFieldMetadata('TEXT', isFieldText, fieldDefinition);
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ export const useTextField = () => {
|
|||||||
: fieldInitialValue?.value ?? fieldTextValue;
|
: fieldInitialValue?.value ?? fieldTextValue;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
maxWidth,
|
||||||
fieldDefinition,
|
fieldDefinition,
|
||||||
fieldValue: fieldTextValue,
|
fieldValue: fieldTextValue,
|
||||||
initialValue,
|
initialValue,
|
||||||
|
|||||||
@ -39,25 +39,20 @@ export const useRecordBoardCardFieldsInternal = (
|
|||||||
.getLoadable(recordBoardCardFieldsScopedState({ scopeId }))
|
.getLoadable(recordBoardCardFieldsScopedState({ scopeId }))
|
||||||
.getValue();
|
.getValue();
|
||||||
|
|
||||||
const existingFieldsUpdated = existingFields.map((previousField) =>
|
const fieldIndex = existingFields.findIndex(
|
||||||
previousField.fieldMetadataId === field.fieldMetadataId
|
|
||||||
? { ...previousField, isVisible: !field.isVisible }
|
|
||||||
: previousField,
|
|
||||||
);
|
|
||||||
|
|
||||||
const isNewField = !existingFields.find(
|
|
||||||
({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId,
|
({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId,
|
||||||
);
|
);
|
||||||
|
const fields = [...existingFields];
|
||||||
|
|
||||||
const fields = isNewField
|
if (fieldIndex === -1) {
|
||||||
? [
|
fields.push({ ...field, position: existingFields.length });
|
||||||
...existingFieldsUpdated,
|
} else {
|
||||||
{
|
fields[fieldIndex] = {
|
||||||
...field,
|
...field,
|
||||||
position: existingFieldsUpdated.length,
|
isVisible: !field.isVisible,
|
||||||
},
|
position: existingFields.length,
|
||||||
]
|
};
|
||||||
: existingFieldsUpdated;
|
}
|
||||||
|
|
||||||
setSavedBoardCardFields(fields);
|
setSavedBoardCardFields(fields);
|
||||||
setBoardCardFields(fields);
|
setBoardCardFields(fields);
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
objectNameSingular: objectMetadataNameSingular ?? '',
|
objectNameSingular: objectMetadataNameSingular ?? '',
|
||||||
});
|
});
|
||||||
|
|
||||||
const modifyObjectMetadataInCache = useModifyRecordFromCache({
|
const modifyRecordFromCache = useModifyRecordFromCache({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -136,7 +136,7 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
modifyObjectMetadataInCache(entityId, {
|
modifyRecordFromCache(entityId, {
|
||||||
[fieldName]: (relationRef, { readField }) => {
|
[fieldName]: (relationRef, { readField }) => {
|
||||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||||
|
|
||||||
@ -168,7 +168,7 @@ export const RecordRelationFieldCardContent = ({
|
|||||||
<FieldDisplay />
|
<FieldDisplay />
|
||||||
</FieldContextProvider>
|
</FieldContextProvider>
|
||||||
{/* TODO: temporary to prevent removing a company from an opportunity */}
|
{/* TODO: temporary to prevent removing a company from an opportunity */}
|
||||||
{isOpportunityCompanyRelation && (
|
{!isOpportunityCompanyRelation && (
|
||||||
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
<DropdownScope dropdownScopeId={dropdownScopeId}>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownScopeId}
|
dropdownId={dropdownScopeId}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-
|
|||||||
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
|
||||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||||
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
|
||||||
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
|
||||||
import { IconForbid, IconPlus } from '@/ui/display/icon';
|
import { IconForbid, IconPlus } from '@/ui/display/icon';
|
||||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||||
@ -111,12 +112,11 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
|
|
||||||
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
const isToOneObject = relationType === 'TO_ONE_OBJECT';
|
||||||
|
|
||||||
const relationRecords = !isToOneObject
|
const relationRecords: ObjectRecord[] =
|
||||||
? fieldValue?.edges.map(({ node }: { node: any }) => node) ?? []
|
fieldValue && isToOneObject
|
||||||
: fieldValue
|
|
||||||
? [fieldValue]
|
? [fieldValue]
|
||||||
: [];
|
: fieldValue?.edges.map(({ node }: { node: ObjectRecord }) => node) ?? [];
|
||||||
const relationRecordIds = relationRecords.map(({ id }: { id: string }) => id);
|
const relationRecordIds = relationRecords.map(({ id }) => id);
|
||||||
|
|
||||||
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
|
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
orderByField: 'createdAt',
|
orderByField: 'createdAt',
|
||||||
mappingFunction: (recordToMap: any) =>
|
mappingFunction: (recordToMap) =>
|
||||||
identifiersMapper?.(recordToMap, relationObjectMetadataNameSingular),
|
identifiersMapper?.(recordToMap, relationObjectMetadataNameSingular),
|
||||||
selectedIds: relationRecordIds,
|
selectedIds: relationRecordIds,
|
||||||
excludeEntityIds: relationRecordIds,
|
excludeEntityIds: relationRecordIds,
|
||||||
@ -154,7 +154,7 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
objectNameSingular: relationObjectMetadataNameSingular,
|
objectNameSingular: relationObjectMetadataNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const modifyObjectMetadataInCache = useModifyRecordFromCache({
|
const modifyRecordFromCache = useModifyRecordFromCache({
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -180,7 +180,7 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
modifyObjectMetadataInCache(entityId, {
|
modifyRecordFromCache(entityId, {
|
||||||
[fieldName]: (relationRef, { readField }) => {
|
[fieldName]: (relationRef, { readField }) => {
|
||||||
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
const edges = readField<{ node: Reference }[]>('edges', relationRef);
|
||||||
|
|
||||||
@ -248,15 +248,13 @@ export const RecordRelationFieldCardSection = () => {
|
|||||||
</StyledHeader>
|
</StyledHeader>
|
||||||
{!!relationRecords.length && (
|
{!!relationRecords.length && (
|
||||||
<Card>
|
<Card>
|
||||||
{relationRecords
|
{relationRecords.slice(0, 5).map((relationRecord, index) => (
|
||||||
.slice(0, 5)
|
<RecordRelationFieldCardContent
|
||||||
.map((relationRecord: any, index: number) => (
|
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
||||||
<RecordRelationFieldCardContent
|
divider={index < relationRecords.length - 1}
|
||||||
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
|
relationRecord={relationRecord}
|
||||||
divider={index < relationRecords.length - 1}
|
/>
|
||||||
relationRecord={relationRecord}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</RelationPickerScope>
|
</RelationPickerScope>
|
||||||
|
|||||||
@ -70,17 +70,15 @@ export const RelationPicker = ({
|
|||||||
onSubmit(selectedEntity ?? null);
|
onSubmit(selectedEntity ?? null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<SingleEntitySelect
|
||||||
<SingleEntitySelect
|
EmptyIcon={IconForbid}
|
||||||
EmptyIcon={IconForbid}
|
emptyLabel={'No ' + fieldDefinition.label}
|
||||||
emptyLabel={'No ' + fieldDefinition.label}
|
entitiesToSelect={entities.entitiesToSelect}
|
||||||
entitiesToSelect={entities.entitiesToSelect}
|
loading={entities.loading}
|
||||||
loading={entities.loading}
|
onCancel={onCancel}
|
||||||
onCancel={onCancel}
|
onEntitySelected={handleEntitySelected}
|
||||||
onEntitySelected={handleEntitySelected}
|
selectedEntity={entities.selectedEntities[0]}
|
||||||
selectedEntity={entities.selectedEntities[0]}
|
width={width}
|
||||||
width={width}
|
/>
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +1,21 @@
|
|||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
const StyledEllipsisDisplay = styled.div`
|
const StyledEllipsisDisplay = styled.div<{ maxWidth?: number }>`
|
||||||
|
max-width: ${({ maxWidth }) => maxWidth ?? '100%'};
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export { StyledEllipsisDisplay as EllipsisDisplay };
|
type EllipsisDisplayProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
maxWidth?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EllipsisDisplay = ({
|
||||||
|
children,
|
||||||
|
maxWidth,
|
||||||
|
}: EllipsisDisplayProps) => (
|
||||||
|
<StyledEllipsisDisplay style={{ maxWidth }}>{children}</StyledEllipsisDisplay>
|
||||||
|
);
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { EllipsisDisplay } from './EllipsisDisplay';
|
|||||||
|
|
||||||
type TextDisplayProps = {
|
type TextDisplayProps = {
|
||||||
text: string;
|
text: string;
|
||||||
|
maxWidth?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextDisplay = ({ text }: TextDisplayProps) => (
|
export const TextDisplay = ({ text, maxWidth }: TextDisplayProps) => (
|
||||||
<EllipsisDisplay>{text}</EllipsisDisplay>
|
<EllipsisDisplay maxWidth={maxWidth}>{text}</EllipsisDisplay>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -163,7 +163,6 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
modifyViewFromCache(view.id, {
|
modifyViewFromCache(view.id, {
|
||||||
// Todo fix typing
|
|
||||||
viewFields: (viewFieldsRef, { readField }) => {
|
viewFields: (viewFieldsRef, { readField }) => {
|
||||||
const edges = readField<{ node: Reference }[]>(
|
const edges = readField<{ node: Reference }[]>(
|
||||||
'edges',
|
'edges',
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { IsSystem } from 'src/workspace/workspace-sync-metadata/decorators/is-sy
|
|||||||
import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
|
import { ObjectMetadata } from 'src/workspace/workspace-sync-metadata/decorators/object-metadata.decorator';
|
||||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||||
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
||||||
|
import { OpportunityObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/opportunity.object-metadata';
|
||||||
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
||||||
import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
|
import { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
|
||||||
|
|
||||||
@ -55,4 +56,14 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata {
|
|||||||
})
|
})
|
||||||
@IsNullable()
|
@IsNullable()
|
||||||
company: CompanyObjectMetadata;
|
company: CompanyObjectMetadata;
|
||||||
|
|
||||||
|
@FieldMetadata({
|
||||||
|
type: FieldMetadataType.RELATION,
|
||||||
|
label: 'Opportunity',
|
||||||
|
description: 'Favorite opportunity',
|
||||||
|
icon: 'IconTargetArrow',
|
||||||
|
joinColumn: 'opportunityId',
|
||||||
|
})
|
||||||
|
@IsNullable()
|
||||||
|
opportunity: OpportunityObjectMetadata;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { RelationMetadata } from 'src/workspace/workspace-sync-metadata/decorato
|
|||||||
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
|
import { ActivityTargetObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/activity-target.object-metadata';
|
||||||
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
import { BaseObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/base.object-metadata';
|
||||||
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
import { CompanyObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/company.object-metadata';
|
||||||
|
import { FavoriteObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/favorite.object-metadata';
|
||||||
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
import { PersonObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/person.object-metadata';
|
||||||
import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata';
|
import { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata';
|
||||||
|
|
||||||
@ -86,6 +87,19 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
|||||||
@IsNullable()
|
@IsNullable()
|
||||||
company: CompanyObjectMetadata;
|
company: CompanyObjectMetadata;
|
||||||
|
|
||||||
|
@FieldMetadata({
|
||||||
|
type: FieldMetadataType.RELATION,
|
||||||
|
label: 'Favorites',
|
||||||
|
description: 'Favorites linked to the opportunity',
|
||||||
|
icon: 'IconHeart',
|
||||||
|
})
|
||||||
|
@RelationMetadata({
|
||||||
|
type: RelationMetadataType.ONE_TO_MANY,
|
||||||
|
objectName: 'favorite',
|
||||||
|
})
|
||||||
|
@IsNullable()
|
||||||
|
favorites: FavoriteObjectMetadata[];
|
||||||
|
|
||||||
@FieldMetadata({
|
@FieldMetadata({
|
||||||
type: FieldMetadataType.RELATION,
|
type: FieldMetadataType.RELATION,
|
||||||
label: 'Activities',
|
label: 'Activities',
|
||||||
|
|||||||
Reference in New Issue
Block a user