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:
@ -138,7 +138,7 @@ export const SettingsObjectFieldSelectFormOptionRow = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!!onRemove && (
|
{!!onRemove && !isDefault && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
accent="danger"
|
accent="danger"
|
||||||
LeftIcon={IconTrash}
|
LeftIcon={IconTrash}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import { useNavigate, useParams } from 'react-router-dom';
|
|||||||
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem';
|
||||||
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
|
||||||
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings';
|
||||||
|
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||||
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug';
|
||||||
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
|
||||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
@ -26,6 +27,15 @@ import {
|
|||||||
RelationMetadataType,
|
RelationMetadataType,
|
||||||
} from '~/generated-metadata/graphql';
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
const canPersistFieldMetadataItemUpdate = (
|
||||||
|
fieldMetadataItem: FieldMetadataItem,
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
fieldMetadataItem.isCustom ||
|
||||||
|
fieldMetadataItem.type === FieldMetadataType.Select
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const SettingsObjectFieldEdit = () => {
|
export const SettingsObjectFieldEdit = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
@ -172,6 +182,9 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
navigate(`/settings/objects/${objectSlug}`);
|
navigate(`/settings/objects/${objectSlug}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const shouldDisplaySaveAndCancel =
|
||||||
|
canPersistFieldMetadataItemUpdate(activeMetadataField);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
<SettingsPageContainer>
|
<SettingsPageContainer>
|
||||||
@ -186,7 +199,7 @@ export const SettingsObjectFieldEdit = () => {
|
|||||||
{ children: activeMetadataField.label },
|
{ children: activeMetadataField.label },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{activeMetadataField.isCustom && (
|
{shouldDisplaySaveAndCancel && (
|
||||||
<SaveAndCancelButtons
|
<SaveAndCancelButtons
|
||||||
isSaveDisabled={!canSave}
|
isSaveDisabled={!canSave}
|
||||||
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
onCancel={() => navigate(`/settings/objects/${objectSlug}`)}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const seedOpportunity = async (
|
|||||||
amountCurrencyCode: 'USD',
|
amountCurrencyCode: 'USD',
|
||||||
closeDate: new Date(),
|
closeDate: new Date(),
|
||||||
probability: 0.5,
|
probability: 0.5,
|
||||||
stage: 'new',
|
stage: 'NEW',
|
||||||
position: 0,
|
position: 0,
|
||||||
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
|
pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
|
||||||
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5',
|
||||||
@ -44,7 +44,7 @@ export const seedOpportunity = async (
|
|||||||
amountCurrencyCode: 'USD',
|
amountCurrencyCode: 'USD',
|
||||||
closeDate: new Date(),
|
closeDate: new Date(),
|
||||||
probability: 0.5,
|
probability: 0.5,
|
||||||
stage: 'meeting',
|
stage: 'MEETING',
|
||||||
position: 1,
|
position: 1,
|
||||||
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
|
pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
|
||||||
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae',
|
||||||
@ -57,7 +57,7 @@ export const seedOpportunity = async (
|
|||||||
amountCurrencyCode: 'USD',
|
amountCurrencyCode: 'USD',
|
||||||
closeDate: new Date(),
|
closeDate: new Date(),
|
||||||
probability: 0.5,
|
probability: 0.5,
|
||||||
stage: 'proposal',
|
stage: 'PROPOSAL',
|
||||||
position: 2,
|
position: 2,
|
||||||
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
||||||
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2',
|
||||||
@ -70,7 +70,7 @@ export const seedOpportunity = async (
|
|||||||
amountCurrencyCode: 'USD',
|
amountCurrencyCode: 'USD',
|
||||||
closeDate: new Date(),
|
closeDate: new Date(),
|
||||||
probability: 0.5,
|
probability: 0.5,
|
||||||
stage: 'proposal',
|
stage: 'PROPOSAL',
|
||||||
position: 3,
|
position: 3,
|
||||||
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02',
|
||||||
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',
|
pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3',
|
||||||
|
|||||||
@ -14,31 +14,31 @@ export const seedPipelineStep = async (
|
|||||||
.values([
|
.values([
|
||||||
{
|
{
|
||||||
id: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
|
id: '6edf4ead-006a-46e1-9c6d-228f1d0143c9',
|
||||||
name: 'New',
|
name: 'NEW',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
|
id: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a',
|
||||||
name: 'Screening',
|
name: 'SCREENING',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '30b14887-d592-427d-bd97-6e670158db02',
|
id: '30b14887-d592-427d-bd97-6e670158db02',
|
||||||
name: 'Meeting',
|
name: 'MEETING',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'db5a6648-d80d-4020-af64-4817ab4a12e8',
|
id: 'db5a6648-d80d-4020-af64-4817ab4a12e8',
|
||||||
name: 'Proposal',
|
name: 'PROPOSAL',
|
||||||
color: 'turquoise',
|
color: 'turquoise',
|
||||||
position: 3,
|
position: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'bea8bb7b-5467-48a6-9a8a-a8fa500123fe',
|
id: 'bea8bb7b-5467-48a6-9a8a-a8fa500123fe',
|
||||||
name: 'Customer',
|
name: 'CUSTOMER',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
position: 4,
|
position: 4,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -186,15 +186,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
throw new NotFoundException('Field does not exist');
|
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 =
|
const objectMetadata =
|
||||||
await this.objectMetadataService.findOneWithinWorkspace(
|
await this.objectMetadataService.findOneWithinWorkspace(
|
||||||
fieldMetadataInput.workspaceId,
|
fieldMetadataInput.workspaceId,
|
||||||
@ -217,7 +208,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
throw new BadRequestException('Cannot deactivate label identifier field');
|
throw new BadRequestException('Cannot deactivate label identifier field');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the id of the options has been provided
|
|
||||||
if (fieldMetadataInput.options) {
|
if (fieldMetadataInput.options) {
|
||||||
for (const option of fieldMetadataInput.options) {
|
for (const option of fieldMetadataInput.options) {
|
||||||
if (!option.id) {
|
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(
|
await this.workspaceMigrationService.createCustomMigration(
|
||||||
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
generateMigrationName(`update-${updatedFieldMetadata.name}`),
|
||||||
existingFieldMetadata.workspaceId,
|
existingFieldMetadata.workspaceId,
|
||||||
@ -288,4 +286,27 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
|
|||||||
public async deleteFieldsMetadata(workspaceId: string) {
|
public async deleteFieldsMetadata(workspaceId: string) {
|
||||||
await this.fieldMetadataRepository.delete({ workspaceId });
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const getRandomPipelineStepId = (pipelineStepIds: { id: string }[]) =>
|
|||||||
pipelineStepIds[Math.floor(Math.random() * pipelineStepIds.length)].id;
|
pipelineStepIds[Math.floor(Math.random() * pipelineStepIds.length)].id;
|
||||||
|
|
||||||
const getRandomStage = () => {
|
const getRandomStage = () => {
|
||||||
const stages = ['new', 'screening', 'meeting', 'proposal', 'customer'];
|
const stages = ['NEW', 'SCREENING', 'MEETING', 'PROPOSAL', 'CUSTOMER'];
|
||||||
|
|
||||||
return stages[Math.floor(Math.random() * stages.length)];
|
return stages[Math.floor(Math.random() * stages.length)];
|
||||||
};
|
};
|
||||||
|
|||||||
@ -11,27 +11,27 @@ export const pipelineStepPrefillData = async (
|
|||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
{
|
{
|
||||||
name: 'New',
|
name: 'NEW',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Screening',
|
name: 'SCREENING',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Meeting',
|
name: 'MEETING',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Proposal',
|
name: 'PROPOSAL',
|
||||||
color: 'turquoise',
|
color: 'turquoise',
|
||||||
position: 3,
|
position: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Customer',
|
name: 'CUSTOMER',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
position: 4,
|
position: 4,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -11,27 +11,27 @@ export const pipelineStepPrefillData = async (
|
|||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
{
|
{
|
||||||
name: 'New',
|
name: 'NEW',
|
||||||
color: 'red',
|
color: 'red',
|
||||||
position: 0,
|
position: 0,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Screening',
|
name: 'SCREENING',
|
||||||
color: 'purple',
|
color: 'purple',
|
||||||
position: 1,
|
position: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Meeting',
|
name: 'MEETING',
|
||||||
color: 'sky',
|
color: 'sky',
|
||||||
position: 2,
|
position: 2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Proposal',
|
name: 'PROPOSAL',
|
||||||
color: 'turquoise',
|
color: 'turquoise',
|
||||||
position: 3,
|
position: 3,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Customer',
|
name: 'CUSTOMER',
|
||||||
color: 'yellow',
|
color: 'yellow',
|
||||||
position: 4,
|
position: 4,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -138,13 +138,13 @@ export class WorkspaceMigrationEnumService {
|
|||||||
.map((e) => `'${e}'`)
|
.map((e) => `'${e}'`)
|
||||||
.join(', ')}]`;
|
.join(', ')}]`;
|
||||||
} else {
|
} else {
|
||||||
defaultValue = `'${columnDefinition.defaultValue}'`;
|
defaultValue = this.getStringifyValue(columnDefinition.defaultValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
UPDATE "${schemaName}"."${tableName}"
|
UPDATE "${schemaName}"."${tableName}"
|
||||||
SET "${columnDefinition.columnName}" = ${defaultValue}
|
SET "${columnDefinition.columnName}" = '${defaultValue}'
|
||||||
WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
|
WHERE "${columnDefinition.columnName}" NOT IN (${enumValues
|
||||||
.map((e) => `'${e}'`)
|
.map((e) => `'${e}'`)
|
||||||
.join(', ')})
|
.join(', ')})
|
||||||
@ -159,7 +159,7 @@ export class WorkspaceMigrationEnumService {
|
|||||||
newEnumTypeName: string,
|
newEnumTypeName: string,
|
||||||
) {
|
) {
|
||||||
await queryRunner.query(
|
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}"
|
RENAME TO "${oldEnumTypeName}"
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getStringifyValue(value: any) {
|
||||||
|
return typeof value === 'string' ? value : `'${value}'`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -275,6 +275,8 @@ export class WorkspaceMigrationRunnerService {
|
|||||||
tableName,
|
tableName,
|
||||||
migrationColumn,
|
migrationColumn,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@ -63,18 +63,18 @@ export class OpportunityObjectMetadata extends BaseObjectMetadata {
|
|||||||
description: 'Opportunity stage',
|
description: 'Opportunity stage',
|
||||||
icon: 'IconProgressCheck',
|
icon: 'IconProgressCheck',
|
||||||
options: [
|
options: [
|
||||||
{ value: 'new', label: 'New', position: 0, color: 'red' },
|
{ value: 'NEW', label: 'New', position: 0, color: 'red' },
|
||||||
{ value: 'screening', label: 'Screening', position: 1, color: 'purple' },
|
{ value: 'SCREENING', label: 'Screening', position: 1, color: 'purple' },
|
||||||
{ value: 'meeting', label: 'Meeting', position: 2, color: 'sky' },
|
{ value: 'MEETING', label: 'Meeting', position: 2, color: 'sky' },
|
||||||
{
|
{
|
||||||
value: 'proposal',
|
value: 'PROPOSAL',
|
||||||
label: 'Proposal',
|
label: 'Proposal',
|
||||||
position: 3,
|
position: 3,
|
||||||
color: 'turquoise',
|
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;
|
stage: string;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user