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
5 changes: 5 additions & 0 deletions .changeset/curly-wolves-swim.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@forgerock/davinci-client': minor
---

Add `ImageCollector` for rendering `IMAGE` form fields from DaVinci Forms.
34 changes: 34 additions & 0 deletions e2e/davinci-app/components/form-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright (c) 2025 - 2026 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 type { ImageCollector } from '@forgerock/davinci-client/types';

export default function (formEl: HTMLFormElement, collector: ImageCollector) {
if (collector.error) {
const errorEl = document.createElement('p');
errorEl.innerText = `Image error: ${collector.error}`;
formEl.appendChild(errorEl);
return;
}

const container = document.createElement('div');

const img = document.createElement('img');
img.src = collector.output.imageUrl;
img.alt = collector.output.description;
img.setAttribute('data-testid', 'form-image');

if (collector.output.hyperlinkUrl) {
const anchor = document.createElement('a');
anchor.href = collector.output.hyperlinkUrl;
anchor.appendChild(img);
container.appendChild(anchor);
} else {
container.appendChild(img);
}

formEl.appendChild(container);
}
3 changes: 3 additions & 0 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import labelComponent from './components/label.js';
import objectValueComponent from './components/object-value.js';
import fidoComponent from './components/fido.js';
import qrCodeComponent from './components/qr-code.js';
import formImageComponent from './components/form-image.js';
import agreementComponent from './components/agreement.js';
import pollingComponent from './components/polling.js';
import booleanComponent from './components/boolean.js';
Expand Down Expand Up @@ -232,6 +233,8 @@ const urlParams = new URLSearchParams(window.location.search);
);
} else if (collector.type === 'QrCodeCollector') {
qrCodeComponent(formEl, collector);
} else if (collector.type === 'ImageCollector') {
formImageComponent(formEl, collector);
} else if (collector.type === 'AgreementCollector') {
agreementComponent(formEl, collector);
} else if (collector.type === 'TextCollector') {
Expand Down
13 changes: 13 additions & 0 deletions e2e/davinci-suites/src/form-fields.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,19 @@ test('Should render form fields', async ({ page }) => {
await expect(page.locator('#single-checkbox-field')).not.toBeChecked();
await expect(page.locator('.single-checkbox-field-error')).not.toBeAttached();

// Image collector: img rendered with correct src and alt (from description)
const formImage = page.getByTestId('form-image');
await expect(formImage).toBeVisible();
await expect(formImage).toHaveAttribute(
'src',
'https://www.pingidentity.com/content/dam/picr/nav/PingIdentity-ProgressPride.svg',
);
await expect(formImage).toHaveAttribute('alt', 'Ping Identity logo');

// Image wrapped in hyperlink (hyperlinkUrl present)
const formImageAnchor = page.locator('a:has([data-testid="form-image"])');
await expect(formImageAnchor).toHaveAttribute('href', 'https://www.pingidentity.com');

await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible();

Expand Down
57 changes: 38 additions & 19 deletions packages/davinci-client/api-report/davinci-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export interface CollectorRichContent {
}

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

// @public
export type CollectorValueType<T> = T extends {
Expand Down Expand Up @@ -289,13 +289,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
resume: (input: {
continueToken: string;
}) => Promise<InternalErrorResponse | NodeStates>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode>;
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
pollStatus: (collector: PollingCollector) => Poller;
getClient: () => {
status: "start";
} | {
action: string;
collectors: Collectors[];
description?: string;
Expand All @@ -309,6 +307,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
status: "error";
} | {
status: "failure";
} | {
status: "start";
} | {
authorization?: {
code?: string;
Expand All @@ -319,7 +319,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
getCollectors: () => Collectors[];
getError: () => DaVinciError | null;
getErrorCollectors: () => CollectorErrors[];
getNode: () => ContinueNode | StartNode | ErrorNode | FailureNode | SuccessNode;
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
getServer: () => {
_links?: Links;
id?: string;
Expand All @@ -328,8 +328,6 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
href?: string;
eventName?: string;
status: "continue";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -345,6 +343,8 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
interactionId?: string;
interactionToken?: string;
status: "failure";
} | {
status: "start";
} | {
_links?: Links;
eventName?: string;
Expand All @@ -361,14 +361,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -385,14 +385,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "error"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -413,14 +413,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand All @@ -437,14 +437,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
} & Omit<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
}, "error"> & Required<Pick<{
requestId: string;
data?: unknown;
error?: FetchBaseQueryError | SerializedError | undefined;
error?: SerializedError | FetchBaseQueryError | undefined;
endpointName: string;
startedTimeStamp: number;
fulfilledTimeStamp?: number;
Expand Down Expand Up @@ -955,6 +955,25 @@ export type GetClient = StartNode['client'] | ContinueNode['client'] | ErrorNode
// @public (undocumented)
export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>;

// @public
export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> {
// (undocumented)
output: NoValueCollectorBase<'ImageCollector'>['output'] & {
imageUrl: string;
description: string;
hyperlinkUrl?: string;
};
}

// @public (undocumented)
export type ImageField = {
type: 'IMAGE';
key: string;
description: string;
imageUrl: string;
hyperlinkUrl?: string;
};

// @public (undocumented)
export type InferActionCollectorType<T extends ActionCollectorTypes> = T extends 'IdpCollector' ? IdpCollector : T extends 'SubmitCollector' ? SubmitCollector : T extends 'FlowCollector' ? FlowCollector : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>;

Expand All @@ -965,7 +984,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' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'AgreementCollector' ? AgreementCollector : NoValueCollectorBase<'NoValueCollector'>;
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'AgreementCollector' ? AgreementCollector : T extends 'ImageCollector' ? ImageCollector : 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 : T extends 'ValidatedPasswordCollector' ? ValidatedPasswordCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
Expand Down Expand Up @@ -1137,10 +1156,10 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
}

// @public (undocumented)
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector;
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector | ImageCollector;

// @public
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'AgreementCollector';
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'AgreementCollector' | 'ImageCollector';

// @public
export interface OAuthDetails {
Expand Down Expand Up @@ -1522,7 +1541,7 @@ export type ReadOnlyField = {
};

// @public (undocumented)
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField;
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField;

// @public (undocumented)
export type RedirectField = {
Expand Down
Loading
Loading