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:
@@ -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';
|
||||
|
||||
+120
@@ -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);
|
||||
});
|
||||
});
|
||||
+44
@@ -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:');
|
||||
});
|
||||
});
|
||||
+84
@@ -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;
|
||||
}
|
||||
+9
-16
@@ -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 } };
|
||||
|
||||
+149
@@ -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],
|
||||
},
|
||||
});
|
||||
+5
-3
@@ -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',
|
||||
|
||||
+21
@@ -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,
|
||||
});
|
||||
+21
@@ -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,
|
||||
});
|
||||
+20
@@ -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,
|
||||
});
|
||||
+21
@@ -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,
|
||||
});
|
||||
+21
@@ -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,
|
||||
});
|
||||
-85
@@ -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,
|
||||
},
|
||||
],
|
||||
});
|
||||
Reference in New Issue
Block a user