e6c6cccafa
## Glowup — app · v1.3.0 (Release ② of the brief + glowup rollout) Partner **workspace self-service**: partners manage their own profile, links, services, and case studies from inside the CRM (new objects + record-page views + a "My Profile" self-service front-component). Evolved superset of the closed #22470 (v1.3.0). App-only — **0 website files**. Version **1.3.0** (prod is currently 1.2.10). SDK **2.19.0**. Supersedes **#22470** (closed). ### Verified locally Provisioned a throwaway workspace, synced the schema, seeded, and exercised the full surface end-to-end: marketplace + public profiles render live; **partner self-service pages** (My Profile / My Case Studies / links / services) load and save when acting as a partner user; both intake forms (partner application + client brief) submit successfully. `oxlint` 0/0, typecheck clean. ### Notes - Committed `APPLICATION_UNIVERSAL_IDENTIFIER` is the **canonical** prod id `e662fc1f-02c1-41ff-b8ba-c95a447b3965` (local bundle rewrites it to a throwaway that stays uncommitted). - New views reference app-owned fields only — no hardcoded system-field ids. ### Remaining before merge - CI lint / typecheck / tests (green locally). - Refresh the partners-doc (new objects/views change the app surface). --- ## 🚦 Release order — do not break ``` ① BRIEF WEB — #22291 ✅ MERGED (website deploy pending prod CLIENT_BRIEF_* env vars) │ ▼ ② GLOWUP APP — THIS PR (rk-partner-profile-page v1.3.0 → main) ⟵ replaces #22470 merge → DEPLOY TO PROD (verify canonical id first, yarn twenty deploy && install -r partner-twenty-com) → set new app variables on prod → refresh partners-doc │ ⟵⟵ GATE for ③ ⟵⟵ ▼ ③ GLOWUP WEB — rk-glowup-web-stacked (reopen ONE PR, base main; was #22471 / #22402) ONLY after ② is LIVE on prod (the site reads the new links / services / case-study objects) ``` - ② gates only ③. After ② deploys, reconcile **#22637** (partners-traffic-web) with ③ — both touch `partners-marketplace/*`.
104 lines
3.2 KiB
TypeScript
104 lines
3.2 KiB
TypeScript
import { CoreApiClient } from 'twenty-client-sdk/core';
|
|
import { defineLogicFunction } from 'twenty-sdk/define';
|
|
|
|
import {
|
|
mapPartnerForMarketplace,
|
|
type MarketplaceListPartner,
|
|
} from './map-partner-for-marketplace';
|
|
|
|
export const LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID =
|
|
'0f91164f-f492-41e8-9bb0-481be5a3d5b9';
|
|
|
|
// CoreApiClient is codegenerated from the synced workspace schema, so the query
|
|
// selection is strictly typed. Keep the fetch in one place and derive the
|
|
// response shape from it, so the HTTP contract can never drift from what we
|
|
// actually ask the API for.
|
|
const queryAvailablePartners = (client: CoreApiClient) =>
|
|
client.query({
|
|
partners: {
|
|
__args: {
|
|
filter: {
|
|
validationStage: { eq: 'VALIDATED' },
|
|
availability: { eq: 'AVAILABLE' },
|
|
slug: { neq: '' },
|
|
},
|
|
orderBy: [{ name: 'AscNullsLast' }],
|
|
first: 100,
|
|
},
|
|
edges: {
|
|
node: {
|
|
id: true,
|
|
name: true,
|
|
slug: true,
|
|
introduction: true,
|
|
languagesSpoken: true,
|
|
deploymentExpertise: true,
|
|
partnerScope: true,
|
|
region: true,
|
|
calendarLink: { primaryLinkUrl: true },
|
|
hourlyRate: { amountMicros: true, currencyCode: true },
|
|
projectBudgetMin: { amountMicros: true, currencyCode: true },
|
|
linkedin: { primaryLinkUrl: true },
|
|
website: { primaryLinkUrl: true },
|
|
// profilePicture is the legacy LINKS url; profilePictureFile is the
|
|
// new FILES upload (its items expose `url`). Display prefers the file.
|
|
profilePicture: { primaryLinkUrl: true },
|
|
profilePictureFile: { url: true },
|
|
skills: true,
|
|
city: true,
|
|
country: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
type AvailablePartnerRaw = NonNullable<
|
|
Awaited<ReturnType<typeof queryAvailablePartners>>['partners']
|
|
>['edges'][number]['node'];
|
|
|
|
type ListAvailablePartnersResult =
|
|
| { ok: true; count: number; partners: MarketplaceListPartner[] }
|
|
| { ok: false; reason: string };
|
|
|
|
const mapListPartner = (node: AvailablePartnerRaw): MarketplaceListPartner => {
|
|
const mapped = mapPartnerForMarketplace(node, 'list');
|
|
|
|
if ('projectBudgetTypical' in mapped) {
|
|
throw new Error(
|
|
'list-available-partners received profile payload from list mapper',
|
|
);
|
|
}
|
|
|
|
return mapped;
|
|
};
|
|
|
|
export const handler = async (): Promise<ListAvailablePartnersResult> => {
|
|
try {
|
|
const client = new CoreApiClient();
|
|
const result = await queryAvailablePartners(client);
|
|
const partners = (result.partners?.edges ?? []).map(({ node }) =>
|
|
mapListPartner(node),
|
|
);
|
|
|
|
return { ok: true, count: partners.length, partners };
|
|
} catch (err) {
|
|
return {
|
|
ok: false,
|
|
reason: err instanceof Error ? err.message : String(err),
|
|
};
|
|
}
|
|
};
|
|
|
|
export default defineLogicFunction({
|
|
universalIdentifier: LIST_AVAILABLE_PARTNERS_LOGIC_FUNCTION_ID,
|
|
name: 'list-available-partners',
|
|
description: 'Returns all partners with validationStage=VALIDATED and availability=AVAILABLE.',
|
|
timeoutSeconds: 10,
|
|
handler,
|
|
httpRouteTriggerSettings: {
|
|
path: '/partners',
|
|
httpMethod: 'GET',
|
|
isAuthRequired: false,
|
|
},
|
|
});
|