Files
twenty/packages/twenty-website/src/partner-application/partner-application-reducer.ts
T
Rashad Karanouh 6b99bcea7f feat(partners): require Twenty experience on apply, drop Cal success (#23223)
## Summary
- Add a dedicated **Experience** step to the partner apply wizard
(milestones, ≥200-char narrative, proof URL) before commercials
- Stop collecting `applicationNotes`; rename Expertise chrome away from
“experience”
- Replace the post-submit Cal.com embed with a review-and-reach-out
thank-you so unqualified inbound no longer books intros automatically

**Companion PR (app):** #23224 — Partner schema, submit persistence,
triage views, Tally CSV mapper (`twenty-partners` v1.4.0). Land the app
PR with or before this one.

## Test plan
- [ ] Open apply modal: wizard order is identity → profile → expertise →
experience → commercials
- [ ] Experience step blocks continue without ≥1 milestone, narrative
≥200 chars, and a valid https URL
- [ ] Submit creates/updates Partner with the three experience fields
(with #23224 deployed)
- [ ] Success screen has no Cal embed / book-later CTA
- [ ] `npx jest --config=jest.config.mjs partner-application` passes
locally (71 tests)

---------

Co-authored-by: Abdullah <125115953+mabdullahabaid@users.noreply.github.com>
2026-07-24 09:40:22 +05:00

95 lines
2.9 KiB
TypeScript

import { PARTNER_APPLICATION_STEP_IDS } from './data/partner-application-step-ids';
import {
INITIAL_PARTNER_APPLICATION_STATE,
type PartnerApplicationAction,
type PartnerApplicationState,
} from './partner-application-state';
import { validatePartnerApplicationStep } from './validate-partner-application-step';
function dropError(
errors: Partial<Record<string, string>>,
field: string,
): Partial<Record<string, string>> {
if (errors[field] === undefined) return errors;
const { [field]: _dropped, ...rest } = errors;
return rest;
}
export function partnerApplicationReducer(
state: PartnerApplicationState,
action: PartnerApplicationAction,
): PartnerApplicationState {
switch (action.type) {
case 'SET_FIELD':
return {
...state,
[action.field]: action.value,
fieldErrors: dropError(state.fieldErrors, action.field),
};
case 'TOGGLE_SCOPE': {
const next = state.partnerScope.includes(action.value)
? state.partnerScope.filter((value) => value !== action.value)
: [...state.partnerScope, action.value];
return {
...state,
partnerScope: next,
fieldErrors: dropError(state.fieldErrors, 'partnerScope'),
};
}
case 'TOGGLE_LANGUAGE': {
const next = state.languages.includes(action.value)
? state.languages.filter((value) => value !== action.value)
: [...state.languages, action.value];
return { ...state, languages: next };
}
case 'TOGGLE_EXPERIENCE': {
const next = state.twentyExperience.includes(action.value)
? state.twentyExperience.filter((value) => value !== action.value)
: [...state.twentyExperience, action.value];
return {
...state,
twentyExperience: next,
fieldErrors: dropError(state.fieldErrors, 'twentyExperience'),
};
}
case 'SET_SKILLS':
return { ...state, skills: action.value };
case 'GO_NEXT': {
const errors = validatePartnerApplicationStep(state);
if (Object.keys(errors).length > 0) {
return { ...state, fieldErrors: errors };
}
const lastIndex = PARTNER_APPLICATION_STEP_IDS.length - 1;
return {
...state,
stepIndex: Math.min(state.stepIndex + 1, lastIndex),
fieldErrors: {},
};
}
case 'GO_BACK':
return {
...state,
stepIndex: Math.max(state.stepIndex - 1, 0),
fieldErrors: {},
};
case 'SET_SUBMITTING':
return { ...state, isSubmitting: action.value };
case 'SET_SUBMIT_ERROR':
return { ...state, submitError: action.value };
case 'SET_SUBMITTED':
return {
...state,
isSubmitted: true,
isSubmitting: false,
submitError: null,
};
case 'RESET':
return INITIAL_PARTNER_APPLICATION_STATE;
default: {
const _exhaustive: never = action;
void _exhaustive;
return state;
}
}
}