-
Notifications
You must be signed in to change notification settings - Fork 3
feat(davinci-client): rich-text-collector #568
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3e97938
8cb7549
802251e
bde8c7e
6738de5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@forgerock/davinci-client': minor | ||
| --- | ||
|
|
||
| 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. | ||
|
|
||
| **New type exports**: `RichContentLink`, `ValidatedReplacement`, `CollectorRichContent` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .opensource/ |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,12 +4,46 @@ | |
| * This software may be modified and distributed under the terms | ||
| * of the MIT license. See the LICENSE file for details. | ||
| */ | ||
| import type { ReadOnlyCollector } from '@forgerock/davinci-client/types'; | ||
| import type { RichTextCollector } from '@forgerock/davinci-client/types'; | ||
|
|
||
| export default function (formEl: HTMLFormElement, collector: ReadOnlyCollector) { | ||
| // create paragraph element with text of "Loading ... " | ||
| export default function (formEl: HTMLFormElement, collector: RichTextCollector) { | ||
| const p = document.createElement('p'); | ||
| p.style.whiteSpace = 'pre-line'; | ||
| 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); | ||
| } | ||
| } | ||
|
Comment on lines
+20
to
+45
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve unmatched placeholders instead of dropping them. The loop currently skips any token without a matching link replacement, so a typo or unsupported placeholder silently disappears from the rendered label. The ♻️ Suggested fix 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);
+ } else {
+ p.appendChild(document.createTextNode(`{{${segments[i]}}}`));
}🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| p.innerText = collector.output.label; | ||
| formEl?.appendChild(p); | ||
| } | ||
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -521,6 +521,7 @@ export type SubmitCollector = ActionCollectorNoUrl<'SubmitCollector'>; | |
| */ | ||
| export type NoValueCollectorTypes = | ||
| | 'ReadOnlyCollector' | ||
| | 'RichTextCollector' | ||
| | 'NoValueCollector' | ||
| | 'QrCodeCollector' | ||
| | 'AgreementCollector'; | ||
|
|
@@ -538,20 +539,65 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> { | |
| }; | ||
| } | ||
|
|
||
| export interface QrCodeCollectorBase { | ||
| category: 'NoValueCollector'; | ||
| error: string | null; | ||
| type: 'QrCodeCollector'; | ||
| id: string; | ||
| name: string; | ||
| output: { | ||
| key: string; | ||
| label: string; | ||
| type: string; | ||
| /** | ||
| * @interface RichContentLink - A hyperlink replacement embedded inside a | ||
| * `RichTextCollector` template. The `key` matches the `{{key}}` token in the | ||
| * template; `href` is passed through from DaVinci unmodified — consumers are | ||
| * responsible for sanitizing it before rendering. | ||
| */ | ||
| export interface RichContentLink { | ||
| key: string; | ||
| type: 'link'; | ||
| value: string; | ||
| href: string; | ||
| target?: '_self' | '_blank'; | ||
| } | ||
|
|
||
| /** | ||
| * @interface CollectorRichContent - The normalized rich-content payload exposed on a | ||
| * `RichTextCollector`. `content` holds the raw template (with `{{key}}` tokens), and | ||
| * `replacements` is the array of substitution entries (the API's keyed Record flattened | ||
| * into an array, with the original key carried on each entry). | ||
| */ | ||
| export interface CollectorRichContent { | ||
| content: string; | ||
| replacements: RichContentLink[]; | ||
| } | ||
|
|
||
| /** | ||
| * @interface QrCodeCollector - Collector for displaying a QR code image. Extends the | ||
| * generic `NoValueCollectorBase` with the image `src` on `output`. | ||
| */ | ||
| export interface QrCodeCollector extends NoValueCollectorBase<'QrCodeCollector'> { | ||
| output: NoValueCollectorBase<'QrCodeCollector'>['output'] & { | ||
| src: string; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @interface ReadOnlyCollector - Display-only collector for plain LABEL fields. | ||
| * Extends `NoValueCollectorBase` with the plain-text `content` from the field. | ||
| */ | ||
| export interface ReadOnlyCollector extends NoValueCollectorBase<'ReadOnlyCollector'> { | ||
| output: NoValueCollectorBase<'ReadOnlyCollector'>['output'] & { | ||
| content: string; | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * @interface RichTextCollector - Display-only collector for LABEL fields that carry | ||
| * inline link replacements. Extends `NoValueCollectorBase` with the plain-text | ||
| * `content` fallback and a structured `richContent` payload (template + | ||
| * normalized replacements). Use this type — not `ReadOnlyCollector` — when you | ||
| * need to render `{{key}}` tokens as anchor elements. | ||
| */ | ||
| export interface RichTextCollector extends NoValueCollectorBase<'RichTextCollector'> { | ||
| output: NoValueCollectorBase<'RichTextCollector'>['output'] & { | ||
| content: string; | ||
| richContent: CollectorRichContent; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than adding more properties to the base collector, can we just add a new collector type for this rich content collector? This new collector can just extent the base collector adding the new property on the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can I get a response on this? I feel like having a |
||
| }; | ||
| } | ||
|
|
||
| export interface AgreementCollector extends NoValueCollectorBase<'AgreementCollector'> { | ||
| output: { | ||
| key: string; | ||
|
|
@@ -576,24 +622,23 @@ export interface AgreementCollector extends NoValueCollectorBase<'AgreementColle | |
| */ | ||
| export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = | ||
| T extends 'ReadOnlyCollector' | ||
| ? NoValueCollectorBase<'ReadOnlyCollector'> | ||
| : T extends 'QrCodeCollector' | ||
| ? QrCodeCollectorBase | ||
| : T extends 'AgreementCollector' | ||
| ? AgreementCollector | ||
| : NoValueCollectorBase<'NoValueCollector'>; | ||
| ? ReadOnlyCollector | ||
| : T extends 'RichTextCollector' | ||
| ? RichTextCollector | ||
| : T extends 'QrCodeCollector' | ||
| ? QrCodeCollector | ||
| : T extends 'AgreementCollector' | ||
| ? AgreementCollector | ||
| : NoValueCollectorBase<'NoValueCollector'>; | ||
|
|
||
| export type NoValueCollectors = | ||
| | NoValueCollectorBase<'NoValueCollector'> | ||
| | NoValueCollectorBase<'ReadOnlyCollector'> | ||
| | QrCodeCollectorBase | ||
| | ReadOnlyCollector | ||
| | RichTextCollector | ||
| | QrCodeCollector | ||
| | AgreementCollector; | ||
|
|
||
| export type NoValueCollector<T extends NoValueCollectorTypes> = NoValueCollectorBase<T>; | ||
|
|
||
| export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>; | ||
|
|
||
| export type QrCodeCollector = QrCodeCollectorBase; | ||
| export type NoValueCollector<T extends NoValueCollectorTypes> = InferNoValueCollectorType<T>; | ||
|
|
||
| /** ********************************************************************* | ||
| * UNKNOWN COLLECTOR | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this file? Do we want to include it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically yes, we all need it but its the submodule of the legacy sdk for the interface mapping.