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
14 changed files with 88 additions and 56 deletions

View File

@ -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: {

View File

@ -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:

View File

@ -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>(

View File

@ -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} />;
}; };

View File

@ -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,

View File

@ -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);

View File

@ -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}

View File

@ -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>

View File

@ -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} />
/>
</>
); );
}; };

View File

@ -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>
);

View File

@ -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>
); );

View File

@ -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',

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 { 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;
} }

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 { 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',