Skip to content

Commit f31ebf5

Browse files
committed
feat(davinci-client): add form image collector
1 parent d5561ef commit f31ebf5

23 files changed

Lines changed: 718 additions & 617 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 for PingOne Forms.

‎.changeset/silent-ideas-joke.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
'@forgerock/davinci-client': minor
33
---
44

5-
Support form agreements with additional `title` property ReadOnlyCollector
5+
Support form agreements with AgreementCollector

‎.changeset/single-checkbox.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
'@forgerock/davinci-client': minor
33
---
44

5-
Adds support for the SINGLE_CHECKBOX field. A new ValidatedBooleanCollector interface was introduced including validation support for required checkboxes and updater support for booleans. A BooleanCollector was also added for parity in the case that no validation is required.
5+
Adds support for the SINGLE_CHECKBOX field. A new ValidatedBooleanCollector interface was introduced including validation support for required checkboxes and updater support for booleans.
66

77
**Type improvements**
88

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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 { AgreementCollector } from '@forgerock/davinci-client/types';
8+
9+
export default function (formEl: HTMLFormElement, collector: AgreementCollector) {
10+
const output = collector.output;
11+
const componentEnabled = output.enabled;
12+
13+
if (!componentEnabled) {
14+
return;
15+
}
16+
17+
const content = output.label;
18+
const titleEnabled = output.titleEnabled;
19+
const title = output.title;
20+
21+
if (titleEnabled) {
22+
const titleEl = document.createElement('h3');
23+
titleEl.innerText = title;
24+
formEl?.appendChild(titleEl);
25+
}
26+
27+
const agreement = document.createElement('p');
28+
agreement.innerText = content;
29+
formEl?.appendChild(agreement);
30+
}

