Skip to content

Commit

Permalink
feature: add option to allow overwriting state values from query para…
Browse files Browse the repository at this point in the history
…m population (#1260)

* Added allowOverwriteFromQueryParam option for list fields

* Updated getValidStateFromQueryParameters to evaluate overwrite option

* Updated tests to match new pre pop fields structure

* Changed naming of vars for readability

* Updated designer to add field for allowing overwrite from query param option to be set

* Renamed allowOverwriteFromQueryParam to allowPrePopulationOverwrite

* Updated getPrePopulatedItems to make it more readable

* Update docs
  • Loading branch information
ziggy-cyb authored Jun 5, 2024
1 parent 7455f0c commit 10fded0
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 38 deletions.
95 changes: 65 additions & 30 deletions designer/client/field-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function FieldEdit({
required = true,
exposeToContext = false,
allowPrePopulation = false,
allowPrePopulationOverwrite = false,
disableChangingFromSummary = false,
} = options;
const isFileUploadField = selectedComponent.type === "FileUploadField";
Expand Down Expand Up @@ -228,37 +229,71 @@ export function FieldEdit({
</div>
</div>
{isListField && (
<div className="govuk-checkboxes govuk-form-group">
<div className="govuk-checkboxes__item">
<input
type="checkbox"
id="field-options-allow-pre-population"
className={`govuk-checkboxes__input`}
name="options.allowPrePopulation"
checked={allowPrePopulation}
onChange={(e) =>
dispatch({
type: Actions.EDIT_OPTIONS_ALLOW_PRE_POPULATION,
payload: e.target.checked,
})
}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="field-options-allow-pre-population"
>
{i18n("common.allowPrePopulationOption.title", {
component:
ComponentTypes.find(
(componentType) => componentType.name === type
)?.title ?? "",
})}
</label>
<span className="govuk-hint govuk-checkboxes__hint">
{i18n("common.allowPrePopulationOption.helpText")}
</span>
<>
<div className="govuk-checkboxes govuk-form-group">
<div className="govuk-checkboxes__item">
<input
type="checkbox"
id="field-options-allow-pre-population"
className={`govuk-checkboxes__input`}
name="options.allowPrePopulation"
checked={allowPrePopulation}
onChange={(e) =>
dispatch({
type: Actions.EDIT_OPTIONS_ALLOW_PRE_POPULATION,
payload: e.target.checked,
})
}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="field-options-allow-pre-population"
>
{i18n("common.allowPrePopulationOption.title", {
component:
ComponentTypes.find(
(componentType) => componentType.name === type
)?.title ?? "",
})}
</label>
<span className="govuk-hint govuk-checkboxes__hint">
{i18n("common.allowPrePopulationOption.helpText")}
</span>
</div>
</div>
</div>
<div className="govuk-checkboxes govuk-form-group">
<div className="govuk-checkboxes__item">
<input
type="checkbox"
id="field-options-allow-overwrite-from-query-param"
className={`govuk-checkboxes__input`}
name="options.allowPrePopulationOverwrite"
checked={allowPrePopulationOverwrite}
onChange={(e) =>
dispatch({
type:
Actions.EDIT_OPTIONS_ALLOW_OVERWRITE_FROM_QUERY_PARAM,
payload: e.target.checked,
})
}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor="field-options-allow-pre-population"
>
{i18n("common.allowPrePopulationOverwriteOption.title", {
component:
ComponentTypes.find(
(componentType) => componentType.name === type
)?.title ?? "",
})}
</label>
<span className="govuk-hint govuk-checkboxes__hint">
{i18n("common.allowPrePopulationOverwriteOption.helpText")}
</span>
</div>
</div>
</>
)}
<div
className="govuk-checkboxes govuk-form-group"
Expand Down
4 changes: 4 additions & 0 deletions designer/client/i18n/translations/en.translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@
"helpText": "Tick this box if you want this field to be available to be pre-populated by query parameter",
"title": "Allow pre-population"
},
"allowPrePopulationOverwriteOption": {
"helpText": "Tick this box if you want query parameters to override state values for this field when passed in. This will only have an effect if 'Allow pre-population' is also ticked",
"title": "Allow overwriting from query parameter"
},
"disableChangingFromSummaryOption": {
"helpText": "Tick this box if you want the change button to be removed for this field on the summary screen",
"title": "Disable changing from summary"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ export function optionsReducer(state, action: OptionsActions) {
options: { ...options, allowPrePopulation: payload },
},
};
case Options.EDIT_OPTIONS_ALLOW_PRE_POPULATION_OVERWRITE:
return {
...state,
selectedComponent: {
...selectedComponent,
options: { ...options, allowPrePopulationOverwrite: payload },
},
};
case Options.EDIT_OPTIONS_DISABLE_CHANGING_FROM_SUMMARY:
return {
...state,
Expand Down
1 change: 1 addition & 0 deletions designer/client/reducers/component/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export enum Options {
EDIT_OPTIONS_SUFFIX = "EDIT_OPTIONS_SUFFIX",
EDIT_OPTIONS_EXPOSE_TO_CONTEXT = "EDIT_OPTIONS_EXPOSE_TO_CONTEXT",
EDIT_OPTIONS_ALLOW_PRE_POPULATION = "EDIT_OPTIONS_ALLOW_PRE_POPULATION",
EDIT_OPTIONS_ALLOW_PRE_POPULATION_OVERWRITE = "EDIT_OPTIONS_ALLOW_PRE_POPULATION_OVERWRITE",
EDIT_OPTIONS_DISABLE_CHANGING_FROM_SUMMARY = "EDIT_OPTIONS_DISABLE_CHANGING_FROM_SUMMARY",
}

Expand Down
7 changes: 6 additions & 1 deletion docs/designer/query-param-prepopulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ To allow a field to be pre-populated, tick the "allow query parameter pre-popula

Once pre-population is allowed on a field, you can pre-populate that field by appending a query parameter with the component name to a form url e.g. `https://your-forms-url/your-form/target-page?firstName=Joe&lastName=Bloggs`.

## allowPrePopulationOverwrite

By default, if a field marked for pre-population already has state set in the user's session, the incoming value will be ignored.
Sometimes you might want a query parameter to always overwrite the current state, for example, if there is a hidden field that the user will change through links with different query parameters.
To allow this, you can pass a second option to the component, `allowPrePopulationOverwrite`.

## caveats

- For the time being, due to complications with validation, this functionality is only available to list type components e.g. select fields or autocomplete fields.
- If a field has pre-population enabled, but the user already has a form state with that field populated, then the incoming query parameter will be ignored.
- If the field is in a section, then the query param will need to be passed with dot notation e.g. `yourDetails.firstName=Joe`.
1 change: 1 addition & 0 deletions model/src/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ interface ListFieldBase {
bold?: boolean;
exposeToContext?: boolean;
allowPrePopulation?: boolean;
allowPrePopulationOverwrite?: boolean;
disableChangingFromSummary?: boolean;
customValidationMessages?: Record<string, string>;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,18 @@ export class ComponentCollection {
getPrePopulatedItems() {
return this.formItems
.filter((item) => item.options?.allowPrePopulation)
.map((item) => item.getStateSchemaKeys())
.map((item) => {
// to access the schema we need to use the component name to retrieve the value from getStateSchemaKeys
const schema = item.getStateSchemaKeys()[item.name];

return {
[item.name]: {
schema,
allowPrePopulationOverwrite:
item.options.allowPrePopulationOverwrite,
},
};
})
.reduce((acc, curr) => merge(acc, curr), {});
}

Expand Down
9 changes: 7 additions & 2 deletions runner/src/server/plugins/engine/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,16 @@ export function getValidStateFromQueryParameters(
) {
return Object.entries(queryParameters).reduce<Record<string, any>>(
(acc, [key, value]) => {
if (reach(prePopFields, key) === undefined || reach(state, key)) {
const prePopField = reach(prePopFields, key);
const stateValue = reach(state, key);
if (
!prePopField ||
(stateValue && !prePopField.allowPrePopulationOverwrite)
) {
return acc;
}

const result = reach(prePopFields, key).validate(value);
const result = prePopField.schema.validate(value);
if (result.error) {
return acc;
}
Expand Down
35 changes: 31 additions & 4 deletions runner/test/cases/server/plugins/engine/helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,9 @@ suite("Helpers", () => {
aBadQueryParam: "A value",
};
const prePopFields = {
eggType: Joi.string().required(),
eggType: {
schema: Joi.string().required(),
},
};
expect(
Object.keys(getValidStateFromQueryParameters(prePopFields, query))
Expand All @@ -321,7 +323,9 @@ suite("Helpers", () => {
eggType: "Hard boiled",
};
const prePopFields = {
eggType: Joi.string().required(),
eggType: {
schema: Joi.string().required(),
},
};
const state = {
eggType: "Fried",
Expand All @@ -334,13 +338,34 @@ suite("Helpers", () => {
).to.equal(0);
});

test("Should allow the value to be overwritten if allowPrePopulationOverwrite is true", () => {
const query = {
eggType: "Hard boiled",
};
const prePopFields = {
eggType: {
schema: Joi.string().required(),
allowPrePopulationOverwrite: true,
},
};
const state = {
eggType: "Fried",
};

expect(
getValidStateFromQueryParameters(prePopFields, query, state)
).to.equal({ eggType: "Hard boiled" });
});

test("Should be able to update nested object values", () => {
const query = {
"yourEggs.eggType": "Fried egg",
};
const prePopFields = {
yourEggs: {
eggType: Joi.string().required(),
eggType: {
schema: Joi.string().required(),
},
},
};
expect(
Expand All @@ -354,7 +379,9 @@ suite("Helpers", () => {
};
const prePopFields = {
yourEggs: {
eggType: Joi.string().valid("boiled", "fried", "poached"),
eggType: {
schema: Joi.string().valid("boiled", "fried", "poached"),
},
},
};
expect(
Expand Down

0 comments on commit 10fded0

Please sign in to comment.