Skip to content

Commit da0521e

Browse files
authored
feat(davinci-client): add form image collector (#698)
* feat(davinci-client): add form image collector * fix(davinci-client): address PR review comments on ImageCollector
1 parent ae71c21 commit da0521e

16 files changed

Lines changed: 532 additions & 67 deletions

.changeset/curly-wolves-swim.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@forgerock/davinci-client': minor
3+
---
4+
5+
Add `ImageCollector` for rendering `IMAGE` form fields from PingOne Forms.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import type { ImageCollector } from '@forgerock/davinci-client/types';
8+
9+
export default function (formEl: HTMLFormElement, collector: ImageCollector) {
10+
if (collector.error) {
11+
const errorEl = document.createElement('p');
12+
errorEl.innerText = `Image error: ${collector.error}`;
13+
formEl.appendChild(errorEl);
14+
return;
15+
}
16+
17+
const container = document.createElement('div');
18+
19+
const img = document.createElement('img');
20+
img.src = collector.output.src;
21+
img.alt = collector.output.alt;
22+
img.setAttribute('data-testid', 'form-image');
23+
24+
if (collector.output.href) {
25+
const anchor = document.createElement('a');
26+
anchor.href = collector.output.href;
27+
anchor.appendChild(img);
28+
container.appendChild(anchor);
29+
} else {
30+
container.appendChild(img);
31+
}
32+
33+
formEl.appendChild(container);
34+
}

e2e/davinci-app/main.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import readOnlyComponent from './components/read-only.js';
3535
import objectValueComponent from './components/object-value.js';
3636
import fidoComponent from './components/fido.js';
3737
import qrCodeComponent from './components/qr-code.js';
38+
import formImageComponent from './components/form-image.js';
3839
import pollingComponent from './components/polling.js';
3940
import booleanComponent from './components/boolean.js';
4041

@@ -232,6 +233,8 @@ const urlParams = new URLSearchParams(window.location.search);
232233
);
233234
} else if (collector.type === 'QrCodeCollector') {
234235
qrCodeComponent(formEl, collector);
236+
} else if (collector.type === 'ImageCollector') {
237+
formImageComponent(formEl, collector);
235238
} else if (collector.type === 'TextCollector') {
236239
textComponent(
237240
formEl, // You can ignore this; it's just for rendering

e2e/davinci-app/server-configs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
4646
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
4747
},
4848
},
49+
/**
50+
* Form Fields (old env — image feature flag enabled)
51+
*/
52+
'60de77d5-dd2c-41ef-8c40-f8bb2381a359': {
53+
clientId: '60de77d5-dd2c-41ef-8c40-f8bb2381a359',
54+
redirectUri: window.location.origin + '/',
55+
scope: 'openid profile email name revoke',
56+
serverConfig: {
57+
wellknown:
58+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
59+
},
60+
},
4961
/**
5062
* Form Fields
5163
*/
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright (c) 2026 Ping Identity Corporation. All rights reserved.
3+
*
4+
* This software may be modified and distributed under the terms
5+
* of the MIT license. See the LICENSE file for details.
6+
*/
7+
import { expect, test } from '@playwright/test';
8+
9+
import { asyncEvents } from './utils/async-events.js';
10+
11+
test('Should render image collector in form', async ({ page }) => {
12+
const { navigate } = asyncEvents(page);
13+
await navigate('/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
14+
15+
await expect(page.getByText('Select Test Form')).toBeVisible();
16+
await page.getByRole('button', { name: 'Form Fields' }).click();
17+
18+
await expect(page.getByText('Form Fields Tests')).toBeVisible();
19+
20+
const formImage = page.getByTestId('form-image');
21+
await expect(formImage).toBeVisible();
22+
await expect(formImage).toHaveAttribute('src', /QC-Montreal-Skyline_hero\.jpg/);
23+
await expect(formImage).toHaveAttribute('alt', 'New Image');
24+
25+
const formImageAnchor = page.locator('a:has([data-testid="form-image"])');
26+
await expect(formImageAnchor).toHaveAttribute('href', 'https://www.pingidentity.com/en.html');
27+
});

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 48 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export interface CollectorRichContent {
173173
}
174174

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

178178
// @public
179179
export type CollectorValueType<T> = T extends {
@@ -283,16 +283,12 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
283283
resume: (input: {
284284
continueToken: string;
285285
}) => Promise<InternalErrorResponse | NodeStates>;
286-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
286+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode>;
287287
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
288288
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
289289
pollStatus: (collector: PollingCollector) => Poller;
290290
getClient: () => {
291-
action: string;
292-
collectors: Collectors[];
293-
description?: string;
294-
name?: string;
295-
status: "continue";
291+
status: "start";
296292
} | {
297293
action: string;
298294
collectors: Collectors[];
@@ -302,7 +298,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
302298
} | {
303299
status: "failure";
304300
} | {
305-
status: "start";
301+
action: string;
302+
collectors: Collectors[];
303+
description?: string;
304+
name?: string;
305+
status: "continue";
306306
} | {
307307
authorization?: {
308308
code?: string;
@@ -313,15 +313,9 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
313313
getCollectors: () => Collectors[];
314314
getError: () => DaVinciError | null;
315315
getErrorCollectors: () => CollectorErrors[];
316-
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
316+
getNode: () => StartNode | ErrorNode | FailureNode | ContinueNode | SuccessNode;
317317
getServer: () => {
318-
_links?: Links;
319-
id?: string;
320-
interactionId?: string;
321-
interactionToken?: string;
322-
href?: string;
323-
eventName?: string;
324-
status: "continue";
318+
status: "start";
325319
} | {
326320
_links?: Links;
327321
eventName?: string;
@@ -338,7 +332,13 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
338332
interactionToken?: string;
339333
status: "failure";
340334
} | {
341-
status: "start";
335+
_links?: Links;
336+
id?: string;
337+
interactionId?: string;
338+
interactionToken?: string;
339+
href?: string;
340+
eventName?: string;
341+
status: "continue";
342342
} | {
343343
_links?: Links;
344344
eventName?: string;
@@ -355,14 +355,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
355355
} & Omit<{
356356
requestId: string;
357357
data?: unknown;
358-
error?: FetchBaseQueryError | SerializedError | undefined;
358+
error?: SerializedError | FetchBaseQueryError | undefined;
359359
endpointName: string;
360360
startedTimeStamp: number;
361361
fulfilledTimeStamp?: number;
362362
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
363363
requestId: string;
364364
data?: unknown;
365-
error?: FetchBaseQueryError | SerializedError | undefined;
365+
error?: SerializedError | FetchBaseQueryError | undefined;
366366
endpointName: string;
367367
startedTimeStamp: number;
368368
fulfilledTimeStamp?: number;
@@ -379,14 +379,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
379379
} & Omit<{
380380
requestId: string;
381381
data?: unknown;
382-
error?: FetchBaseQueryError | SerializedError | undefined;
382+
error?: SerializedError | FetchBaseQueryError | undefined;
383383
endpointName: string;
384384
startedTimeStamp: number;
385385
fulfilledTimeStamp?: number;
386386
}, "error"> & Required<Pick<{
387387
requestId: string;
388388
data?: unknown;
389-
error?: FetchBaseQueryError | SerializedError | undefined;
389+
error?: SerializedError | FetchBaseQueryError | undefined;
390390
endpointName: string;
391391
startedTimeStamp: number;
392392
fulfilledTimeStamp?: number;
@@ -407,14 +407,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
407407
} & Omit<{
408408
requestId: string;
409409
data?: unknown;
410-
error?: FetchBaseQueryError | SerializedError | undefined;
410+
error?: SerializedError | FetchBaseQueryError | undefined;
411411
endpointName: string;
412412
startedTimeStamp: number;
413413
fulfilledTimeStamp?: number;
414414
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
415415
requestId: string;
416416
data?: unknown;
417-
error?: FetchBaseQueryError | SerializedError | undefined;
417+
error?: SerializedError | FetchBaseQueryError | undefined;
418418
endpointName: string;
419419
startedTimeStamp: number;
420420
fulfilledTimeStamp?: number;
@@ -431,14 +431,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
431431
} & Omit<{
432432
requestId: string;
433433
data?: unknown;
434-
error?: FetchBaseQueryError | SerializedError | undefined;
434+
error?: SerializedError | FetchBaseQueryError | undefined;
435435
endpointName: string;
436436
startedTimeStamp: number;
437437
fulfilledTimeStamp?: number;
438438
}, "error"> & Required<Pick<{
439439
requestId: string;
440440
data?: unknown;
441-
error?: FetchBaseQueryError | SerializedError | undefined;
441+
error?: SerializedError | FetchBaseQueryError | undefined;
442442
endpointName: string;
443443
startedTimeStamp: number;
444444
fulfilledTimeStamp?: number;
@@ -945,6 +945,25 @@ export type GetClient = StartNode['client'] | ContinueNode['client'] | ErrorNode
945945
// @public (undocumented)
946946
export type IdpCollector = ActionCollectorWithUrl<'IdpCollector'>;
947947

948+
// @public
949+
export interface ImageCollector extends NoValueCollectorBase<'ImageCollector'> {
950+
// (undocumented)
951+
output: NoValueCollectorBase<'ImageCollector'>['output'] & {
952+
src: string;
953+
alt: string;
954+
href?: string;
955+
};
956+
}
957+
958+
// @public (undocumented)
959+
export type ImageField = {
960+
type: 'IMAGE';
961+
key: string;
962+
description: string;
963+
imageUrl: string;
964+
hyperlinkUrl?: string;
965+
};
966+
948967
// @public (undocumented)
949968
export type InferActionCollectorType<T extends ActionCollectorTypes> = T extends 'IdpCollector' ? IdpCollector : T extends 'SubmitCollector' ? SubmitCollector : T extends 'FlowCollector' ? FlowCollector : ActionCollectorWithUrl<'ActionCollector'> | ActionCollectorNoUrl<'ActionCollector'>;
950969

@@ -955,7 +974,7 @@ export type InferAutoCollectorType<T extends AutoCollectorTypes> = T extends 'Pr
955974
export type InferMultiValueCollectorType<T extends MultiValueCollectorTypes> = T extends 'MultiSelectCollector' ? MultiValueCollectorWithValue<'MultiSelectCollector'> : MultiValueCollectorWithValue<'MultiValueCollector'> | MultiValueCollectorNoValue<'MultiValueCollector'>;
956975

957976
// @public
958-
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : NoValueCollectorBase<'NoValueCollector'>;
977+
export type InferNoValueCollectorType<T extends NoValueCollectorTypes> = T extends 'ReadOnlyCollector' ? ReadOnlyCollector : T extends 'RichTextCollector' ? RichTextCollector : T extends 'QrCodeCollector' ? QrCodeCollector : T extends 'ImageCollector' ? ImageCollector : NoValueCollectorBase<'NoValueCollector'>;
959978

960979
// @public
961980
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 'BooleanCollector' ? BooleanCollector : T extends 'ValidatedBooleanCollector' ? ValidatedBooleanCollector : SingleValueCollectorWithValue<'SingleValueCollector'> | SingleValueCollectorNoValue<'SingleValueCollector'>;
@@ -1127,10 +1146,10 @@ export interface NoValueCollectorBase<T extends NoValueCollectorTypes> {
11271146
}
11281147

11291148
// @public (undocumented)
1130-
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector;
1149+
export type NoValueCollectors = NoValueCollectorBase<'NoValueCollector'> | ReadOnlyCollector | RichTextCollector | QrCodeCollector | ImageCollector;
11311150

11321151
// @public
1133-
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector';
1152+
export type NoValueCollectorTypes = 'ReadOnlyCollector' | 'RichTextCollector' | 'NoValueCollector' | 'QrCodeCollector' | 'ImageCollector';
11341153

11351154
// @public
11361155
export interface OAuthDetails {
@@ -1513,7 +1532,7 @@ export type ReadOnlyField = {
15131532
};
15141533

15151534
// @public (undocumented)
1516-
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField;
1535+
export type ReadOnlyFields = ReadOnlyField | QrCodeField | AgreementField | ImageField;
15171536

15181537
// @public (undocumented)
15191538
export type RedirectField = {

0 commit comments

Comments
 (0)