Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/rich-content-links.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@forgerock/davinci-client': minor
---

**Breaking change**: `ReadOnlyCollector.output.content` now returns a plain `string` (the label text) instead of `ContentPart[]`.

A new `ReadOnlyCollector.output.richContent` property is always present and contains the structured link data when a LABEL field includes `richContent`. Its shape is `CollectorRichContent` — a template string with `{{key}}` placeholders (`content`) and a validated `replacements` array (`ValidatedReplacement[]`). When no `richContent` is present, `replacements` is an empty array.

**Removed type exports**: `ContentPart`, `TextContentPart`, `LinkContentPart`

**New type exports**: `RichContentLink`, `ValidatedReplacement`, `CollectorRichContent`

Includes href protocol validation that rejects unsafe URI schemes (e.g. `javascript:`, `data:`).
37 changes: 35 additions & 2 deletions e2e/davinci-app/components/label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,42 @@
import type { ReadOnlyCollector } from '@forgerock/davinci-client/types';

export default function (formEl: HTMLFormElement, collector: ReadOnlyCollector) {
// create paragraph element with text of "Loading ... "
const p = document.createElement('p');
const { richContent } = collector.output;

if (richContent.replacements.length === 0) {
p.innerText = collector.output.content;
formEl?.appendChild(p);
return;
}

// Interpolate the template by splitting on {{key}} and inserting links
const segments = richContent.content.split(/\{\{(\w+)\}\}/);
const replacementMap = new Map(richContent.replacements.map((r) => [r.key, r]));

for (let i = 0; i < segments.length; i++) {
if (i % 2 === 0) {
// Text segment
if (segments[i]) {
p.appendChild(document.createTextNode(segments[i]));
}
} else {
// Replacement key
const replacement = replacementMap.get(segments[i]);
if (replacement?.type === 'link') {
const a = document.createElement('a');
a.href = replacement.href;
a.textContent = replacement.value;
if (replacement.target) {
a.target = replacement.target;
if (replacement.target === '_blank') {
a.rel = 'noopener noreferrer';
}
}
p.appendChild(a);
}
}
}

p.innerText = collector.output.label;
formEl?.appendChild(p);
}
83 changes: 77 additions & 6 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ export interface CollectorErrors {
target: string;
}

// @public (undocumented)
export interface CollectorRichContent {
// (undocumented)
content: string;
// (undocumented)
replacements: ValidatedReplacement[];
}

// @public (undocumented)
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector;

Expand Down Expand Up @@ -998,7 +1006,7 @@ export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'Pr
export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>;

// @public
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? ReadOnlyCollectorBase : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;

// @public
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
Expand Down Expand Up @@ -1139,15 +1147,15 @@ value: Record<string, unknown>;
}, string>;

// @public
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollectorBase | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollectorBase | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
};

// @public (undocumented)
export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode;

// @public (undocumented)
export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>;
export type NoValueCollector<T extends NoValueCollectorTypes> = InferNoValueCollectorType<T>;

// @public (undocumented)
export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
Expand All @@ -1170,7 +1178,7 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
}

// @public (undocumented)
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | NoValueCollectorBase<'ReadOnlyCollector'> | QrCodeCollectorBase;
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollectorBase | QrCodeCollectorBase;

// @public
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector' | 'QrCodeCollector';
Expand Down Expand Up @@ -1417,12 +1425,35 @@ export type QrCodeField = {
};

// @public (undocumented)
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
export type ReadOnlyCollector = ReadOnlyCollectorBase;

// @public (undocumented)
export interface ReadOnlyCollectorBase {
// (undocumented)
category: 'NoValueCollector';
// (undocumented)
error: string | null;
// (undocumented)
id: string;
// (undocumented)
name: string;
// (undocumented)
output: {
key: string;
label: string;
type: string;
content: string;
richContent: CollectorRichContent;
};
// (undocumented)
type: 'ReadOnlyCollector';
}

// @public (undocumented)
export type ReadOnlyField = {
type: 'LABEL';
content: string;
richContent?: RichContent;
key?: string;
};

Expand All @@ -1442,6 +1473,34 @@ export type RedirectFields = RedirectField;

export { RequestMiddleware }

// @public (undocumented)
export type RichContent = {
content: string;
replacements?: Record<string, RichContentReplacement>;
};

// @public (undocumented)
export interface RichContentLink {
// (undocumented)
href: string;
// (undocumented)
key: string;
// (undocumented)
target?: '_self' | '_blank';
// (undocumented)
type: 'link';
// (undocumented)
value: string;
}

// @public (undocumented)
export type RichContentReplacement = {
type: 'link';
value: string;
href: string;
target?: '_self' | '_blank';
};

// @public (undocumented)
export interface SelectorOption {
// (undocumented)
Expand Down Expand Up @@ -1712,6 +1771,9 @@ export type ValidatedField = {
};
};

// @public (undocumented)
export type ValidatedReplacement = RichContentLink;

