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
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
|
||||
@ -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}`)}
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)];
|
||||
};
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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}'`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,6 +275,8 @@ export class WorkspaceMigrationRunnerService {
|
||||
tableName,
|
||||
migrationColumn,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user