From b85d53de246998ced7f11724a8a35eb5a05cd181 Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 11:47:00 +0300 Subject: [PATCH 1/8] feat(trustlab): add includeLink toggle and update linkGroup on Organisations - Add `includeLink` checkbox (default true) to control whether an org displays a link; conditionally show the link group in admin UI - Update linkGroup to disable label and open-in-new-tab fields, keeping link config minimal - Fix Organisation/Organization label typos in both the collection and the ParticipatingOrganizationList block - Add isSortable and sortOptions to the organisations relationship field - Regenerate importMap (quote style only) --- .../blocks/ParticipatingOrganizationList.js | 10 ++++++---- .../src/payload/collections/Organisations.js | 20 ++++++++++++++++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/apps/trustlab/src/payload/blocks/ParticipatingOrganizationList.js b/apps/trustlab/src/payload/blocks/ParticipatingOrganizationList.js index be47bc4b4..b63a3d345 100644 --- a/apps/trustlab/src/payload/blocks/ParticipatingOrganizationList.js +++ b/apps/trustlab/src/payload/blocks/ParticipatingOrganizationList.js @@ -1,8 +1,8 @@ const ParticipatingOrganizationList = { slug: "participating-organization-list", labels: { - singular: "Participating Organization List", - plural: "Participating Organization Lists", + singular: "Participating Organisation List", + plural: "Participating Organisation Lists", }, imageURL: "/images/cms/blocks/participating-organization-list.png", fields: [ @@ -34,9 +34,11 @@ const ParticipatingOrganizationList = { type: "relationship", relationTo: "organisations", hasMany: true, - label: { en: "Organizations" }, + label: { en: "Organisations" }, admin: { - description: "Select organizations to display in this list", + description: "Select organisations to display in this list", + isSortable: true, + sortOptions: "name", }, }, { diff --git a/apps/trustlab/src/payload/collections/Organisations.js b/apps/trustlab/src/payload/collections/Organisations.js index 546181997..3d70ce35d 100644 --- a/apps/trustlab/src/payload/collections/Organisations.js +++ b/apps/trustlab/src/payload/collections/Organisations.js @@ -6,8 +6,8 @@ import blocks from "@/trustlab/payload/blocks"; const Organisations = { slug: "organisations", labels: { - singular: "Organization", - plural: "Organizations", + singular: "Organisation", + plural: "Organisations", }, admin: { group: "Publication", @@ -36,9 +36,20 @@ const Organisations = { name: "image", }, }), + { + name: "includeLink", + type: "checkbox", + defaultValue: true, // We already have some links in CMS so default to showing them + }, linkGroup({ + linkConfig: { + disableLabel: true, + disableOpenInNewTab: true, + }, overrides: { - label: "Organization Link", + admin: { + condition: (_, siblingData) => Boolean(siblingData?.includeLink), + }, }, }), { @@ -48,6 +59,9 @@ const Organisations = { }, slug({ fieldToUse: "name", + overrides: { + required: true, + }, }), ], }; From 341963e98fe0c7f1862bb0e98e465ac66e194370 Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 11:47:52 +0300 Subject: [PATCH 2/8] refactor(trustlab): resolve org links from per-org link fields instead of page tree Replace the page-tree path derivation (api.findPage + fullSlugFromParents) with per-org includeLink/linkType/link.href fields set directly in the CMS. Custom links use link.href as-is; internal links append org.slug to the stored parent-page href. --- .../getParticipatingOrganizationListBlock.js | 69 +++++++++---------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/apps/trustlab/src/lib/data/blockify/getParticipatingOrganizationListBlock.js b/apps/trustlab/src/lib/data/blockify/getParticipatingOrganizationListBlock.js index bd7a71d2c..25033c8b7 100644 --- a/apps/trustlab/src/lib/data/blockify/getParticipatingOrganizationListBlock.js +++ b/apps/trustlab/src/lib/data/blockify/getParticipatingOrganizationListBlock.js @@ -1,50 +1,45 @@ -function fullSlugFromParents(doc) { - if (!doc) { - return ""; - } - const { slug, parent } = doc; - if (!parent) { - return slug; - } - return `${fullSlugFromParents(parent)}/${slug}`; -} - -async function getParticipatingOrganizationListBlock(block, api) { - const { - title, - subtitle = null, - variant, - organizations = [], - buttonLabel = "Learn More", - ...rest - } = block; - - const parentSlug = "organisations"; - const { docs } = await api.findPage(parentSlug, {}); - const doc = docs[0]; - const pagePath = fullSlugFromParents(doc); - const resolvedOrganizations = (organizations || []).map((org) => { - if (!org || typeof org !== "object") { +function propsifyOrg(buttonLabel) { + return function propsifyOrgWithButtonLabel(org) { + // valid org must have at least an id and a name + if (!(org?.id && org.name)) { return null; } const image = org.image ?? null; - - // For card variant, generate link from breadcrumbs/slug - const href = - variant === "card" && org.slug - ? `/${[pagePath, org.slug].filter(Boolean).join("/")}` - : org.link?.href; - + let href = null; + if (org.includeLink && org.link?.href) { + if (org.link.linkType === "custom") { + // link.href manually set to external url + ({ href } = org.link); + } else { + // linkType == internal: link.href points to org parent page via beforeValidate hook + href = `${org.link.href}/${org.slug}`; + } + } + const link = href ? { href } : null; return { id: org.id, name: org.name, description: org.description ?? null, image, - link: href ? { href } : null, + link, buttonLabel: buttonLabel || "Learn More", }; - }); + }; +} + +function getParticipatingOrganizationListBlock(block) { + const { + title, + subtitle = null, + variant, + organizations: orgs, + buttonLabel, + ...rest + } = block; + + const organizations = + orgs?.map(propsifyOrg(buttonLabel)).filter(Boolean) ?? []; return { ...rest, @@ -52,7 +47,7 @@ async function getParticipatingOrganizationListBlock(block, api) { title, subtitle, variant, - organizations: resolvedOrganizations.filter(Boolean), + organizations, }; } From 6abbfa4fae0726990ae7987bfadf79deb336bd62 Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 11:48:07 +0300 Subject: [PATCH 3/8] fix(trustlab): remove redundant blockify call from pagifyOpportunities blockify is already called after pagify in the common pipeline, so calling it inside pagifyOpportunities was double-processing every block. Remove the inner call; inject date/location into overview blocks directly before returning to the pipeline. --- apps/trustlab/src/lib/data/pagify/opportunities.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/trustlab/src/lib/data/pagify/opportunities.js b/apps/trustlab/src/lib/data/pagify/opportunities.js index 792589d27..ee567f382 100644 --- a/apps/trustlab/src/lib/data/pagify/opportunities.js +++ b/apps/trustlab/src/lib/data/pagify/opportunities.js @@ -1,10 +1,9 @@ -import blockify from "@/trustlab/lib/data/blockify"; import formatDate from "@/trustlab/payload/utils/formatDate"; const overviewBlockTypes = ["content-overview", "opportunity-overview"]; function pagifyOpportunities(collection) { - return async function pagifyOpportunity(api, context, parentPage) { + return async function pagifyOpportunityCollection(api, context, parentPage) { const { params, locale } = context; const { slugs } = params; if (!slugs || slugs.length < 2) { @@ -26,13 +25,13 @@ function pagifyOpportunities(collection) { const doc = docs[0]; const date = formatDate(doc.date); const contentBlocks = doc.blocks || []; - const processedBlocks = await blockify(contentBlocks, api, { locale }); - const blocks = processedBlocks.filter(Boolean).map((block) => { + // blockify is called after pagefy so *MUST* not call it here + const blocks = contentBlocks.filter(Boolean).map((block) => { if (overviewBlockTypes.includes(block.blockType)) { return { ...block, date, - location: doc.location || null, + location: doc.location ?? null, title: block.title || "Overview", }; } From c46b6038b300a3e6bf522749979a5606502b96ef Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 11:48:30 +0300 Subject: [PATCH 4/8] fix(trustlab): remove hardcoded target=_blank from org chip links Link behaviour (same-tab vs new-tab) is now controlled at the org level via the CMS linkGroup config (disableOpenInNewTab). Rename ExternalLinkIcon import to LinkIcon to reflect that it appears on any linked chip, not only external ones. --- .../components/ParticipatingOrganizationList/ChipList.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/trustlab/src/components/ParticipatingOrganizationList/ChipList.js b/apps/trustlab/src/components/ParticipatingOrganizationList/ChipList.js index 015c3ed41..6401c3bc8 100644 --- a/apps/trustlab/src/components/ParticipatingOrganizationList/ChipList.js +++ b/apps/trustlab/src/components/ParticipatingOrganizationList/ChipList.js @@ -3,7 +3,7 @@ import { Link } from "@commons-ui/next"; import { Box, Chip, SvgIcon, Typography } from "@mui/material"; import { forwardRef } from "react"; -import ExternalLinkIcon from "@/trustlab/assets/icons/Type=external-link, Size=24, Color=CurrentColor.svg"; +import LinkIcon from "@/trustlab/assets/icons/Type=external-link, Size=24, Color=CurrentColor.svg"; const ChipList = forwardRef(function ChipList(props, ref) { const { title, organizations = [], sx } = props; @@ -50,8 +50,6 @@ const ChipList = forwardRef(function ChipList(props, ref) { ? { component: Link, href: org.link.href, - target: "_blank", - rel: "noopener noreferrer", clickable: true, } : {}; @@ -72,7 +70,7 @@ const ChipList = forwardRef(function ChipList(props, ref) { {hasLink && ( Date: Mon, 22 Jun 2026 11:52:35 +0300 Subject: [PATCH 5/8] Add missing `required:true` to `includeLink` field --- apps/trustlab/src/payload/collections/Organisations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/trustlab/src/payload/collections/Organisations.js b/apps/trustlab/src/payload/collections/Organisations.js index 3d70ce35d..128c75138 100644 --- a/apps/trustlab/src/payload/collections/Organisations.js +++ b/apps/trustlab/src/payload/collections/Organisations.js @@ -39,6 +39,7 @@ const Organisations = { { name: "includeLink", type: "checkbox", + required: true, defaultValue: true, // We already have some links in CMS so default to showing them }, linkGroup({ From 3796fbcd9312f9ba02bc2728abd2b63e566eb47d Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 14:47:14 +0300 Subject: [PATCH 6/8] fix(trustlab): make org linkGroup optional when includeLink is unchecked --- apps/trustlab/src/payload/collections/Organisations.js | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/trustlab/src/payload/collections/Organisations.js b/apps/trustlab/src/payload/collections/Organisations.js index 128c75138..613287bd5 100644 --- a/apps/trustlab/src/payload/collections/Organisations.js +++ b/apps/trustlab/src/payload/collections/Organisations.js @@ -48,6 +48,7 @@ const Organisations = { disableOpenInNewTab: true, }, overrides: { + required: false, admin: { condition: (_, siblingData) => Boolean(siblingData?.includeLink), }, From 0102ff75f083765f34528912f2920eafc1546465 Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 14:47:24 +0300 Subject: [PATCH 7/8] fix(trustlab): update payload-types --- apps/trustlab/payload-types.ts | 138 ++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/apps/trustlab/payload-types.ts b/apps/trustlab/payload-types.ts index 221f7404a..c17fa5b4e 100644 --- a/apps/trustlab/payload-types.ts +++ b/apps/trustlab/payload-types.ts @@ -490,6 +490,14 @@ export interface Page { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; card: { title: string; cardType?: ("items" | "richtext") | null; @@ -1109,6 +1117,14 @@ export interface Page { }; [k: string]: unknown; } | null; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; opportunityType: "incubator" | "intelligence-briefing" | "baraza"; hasFilters?: boolean | null; filterByLabel?: string | null; @@ -1347,7 +1363,7 @@ export interface Page { subtitle?: string | null; variant: "chip" | "card"; /** - * Select organizations to display in this list + * Select organisations to display in this list */ organizations?: (string | Organisation)[] | null; /** @@ -1687,6 +1703,14 @@ export interface Page { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; image: string | Media; /** * Logo or signature image displayed below the description @@ -2104,6 +2128,14 @@ export interface Post { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; card: { title: string; cardType?: ("items" | "richtext") | null; @@ -2720,6 +2752,14 @@ export interface Post { }; [k: string]: unknown; } | null; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; opportunityType: "incubator" | "intelligence-briefing" | "baraza"; hasFilters?: boolean | null; filterByLabel?: string | null; @@ -2958,7 +2998,7 @@ export interface Post { subtitle?: string | null; variant: "chip" | "card"; /** - * Select organizations to display in this list + * Select organisations to display in this list */ organizations?: (string | Organisation)[] | null; /** @@ -3298,6 +3338,14 @@ export interface Post { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; image: string | Media; /** * Logo or signature image displayed below the description @@ -3468,8 +3516,8 @@ export interface Organisation { [k: string]: unknown; } | null; image?: (string | null) | Media; - link: { - label: string; + includeLink: boolean; + link?: { linkType?: ("custom" | "internal") | null; doc?: { relationTo: "pages"; @@ -3477,7 +3525,6 @@ export interface Organisation { } | null; url?: string | null; href: string; - newTab?: boolean | null; }; blocks?: | ( @@ -3707,6 +3754,14 @@ export interface Organisation { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; card: { title: string; cardType?: ("items" | "richtext") | null; @@ -4326,6 +4381,14 @@ export interface Organisation { }; [k: string]: unknown; } | null; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; opportunityType: "incubator" | "intelligence-briefing" | "baraza"; hasFilters?: boolean | null; filterByLabel?: string | null; @@ -4564,7 +4627,7 @@ export interface Organisation { subtitle?: string | null; variant: "chip" | "card"; /** - * Select organizations to display in this list + * Select organisations to display in this list */ organizations?: (string | Organisation)[] | null; /** @@ -4904,6 +4967,14 @@ export interface Organisation { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; image: string | Media; /** * Logo or signature image displayed below the description @@ -5012,7 +5083,7 @@ export interface Organisation { } )[] | null; - slug?: string | null; + slug: string; updatedAt: string; createdAt: string; } @@ -5439,6 +5510,14 @@ export interface Opportunity { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; card: { title: string; cardType?: ("items" | "richtext") | null; @@ -6058,6 +6137,14 @@ export interface Opportunity { }; [k: string]: unknown; } | null; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; opportunityType: "incubator" | "intelligence-briefing" | "baraza"; hasFilters?: boolean | null; filterByLabel?: string | null; @@ -6296,7 +6383,7 @@ export interface Opportunity { subtitle?: string | null; variant: "chip" | "card"; /** - * Select organizations to display in this list + * Select organisations to display in this list */ organizations?: (string | Organisation)[] | null; /** @@ -6636,6 +6723,14 @@ export interface Opportunity { }; [k: string]: unknown; }; + /** + * Background color in hex format + */ + backgroundColor: string; + /** + * Text color in hex format + */ + textColor: string; image: string | Media; /** * Logo or signature image displayed below the description @@ -7061,6 +7156,8 @@ export interface PagesSelect { | { title?: T; content?: T; + backgroundColor?: T; + textColor?: T; card?: | T | { @@ -7356,6 +7453,8 @@ export interface PagesSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; opportunityType?: T; hasFilters?: T; filterByLabel?: T; @@ -7672,6 +7771,8 @@ export interface PagesSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; image?: T; signatureIcon?: T; id?: T; @@ -7837,6 +7938,8 @@ export interface PostsSelect { | { title?: T; content?: T; + backgroundColor?: T; + textColor?: T; card?: | T | { @@ -8132,6 +8235,8 @@ export interface PostsSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; opportunityType?: T; hasFilters?: T; filterByLabel?: T; @@ -8448,6 +8553,8 @@ export interface PostsSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; image?: T; signatureIcon?: T; id?: T; @@ -8576,15 +8683,14 @@ export interface OrganisationsSelect { name?: T; description?: T; image?: T; + includeLink?: T; link?: | T | { - label?: T; linkType?: T; doc?: T; url?: T; href?: T; - newTab?: T; }; blocks?: | T @@ -8676,6 +8782,8 @@ export interface OrganisationsSelect { | { title?: T; content?: T; + backgroundColor?: T; + textColor?: T; card?: | T | { @@ -8971,6 +9079,8 @@ export interface OrganisationsSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; opportunityType?: T; hasFilters?: T; filterByLabel?: T; @@ -9287,6 +9397,8 @@ export interface OrganisationsSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; image?: T; signatureIcon?: T; id?: T; @@ -9541,6 +9653,8 @@ export interface OpportunitiesSelect { | { title?: T; content?: T; + backgroundColor?: T; + textColor?: T; card?: | T | { @@ -9836,6 +9950,8 @@ export interface OpportunitiesSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; opportunityType?: T; hasFilters?: T; filterByLabel?: T; @@ -10152,6 +10268,8 @@ export interface OpportunitiesSelect { | { title?: T; description?: T; + backgroundColor?: T; + textColor?: T; image?: T; signatureIcon?: T; id?: T; From ae28aaf7cdd6ec2a8f8ee2e6e969596d4e01e086 Mon Sep 17 00:00:00 2001 From: kilemensi Date: Mon, 22 Jun 2026 14:49:02 +0300 Subject: [PATCH 8/8] =?UTF-8?q?Bump=20trustlab=20version:=200.0.21=20?= =?UTF-8?q?=E2=86=92=20=200.0.22?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/trustlab/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/trustlab/package.json b/apps/trustlab/package.json index 119cbae88..d77da84b1 100644 --- a/apps/trustlab/package.json +++ b/apps/trustlab/package.json @@ -1,6 +1,6 @@ { "name": "trustlab", - "version": "0.0.21", + "version": "0.0.22", "private": true, "scripts": { "dev": "next dev",