// @public (undocumented)
export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
// (undocumented)
Expand Down Expand Up @@ -1743,6 +1805,15 @@ export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCol
// @public (undocumented)
export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>;

// @public (undocumented)
export type ValidateReplacementsResult = {
ok: true;
replacements: ValidatedReplacement[];
} | {
ok: false;
error: string;
};

// @public (undocumented)
export interface ValidationPhoneNumber {
// (undocumented)
Expand Down
83 changes: 77 additions & 6 deletions packages/davinci-client/api-report/davinci-client.types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ export interface CollectorErrors {
target: string;
}

// @public (undocumented)
export interface CollectorRichContent {
// (undocumented)
content: string;
// (undocumented)
replacements: ValidatedReplacement[];
}

// @public (undocumented)
export type Collectors = FlowCollector | PasswordCollector | TextCollector | SingleSelectCollector | IdpCollector | SubmitCollector | ActionCollector<'ActionCollector'> | SingleValueCollector<'SingleValueCollector'> | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | ReadOnlyCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | QrCodeCollector | UnknownCollector;

Expand Down Expand Up @@ -995,7 +1003,7 @@ export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'Pr
export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>;

// @public
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? NoValueCollectorBase<'ReadOnlyCollector'> : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? ReadOnlyCollectorBase : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>;

// @public
export type InferSingleValueCollectorType<T extends SingleValueCollectorTypes> = T extends 'TextCollector' ? TextCollector : T extends 'SingleSelectCollector' ? SingleSelectCollector : T extends 'ValidatedTextCollector' ? ValidatedTextCollector : T extends 'PasswordCollector' ? PasswordCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
Expand Down Expand Up @@ -1136,15 +1144,15 @@ value: Record<string, unknown>;
}, string>;

// @public
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollector | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollectorBase | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
getInitialState: () => (TextCollector | SingleSelectCollector | ValidatedTextCollector | PasswordCollector | MultiSelectCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | IdpCollector | SubmitCollector | FlowCollector | QrCodeCollectorBase | ReadOnlyCollectorBase | UnknownCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
};

// @public (undocumented)
export type NodeStates = StartNode | ContinueNode | ErrorNode | SuccessNode | FailureNode;

// @public (undocumented)
export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>;
export type NoValueCollector<T extends NoValueCollectorTypes> = InferNoValueCollectorType<T>;

// @public (undocumented)
export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
Expand All @@ -1167,7 +1175,7 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
}

// @public (undocumented)
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | NoValueCollectorBase<'ReadOnlyCollector'> | QrCodeCollectorBase;
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollectorBase | QrCodeCollectorBase;

// @public
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'NoValueCollector' | 'QrCodeCollector';
Expand Down Expand Up @@ -1414,12 +1422,35 @@ export type QrCodeField = {
};

// @public (undocumented)
export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>;
export type ReadOnlyCollector = ReadOnlyCollectorBase;

// @public (undocumented)
export interface ReadOnlyCollectorBase {
// (undocumented)
category: 'NoValueCollector';
// (undocumented)
error: string | null;
// (undocumented)
id: string;
// (undocumented)
name: string;
// (undocumented)
output: {
key: string;
label: string;
type: string;
content: string;
richContent: CollectorRichContent;
};
// (undocumented)
type: 'ReadOnlyCollector';
}

// @public (undocumented)
export type ReadOnlyField = {
type: 'LABEL';
content: string;
richContent?: RichContent;
key?: string;
};

Expand All @@ -1439,6 +1470,34 @@ export type RedirectFields = RedirectField;

export { RequestMiddleware }

// @public (undocumented)
export type RichContent = {
content: string;
replacements?: Record<string, RichContentReplacement>;
};

// @public (undocumented)
export interface RichContentLink {
// (undocumented)
href: string;
// (undocumented)
key: string;
// (undocumented)
target?: '_self' | '_blank';
// (undocumented)
type: 'link';
// (undocumented)
value: string;
}

// @public (undocumented)
export type RichContentReplacement = {
type: 'link';
value: string;
href: string;
target?: '_self' | '_blank';
};

// @public (undocumented)
export interface SelectorOption {
// (undocumented)
Expand Down Expand Up @@ -1709,6 +1768,9 @@ export type ValidatedField = {
};
};

// @public (undocumented)
export type ValidatedReplacement = RichContentLink;

// @public (undocumented)
export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCollectorTypes> {
// (undocumented)
Expand Down Expand Up @@ -1740,6 +1802,15 @@ export interface ValidatedSingleValueCollectorWithValue<T extends SingleValueCol
// @public (undocumented)
export type ValidatedTextCollector = ValidatedSingleValueCollectorWithValue<'TextCollector'>;

// @public (undocumented)
export type ValidateReplacementsResult = {
ok: true;
replacements: ValidatedReplacement[];
} | {
ok: false;
error: string;
};

// @public (undocumented)
export interface ValidationPhoneNumber {
// (undocumented)
Expand Down
Loading
Loading