‎e2e/davinci-app/components/boolean.ts‎

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
* of the MIT license. See the LICENSE file for details.
66
*/
77
import type {
8-
BooleanCollector,
98
ValidatedBooleanCollector,
109
Updater,
1110
Validator,
@@ -15,15 +14,15 @@ import { dotToCamelCase, richContentInterpolation } from '../helper.js';
1514
/**
1615
* Creates a single checkbox and attaches it to the form
1716
* @param {HTMLFormElement} formEl - The form element to attach the checkbox to
18-
* @param {BooleanCollector | ValidatedBooleanCollector} collector - Contains the configuration
17+
* @param {ValidatedBooleanCollector} collector - Contains the configuration
1918
* @param {Updater} updater - Function to call when selection changes
2019
* @param {Validator} validator - Function to validate the input
2120
*/
2221
export default function booleanComponent(
2322
formEl: HTMLFormElement,
24-
collector: BooleanCollector | ValidatedBooleanCollector,
25-
updater: Updater<BooleanCollector | ValidatedBooleanCollector>,
26-
validator?: Validator<ValidatedBooleanCollector>,
23+
collector: ValidatedBooleanCollector,
24+
updater: Updater<ValidatedBooleanCollector>,
25+
validator: Validator<ValidatedBooleanCollector>,
2726
) {
2827
const collectorKey = dotToCamelCase(collector.output.key);
2928

@@ -58,16 +57,7 @@ export default function booleanComponent(
5857
// Add event listener to handle single-select behavior
5958
checkbox.addEventListener('change', (event) => {
6059
const checked = (event.target as HTMLInputElement).checked;
61-
62-
if (collector.type === 'BooleanCollector') {
63-
const updateError = updater(checked);
64-
if (updateError && 'error' in updateError) {
65-
console.error(updateError.error.message);
66-
}
67-
return;
68-
}
69-
70-
const result = validator && validator(checked);
60+
const result = validator(checked);
7161
const errorEl = formEl?.querySelector(`.${collectorKey}-error`);
7262

7363
// Validate the input
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright (c) 2025 - 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+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2025 - 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 { ReadOnlyCollector, RichTextCollector } from '@forgerock/davinci-client/types';
8+
import { richContentInterpolation } from '../helper.js';
9+
10+
export default function (
11+
formEl: HTMLFormElement,
12+
collector: ReadOnlyCollector | RichTextCollector,
13+
) {
14+
const p = document.createElement('p');
15+
p.style.whiteSpace = 'pre-line';
16+
17+
if (collector.type !== 'RichTextCollector') {
18+
p.innerText = collector.output.content;
19+
formEl?.appendChild(p);
20+
return;
21+
}
22+
23+
const { richContent } = collector.output;
24+
25+
if (richContent.replacements.length === 0) {
26+
p.innerText = collector.output.content;
27+
formEl?.appendChild(p);
28+
return;
29+
}
30+
31+
// Interpolate the template by splitting on {{key}} and inserting links
32+
const pRichText = richContentInterpolation(richContent);
33+
34+
formEl?.appendChild(pRichText);
35+
}

‎e2e/davinci-app/components/read-only.ts‎

Lines changed: 0 additions & 41 deletions
This file was deleted.

‎e2e/davinci-app/main.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { davinci } from '@forgerock/davinci-client';
1010
import { oidc } from '@forgerock/oidc-client';
1111
import type { OidcConfig } from '@forgerock/oidc-client/types';
1212
import type {
13-
Collectors,
1413
CustomLogger,
1514
DaVinciConfig,
1615
DavinciClient,
@@ -31,10 +30,12 @@ import socialLoginButtonComponent from './components/social-login-button.js';
3130
import { serverConfigs } from './server-configs.js';
3231
import singleValueComponent from './components/single-value.js';
3332
import multiValueComponent from './components/multi-value.js';
34-
import readOnlyComponent from './components/read-only.js';
33+
import labelComponent from './components/label.js';
3534
import objectValueComponent from './components/object-value.js';
3635
import fidoComponent from './components/fido.js';
3736
import qrCodeComponent from './components/qr-code.js';
37+
import formImageComponent from './components/form-image.js';
38+
import agreementComponent from './components/agreement.js';
3839
import pollingComponent from './components/polling.js';
3940
import booleanComponent from './components/boolean.js';
4041

@@ -206,7 +207,7 @@ const urlParams = new URLSearchParams(window.location.search);
206207

207208
const collectors = davinciClient.getCollectors();
208209

209-
collectors.forEach((collector: Collectors) => {
210+
collectors.forEach((collector) => {
210211
if (collector.type === 'TextCollector' && collector.name === 'protectsdk') {
211212
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
212213
collector;
@@ -226,12 +227,16 @@ const urlParams = new URLSearchParams(window.location.search);
226227
submitForm,
227228
);
228229
} else if (collector.type === 'ReadOnlyCollector' || collector.type === 'RichTextCollector') {
229-
readOnlyComponent(
230+
labelComponent(
230231
formEl, // You can ignore this; it's just for rendering
231232
collector, // This is the plain object of the collector
232233
);
233234
} else if (collector.type === 'QrCodeCollector') {
234235
qrCodeComponent(formEl, collector);
236+
} else if (collector.type === 'ImageCollector') {
237+
formImageComponent(formEl, collector);
238+
} else if (collector.type === 'AgreementCollector') {
239+
agreementComponent(formEl, collector);
235240
} else if (collector.type === 'TextCollector') {
236241
textComponent(
237242
formEl, // You can ignore this; it's just for rendering
@@ -286,15 +291,6 @@ const urlParams = new URLSearchParams(window.location.search);
286291
davinciClient.update(collector), // Returns an update function for this collector
287292
submitForm,
288293
);
289-
} else if (collector.type === 'BooleanCollector') {
290-
booleanComponent(formEl, collector, davinciClient.update(collector));
291-
} else if (collector.type === 'ValidatedBooleanCollector') {
292-
booleanComponent(
293-
formEl,
294-
collector,
295-
davinciClient.update(collector),
296-
davinciClient.validate(collector),
297-
);
298294
} else if (collector.type === 'FlowCollector') {
299295
flowLinkComponent(
300296
formEl, // You can ignore this; it's just for rendering
@@ -309,6 +305,13 @@ const urlParams = new URLSearchParams(window.location.search);
309305
singleValueComponent(formEl, collector, davinciClient.update(collector));
310306
} else if (collector.type === 'MultiSelectCollector') {
311307
multiValueComponent(formEl, collector, davinciClient.update(collector));
308+
} else if (collector.type === 'ValidatedBooleanCollector') {
309+
booleanComponent(
310+
formEl,
311+
collector,
312+
davinciClient.update(collector),
313+
davinciClient.validate(collector),
314+
);
312315
}
313316
});
314317

‎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
*/

0 commit comments

Comments
 (0)