v1.4.0 — Raise partner bar: Twenty experience fields + triage (#23224)
## Summary **Version:** `twenty-partners` **v1.4.0** (minor — new Partner fields + apply contract) - Add Partner fields `twentyExperience`, `twentyExperienceNotes`, `twentyExperienceProofLink` and persist them from `submit-partner-application` (≥200-char narrative at API boundary) - Surface Twenty experience on applications / validated / per-stage triage views and the Partner record side panel (drop empty Introduction from that panel) - Add pure Tally CSV match/map helpers (ops import script stays outside the repo) for backfilling existing partners by `partnerId` **Companion PR (website):** #23223 — Experience step on apply + thank-you without Cal. ## Test plan - [ ] `yarn twenty apply -r <remote>` on a workspace — Partner gains the three experience fields - [ ] Website apply (with #23223) persists milestones / notes / proof link on create and email-linked update - [ ] Applications + Validated views show experience columns; record side panel lists experience fields - [ ] `yarn lint` clean; `yarn test:unit` covers schema + map-tally helpers - [ ] After Tally campaign: dry-run then apply CSV import via local ops script under `~/twenty/docs/superpowers-specs/raise-bar-import/`
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-partners",
|
||||
"version": "1.3.2",
|
||||
"version": "1.4.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
mapMilestoneLabels,
|
||||
mapTallyExperienceCsvRow,
|
||||
TWENTY_EXPERIENCE_NOTES_MIN_LENGTH,
|
||||
} from '../mappers/map-tally-experience-csv-row.mapper';
|
||||
|
||||
const VALID_PARTNER_ID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890';
|
||||
|
||||
const longNotes = (suffix = ''): string => {
|
||||
const base =
|
||||
'Built a Twenty workspace for Acme with custom objects, roles, and a sales pipeline. ';
|
||||
let notes = '';
|
||||
while (notes.length < TWENTY_EXPERIENCE_NOTES_MIN_LENGTH) {
|
||||
notes += base;
|
||||
}
|
||||
return `${notes}${suffix}`;
|
||||
};
|
||||
|
||||
const validRow = (
|
||||
overrides: Record<string, string | undefined> = {},
|
||||
): Record<string, string | undefined> => ({
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
"What you've built in Twenty": 'Custom apps, Workflows',
|
||||
'Tell us about the implementation': longNotes(),
|
||||
'Proof URL': 'https://www.loom.com/share/abc123',
|
||||
Email: 'partner@example.com',
|
||||
...overrides,
|
||||
});
|
||||
|
||||
describe('mapMilestoneLabels', () => {
|
||||
it('maps the four canonical Tally labels to enums', () => {
|
||||
expect(
|
||||
mapMilestoneLabels(
|
||||
'Custom apps, Data models; Workflows\nFront components',
|
||||
),
|
||||
).toEqual({
|
||||
ok: true,
|
||||
milestones: [
|
||||
'CUSTOM_APPS',
|
||||
'DATA_MODELS',
|
||||
'WORKFLOWS',
|
||||
'FRONT_COMPONENTS',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('dedupes repeated labels', () => {
|
||||
expect(mapMilestoneLabels('Workflows, Workflows')).toEqual({
|
||||
ok: true,
|
||||
milestones: ['WORKFLOWS'],
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown labels without guessing', () => {
|
||||
expect(mapMilestoneLabels('Custom apps, Integrations')).toEqual({
|
||||
ok: false,
|
||||
reason: 'unknown_milestone_label:Integrations',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects empty milestone input', () => {
|
||||
expect(mapMilestoneLabels(' ')).toEqual({
|
||||
ok: false,
|
||||
reason: 'missing_milestones',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapTallyExperienceCsvRow', () => {
|
||||
it('maps a valid Tally row to an update intent by partnerId', () => {
|
||||
const notes = longNotes();
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ 'Tell us about the implementation': notes }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: true,
|
||||
intent: {
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
twentyExperience: ['CUSTOM_APPS', 'WORKFLOWS'],
|
||||
twentyExperienceNotes: notes.trim(),
|
||||
twentyExperienceProofLink: 'https://www.loom.com/share/abc123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('never falls back to email when partnerId is missing', () => {
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ partnerId: undefined, Email: 'partner@example.com' }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({ ok: false, reason: 'missing_partner_id' });
|
||||
});
|
||||
|
||||
it('rejects invalid partnerId format', () => {
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ partnerId: 'not-a-uuid' }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
reason: 'invalid_partner_id',
|
||||
partnerId: 'not-a-uuid',
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects narrative shorter than 200 characters', () => {
|
||||
const shortNotes = 'Too short for a real implementation narrative.';
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ 'Tell us about the implementation': shortNotes }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
reason: `notes_too_short:${shortNotes.length}`,
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects missing proof URL', () => {
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ 'Proof URL': undefined }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
reason: 'missing_proof_url',
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects non-http(s) proof URLs', () => {
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({ 'Proof URL': 'ftp://files.example.com/case-study' }),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
reason: 'invalid_proof_url',
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown milestone labels on the row', () => {
|
||||
const result = mapTallyExperienceCsvRow(
|
||||
validRow({
|
||||
"What you've built in Twenty": 'Custom apps, Self-hosting',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
reason: 'unknown_milestone_label:Self-hosting',
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves Tally-style headers with helper copy via partial match', () => {
|
||||
const result = mapTallyExperienceCsvRow({
|
||||
partnerId: VALID_PARTNER_ID,
|
||||
"What you've built in Twenty (select every area)": 'Data models',
|
||||
'Tell us about the implementation — min 200 chars': longNotes('x'),
|
||||
'Proof URL (Loom ok)': 'http://example.com/proof',
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) {
|
||||
expect(result.intent.twentyExperience).toEqual(['DATA_MODELS']);
|
||||
expect(result.intent.twentyExperienceProofLink).toBe(
|
||||
'http://example.com/proof',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
+82
@@ -280,6 +280,88 @@ describe('submit-partner-application handler — upsert', () => {
|
||||
expect(result.reason).toBe('invalid_input');
|
||||
});
|
||||
|
||||
it('persists twenty experience fields on create', async () => {
|
||||
const twentyExperienceNotes =
|
||||
'Implemented Twenty for a logistics client: custom apps for shipment intake, ' +
|
||||
'data models for carriers and lanes, workflows for exception routing, and a front ' +
|
||||
'component for the ops board with daily exception triage.';
|
||||
const result = await handler(
|
||||
authedEvent(
|
||||
baseInput({
|
||||
email: 'experience.create@example.com',
|
||||
companyName: 'Experience Create Co',
|
||||
twentyExperience: ['CUSTOM_APPS', 'DATA_MODELS', 'WORKFLOWS'],
|
||||
twentyExperienceNotes,
|
||||
twentyExperienceProofLink: 'https://www.loom.com/share/experience-create',
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) return;
|
||||
await trackCreated(result);
|
||||
|
||||
const fetched = await client.query({
|
||||
partner: {
|
||||
__args: { filter: { id: { eq: result.partnerId } } },
|
||||
twentyExperience: true,
|
||||
twentyExperienceNotes: true,
|
||||
twentyExperienceProofLink: { primaryLinkUrl: true },
|
||||
},
|
||||
});
|
||||
const node = fetched.partner;
|
||||
expect(node?.twentyExperience).toEqual(
|
||||
expect.arrayContaining(['CUSTOM_APPS', 'DATA_MODELS', 'WORKFLOWS']),
|
||||
);
|
||||
expect(node?.twentyExperienceNotes).toBe(twentyExperienceNotes);
|
||||
expect(node?.twentyExperienceProofLink?.primaryLinkUrl).toBe(
|
||||
'https://www.loom.com/share/experience-create',
|
||||
);
|
||||
});
|
||||
|
||||
it('persists twenty experience fields on update for the same email', async () => {
|
||||
const first = await handler(
|
||||
authedEvent(baseInput({ email: 'experience.update@example.com' })),
|
||||
);
|
||||
await trackCreated(first);
|
||||
expect(first.ok).toBe(true);
|
||||
if (!first.ok) return;
|
||||
|
||||
const twentyExperienceNotes =
|
||||
'Updated narrative after resubmit: rebuilt the client data model, added workflows ' +
|
||||
'for renewals, and linked a Loom walkthrough of the front components we shipped for ' +
|
||||
'broker and ops roles across two workspaces.';
|
||||
const second = await handler(
|
||||
authedEvent(
|
||||
baseInput({
|
||||
email: 'experience.update@example.com',
|
||||
twentyExperience: ['FRONT_COMPONENTS', 'WORKFLOWS'],
|
||||
twentyExperienceNotes,
|
||||
twentyExperienceProofLink: 'https://github.com/example/twenty-case-study',
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(second.ok).toBe(true);
|
||||
if (!second.ok) return;
|
||||
expect(second.created).toBe(false);
|
||||
|
||||
const fetched = await client.query({
|
||||
partner: {
|
||||
__args: { filter: { id: { eq: second.partnerId } } },
|
||||
twentyExperience: true,
|
||||
twentyExperienceNotes: true,
|
||||
twentyExperienceProofLink: { primaryLinkUrl: true },
|
||||
},
|
||||
});
|
||||
const node = fetched.partner;
|
||||
expect(node?.twentyExperience).toEqual(
|
||||
expect.arrayContaining(['FRONT_COMPONENTS', 'WORKFLOWS']),
|
||||
);
|
||||
expect(node?.twentyExperienceNotes).toBe(twentyExperienceNotes);
|
||||
expect(node?.twentyExperienceProofLink?.primaryLinkUrl).toBe(
|
||||
'https://github.com/example/twenty-case-study',
|
||||
);
|
||||
});
|
||||
|
||||
it('returns ok: false on malformed input (empty email)', async () => {
|
||||
const result = await handler(
|
||||
authedEvent({
|
||||
|
||||
+73
@@ -70,4 +70,77 @@ describe('submitPartnerApplicationSchema', () => {
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
const validExperienceNotes =
|
||||
'Built a custom Twenty app for a property-management client, modeled leases and ' +
|
||||
'tenants as data models, automated renewal workflows, and shipped a front component ' +
|
||||
'for the broker dashboard with role-based views.';
|
||||
|
||||
it('accepts twenty experience milestones, narrative, and proof link', () => {
|
||||
const result = submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: [
|
||||
'CUSTOM_APPS',
|
||||
'DATA_MODELS',
|
||||
'WORKFLOWS',
|
||||
'FRONT_COMPONENTS',
|
||||
],
|
||||
twentyExperienceNotes: validExperienceNotes,
|
||||
twentyExperienceProofLink: 'https://www.loom.com/share/example',
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects unknown twentyExperience milestone values', () => {
|
||||
expect(
|
||||
submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: ['INTEGRATIONS'],
|
||||
twentyExperienceNotes: validExperienceNotes,
|
||||
twentyExperienceProofLink: 'https://github.com/example/case',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects twentyExperienceNotes shorter than 200 characters', () => {
|
||||
expect(
|
||||
submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: ['WORKFLOWS'],
|
||||
twentyExperienceNotes: 'Too short for a real implementation narrative.',
|
||||
twentyExperienceProofLink: 'https://www.loom.com/share/example',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a partial experience triad', () => {
|
||||
expect(
|
||||
submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: ['WORKFLOWS'],
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects a non-http twentyExperienceProofLink', () => {
|
||||
expect(
|
||||
submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: ['WORKFLOWS'],
|
||||
twentyExperienceNotes: validExperienceNotes,
|
||||
twentyExperienceProofLink: 'javascript:alert(1)',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('rejects an empty twentyExperience array when other experience fields are set', () => {
|
||||
expect(
|
||||
submitPartnerApplicationSchema.safeParse({
|
||||
...base,
|
||||
twentyExperience: [],
|
||||
twentyExperienceNotes: validExperienceNotes,
|
||||
twentyExperienceProofLink: 'https://www.loom.com/share/example',
|
||||
}).success,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
+15
@@ -31,6 +31,9 @@ export type PartnerFieldsForUpsert = {
|
||||
projectBudgetMin?: { amountMicros: number; currencyCode: 'USD' };
|
||||
calendarLink?: { primaryLinkUrl: string };
|
||||
applicationNotes?: string | null;
|
||||
twentyExperience?: CoreSchema.PartnerTwentyExperienceEnum[];
|
||||
twentyExperienceNotes?: string;
|
||||
twentyExperienceProofLink?: { primaryLinkUrl: string };
|
||||
};
|
||||
|
||||
export function buildPartnerFields(input: SubmitPartnerApplicationInput): PartnerFieldsForUpsert {
|
||||
@@ -56,6 +59,18 @@ export function buildPartnerFields(input: SubmitPartnerApplicationInput): Partne
|
||||
if (isNonEmptyString(input.calendarLink)) fields.calendarLink = { primaryLinkUrl: input.calendarLink.trim() };
|
||||
const notes = buildApplicationNotes(input);
|
||||
if (notes !== null) fields.applicationNotes = notes;
|
||||
if (input.twentyExperience !== undefined && input.twentyExperience.length > 0) {
|
||||
fields.twentyExperience =
|
||||
input.twentyExperience as CoreSchema.PartnerTwentyExperienceEnum[];
|
||||
}
|
||||
if (isNonEmptyString(input.twentyExperienceNotes)) {
|
||||
fields.twentyExperienceNotes = input.twentyExperienceNotes.trim();
|
||||
}
|
||||
if (isNonEmptyString(input.twentyExperienceProofLink)) {
|
||||
fields.twentyExperienceProofLink = {
|
||||
primaryLinkUrl: input.twentyExperienceProofLink.trim(),
|
||||
};
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
||||
+219
@@ -0,0 +1,219 @@
|
||||
// Pure Tally CSV row → Partner experience update intent.
|
||||
// Used by the local (uncommitted) raise-bar import ops script.
|
||||
// Match key is partnerId only — never email.
|
||||
|
||||
import { TWENTY_EXPERIENCE_NOTES_MIN_LENGTH } from 'src/modules/partner/constants/partner-option-values.constant';
|
||||
import { isNonEmptyString } from 'src/modules/shared/utils/is-non-empty-string.util';
|
||||
|
||||
export { TWENTY_EXPERIENCE_NOTES_MIN_LENGTH };
|
||||
|
||||
export const MILESTONE_LABEL_TO_ENUM = {
|
||||
'Custom apps': 'CUSTOM_APPS',
|
||||
'Data models': 'DATA_MODELS',
|
||||
Workflows: 'WORKFLOWS',
|
||||
'Front components': 'FRONT_COMPONENTS',
|
||||
} as const;
|
||||
|
||||
export type TwentyExperienceMilestone =
|
||||
(typeof MILESTONE_LABEL_TO_ENUM)[keyof typeof MILESTONE_LABEL_TO_ENUM];
|
||||
|
||||
export type PartnerExperienceUpdateIntent = {
|
||||
partnerId: string;
|
||||
twentyExperience: TwentyExperienceMilestone[];
|
||||
twentyExperienceNotes: string;
|
||||
twentyExperienceProofLink: string;
|
||||
};
|
||||
|
||||
export type MapTallyExperienceCsvRowResult =
|
||||
| { ok: true; intent: PartnerExperienceUpdateIntent }
|
||||
| { ok: false; reason: string; partnerId?: string };
|
||||
|
||||
const PARTNER_ID_HEADER_ALIASES = [
|
||||
'partnerid',
|
||||
'partner id',
|
||||
'partner_id',
|
||||
'hidden partnerid',
|
||||
] as const;
|
||||
|
||||
const MILESTONES_HEADER_ALIASES = [
|
||||
"what you've built in twenty",
|
||||
'what youve built in twenty',
|
||||
'twenty experience',
|
||||
] as const;
|
||||
|
||||
const NOTES_HEADER_ALIASES = [
|
||||
'tell us about the implementation',
|
||||
'twenty experience notes',
|
||||
] as const;
|
||||
|
||||
const PROOF_HEADER_ALIASES = [
|
||||
'proof url',
|
||||
'twenty experience proof link',
|
||||
'proof link',
|
||||
] as const;
|
||||
|
||||
const PARTNER_ID_UUID_PATTERN =
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
const normalizeHeader = (header: string): string =>
|
||||
header
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
|
||||
export const getTallyExperienceCsvCell = (
|
||||
row: Record<string, string | undefined | null>,
|
||||
aliases: readonly string[],
|
||||
): string | undefined => {
|
||||
const normalizedEntries = Object.entries(row).map(([header, value]) => [
|
||||
normalizeHeader(header),
|
||||
value,
|
||||
]);
|
||||
|
||||
for (const alias of aliases) {
|
||||
const normalizedAlias = normalizeHeader(alias);
|
||||
for (const [header, value] of normalizedEntries) {
|
||||
if (header === normalizedAlias && isNonEmptyString(value)) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Partial match for long Tally titles that include helper copy.
|
||||
for (const alias of aliases) {
|
||||
const normalizedAlias = normalizeHeader(alias);
|
||||
for (const [header, value] of normalizedEntries) {
|
||||
if (
|
||||
header.includes(normalizedAlias) &&
|
||||
isNonEmptyString(value) &&
|
||||
// Never treat the optional email column as a match/notes/proof cell.
|
||||
!header.includes('email')
|
||||
) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const mapMilestoneLabels = (
|
||||
rawMilestones: string,
|
||||
):
|
||||
| { ok: true; milestones: TwentyExperienceMilestone[] }
|
||||
| { ok: false; reason: string } => {
|
||||
const labels = rawMilestones
|
||||
.split(/[,;\n|]+/)
|
||||
.map((label) => label.trim())
|
||||
.filter((label) => label.length > 0);
|
||||
|
||||
if (labels.length === 0) {
|
||||
return { ok: false, reason: 'missing_milestones' };
|
||||
}
|
||||
|
||||
const milestones: TwentyExperienceMilestone[] = [];
|
||||
const seen = new Set<TwentyExperienceMilestone>();
|
||||
|
||||
for (const label of labels) {
|
||||
const mapped =
|
||||
MILESTONE_LABEL_TO_ENUM[
|
||||
label as keyof typeof MILESTONE_LABEL_TO_ENUM
|
||||
];
|
||||
|
||||
if (mapped === undefined) {
|
||||
return { ok: false, reason: `unknown_milestone_label:${label}` };
|
||||
}
|
||||
|
||||
if (!seen.has(mapped)) {
|
||||
seen.add(mapped);
|
||||
milestones.push(mapped);
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true, milestones };
|
||||
};
|
||||
|
||||
const isHttpOrHttpsUrl = (value: string): boolean => {
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.protocol === 'http:' || url.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const mapTallyExperienceCsvRow = (
|
||||
row: Record<string, string | undefined | null>,
|
||||
): MapTallyExperienceCsvRowResult => {
|
||||
const partnerId = getTallyExperienceCsvCell(row, PARTNER_ID_HEADER_ALIASES);
|
||||
|
||||
if (!isNonEmptyString(partnerId)) {
|
||||
return { ok: false, reason: 'missing_partner_id' };
|
||||
}
|
||||
|
||||
if (!PARTNER_ID_UUID_PATTERN.test(partnerId)) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'invalid_partner_id',
|
||||
partnerId,
|
||||
};
|
||||
}
|
||||
|
||||
const rawMilestones = getTallyExperienceCsvCell(row, MILESTONES_HEADER_ALIASES);
|
||||
if (!isNonEmptyString(rawMilestones)) {
|
||||
return { ok: false, reason: 'missing_milestones', partnerId };
|
||||
}
|
||||
|
||||
const mappedMilestones = mapMilestoneLabels(rawMilestones);
|
||||
if (!mappedMilestones.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: mappedMilestones.reason,
|
||||
partnerId,
|
||||
};
|
||||
}
|
||||
|
||||
const twentyExperienceNotes = getTallyExperienceCsvCell(
|
||||
row,
|
||||
NOTES_HEADER_ALIASES,
|
||||
);
|
||||
if (!isNonEmptyString(twentyExperienceNotes)) {
|
||||
return { ok: false, reason: 'missing_notes', partnerId };
|
||||
}
|
||||
|
||||
if (twentyExperienceNotes.length < TWENTY_EXPERIENCE_NOTES_MIN_LENGTH) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: `notes_too_short:${twentyExperienceNotes.length}`,
|
||||
partnerId,
|
||||
};
|
||||
}
|
||||
|
||||
const twentyExperienceProofLink = getTallyExperienceCsvCell(
|
||||
row,
|
||||
PROOF_HEADER_ALIASES,
|
||||
);
|
||||
if (!isNonEmptyString(twentyExperienceProofLink)) {
|
||||
return { ok: false, reason: 'missing_proof_url', partnerId };
|
||||
}
|
||||
|
||||
if (!isHttpOrHttpsUrl(twentyExperienceProofLink)) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: 'invalid_proof_url',
|
||||
partnerId,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
intent: {
|
||||
partnerId,
|
||||
twentyExperience: mappedMilestones.milestones,
|
||||
twentyExperienceNotes,
|
||||
twentyExperienceProofLink,
|
||||
},
|
||||
};
|
||||
};
|
||||
+69
-18
@@ -5,30 +5,81 @@ import {
|
||||
PARTNER_LANGUAGE_VALUES,
|
||||
PARTNER_SCOPE_VALUES,
|
||||
PARTNER_TYPE_OF_TEAM_VALUES,
|
||||
TWENTY_EXPERIENCE_NOTES_MIN_LENGTH,
|
||||
TWENTY_EXPERIENCE_VALUES,
|
||||
} from 'src/modules/partner/constants/partner-option-values.constant';
|
||||
import { isHttpUrl } from 'src/modules/shared/utils/http-url.util';
|
||||
|
||||
// The request contract. zod is the single source of truth: it validates the
|
||||
// incoming body at runtime and the input type is inferred from it, so the two
|
||||
// can never drift. Enum-valued fields are constrained to the same option sets
|
||||
// the Partner object accepts.
|
||||
export const submitPartnerApplicationSchema = z.object({
|
||||
firstName: z.string().trim().min(1),
|
||||
lastName: z.string(),
|
||||
email: z.string().trim().min(1),
|
||||
companyName: z.string().trim().min(1),
|
||||
domainName: z.string().optional(),
|
||||
linkedin: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
country: z.enum(PARTNER_COUNTRY_VALUES).optional(),
|
||||
languages: z.array(z.enum(PARTNER_LANGUAGE_VALUES)).optional(),
|
||||
typeOfTeam: z.enum(PARTNER_TYPE_OF_TEAM_VALUES).optional(),
|
||||
partnerScope: z.array(z.enum(PARTNER_SCOPE_VALUES)).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
applicationNotes: z.string().optional(),
|
||||
hourlyRate: z.number().optional(),
|
||||
projectBudgetMin: z.number().optional(),
|
||||
calendarLink: z.string().optional(),
|
||||
});
|
||||
export const submitPartnerApplicationSchema = z
|
||||
.object({
|
||||
firstName: z.string().trim().min(1),
|
||||
lastName: z.string(),
|
||||
email: z.string().trim().min(1),
|
||||
companyName: z.string().trim().min(1),
|
||||
domainName: z.string().optional(),
|
||||
linkedin: z.string().optional(),
|
||||
city: z.string().optional(),
|
||||
country: z.enum(PARTNER_COUNTRY_VALUES).optional(),
|
||||
languages: z.array(z.enum(PARTNER_LANGUAGE_VALUES)).optional(),
|
||||
typeOfTeam: z.enum(PARTNER_TYPE_OF_TEAM_VALUES).optional(),
|
||||
partnerScope: z.array(z.enum(PARTNER_SCOPE_VALUES)).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
applicationNotes: z.string().optional(),
|
||||
twentyExperience: z.array(z.enum(TWENTY_EXPERIENCE_VALUES)).optional(),
|
||||
// Apply narrative floor at the API boundary (defense in depth vs website).
|
||||
twentyExperienceNotes: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(TWENTY_EXPERIENCE_NOTES_MIN_LENGTH)
|
||||
.optional(),
|
||||
twentyExperienceProofLink: z
|
||||
.string()
|
||||
.trim()
|
||||
.refine(isHttpUrl, { message: 'URL must use http or https' })
|
||||
.optional(),
|
||||
hourlyRate: z.number().optional(),
|
||||
projectBudgetMin: z.number().optional(),
|
||||
calendarLink: z.string().optional(),
|
||||
})
|
||||
.superRefine((data, context) => {
|
||||
const hasMilestones = (data.twentyExperience?.length ?? 0) > 0;
|
||||
const hasNotes = data.twentyExperienceNotes !== undefined;
|
||||
const hasProof = data.twentyExperienceProofLink !== undefined;
|
||||
const anyExperienceSet = hasMilestones || hasNotes || hasProof;
|
||||
|
||||
if (!anyExperienceSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When any experience field is present, require the full triad (website parity).
|
||||
if (!hasMilestones) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: 'At least one Twenty experience milestone is required.',
|
||||
path: ['twentyExperience'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasNotes) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: `Twenty experience notes must be at least ${TWENTY_EXPERIENCE_NOTES_MIN_LENGTH} characters.`,
|
||||
path: ['twentyExperienceNotes'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!hasProof) {
|
||||
context.addIssue({
|
||||
code: 'custom',
|
||||
message: 'Twenty experience proof link is required.',
|
||||
path: ['twentyExperienceProofLink'],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type SubmitPartnerApplicationInput = z.infer<
|
||||
typeof submitPartnerApplicationSchema
|
||||
|
||||
+25
-6
@@ -7,8 +7,10 @@ import {
|
||||
|
||||
import {
|
||||
PARTNER_COUNTRY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_INTRODUCTION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_NOTES_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/partner/constants/partner-field-universal-identifiers';
|
||||
import {
|
||||
@@ -52,23 +54,39 @@ export default defineView({
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2d198c8a-0199-4b9f-ae07-255172ab6e7e',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_INTRODUCTION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '88d68442-514d-43a5-b974-8aa22b65cac8',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '1bb5fa0a-7724-4b72-812b-494e3cda7e9a',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 220,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5f520664-38ba-4151-b1b8-3dfe7b640707',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_NOTES_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 280,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '835c9a7e-72ec-46c5-8d90-39a02998f561',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_CREATED_AT_FIELD_ID,
|
||||
position: 3,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 180,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0175e169-377c-4ff5-b0f0-c3359cac48d9',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_USER_ON_PARTNER_FIELD_ID,
|
||||
position: 4,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
@@ -76,7 +94,8 @@ export default defineView({
|
||||
filters: [
|
||||
{
|
||||
universalIdentifier: 'a1ef0bcd-6f2c-44fc-ab3d-11df2523a952',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
operand: ViewFilterOperand.IS,
|
||||
value: ['APPLICATION'],
|
||||
},
|
||||
|
||||
+9
@@ -61,3 +61,12 @@ export const PARTNER_WEBSITE_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
|
||||
export const PARTNER_CALENDAR_LINK_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'a0000008-0000-4000-8000-000000000008';
|
||||
|
||||
export const PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'd8616962-3c4b-475f-a508-a237fffde0b9';
|
||||
|
||||
export const PARTNER_TWENTY_EXPERIENCE_NOTES_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'7a0b7526-6695-4b47-8835-d31786174fcd';
|
||||
|
||||
export const PARTNER_TWENTY_EXPERIENCE_PROOF_LINK_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'80e9b14e-450c-467f-9c06-9a84c07c21f0';
|
||||
|
||||
+10
@@ -11,3 +11,13 @@ export const PARTNER_SCOPE_VALUES = [
|
||||
] as const;
|
||||
|
||||
export const PARTNER_TYPE_OF_TEAM_VALUES = ['SOLO','AGENCY'] as const;
|
||||
|
||||
export const TWENTY_EXPERIENCE_VALUES = [
|
||||
'CUSTOM_APPS',
|
||||
'DATA_MODELS',
|
||||
'WORKFLOWS',
|
||||
'FRONT_COMPONENTS',
|
||||
] as const;
|
||||
|
||||
export const TWENTY_EXPERIENCE_NOTES_MIN_LENGTH = 200;
|
||||
|
||||
|
||||
+72
-43
@@ -7,7 +7,6 @@ import {
|
||||
PARTNER_COUNTRY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_DEPLOYMENT_EXPERTISE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_HOURLY_RATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_INTRODUCTION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_LANGUAGES_SPOKEN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_LINKEDIN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -19,6 +18,9 @@ import {
|
||||
PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_SKILLS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TIER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_NOTES_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_PROOF_LINK_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TYPE_OF_TEAM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_WEBSITE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
@@ -44,6 +46,8 @@ export const PARTNER_RECORD_PAGE_FIELDS_VIEW_ID =
|
||||
// in the fields widget when an explicit view marks them visible — this is that view.
|
||||
// Validation Stage and Partner Tier stay listed for admins but are read-locked for
|
||||
// the Partner role (see partner.role.ts), so partners don't see them on My Profile.
|
||||
// Introduction is omitted here (marketplace bio lives on My Profile / public site);
|
||||
// Twenty experience fields are first-class for triage.
|
||||
export default defineView({
|
||||
universalIdentifier: PARTNER_RECORD_PAGE_FIELDS_VIEW_ID,
|
||||
name: 'Partner Record Page Fields',
|
||||
@@ -64,146 +68,171 @@ export default defineView({
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'c473b085-a043-48e3-8f30-2936667ae93b',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_PROFILE_PICTURE_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_PROFILE_PICTURE_FILE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '94bc6866-9a59-442d-9766-e0433420d61a',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_PROFILE_PICTURE_LEGACY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_PROFILE_PICTURE_LEGACY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e82ab8fa-8d78-4ef4-97f6-5456c9df30b4',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_INTRODUCTION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd61ac88b-139a-417d-8767-1db77a44d1a5',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_AVAILABILITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 5,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_AVAILABILITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2304f4b9-4faf-4813-8ef7-fb7697d64a92',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '6b1e51b9-a032-4223-8880-7b04559696f0',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0228335c-d634-4fed-a104-568cc181f637',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_NOTES_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '567860a8-af4c-4f45-b75b-73ef4d43584e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_PROOF_LINK_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 8,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0c47e12-6435-458a-b608-621bcee57474',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_SKILLS_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 7,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'cbd9d84c-3d71-495c-b412-08c77f2c124c',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_DEPLOYMENT_EXPERTISE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 9,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '42521613-4eeb-4a00-9a25-1d9de793d3c0',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_LANGUAGES_SPOKEN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: 'cbd9d84c-3d71-495c-b412-08c77f2c124c',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_DEPLOYMENT_EXPERTISE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 10,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2d595d67-8317-4c67-aeec-fe6e030e4830',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_TYPE_OF_TEAM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '42521613-4eeb-4a00-9a25-1d9de793d3c0',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_LANGUAGES_SPOKEN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 11,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '2d595d67-8317-4c67-aeec-fe6e030e4830',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TYPE_OF_TEAM_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 12,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a85dc9bc-a5f5-446e-a36c-492f1b6c0035',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_REGION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 12,
|
||||
position: 13,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '90d11276-1666-4c36-9055-00c4680c3168',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_COUNTRY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 13,
|
||||
position: 14,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '23f76380-3532-4e84-9115-55d395d6646b',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_CITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 14,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'cf5e9649-a3e9-4b3f-b746-e701e3db7169',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_HOURLY_RATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 15,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '388ae22b-956c-4e5a-8b78-6f8cb0105c1a',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_PROJECT_BUDGET_MIN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: 'cf5e9649-a3e9-4b3f-b746-e701e3db7169',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_HOURLY_RATE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 16,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '388ae22b-956c-4e5a-8b78-6f8cb0105c1a',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_PROJECT_BUDGET_MIN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 17,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5c8727a3-6e4b-41c7-adf9-530ab6599bf9',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_LINKEDIN_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 17,
|
||||
position: 18,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a6ef3c3b-103b-41b7-ba85-9d93a9af3296',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_WEBSITE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 18,
|
||||
position: 19,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '4bc68141-963f-4e42-8aa4-c081afc613c7',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_CALENDAR_LINK_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 19,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_CALENDAR_LINK_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 20,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '5b51f559-d78c-48df-9347-7929f87ad8d1',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_LINKS_ON_PARTNER_FIELD_ID,
|
||||
position: 20,
|
||||
position: 21,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'fe7a958a-fc56-4e65-8e37-1925014fb1ae',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_SERVICES_ON_PARTNER_FIELD_ID,
|
||||
position: 21,
|
||||
position: 22,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '39b34dc8-7daa-4e30-9183-2ce8f035657a',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_CONTENTS_ON_PARTNER_FIELD_ID,
|
||||
position: 22,
|
||||
position: 23,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '3487e2ae-0feb-41c4-ad0c-f94dff435015',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 23,
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 24,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '971c79e6-381e-4346-86a4-7119de4824be',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_TIER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 24,
|
||||
position: 25,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ec1148d4-01ff-4128-9863-0ac42ea8eb47',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_USER_ON_PARTNER_FIELD_ID,
|
||||
position: 25,
|
||||
position: 26,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '3ec293a1-da90-4104-a22e-771c0059e9b5',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_COMPANY_FIELD_ID,
|
||||
position: 26,
|
||||
position: 27,
|
||||
isVisible: true,
|
||||
},
|
||||
],
|
||||
|
||||
+10
-1
@@ -5,6 +5,7 @@ import {
|
||||
PARTNER_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TIER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/partner/constants/partner-field-universal-identifiers';
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
@@ -77,10 +78,18 @@ export default defineView({
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e9908bdb-bfaa-4c2f-a193-9b25a8a7b1d0',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 220,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'ff01c4e3-b5c6-4832-90eb-ad05d8276e60',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_TIER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
},
|
||||
],
|
||||
|
||||
+19
-2
@@ -4,7 +4,9 @@ import {
|
||||
PARTNER_AVAILABILITY_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_REGION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TIER_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
PARTNER_VALIDATION_STAGE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
} from 'src/modules/partner/constants/partner-field-universal-identifiers';
|
||||
import { PARTNER_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/constants/universal-identifiers';
|
||||
@@ -43,17 +45,32 @@ export default defineView({
|
||||
position: 2,
|
||||
isVisible: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'e301a359-d38b-4ae9-aecb-593f5744f06b',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_SCOPE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '919124f8-46ae-449e-819f-c1b93ec4391e',
|
||||
fieldMetadataUniversalIdentifier:
|
||||
PARTNER_TWENTY_EXPERIENCE_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 4,
|
||||
isVisible: true,
|
||||
size: 220,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '0173bc1a-4884-4b58-b12d-77afa1fb1fe1',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_REGION_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
position: 3,
|
||||
position: 5,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '866578d7-8a53-4276-bda0-16e657102392',
|
||||
fieldMetadataUniversalIdentifier: PARTNER_USER_ON_PARTNER_FIELD_ID,
|
||||
position: 4,
|
||||
position: 6,
|
||||
isVisible: true,
|
||||
size: 200,
|
||||
},
|
||||
|
||||
+54
@@ -492,6 +492,60 @@ export default defineObject({
|
||||
icon: 'IconClipboardText',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'd8616962-3c4b-475f-a508-a237fffde0b9',
|
||||
type: FieldType.MULTI_SELECT,
|
||||
name: 'twentyExperience',
|
||||
label: 'Twenty Experience',
|
||||
icon: 'IconTool',
|
||||
isNullable: true,
|
||||
options: [
|
||||
{
|
||||
id: '879d6ae4-09a2-425e-8292-bfa9f4df3983',
|
||||
value: 'CUSTOM_APPS',
|
||||
label: 'Custom apps',
|
||||
position: 0,
|
||||
color: 'blue',
|
||||
},
|
||||
{
|
||||
id: '20d4f654-54dc-4e57-902f-f5f4550d0b63',
|
||||
value: 'DATA_MODELS',
|
||||
label: 'Data models',
|
||||
position: 1,
|
||||
color: 'green',
|
||||
},
|
||||
{
|
||||
id: '2919caa4-e6ec-47d7-870e-9e115fd9da58',
|
||||
value: 'WORKFLOWS',
|
||||
label: 'Workflows',
|
||||
position: 2,
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
id: '542f843f-ba27-40db-8087-a75d77bdda29',
|
||||
value: 'FRONT_COMPONENTS',
|
||||
label: 'Front components',
|
||||
position: 3,
|
||||
color: 'orange',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
universalIdentifier: '7a0b7526-6695-4b47-8835-d31786174fcd',
|
||||
type: FieldType.TEXT,
|
||||
name: 'twentyExperienceNotes',
|
||||
label: 'Twenty Experience Notes',
|
||||
icon: 'IconFileText',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: '80e9b14e-450c-467f-9c06-9a84c07c21f0',
|
||||
type: FieldType.LINKS,
|
||||
name: 'twentyExperienceProofLink',
|
||||
label: 'Twenty Experience Proof Link',
|
||||
icon: 'IconLink',
|
||||
isNullable: true,
|
||||
},
|
||||
{
|
||||
universalIdentifier: 'a0000010-0000-4000-8000-000000000010',
|
||||
type: FieldType.DATE_TIME,
|
||||
|
||||
Reference in New Issue
Block a user