From 24490a2f767d3aaa8bb36e75ec9d0bc03456a7fd Mon Sep 17 00:00:00 2001 From: Ryan Bas Date: Mon, 13 Apr 2026 13:10:40 -0600 Subject: [PATCH] feat(davinci-client): add RichContent types to ReadOnlyField Support RichContent link types by creating a NoValueCollector for it --- .changeset/rich-content-links.md | 13 + e2e/davinci-app/components/label.ts | 37 +- .../src/lib/collector.richcontent.test-d.ts | 108 ++++++ .../davinci-client/src/lib/collector.types.ts | 42 ++- .../src/lib/collector.utils.test.ts | 351 +++++++++++++++++- .../davinci-client/src/lib/collector.utils.ts | 107 +++++- .../davinci-client/src/lib/davinci.types.ts | 13 + .../lib/mock-data/mock-form-fields.data.ts | 16 + packages/davinci-client/src/types.ts | 3 + 9 files changed, 678 insertions(+), 12 deletions(-) create mode 100644 .changeset/rich-content-links.md create mode 100644 packages/davinci-client/src/lib/collector.richcontent.test-d.ts diff --git a/.changeset/rich-content-links.md b/.changeset/rich-content-links.md new file mode 100644 index 0000000000..4a0f40881c --- /dev/null +++ b/.changeset/rich-content-links.md @@ -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:`). diff --git a/e2e/davinci-app/components/label.ts b/e2e/davinci-app/components/label.ts index 29fc355fe7..84a9c41eff 100644 --- a/e2e/davinci-app/components/label.ts +++ b/e2e/davinci-app/components/label.ts @@ -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); } diff --git a/packages/davinci-client/src/lib/collector.richcontent.test-d.ts b/packages/davinci-client/src/lib/collector.richcontent.test-d.ts new file mode 100644 index 0000000000..6142ef2324 --- /dev/null +++ b/packages/davinci-client/src/lib/collector.richcontent.test-d.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2025 Ping Identity Corporation. All rights reserved. + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ +import { describe, expectTypeOf, it } from 'vitest'; +import type { + ReadOnlyCollectorBase, + ReadOnlyCollector, + RichContentLink, + ValidatedReplacement, + CollectorRichContent, + ValidateReplacementsResult, + NoValueCollector, +} from './collector.types.js'; + +describe('Rich Content Types', () => { + describe('RichContentLink', () => { + it('should require key, type, value, and href', () => { + expectTypeOf().toHaveProperty('key').toBeString(); + expectTypeOf().toHaveProperty('type').toEqualTypeOf<'link'>(); + expectTypeOf().toHaveProperty('value').toBeString(); + expectTypeOf().toHaveProperty('href').toBeString(); + }); + + it('should have optional target constrained to _self or _blank', () => { + expectTypeOf() + .toHaveProperty('target') + .toEqualTypeOf<'_self' | '_blank' | undefined>(); + }); + }); + + describe('ValidatedReplacement', () => { + it('should be assignable from RichContentLink', () => { + expectTypeOf().toMatchTypeOf(); + }); + + it('should be assignable to RichContentLink', () => { + expectTypeOf().toMatchTypeOf(); + }); + }); + + describe('CollectorRichContent', () => { + it('should have required content string and replacements array', () => { + expectTypeOf().toHaveProperty('content').toBeString(); + expectTypeOf() + .toHaveProperty('replacements') + .toEqualTypeOf(); + }); + }); + + describe('ValidateReplacementsResult', () => { + it('should narrow to replacements on ok: true', () => { + const result = {} as ValidateReplacementsResult; + if (result.ok) { + expectTypeOf(result.replacements).toEqualTypeOf(); + } + }); + + it('should narrow to error on ok: false', () => { + const result = {} as ValidateReplacementsResult; + if (!result.ok) { + expectTypeOf(result.error).toBeString(); + } + }); + }); + + describe('ReadOnlyCollectorBase', () => { + it('should have content as string, not array', () => { + expectTypeOf().toBeString(); + }); + + it('should have required richContent with CollectorRichContent shape', () => { + expectTypeOf< + ReadOnlyCollectorBase['output']['richContent'] + >().toEqualTypeOf(); + }); + + it('should have standard collector fields', () => { + expectTypeOf() + .toHaveProperty('category') + .toEqualTypeOf<'NoValueCollector'>(); + expectTypeOf() + .toHaveProperty('type') + .toEqualTypeOf<'ReadOnlyCollector'>(); + expectTypeOf().toHaveProperty('error').toEqualTypeOf(); + }); + }); + + describe('NoValueCollector', () => { + it('should resolve to ReadOnlyCollectorBase', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + it('should have content and richContent on output', () => { + type Resolved = NoValueCollector<'ReadOnlyCollector'>; + expectTypeOf().toBeString(); + expectTypeOf().toEqualTypeOf(); + }); + }); + + describe('ReadOnlyCollector alias', () => { + it('should equal ReadOnlyCollectorBase', () => { + expectTypeOf().toEqualTypeOf(); + }); + }); +}); diff --git a/packages/davinci-client/src/lib/collector.types.ts b/packages/davinci-client/src/lib/collector.types.ts index 66654ef296..fae265b11f 100644 --- a/packages/davinci-client/src/lib/collector.types.ts +++ b/packages/davinci-client/src/lib/collector.types.ts @@ -497,6 +497,25 @@ export interface NoValueCollectorBase { }; } +export interface RichContentLink { + key: string; + type: 'link'; + value: string; + href: string; + target?: '_self' | '_blank'; +} + +export type ValidatedReplacement = RichContentLink; + +export interface CollectorRichContent { + content: string; + replacements: ValidatedReplacement[]; +} + +export type ValidateReplacementsResult = + | { ok: true; replacements: ValidatedReplacement[] } + | { ok: false; error: string }; + export interface QrCodeCollectorBase { category: 'NoValueCollector'; error: string | null; @@ -511,6 +530,21 @@ export interface QrCodeCollectorBase { }; } +export interface ReadOnlyCollectorBase { + category: 'NoValueCollector'; + error: string | null; + type: 'ReadOnlyCollector'; + id: string; + name: string; + output: { + key: string; + label: string; + type: string; + content: string; + richContent: CollectorRichContent; + }; +} + /** * Type to help infer the collector based on the collector type * Used specifically in the returnNoValueCollector wrapper function. @@ -520,19 +554,19 @@ export interface QrCodeCollectorBase { */ export type InferNoValueCollectorType = T extends 'ReadOnlyCollector' - ? NoValueCollectorBase<'ReadOnlyCollector'> + ? ReadOnlyCollectorBase : T extends 'QrCodeCollector' ? QrCodeCollectorBase : NoValueCollectorBase<'NoValueCollector'>; export type NoValueCollectors = | NoValueCollectorBase<'NoValueCollector'> - | NoValueCollectorBase<'ReadOnlyCollector'> + | ReadOnlyCollectorBase | QrCodeCollectorBase; -export type NoValueCollector = NoValueCollectorBase; +export type NoValueCollector = InferNoValueCollectorType; -export type ReadOnlyCollector = NoValueCollectorBase<'ReadOnlyCollector'>; +export type ReadOnlyCollector = ReadOnlyCollectorBase; export type QrCodeCollector = QrCodeCollectorBase; diff --git a/packages/davinci-client/src/lib/collector.utils.test.ts b/packages/davinci-client/src/lib/collector.utils.test.ts index e3d488d330..9ff5804b62 100644 --- a/packages/davinci-client/src/lib/collector.utils.test.ts +++ b/packages/davinci-client/src/lib/collector.utils.test.ts @@ -23,6 +23,7 @@ import { returnSingleValueAutoCollector, returnObjectValueAutoCollector, returnQrCodeCollector, + validateReplacements, } from './collector.utils.js'; import type { DaVinciField, @@ -35,6 +36,7 @@ import type { QrCodeField, ReadOnlyField, RedirectField, + RichContentReplacement, StandardField, } from './davinci.types.js'; import type { @@ -793,7 +795,7 @@ describe('No Value Collectors', () => { }); describe('returnReadOnlyCollector', () => { - it('should return a valid ReadOnlyCollector with value in output', () => { + it('should return a ReadOnlyCollector with plain content and empty richContent when no richContent on field', () => { const result = returnReadOnlyCollector(mockField, 0); expect(result).toEqual({ category: 'NoValueCollector', @@ -805,9 +807,130 @@ describe('No Value Collectors', () => { key: 'LABEL-0', label: mockField.content, type: mockField.type, + content: mockField.content, + richContent: { + content: mockField.content, + replacements: [], + }, + }, + }); + }); + + it('should pass through richContent template and validated replacements', () => { + const field: ReadOnlyField = { + type: 'LABEL', + content: 'I agree to the terms and conditions', + richContent: { + content: 'I agree to the {{link}}', + replacements: { + link: { + type: 'link', + value: 'terms and conditions', + href: 'https://example.com', + target: '_blank', + }, + }, + }, + key: 'terms', + }; + + const result = returnReadOnlyCollector(field, 0); + + expect(result).toEqual({ + category: 'NoValueCollector', + error: null, + type: 'ReadOnlyCollector', + id: 'terms-0', + name: 'terms-0', + output: { + key: 'terms-0', + label: 'I agree to the terms and conditions', + type: 'LABEL', + content: 'I agree to the terms and conditions', + richContent: { + content: 'I agree to the {{link}}', + replacements: [ + { + key: 'link', + type: 'link', + value: 'terms and conditions', + href: 'https://example.com', + target: '_blank', + }, + ], + }, + }, + }); + }); + + it('should set error and empty replacements when richContent has unsafe href', () => { + const field: ReadOnlyField = { + type: 'LABEL', + content: 'Click the link', + richContent: { + content: 'Click {{bad}}', + replacements: { + bad: { + type: 'link', + value: 'here', + href: 'javascript:alert(1)', + }, + }, + }, + }; + + const result = returnReadOnlyCollector(field, 0); + + expect(result.error).toBe('Unsafe href protocol for key: bad'); + expect(result.output.content).toBe('Click the link'); + expect(result.output.richContent).toEqual({ + content: 'Click {{bad}}', + replacements: [], + }); + }); + + it('should validate template keys exist in replacements', () => { + const field: ReadOnlyField = { + type: 'LABEL', + content: 'Fallback text', + richContent: { + content: 'Click {{broken}}', + replacements: {}, }, + }; + + const result = returnReadOnlyCollector(field, 0); + + expect(result.error).toBe('Missing replacement for key: {{broken}}'); + expect(result.output.content).toBe('Fallback text'); + expect(result.output.richContent).toEqual({ + content: 'Click {{broken}}', + replacements: [], }); }); + + it('should report all missing keys when template has partial replacement coverage', () => { + const field: ReadOnlyField = { + type: 'LABEL', + content: 'Read our terms and policy', + richContent: { + content: 'Read our {{link1}} and {{link2}}', + replacements: { + link1: { + type: 'link', + value: 'terms', + href: 'https://example.com/terms', + }, + }, + }, + }; + + const result = returnReadOnlyCollector(field, 0); + + expect(result.error).toBe('Missing replacement for key: {{link2}}'); + expect(result.output.content).toBe('Read our terms and policy'); + expect(result.output.richContent.replacements).toEqual([]); + }); }); }); @@ -1101,3 +1224,229 @@ describe('Return collector validator', () => { ); }); }); + +describe('validateReplacements', () => { + it('should validate a single link replacement', () => { + const replacements: Record = { + link1: { + type: 'link', + value: 'terms and conditions', + href: 'https://example.com', + target: '_blank', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ + ok: true, + replacements: [ + { + key: 'link1', + type: 'link', + value: 'terms and conditions', + href: 'https://example.com', + target: '_blank', + }, + ], + }); + }); + + it('should validate multiple link replacements', () => { + const replacements: Record = { + link1: { + type: 'link', + value: 'terms', + href: 'https://example.com', + target: '_blank', + }, + link2: { + type: 'link', + value: 'policy', + href: 'https://xyz.com', + target: '_self', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ + ok: true, + replacements: [ + { + key: 'link1', + type: 'link', + value: 'terms', + href: 'https://example.com', + target: '_blank', + }, + { key: 'link2', type: 'link', value: 'policy', href: 'https://xyz.com', target: '_self' }, + ], + }); + }); + + it('should omit target when not provided', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'https://example.com', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ + ok: true, + replacements: [{ key: 'link', type: 'link', value: 'here', href: 'https://example.com' }], + }); + }); + + it('should return empty array for empty replacements', () => { + const result = validateReplacements({}); + + expect(result).toEqual({ ok: true, replacements: [] }); + }); + + it('should return error for javascript: URI scheme', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'javascript:alert(1)', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ ok: false, error: 'Unsafe href protocol for key: link' }); + }); + + it('should return error for data: URI scheme', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'data:text/html,', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ ok: false, error: 'Unsafe href protocol for key: link' }); + }); + + it('should return error for invalid href', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'not a url', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ ok: false, error: 'Invalid href for key: link' }); + }); + + it('should allow https: href', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'https://example.com/terms', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ + ok: true, + replacements: [ + { key: 'link', type: 'link', value: 'here', href: 'https://example.com/terms' }, + ], + }); + }); + + it('should allow http: href', () => { + const replacements: Record = { + link: { + type: 'link', + value: 'here', + href: 'http://example.com/terms', + }, + }; + + const result = validateReplacements(replacements); + + expect(result).toEqual({ + ok: true, + replacements: [ + { key: 'link', type: 'link', value: 'here', href: 'http://example.com/terms' }, + ], + }); + }); +}); + +describe('Terms and Conditions Integration', () => { + it('should handle a form with a checkbox and a label with a T&C link', () => { + const labelField: ReadOnlyField = { + type: 'LABEL', + content: 'I agree to the terms and conditions', + richContent: { + content: 'I agree to the {{link}}', + replacements: { + link: { + type: 'link', + value: 'terms and conditions', + href: 'https://example.com/terms', + target: '_blank', + }, + }, + }, + key: 'terms-label', + }; + + const checkboxField = { + type: 'CHECKBOX' as const, + key: 'agree-checkbox', + label: 'Agreement', + required: true, + options: [{ label: 'I agree', value: 'agree' }], + inputType: 'MULTI_SELECT' as const, + }; + + const labelCollector = returnReadOnlyCollector(labelField, 0); + const checkboxCollector = returnMultiSelectCollector(checkboxField, 1, []); + + // Verify label collector has pass-through richContent + expect(labelCollector.type).toBe('ReadOnlyCollector'); + expect(labelCollector.category).toBe('NoValueCollector'); + expect(labelCollector.error).toBeNull(); + expect(labelCollector.output.label).toBe('I agree to the terms and conditions'); + expect(labelCollector.output.content).toBe('I agree to the terms and conditions'); + expect(labelCollector.output.richContent).toEqual({ + content: 'I agree to the {{link}}', + replacements: [ + { + key: 'link', + type: 'link', + value: 'terms and conditions', + href: 'https://example.com/terms', + target: '_blank', + }, + ], + }); + + // Verify checkbox collector works alongside + expect(checkboxCollector.type).toBe('MultiSelectCollector'); + expect(checkboxCollector.category).toBe('MultiValueCollector'); + expect(checkboxCollector.error).toBeNull(); + expect(checkboxCollector.output.options).toEqual([{ label: 'I agree', value: 'agree' }]); + expect(checkboxCollector.input.value).toEqual([]); + expect(checkboxCollector.input.validation).toEqual([ + { type: 'required', message: 'Value cannot be empty', rule: true }, + ]); + }); +}); diff --git a/packages/davinci-client/src/lib/collector.utils.ts b/packages/davinci-client/src/lib/collector.utils.ts index 32d1f337e1..17994836eb 100644 --- a/packages/davinci-client/src/lib/collector.utils.ts +++ b/packages/davinci-client/src/lib/collector.utils.ts @@ -30,6 +30,9 @@ import type { SingleValueAutoCollectorTypes, ObjectValueAutoCollectorTypes, QrCodeCollectorBase, + ValidatedReplacement, + ValidateReplacementsResult, + ReadOnlyCollectorBase, } from './collector.types.js'; import type { DeviceAuthenticationField, @@ -42,6 +45,7 @@ import type { QrCodeField, ReadOnlyField, RedirectField, + RichContentReplacement, SingleSelectField, StandardField, ValidatedField, @@ -672,6 +676,42 @@ export function returnObjectValueCollector( return returnObjectCollector(field, idx, 'PhoneNumberCollector', prefillData); } +/** + * @function validateReplacements - Validates replacement hrefs and converts the + * Record from the API response into a ValidatedReplacement[]. + * Returns a discriminated result — never throws. + * + * @param {Record} replacements - The replacements object from the API. + * @returns {ValidateReplacementsResult} Success with validated array, or failure with error message. + */ +export function validateReplacements( + replacements: Record, +): ValidateReplacementsResult { + const validated: ValidatedReplacement[] = []; + + for (const [key, replacement] of Object.entries(replacements)) { + let href: URL; + try { + href = new URL(replacement.href); + } catch { + return { ok: false, error: `Invalid href for key: ${key}` }; + } + if (!['https:', 'http:'].includes(href.protocol)) { + return { ok: false, error: `Unsafe href protocol for key: ${key}` }; + } + + validated.push({ + key, + type: replacement.type, + value: replacement.value, + href: replacement.href, + ...(replacement.target && { target: replacement.target }), + }); + } + + return { ok: true, replacements: validated }; +} + /** * @function returnNoValueCollector - Creates a NoValueCollector object based on the provided field, index, and optional collector type. * @param {DaVinciField} field - The field object containing key, label, type, and links. @@ -706,13 +746,70 @@ export function returnNoValueCollector< } /** - * @function returnReadOnlyCollector - Creates a ReadOnlyCollector object based on the provided field and index. - * @param {DaVinciField} field - The field object containing key, label, type, and links. + * @function returnReadOnlyCollector - Creates a ReadOnlyCollector with pass-through rich content. + * When richContent is present, validates replacements and passes through the template. + * When absent, richContent echoes the plain content with empty replacements. + * + * @param {ReadOnlyField} field - The LABEL field from the API response. * @param {number} idx - The index to be used in the id of the ReadOnlyCollector. - * @returns {ReadOnlyCollector} The constructed ReadOnlyCollector object. + * @returns {ReadOnlyCollectorBase} The constructed ReadOnlyCollector. */ -export function returnReadOnlyCollector(field: ReadOnlyField, idx: number) { - return returnNoValueCollector(field, idx, 'ReadOnlyCollector'); +export function returnReadOnlyCollector(field: ReadOnlyField, idx: number): ReadOnlyCollectorBase { + const fieldErrors = [ + ...(!('content' in field) ? ['Content is not found in the field object.'] : []), + ...(!('type' in field) ? ['Type is not found in the field object.'] : []), + ]; + + const id = `${field.key || field.type}-${idx}`; + + if (!field.richContent) { + const errors = fieldErrors; + return { + category: 'NoValueCollector', + error: errors.length > 0 ? errors.join(' ') : null, + type: 'ReadOnlyCollector', + id, + name: id, + output: { + key: id, + label: field.content, + type: field.type, + content: field.content, + richContent: { content: field.content, replacements: [] }, + }, + }; + } + + // Validate that all {{key}} references in the template have corresponding replacements + const templateKeys = [...field.richContent.content.matchAll(/\{\{(\w+)\}\}/g)].map((m) => m[1]); + const apiReplacements = field.richContent.replacements ?? {}; + const missingKeys = templateKeys.filter((k) => !(k in apiReplacements)); + const templateErrors = missingKeys.map((k) => `Missing replacement for key: {{${k}}}`); + + const validationResult = + templateErrors.length === 0 ? validateReplacements(apiReplacements) : null; + + const replacements = validationResult?.ok ? validationResult.replacements : []; + const validationErrors = validationResult && !validationResult.ok ? [validationResult.error] : []; + const errors = [...fieldErrors, ...templateErrors, ...validationErrors]; + + return { + category: 'NoValueCollector', + error: errors.length > 0 ? errors.join(' ') : null, + type: 'ReadOnlyCollector', + id, + name: id, + output: { + key: id, + label: field.content, + type: field.type, + content: field.content, + richContent: { + content: field.richContent.content, + replacements, + }, + }, + }; } /** diff --git a/packages/davinci-client/src/lib/davinci.types.ts b/packages/davinci-client/src/lib/davinci.types.ts index ef6ebc23ee..596edf6f94 100644 --- a/packages/davinci-client/src/lib/davinci.types.ts +++ b/packages/davinci-client/src/lib/davinci.types.ts @@ -68,9 +68,22 @@ export type StandardField = { required?: boolean; }; +export type RichContentReplacement = { + type: 'link'; + value: string; + href: string; + target?: '_self' | '_blank'; +}; + +export type RichContent = { + content: string; + replacements?: Record; +}; + export type ReadOnlyField = { type: 'LABEL'; content: string; + richContent?: RichContent; key?: string; }; diff --git a/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts b/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts index 4962470b45..2e8f9061ae 100644 --- a/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts +++ b/packages/davinci-client/src/lib/mock-data/mock-form-fields.data.ts @@ -23,6 +23,22 @@ export const obj = { type: 'LABEL', content: 'Welcome to Ping Identity', }, + { + type: 'LABEL', + content: 'I agree to the terms and conditions', + richContent: { + content: 'I agree to the {{link}}', + replacements: { + link: { + type: 'link', + value: 'terms and conditions', + href: 'https://example.com/terms', + target: '_blank', + }, + }, + }, + key: 'terms-label', + }, { type: 'ERROR_DISPLAY', }, diff --git a/packages/davinci-client/src/types.ts b/packages/davinci-client/src/types.ts index f274efcb95..42745b0cbe 100644 --- a/packages/davinci-client/src/types.ts +++ b/packages/davinci-client/src/types.ts @@ -44,6 +44,9 @@ export type IdpCollector = collectors.IdpCollector; export type SubmitCollector = collectors.SubmitCollector; export type ValidatedTextCollector = collectors.ValidatedTextCollector; export type ReadOnlyCollector = collectors.ReadOnlyCollector; +export type RichContentLink = collectors.RichContentLink; +export type ValidatedReplacement = collectors.ValidatedReplacement; +export type CollectorRichContent = collectors.CollectorRichContent; export type MultiSelectCollector = collectors.MultiSelectCollector; export type SingleSelectCollector = collectors.SingleSelectCollector; export type DeviceRegistrationCollector = collectors.DeviceRegistrationCollector;