diff --git a/packages/twenty-apps/internal/twenty-partners/package.json b/packages/twenty-apps/internal/twenty-partners/package.json index 5a796c4bc2..bcd29373c5 100644 --- a/packages/twenty-apps/internal/twenty-partners/package.json +++ b/packages/twenty-apps/internal/twenty-partners/package.json @@ -1,6 +1,6 @@ { "name": "twenty-partners", - "version": "1.1.16", + "version": "1.2.0", "license": "MIT", "engines": { "node": "^24.5.0", diff --git a/packages/twenty-apps/internal/twenty-partners/src/constants/universal-identifiers.ts b/packages/twenty-apps/internal/twenty-partners/src/constants/universal-identifiers.ts index 1e9768f36b..48b27d3a84 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/constants/universal-identifiers.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/constants/universal-identifiers.ts @@ -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'; diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.integration-test.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.integration-test.ts new file mode 100644 index 0000000000..06c0dd66c3 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.integration-test.ts @@ -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 => ({ + 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 { + 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 { + 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); + }); +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.test.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.test.ts new file mode 100644 index 0000000000..619bba97da --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/__tests__/submit-client-brief.test.ts @@ -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:'); + }); +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/find-or-create-company-and-person.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/find-or-create-company-and-person.ts new file mode 100644 index 0000000000..ad920c3d75 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/find-or-create-company-and-person.ts @@ -0,0 +1,84 @@ +import type { CoreApiClient } from 'twenty-client-sdk/core'; + +export async function findCompanyIdByExactName( + client: CoreApiClient, + companyName: string, +): Promise { + 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 { + 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 { + 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 { + 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; +} diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts index 0b43b2c11d..35bb3b829d 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/import-opportunity-from-tft.logic-function.ts @@ -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 } }; diff --git a/packages/twenty-apps/internal/twenty-partners/src/logic-functions/submit-client-brief.logic-function.ts b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/submit-client-brief.logic-function.ts new file mode 100644 index 0000000000..da0fda42d3 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/logic-functions/submit-client-brief.logic-function.ts @@ -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; + +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; + body?: unknown; +}; + +const APPLICATION_SECRET_HEADER = 'x-application-secret'; + +export const handler = async ( + event: SubmitClientBriefEvent | SubmitClientBriefInput, +): Promise => { + 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], + }, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/page-layouts/opportunity.page-layout.ts b/packages/twenty-apps/internal/twenty-partners/src/page-layouts/opportunity.page-layout.ts index d59a947b56..bee7533d5f 100644 --- a/packages/twenty-apps/internal/twenty-partners/src/page-layouts/opportunity.page-layout.ts +++ b/packages/twenty-apps/internal/twenty-partners/src/page-layouts/opportunity.page-layout.ts @@ -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', diff --git a/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-applications.view-field.ts b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-applications.view-field.ts new file mode 100644 index 0000000000..70d479fffe --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-applications.view-field.ts @@ -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, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-is-listed.view-field.ts b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-is-listed.view-field.ts new file mode 100644 index 0000000000..3caea4ee32 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-is-listed.view-field.ts @@ -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, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-need.view-field.ts b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-need.view-field.ts new file mode 100644 index 0000000000..48d06ae21b --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-need.view-field.ts @@ -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, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-partner.view-field.ts b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-partner.view-field.ts new file mode 100644 index 0000000000..69ab044283 --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-partner.view-field.ts @@ -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, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-requirements.view-field.ts b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-requirements.view-field.ts new file mode 100644 index 0000000000..af543eb02a --- /dev/null +++ b/packages/twenty-apps/internal/twenty-partners/src/view-fields/opportunity-record-page-requirements.view-field.ts @@ -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, +}); diff --git a/packages/twenty-apps/internal/twenty-partners/src/views/opportunity-record-page-fields.view.ts b/packages/twenty-apps/internal/twenty-partners/src/views/opportunity-record-page-fields.view.ts deleted file mode 100644 index 92e216b923..0000000000 --- a/packages/twenty-apps/internal/twenty-partners/src/views/opportunity-record-page-fields.view.ts +++ /dev/null @@ -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, - }, - ], -});