v1.2.0 — Client brief intake (marketplace B2) (#22290)

## Summary

**Version:** 1.2.0
(`packages/twenty-apps/internal/twenty-partners/package.json`)

Adds marketplace B2 client brief intake to the partners app:

- `POST /s/client-briefs` logic function — validates payload, creates an
unlisted Opportunity (`isListed = false`) with `— client brief` name
suffix
- **Client briefs** ops view for review before listing
- Unit + integration tests

Merge this **before** the website PR (`rk-client-brief-web`).

## Test plan

- [ ] `yarn twenty dev --once` syncs schema
- [ ] `yarn test` passes in `twenty-partners`
- [ ] Smoke: `POST /s/client-briefs` with `x-application-secret` creates
Opportunity with `isListed = false`
- [ ] Client briefs view visible in nav

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22290?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Rashad Karanouh
2026-06-30 14:08:50 +02:00
committed by GitHub
parent 3e53a16b27
commit e0ebe0e029
14 changed files with 529 additions and 105 deletions
@@ -1,6 +1,6 @@
{
"name": "twenty-partners",
"version": "1.1.16",
"version": "1.2.0",
"license": "MIT",
"engines": {
"node": "^24.5.0",
@@ -39,3 +39,16 @@ export const PARTNER_CONTENT_VIEW_UNIVERSAL_IDENTIFIER = 'd9db705c-795a-4a14-b89
export const PARTNER_APPLICATIONS_NAV_UNIVERSAL_IDENTIFIER = '13e2334a-6b1e-4080-8c74-d11109990cc1';
export const VALIDATED_PARTNERS_NAV_UNIVERSAL_IDENTIFIER = '6aed30c6-d80f-4ac6-aab0-db5bc59e5c4b';
export const PARTNER_CONTENT_NAV_UNIVERSAL_IDENTIFIER = '3543723d-80c1-466a-ac35-86f7b284917b';
// Opportunity record page (standard side panel Fields widget) — view-field UIDs
// reused from the former custom FIELDS_WIDGET view so sync updates in place.
export const OPPORTUNITY_RECORD_PAGE_IS_LISTED_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'5f8c539e-82df-4db6-af05-d4c4f9b262b1';
export const OPPORTUNITY_RECORD_PAGE_NEED_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'd44a8293-f9c5-41f6-9e08-85441475a1ce';
export const OPPORTUNITY_RECORD_PAGE_REQUIREMENTS_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'3a97a3ce-ecfd-417f-8518-120899f1111e';
export const OPPORTUNITY_RECORD_PAGE_PARTNER_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'd186d83a-2f6b-4693-b7eb-abc47d110ae8';
export const OPPORTUNITY_RECORD_PAGE_APPLICATIONS_VIEW_FIELD_UNIVERSAL_IDENTIFIER =
'5c286d5d-c849-4210-ba55-7a2e7dc28ff1';
@@ -0,0 +1,120 @@
import { CoreApiClient } from 'twenty-client-sdk/core';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
import { handler, type SubmitClientBriefInput } from '../submit-client-brief.logic-function';
const TEST_SECRET = 'test-secret-abc123';
process.env.PARTNER_APPLICATION_SECRET = TEST_SECRET;
const client = new CoreApiClient();
const baseInput = (overrides: Partial<SubmitClientBriefInput> = {}): SubmitClientBriefInput => ({
firstName: 'Jane',
lastName: 'Smith',
email: `jane.brief.${Date.now()}@example.com`,
companyName: `Acme Brief Co ${Date.now()}`,
need: 'Set up CRM pipelines for sales',
...overrides,
});
const authedEvent = (input: SubmitClientBriefInput) => ({
body: input,
headers: { 'x-application-secret': TEST_SECRET },
});
const createdOpportunityIds: string[] = [];
const createdPersonIds: string[] = [];
const createdCompanyIds: string[] = [];
async function cleanup(): Promise<void> {
for (const id of createdOpportunityIds.splice(0)) {
await client.mutation({ destroyOpportunity: { __args: { id }, id: true } }).catch(() => {});
}
for (const id of createdPersonIds.splice(0)) {
await client.mutation({ destroyPerson: { __args: { id }, id: true } }).catch(() => {});
}
for (const id of createdCompanyIds.splice(0)) {
await client.mutation({ destroyCompany: { __args: { id }, id: true } }).catch(() => {});
}
}
async function trackCreatedOpportunity(opportunityId: string): Promise<void> {
createdOpportunityIds.push(opportunityId);
const fetched = await client.query({
opportunity: {
__args: { filter: { id: { eq: opportunityId } } },
company: { id: true },
pointOfContact: { id: true },
},
});
if (fetched.opportunity?.company?.id) createdCompanyIds.push(fetched.opportunity.company.id);
if (fetched.opportunity?.pointOfContact?.id) {
createdPersonIds.push(fetched.opportunity.pointOfContact.id);
}
}
afterEach(async () => {
await cleanup();
});
beforeAll(async () => {
await client.query({ opportunities: { __args: { first: 1 }, edges: { node: { id: true } } } });
});
describe('submit-client-brief handler', () => {
it('returns unauthorized without secret', async () => {
const result = await handler({ body: baseInput(), headers: {} });
expect(result).toEqual({ ok: false, reason: 'unauthorized' });
});
it('creates an unlisted opportunity with marketplace brief name suffix', async () => {
const input = baseInput({
requirements: 'French UI',
hostingType: 'CLOUD',
seatCount: '~30',
});
const result = await handler(authedEvent(input));
expect(result.ok).toBe(true);
if (!result.ok) return;
await trackCreatedOpportunity(result.opportunityId);
const fetched = await client.query({
opportunity: {
__args: { filter: { id: { eq: result.opportunityId } } },
id: true,
name: true,
need: true,
requirements: true,
isListed: true,
stage: true,
company: { id: true, name: true },
pointOfContact: { id: true, emails: { primaryEmail: true } },
},
});
const opp = fetched.opportunity;
expect(opp?.name).toBe(`${input.companyName} — marketplace brief`);
expect(opp?.need).toBe(input.need);
expect(opp?.requirements).toContain('French UI');
expect(opp?.requirements).toContain('Additional context:');
expect(opp?.isListed).toBe(false);
expect(opp?.stage).toBe('NEW');
expect(opp?.company?.name).toBe(input.companyName);
expect(opp?.pointOfContact?.emails?.primaryEmail).toBe(input.email);
});
it('creates a second opportunity for the same company with different need', async () => {
const companyName = `Repeat Co ${Date.now()}`;
const email = `repeat.${Date.now()}@example.com`;
const first = await handler(authedEvent(baseInput({ companyName, email, need: 'Project A' })));
const second = await handler(authedEvent(baseInput({ companyName, email, need: 'Project B' })));
expect(first.ok && second.ok).toBe(true);
if (!first.ok || !second.ok) return;
expect(first.opportunityId).not.toBe(second.opportunityId);
await trackCreatedOpportunity(first.opportunityId);
await trackCreatedOpportunity(second.opportunityId);
});
});
@@ -0,0 +1,44 @@
import { describe, expect, it } from 'vitest';
import { buildRequirementsText } from '../submit-client-brief.logic-function';
const base = {
firstName: 'Jane',
lastName: 'Smith',
email: 'jane@acme.com',
companyName: 'Acme Real Estate',
need: 'Migrate from HubSpot',
};
describe('buildRequirementsText', () => {
it('returns null when no requirements or context fields', () => {
expect(buildRequirementsText(base)).toBeNull();
});
it('returns only base requirements when no context', () => {
expect(buildRequirementsText({ ...base, requirements: 'Must go live Q4' })).toBe(
'Must go live Q4',
);
});
it('appends additional context block when context fields present', () => {
const text = buildRequirementsText({
...base,
requirements: 'French UI',
hostingType: 'CLOUD',
seatCount: '~30',
country: 'France',
});
expect(text).toContain('French UI');
expect(text).toContain('Additional context:');
expect(text).toContain('• Hosting: Cloud');
expect(text).toContain('• Seats: ~30');
expect(text).toContain('• Country: France');
});
it('omits empty context bullets', () => {
const text = buildRequirementsText({ ...base, hostingType: 'SELF_HOSTING' });
expect(text).toContain('• Hosting: Self-hosting');
expect(text).not.toContain('• Seats:');
});
});
@@ -0,0 +1,84 @@
import type { CoreApiClient } from 'twenty-client-sdk/core';
export async function findCompanyIdByExactName(
client: CoreApiClient,
companyName: string,
): Promise<string | undefined> {
const name = companyName.trim();
const lookup = await client.query({
companies: {
__args: { filter: { name: { eq: name } }, first: 1 },
edges: { node: { id: true } },
},
});
return lookup.companies?.edges?.[0]?.node?.id;
}
export async function findOrCreateCompanyByName(
client: CoreApiClient,
companyName: string,
): Promise<string> {
const existing = await findCompanyIdByExactName(client, companyName);
if (existing !== undefined) return existing;
const name = companyName.trim();
const result = await client.mutation({
createCompany: { __args: { data: { name } }, id: true },
});
const id = result.createCompany?.id;
if (id === undefined) throw new Error('createCompany did not return an id');
return id;
}
export async function findPersonIdByPrimaryEmail(
client: CoreApiClient,
email: string,
): Promise<string | undefined> {
const primaryEmail = email.trim();
const lookup = await client.query({
people: {
__args: { filter: { emails: { primaryEmail: { eq: primaryEmail } } }, first: 1 },
edges: { node: { id: true } },
},
});
return lookup.people?.edges?.[0]?.node?.id;
}
export type FindOrCreatePersonByEmailInput = {
email: string;
firstName: string;
lastName: string;
companyId: string;
};
export async function findOrCreatePersonByEmail(
client: CoreApiClient,
input: FindOrCreatePersonByEmailInput,
): Promise<string> {
const email = input.email.trim();
const firstName = input.firstName.trim();
const lastName = input.lastName.trim();
const existing = await findPersonIdByPrimaryEmail(client, email);
if (existing !== undefined) return existing;
const result = await client.mutation({
createPerson: {
__args: {
data: {
name: { firstName, lastName },
emails: { primaryEmail: email },
companyId: input.companyId,
},
},
id: true,
},
});
const id = result.createPerson?.id;
if (id === undefined) throw new Error('createPerson did not return an id');
return id;
}
@@ -2,6 +2,11 @@ import { CoreApiClient, type CoreSchema } from 'twenty-client-sdk/core';
import { defineLogicFunction } from 'twenty-sdk/define';
import { z } from 'zod';
import {
findCompanyIdByExactName,
findPersonIdByPrimaryEmail,
} from './find-or-create-company-and-person';
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
@@ -71,14 +76,8 @@ async function findOrCreateCompanyId(
if (name === undefined && domain === undefined) return undefined;
if (name !== undefined) {
const lookup = await client.query({
companies: {
__args: { filter: { name: { eq: name } }, first: 1 },
edges: { node: { id: true } },
},
});
const existing = lookup.companies?.edges?.[0]?.node;
if (existing) return existing.id;
const existing = await findCompanyIdByExactName(client, name);
if (existing !== undefined) return existing;
}
const companyData: CoreSchema.CompanyCreateInput = { name: name ?? domain! };
@@ -110,14 +109,8 @@ async function findOrCreatePersonId(
if (email === undefined && firstName === '' && lastName === '') return undefined;
if (email !== undefined) {
const lookup = await client.query({
people: {
__args: { filter: { emails: { primaryEmail: { eq: email } } }, first: 1 },
edges: { node: { id: true } },
},
});
const existing = lookup.people?.edges?.[0]?.node;
if (existing) return existing.id;
const existing = await findPersonIdByPrimaryEmail(client, email);
if (existing !== undefined) return existing;
}
const personData: CoreSchema.PersonCreateInput = { name: { firstName, lastName } };
@@ -0,0 +1,149 @@
import { CoreApiClient, type CoreSchema } from 'twenty-client-sdk/core';
import { defineLogicFunction } from 'twenty-sdk/define';
import { z } from 'zod';
import {
findOrCreateCompanyByName,
findOrCreatePersonByEmail,
} from './find-or-create-company-and-person';
export const SUBMIT_CLIENT_BRIEF_LOGIC_FUNCTION_ID =
'a8f3c2e1-9b4d-4a7f-8c6e-1d2f3a4b5c6d';
const HOSTING_LABEL: Record<'CLOUD' | 'SELF_HOSTING', string> = {
CLOUD: 'Cloud',
SELF_HOSTING: 'Self-hosting',
};
export const submitClientBriefSchema = z.object({
firstName: z.string().trim().min(1),
lastName: z.string(),
email: z.string().trim().email(),
companyName: z.string().trim().min(1),
need: z.string().trim().min(1),
requirements: z.string().optional(),
hostingType: z.enum(['CLOUD', 'SELF_HOSTING']).optional(),
country: z.string().optional(),
languages: z.array(z.string()).optional(),
seatCount: z.string().optional(),
timeline: z.string().optional(),
budgetRange: z.string().optional(),
});
export type SubmitClientBriefInput = z.infer<typeof submitClientBriefSchema>;
export type SubmitClientBriefResult =
| { ok: true; opportunityId: string }
| { ok: false; reason: string };
function isNonEmptyString(value: unknown): value is string {
return typeof value === 'string' && value.trim().length > 0;
}
export function buildRequirementsText(input: SubmitClientBriefInput): string | null {
const base = isNonEmptyString(input.requirements) ? input.requirements.trim() : '';
const bullets: string[] = [];
if (input.hostingType !== undefined) {
bullets.push(`• Hosting: ${HOSTING_LABEL[input.hostingType]}`);
}
if (isNonEmptyString(input.seatCount)) bullets.push(`• Seats: ${input.seatCount.trim()}`);
if (isNonEmptyString(input.country)) bullets.push(`• Country: ${input.country.trim()}`);
if (input.languages !== undefined && input.languages.length > 0) {
bullets.push(`• Languages: ${input.languages.join(', ')}`);
}
if (isNonEmptyString(input.timeline)) bullets.push(`• Timeline: ${input.timeline.trim()}`);
if (isNonEmptyString(input.budgetRange)) bullets.push(`• Budget: ${input.budgetRange.trim()}`);
if (bullets.length === 0) return base.length > 0 ? base : null;
const block = `---\nAdditional context:\n${bullets.join('\n')}`;
return base ? `${base}\n\n${block}` : block;
}
type SubmitClientBriefEvent = {
headers?: Record<string, string | undefined>;
body?: unknown;
};
const APPLICATION_SECRET_HEADER = 'x-application-secret';
export const handler = async (
event: SubmitClientBriefEvent | SubmitClientBriefInput,
): Promise<SubmitClientBriefResult> => {
const looksLikeEvent =
typeof event === 'object' &&
event !== null &&
('body' in event || 'headers' in event);
const headers = looksLikeEvent
? (event as SubmitClientBriefEvent).headers ?? {}
: {};
const rawInput = looksLikeEvent
? (event as SubmitClientBriefEvent).body
: event;
const expectedSecret = process.env.PARTNER_APPLICATION_SECRET;
if (!isNonEmptyString(expectedSecret)) {
return { ok: false, reason: 'unauthorized' };
}
const providedSecret = headers[APPLICATION_SECRET_HEADER];
if (providedSecret !== expectedSecret) {
return { ok: false, reason: 'unauthorized' };
}
const parsed = submitClientBriefSchema.safeParse(rawInput);
if (!parsed.success) {
return { ok: false, reason: 'invalid_input' };
}
const input = parsed.data;
try {
const client = new CoreApiClient();
const name = `${input.companyName.trim()} — marketplace brief`;
const requirements = buildRequirementsText(input);
const companyId = await findOrCreateCompanyByName(client, input.companyName);
const pointOfContactId = await findOrCreatePersonByEmail(client, {
email: input.email,
firstName: input.firstName,
lastName: input.lastName,
companyId,
});
const opportunityData: CoreSchema.OpportunityCreateInput = {
name,
need: input.need,
isListed: false,
stage: 'NEW',
companyId,
pointOfContactId,
};
if (requirements !== null) {
opportunityData.requirements = requirements;
}
const result = await client.mutation({
createOpportunity: { __args: { data: opportunityData }, id: true },
});
const opportunityId = result.createOpportunity?.id;
if (opportunityId === undefined) {
throw new Error('createOpportunity did not return an id');
}
return { ok: true, opportunityId };
} catch (err) {
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
}
};
export default defineLogicFunction({
universalIdentifier: SUBMIT_CLIENT_BRIEF_LOGIC_FUNCTION_ID,
name: 'submit-client-brief',
description: 'Create an unlisted Opportunity from the public marketplace brief form.',
timeoutSeconds: 15,
handler,
httpRouteTriggerSettings: {
path: '/client-briefs',
httpMethod: 'POST',
isAuthRequired: false,
forwardedRequestHeaders: [APPLICATION_SECRET_HEADER],
},
});
@@ -4,11 +4,13 @@ import {
definePageLayout,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_FIELDS_VIEW_ID } from 'src/views/opportunity-record-page-fields.view';
const OPPORTUNITY_RECORD_PAGE_FIELDS_VIEW_ID =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views
.opportunityRecordPageFields.universalIdentifier;
// Opportunity is a standard object, but we override its record page so the Fields widget
// points at a FIELDS_WIDGET view — surfacing partner + applications relations in the side
// panel (hidden by the platform default).
// points at the standard record-page view — extended with app view-fields for brief
// fields plus partner + applications relations in the side panel.
export default definePageLayout({
universalIdentifier: 'cf2c66e4-0a4b-48ce-8669-fdf39dd64148',
name: 'Default Opportunity Layout',
@@ -0,0 +1,21 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_APPLICATIONS_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { APPLICATIONS_ON_OPPORTUNITY_FIELD_ID } from 'src/objects/application.object';
const OPPORTUNITY_RECORD_PAGE_FIELDS =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.opportunityRecordPageFields;
export default defineViewField({
universalIdentifier:
OPPORTUNITY_RECORD_PAGE_APPLICATIONS_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS.universalIdentifier,
fieldMetadataUniversalIdentifier: APPLICATIONS_ON_OPPORTUNITY_FIELD_ID,
viewFieldGroupUniversalIdentifier:
OPPORTUNITY_RECORD_PAGE_FIELDS.viewFieldGroups.relations.universalIdentifier,
position: 4,
isVisible: true,
});
@@ -0,0 +1,21 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_IS_LISTED_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { OPPORTUNITY_IS_LISTED_FIELD_ID } from 'src/fields/opportunity-is-listed.field';
const OPPORTUNITY_RECORD_PAGE_FIELDS =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.opportunityRecordPageFields;
export default defineViewField({
universalIdentifier:
OPPORTUNITY_RECORD_PAGE_IS_LISTED_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS.universalIdentifier,
fieldMetadataUniversalIdentifier: OPPORTUNITY_IS_LISTED_FIELD_ID,
viewFieldGroupUniversalIdentifier:
OPPORTUNITY_RECORD_PAGE_FIELDS.viewFieldGroups.deal.universalIdentifier,
position: 3,
isVisible: true,
});
@@ -0,0 +1,20 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_NEED_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { OPPORTUNITY_NEED_FIELD_ID } from 'src/fields/opportunity-need.field';
const OPPORTUNITY_RECORD_PAGE_FIELDS =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.opportunityRecordPageFields;
export default defineViewField({
universalIdentifier: OPPORTUNITY_RECORD_PAGE_NEED_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS.universalIdentifier,
fieldMetadataUniversalIdentifier: OPPORTUNITY_NEED_FIELD_ID,
viewFieldGroupUniversalIdentifier:
OPPORTUNITY_RECORD_PAGE_FIELDS.viewFieldGroups.deal.universalIdentifier,
position: 8,
isVisible: true,
});
@@ -0,0 +1,21 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_PARTNER_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { PARTNER_ON_OPPORTUNITY_FIELD_ID } from 'src/fields/partner-on-opportunity.field';
const OPPORTUNITY_RECORD_PAGE_FIELDS =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.opportunityRecordPageFields;
export default defineViewField({
universalIdentifier:
OPPORTUNITY_RECORD_PAGE_PARTNER_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS.universalIdentifier,
fieldMetadataUniversalIdentifier: PARTNER_ON_OPPORTUNITY_FIELD_ID,
viewFieldGroupUniversalIdentifier:
OPPORTUNITY_RECORD_PAGE_FIELDS.viewFieldGroups.relations.universalIdentifier,
position: 3,
isVisible: true,
});
@@ -0,0 +1,21 @@
import {
defineViewField,
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
} from 'twenty-sdk/define';
import { OPPORTUNITY_RECORD_PAGE_REQUIREMENTS_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
import { OPPORTUNITY_REQUIREMENTS_FIELD_ID } from 'src/fields/opportunity-requirements.field';
const OPPORTUNITY_RECORD_PAGE_FIELDS =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.views.opportunityRecordPageFields;
export default defineViewField({
universalIdentifier:
OPPORTUNITY_RECORD_PAGE_REQUIREMENTS_VIEW_FIELD_UNIVERSAL_IDENTIFIER,
viewUniversalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS.universalIdentifier,
fieldMetadataUniversalIdentifier: OPPORTUNITY_REQUIREMENTS_FIELD_ID,
viewFieldGroupUniversalIdentifier:
OPPORTUNITY_RECORD_PAGE_FIELDS.viewFieldGroups.deal.universalIdentifier,
position: 9,
isVisible: true,
});
@@ -1,85 +0,0 @@
import {
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
ViewType,
defineView,
} from 'twenty-sdk/define';
import { OPPORTUNITY_IS_LISTED_FIELD_ID } from 'src/fields/opportunity-is-listed.field';
import { OPPORTUNITY_NEED_FIELD_ID } from 'src/fields/opportunity-need.field';
import { OPPORTUNITY_REQUIREMENTS_FIELD_ID } from 'src/fields/opportunity-requirements.field';
import { PARTNER_ON_OPPORTUNITY_FIELD_ID } from 'src/fields/partner-on-opportunity.field';
import { APPLICATIONS_ON_OPPORTUNITY_FIELD_ID } from 'src/objects/application.object';
const OPPORTUNITY_STAGE_FIELD_ID =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.stage
.universalIdentifier;
const OPPORTUNITY_AMOUNT_FIELD_ID =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.amount
.universalIdentifier;
const OPPORTUNITY_CLOSE_DATE_FIELD_ID =
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.fields.closeDate
.universalIdentifier;
export const OPPORTUNITY_RECORD_PAGE_FIELDS_VIEW_ID =
'aa8a976c-aca6-4ca1-811e-fc3035e055aa';
// FIELDS_WIDGET view backing the Opportunity record page side panel. Relation fields
// (partner, applications) only render in the fields widget when an explicit view marks
// them visible — this is that view.
export default defineView({
universalIdentifier: OPPORTUNITY_RECORD_PAGE_FIELDS_VIEW_ID,
name: 'Opportunity Record Page Fields',
objectUniversalIdentifier:
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity.universalIdentifier,
type: ViewType.FIELDS_WIDGET,
fields: [
{
universalIdentifier: '979ac0d4-2db4-48a2-8474-ff8cebd56669',
fieldMetadataUniversalIdentifier: OPPORTUNITY_STAGE_FIELD_ID,
position: 0,
isVisible: true,
},
{
universalIdentifier: '3c0f8ac5-914d-4840-b124-36f089b49dcc',
fieldMetadataUniversalIdentifier: OPPORTUNITY_AMOUNT_FIELD_ID,
position: 1,
isVisible: true,
},
{
universalIdentifier: 'a6ee4ae4-c905-4143-b7af-fffa0ceabf2d',
fieldMetadataUniversalIdentifier: OPPORTUNITY_CLOSE_DATE_FIELD_ID,
position: 2,
isVisible: true,
},
{
universalIdentifier: '5f8c539e-82df-4db6-af05-d4c4f9b262b1',
fieldMetadataUniversalIdentifier: OPPORTUNITY_IS_LISTED_FIELD_ID,
position: 3,
isVisible: true,
},
{
universalIdentifier: 'd44a8293-f9c5-41f6-9e08-85441475a1ce',
fieldMetadataUniversalIdentifier: OPPORTUNITY_NEED_FIELD_ID,
position: 4,
isVisible: true,
},
{
universalIdentifier: '3a97a3ce-ecfd-417f-8518-120899f1111e',
fieldMetadataUniversalIdentifier: OPPORTUNITY_REQUIREMENTS_FIELD_ID,
position: 5,
isVisible: true,
},
{
universalIdentifier: 'd186d83a-2f6b-4693-b7eb-abc47d110ae8',
fieldMetadataUniversalIdentifier: PARTNER_ON_OPPORTUNITY_FIELD_ID,
position: 6,
isVisible: true,
},
{
universalIdentifier: '5c286d5d-c849-4210-ba55-7a2e7dc28ff1',
fieldMetadataUniversalIdentifier: APPLICATIONS_ON_OPPORTUNITY_FIELD_ID,
position: 7,
isVisible: true,
},
],
});