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:
Charles Bochet 2024-01-16 15:43:19 +01:00 committed by GitHub
parent bb91917ff8
commit f3f20ad974
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 88 additions and 56 deletions

View File

@ -229,6 +229,7 @@ export const CompanyBoardCard = () => {
<FieldContext.Provider
value={{
entityId: boardCardId,
maxWidth: 156,
recoilScopeId: boardCardId + viewField.fieldMetadataId,
isLabelIdentifier: false,
fieldDefinition: {

View File

@ -260,6 +260,7 @@ export const RecordShowPage = () => {
key={record.id + fieldMetadataItem.id}
value={{
entityId: record.id,
maxWidth: 272,
recoilScopeId: record.id + fieldMetadataItem.id,
isLabelIdentifier: false,
fieldDefinition:

View File

@ -28,6 +28,7 @@ export type GenericFieldContextType = {
isLabelIdentifier: boolean;
basePathToShowPage?: string;
clearable?: boolean;
maxWidth?: number;
};
export const FieldContext = createContext<GenericFieldContextType>(

View File

@ -3,7 +3,7 @@ import { TextDisplay } from '@/ui/field/display/components/TextDisplay';
import { useTextField } from '../../hooks/useTextField';
export const TextFieldDisplay = () => {
const { fieldValue } = useTextField();
const { fieldValue, maxWidth } = useTextField();
return <TextDisplay text={fieldValue} />;
return <TextDisplay text={fieldValue} maxWidth={maxWidth} />;
};

View File

@ -9,7 +9,8 @@ import { isFieldText } from '../../types/guards/isFieldText';
import { isFieldTextValue } from '../../types/guards/isFieldTextValue';
export const useTextField = () => {
const { entityId, fieldDefinition, hotkeyScope } = useContext(FieldContext);
const { entityId, fieldDefinition, hotkeyScope, maxWidth } =
useContext(FieldContext);
assertFieldMetadata('TEXT', isFieldText, fieldDefinition);
@ -30,6 +31,7 @@ export const useTextField = () => {
: fieldInitialValue?.value ?? fieldTextValue;
return {
maxWidth,
fieldDefinition,
fieldValue: fieldTextValue,
initialValue,

View File

@ -39,25 +39,20 @@ export const useRecordBoardCardFieldsInternal = (
.getLoadable(recordBoardCardFieldsScopedState({ scopeId }))
.getValue();
const existingFieldsUpdated = existingFields.map((previousField) =>
previousField.fieldMetadataId === field.fieldMetadataId
? { ...previousField, isVisible: !field.isVisible }
: previousField,
);
const isNewField = !existingFields.find(
const fieldIndex = existingFields.findIndex(
({ fieldMetadataId }) => field.fieldMetadataId === fieldMetadataId,
);
const fields = [...existingFields];
const fields = isNewField
? [
...existingFieldsUpdated,
{
...field,
position: existingFieldsUpdated.length,
},
]
: existingFieldsUpdated;
if (fieldIndex === -1) {
fields.push({ ...field, position: existingFields.length });
} else {
fields[fieldIndex] = {
...field,
isVisible: !field.isVisible,
position: existingFields.length,
};
}
setSavedBoardCardFields(fields);
setBoardCardFields(fields);

View File

@ -75,7 +75,7 @@ export const RecordRelationFieldCardContent = ({
objectNameSingular: objectMetadataNameSingular ?? '',
});
const modifyObjectMetadataInCache = useModifyRecordFromCache({
const modifyRecordFromCache = useModifyRecordFromCache({
objectMetadataItem,
});
@ -136,7 +136,7 @@ export const RecordRelationFieldCardContent = ({
},
});
modifyObjectMetadataInCache(entityId, {
modifyRecordFromCache(entityId, {
[fieldName]: (relationRef, { readField }) => {
const edges = readField<{ node: Reference }[]>('edges', relationRef);
@ -168,7 +168,7 @@ export const RecordRelationFieldCardContent = ({
<FieldDisplay />
</FieldContextProvider>
{/* TODO: temporary to prevent removing a company from an opportunity */}
{isOpportunityCompanyRelation && (
{!isOpportunityCompanyRelation && (
<DropdownScope dropdownScopeId={dropdownScopeId}>
<Dropdown
dropdownId={dropdownScopeId}

View File

@ -20,6 +20,7 @@ import { SingleEntitySelectMenuItemsWithSearch } from '@/object-record/relation-
import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useFilteredSearchEntityQuery } from '@/search/hooks/useFilteredSearchEntityQuery';
import { IconForbid, IconPlus } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
@ -111,12 +112,11 @@ export const RecordRelationFieldCardSection = () => {
const isToOneObject = relationType === 'TO_ONE_OBJECT';
const relationRecords = !isToOneObject
? fieldValue?.edges.map(({ node }: { node: any }) => node) ?? []
: fieldValue
const relationRecords: ObjectRecord[] =
fieldValue && isToOneObject
? [fieldValue]
: [];
const relationRecordIds = relationRecords.map(({ id }: { id: string }) => id);
: fieldValue?.edges.map(({ node }: { node: ObjectRecord }) => node) ?? [];
const relationRecordIds = relationRecords.map(({ id }) => id);
const dropdownId = `record-field-card-relation-picker-${fieldDefinition.label}`;
@ -138,7 +138,7 @@ export const RecordRelationFieldCardSection = () => {
},
],
orderByField: 'createdAt',
mappingFunction: (recordToMap: any) =>
mappingFunction: (recordToMap) =>
identifiersMapper?.(recordToMap, relationObjectMetadataNameSingular),
selectedIds: relationRecordIds,
excludeEntityIds: relationRecordIds,
@ -154,7 +154,7 @@ export const RecordRelationFieldCardSection = () => {
objectNameSingular: relationObjectMetadataNameSingular,
});
const modifyObjectMetadataInCache = useModifyRecordFromCache({
const modifyRecordFromCache = useModifyRecordFromCache({
objectMetadataItem,
});
@ -180,7 +180,7 @@ export const RecordRelationFieldCardSection = () => {
},
});
modifyObjectMetadataInCache(entityId, {
modifyRecordFromCache(entityId, {
[fieldName]: (relationRef, { readField }) => {
const edges = readField<{ node: Reference }[]>('edges', relationRef);
@ -248,15 +248,13 @@ export const RecordRelationFieldCardSection = () => {
</StyledHeader>
{!!relationRecords.length && (
<Card>
{relationRecords
.slice(0, 5)
.map((relationRecord: any, index: number) => (
<RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1}
relationRecord={relationRecord}
/>
))}
{relationRecords.slice(0, 5).map((relationRecord, index) => (
<RecordRelationFieldCardContent
key={`${relationRecord.id}${relationLabelIdentifierFieldMetadata?.id}`}
divider={index < relationRecords.length - 1}
relationRecord={relationRecord}
/>
))}
</Card>
)}
</RelationPickerScope>

View File

@ -70,17 +70,15 @@ export const RelationPicker = ({
onSubmit(selectedEntity ?? null);
return (
<>
<SingleEntitySelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
selectedEntity={entities.selectedEntities[0]}
width={width}
/>
</>
<SingleEntitySelect
EmptyIcon={IconForbid}
emptyLabel={'No ' + fieldDefinition.label}
entitiesToSelect={entities.entitiesToSelect}
loading={entities.loading}
onCancel={onCancel}
onEntitySelected={handleEntitySelected}
selectedEntity={entities.selectedEntities[0]}
width={width}
/>
);
};

View File

@ -1,10 +1,21 @@
import styled from '@emotion/styled';
const StyledEllipsisDisplay = styled.div`
const StyledEllipsisDisplay = styled.div<{ maxWidth?: number }>`
max-width: ${({ maxWidth }) => maxWidth ?? '100%'};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
`;
export { StyledEllipsisDisplay as EllipsisDisplay };
type EllipsisDisplayProps = {
children: React.ReactNode;
maxWidth?: number;
};
export const EllipsisDisplay = ({
children,
maxWidth,
}: EllipsisDisplayProps) => (
<StyledEllipsisDisplay style={{ maxWidth }}>{children}</StyledEllipsisDisplay>
);

View File

@ -2,8 +2,9 @@ import { EllipsisDisplay } from './EllipsisDisplay';
type TextDisplayProps = {
text: string;
maxWidth?: number;
};
export const TextDisplay = ({ text }: TextDisplayProps) => (
<EllipsisDisplay>{text}</EllipsisDisplay>
export const TextDisplay = ({ text, maxWidth }: TextDisplayProps) => (
<EllipsisDisplay maxWidth={maxWidth}>{text}</EllipsisDisplay>
);

View File

@ -163,7 +163,6 @@ export const SettingsObjectNewFieldStep2 = () => {
};
modifyViewFromCache(view.id, {
// Todo fix typing
viewFields: (viewFieldsRef, { readField }) => {
const edges = readField<{ node: Reference }[]>(
'edges',

View File

@ -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 { 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 { 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 { WorkspaceMemberObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/workspace-member.object-metadata';
@ -55,4 +56,14 @@ export class FavoriteObjectMetadata extends BaseObjectMetadata {
})
@IsNullable()
company: CompanyObjectMetadata;
@FieldMetadata({
type: FieldMetadataType.RELATION,
label: 'Opportunity',
description: 'Favorite opportunity',
icon: 'IconTargetArrow',
joinColumn: 'opportunityId',
})
@IsNullable()
opportunity: OpportunityObjectMetadata;
}

View File

@ -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 { 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 { 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 { PipelineStepObjectMetadata } from 'src/workspace/workspace-sync-metadata/standard-objects/pipeline-step.object-metadata';
@ -86,6 +87,19 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
@IsNullable()
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({
type: FieldMetadataType.RELATION,
label: 'Activities',