Set opportunity stage as editable (#3838)

* Set opportunity stage as editable

* Fix comments

* Add command for migration

* Fixes

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Thomas Trompette 2024-02-09 14:44:11 +01:00 committed by GitHub
parent 0185c2a36e
commit 9ceff84bbf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 83 additions and 43 deletions

View File

@ -138,7 +138,7 @@ export const SettingsObjectFieldSelectFormOptionRow = ({
}}
/>
)}
{!!onRemove && (
{!!onRemove && !isDefault && (
<MenuItem
accent="danger"
LeftIcon={IconTrash}

View File

@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
@ -26,6 +27,15 @@ import {
RelationMetadataType,
} from '~/generated-metadata/graphql';
const canPersistFieldMetadataItemUpdate = (
fieldMetadataItem: FieldMetadataItem,
) => {
return (
fieldMetadataItem.isCustom ||
fieldMetadataItem.type === FieldMetadataType.Select
);
};
export const SettingsObjectFieldEdit = () => {
const navigate = useNavigate();
const { enqueueSnackBar } = useSnackBar();
@ -172,6 +182,9 @@ export const SettingsObjectFieldEdit = () => {
navigate(`/settings/objects/${objectSlug}`);
};
const shouldDisplaySaveAndCancel =
canPersistFieldMetadataItemUpdate(activeMetadataField);
return (
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
<SettingsPageContainer>
@ -186,7 +199,7 @@ export const SettingsObjectFieldEdit = () => {
{ children: activeMetadataField.label },
]}
/>
{activeMetadataField.isCustom && (
{shouldDisplaySaveAndCancel && (
<SaveAndCancelButtons
isSaveDisabled={!canSave}
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}

View File

@ -31,7 +31,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'new',
stage: 'NEW',
position: 0,
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
@ -44,7 +44,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'meeting',
stage: 'MEETING',
position: 1,
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
@ -57,7 +57,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'proposal',
stage: 'PROPOSAL',
position: 2,
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
@ -70,7 +70,7 @@ export const seedOpportunity = async (
amountCurrencyCode: 'USD',
closeDate: new Date(),
probability: 0.5,
stage: 'proposal',
stage: 'PROPOSAL',
position: 3,
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',

View File

@ -14,31 +14,31 @@ export const seedPipelineStep = async (
.values([
{
id: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
name: 'New',
name: 'NEW',
color: 'red',
position: 0,
},
{
id: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
name: 'Screening',
name: 'SCREENING',
color: 'purple',
position: 1,
},
{
id: '30b14887-d592-427d-bd97-6e670158db02',
name: 'Meeting',
name: 'MEETING',
color: 'sky',
position: 2,
},
{
id: 'db5a6648-d80d-4020-af64-4817ab4a12e8',
name: 'Proposal',
name: 'PROPOSAL',
color: 'turquoise',
position: 3,
},
{
id: 'bea8bb7b-5467-48a6-9a8a-a8fa500123fe',
name: 'Customer',
name: 'CUSTOMER',
color: 'yellow',
position: 4,
},

View File

@ -186,15 +186,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw new NotFoundException('Field does not exist');
}
if (existingFieldMetadata.isCustom === false) {
// We can only update the isActive field for standard fields
fieldMetadataInput = {
id: fieldMetadataInput.id,
isActive: fieldMetadataInput.isActive,
workspaceId: fieldMetadataInput.workspaceId,
};
}
const objectMetadata =
await this.objectMetadataService.findOneWithinWorkspace(
fieldMetadataInput.workspaceId,
@ -217,7 +208,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
throw new BadRequestException('Cannot deactivate label identifier field');
}
// Check if the id of the options has been provided
if (fieldMetadataInput.options) {
for (const option of fieldMetadataInput.options) {
if (!option.id) {
@ -226,9 +216,17 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
}
}
const updatedFieldMetadata = await super.updateOne(id, fieldMetadataInput);
const updatableFieldInput =
existingFieldMetadata.isCustom === false
? this.buildUpdatableStandardFieldInput(
fieldMetadataInput,
existingFieldMetadata,
)
: fieldMetadataInput;
if (fieldMetadataInput.options || fieldMetadataInput.defaultValue) {
const updatedFieldMetadata = await super.updateOne(id, updatableFieldInput);
if (updatableFieldInput.options || updatableFieldInput.defaultValue) {
await this.workspaceMigrationService.createCustomMigration(
generateMigrationName(`update-${updatedFieldMetadata.name}`),
existingFieldMetadata.workspaceId,
@ -288,4 +286,27 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
public async deleteFieldsMetadata(workspaceId: string) {
await this.fieldMetadataRepository.delete({ workspaceId });
}
private buildUpdatableStandardFieldInput(
fieldMetadataInput: UpdateFieldInput,
existingFieldMetadata: FieldMetadataEntity,
) {
let fieldMetadataInputOverrided = {};
fieldMetadataInputOverrided = {
id: fieldMetadataInput.id,
isActive: fieldMetadataInput.isActive,
workspaceId: fieldMetadataInput.workspaceId,
defaultValue: fieldMetadataInput.defaultValue,
};
if (existingFieldMetadata.type === FieldMetadataType.SELECT) {
fieldMetadataInputOverrided = {
...fieldMetadataInputOverrided,
options: fieldMetadataInput.options,
};
}
return fieldMetadataInputOverrided as UpdateFieldInput;
}
}

View File

@ -13,7 +13,7 @@ const getRandomPipelineStepId = (pipelineStepIds: { id: string }[]) =>
pipelineStepIds[Math.floor(Math.random() * pipelineStepIds.length)].id;
const getRandomStage = () => {
const stages = ['new', 'screening', 'meeting', 'proposal', 'customer'];
const stages = ['NEW', 'SCREENING', 'MEETING', 'PROPOSAL', 'CUSTOMER'];
return stages[Math.floor(Math.random() * stages.length)];
};

View File

@ -11,27 +11,27 @@ export const pipelineStepPrefillData = async (
.orIgnore()
.values([
{
name: 'New',
name: 'NEW',
color: 'red',
position: 0,
},
{
name: 'Screening',
name: 'SCREENING',
color: 'purple',
position: 1,
},
{
name: 'Meeting',
name: 'MEETING',
color: 'sky',
position: 2,
},
{
name: 'Proposal',
name: 'PROPOSAL',
color: 'turquoise',
position: 3,
},
{
name: 'Customer',
name: 'CUSTOMER',
color: 'yellow',
position: 4,
},

View File

@ -11,27 +11,27 @@ export const pipelineStepPrefillData = async (
.orIgnore()
.values([
{
name: 'New',
name: 'NEW',
color: 'red',
position: 0,
},
{
name: 'Screening',
name: 'SCREENING',
color: 'purple',
position: 1,
},
{
name: 'Meeting',
name: 'MEETING',
color: 'sky',
position: 2,
},
{
name: 'Proposal',
name: 'PROPOSAL',
color: 'turquoise',
position: 3,
},
{
name: 'Customer',
name: 'CUSTOMER',
color: 'yellow',
position: 4,
},

View File

@ -138,13 +138,13 @@ export class WorkspaceMigrationEnumService {
.map((e) => `'${e}'`)
.join(', ')}]`;
} else {
defaultValue = `'${columnDefinition.defaultValue}'`;
defaultValue = this.getStringifyValue(columnDefinition.defaultValue);
}
}
await queryRunner.query(`
UPDATE "${schemaName}"."${tableName}"
SET "${columnDefinition.columnName}" = ${defaultValue}
SET "${columnDefinition.columnName}" = '${defaultValue}'
WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
.map((e) => `'${e}'`)
.join(', ')})
@ -159,7 +159,7 @@ export class WorkspaceMigrationEnumService {
newEnumTypeName: string,
) {
await queryRunner.query(
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" TYPE "${schemaName}"."${newEnumTypeName}" USING ("${columnName}"::text::"${schemaName}"."${newEnumTypeName}")`,
`ALTER TABLE "${schemaName}"."${tableName}" ALTER COLUMN "${columnName}" DROP DEFAULT, ALTER COLUMN "${columnName}" TYPE "${schemaName}"."${newEnumTypeName}" USING ("${columnName}"::text::"${schemaName}"."${newEnumTypeName}")`,
);
}
@ -184,4 +184,8 @@ export class WorkspaceMigrationEnumService {
RENAME TO "${oldEnumTypeName}"
`);
}
private getStringifyValue(value: any) {
return typeof value === 'string' ? value : `'${value}'`;
}
}

View File

@ -275,6 +275,8 @@ export class WorkspaceMigrationRunnerService {
tableName,
migrationColumn,
);
return;
}
if (

View File

@ -63,18 +63,18 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
description: 'Opportunity stage',
icon: 'IconProgressCheck',
options: [
{ value: 'new', label: 'New', position: 0, color: 'red' },
{ value: 'screening', label: 'Screening', position: 1, color: 'purple' },
{ value: 'meeting', label: 'Meeting', position: 2, color: 'sky' },
{ value: 'NEW', label: 'New', position: 0, color: 'red' },
{ value: 'SCREENING', label: 'Screening', position: 1, color: 'purple' },
{ value: 'MEETING', label: 'Meeting', position: 2, color: 'sky' },
{
value: 'proposal',
value: 'PROPOSAL',
label: 'Proposal',
position: 3,
color: 'turquoise',
},
{ value: 'customer', label: 'Customer', position: 4, color: 'yellow' },
{ value: 'CUSTOMER', label: 'Customer', position: 4, color: 'yellow' },
],
defaultValue: { value: 'new' },
defaultValue: { value: 'NEW' },
})
stage: string;