Skip to content

Commit

Permalink
Static query parameters for options (#1370)
Browse files Browse the repository at this point in the history
* possible to add fixed query parameters

* updated existing unit tests

* remove queryParameters from list schema

* added unit tests

* more unit tests & fix getOptionUrl

* move logic to getOptionLookupKeys
  • Loading branch information
bjosttveit authored Aug 10, 2023
1 parent cd91980 commit bc4fb43
Show file tree
Hide file tree
Showing 14 changed files with 260 additions and 29 deletions.
25 changes: 25 additions & 0 deletions schemas/json/layout/layout.schema.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@
"mapping": {
"$ref": "#/definitions/mapping",
"description": "Optionally used to map options"
},
"queryParameters": {
"$ref": "#/definitions/queryParameters"
}
},
"required": ["optionsId"]
Expand Down Expand Up @@ -387,6 +390,9 @@
"mapping": {
"$ref": "#/definitions/mapping",
"description": "Creates a new app instance with data collected from a stateless part of the app."
},
"queryParameters": {
"$ref": "#/definitions/queryParameters"
}
}
},
Expand Down Expand Up @@ -968,6 +974,9 @@
"$ref": "#/definitions/mapping",
"description": "Optionally used to map options"
},
"queryParameters": {
"$ref": "#/definitions/queryParameters"
},
"autocomplete": {
"$ref": "#/definitions/autocomplete"
}
Expand Down Expand Up @@ -1266,6 +1275,22 @@
"type": "string"
}
},
"queryParameters": {
"type": "object",
"title": "Query parameters",
"description": "Fixed query parameters to add to the url.",
"patternProperties": {
"^.+$": {
"type": "string",
"title": "Query parameter value"
}
},
"examples": [
{
"level": "1"
}
]
},
"iframeComponent": {
"type": "object",
"properties": {
Expand Down
143 changes: 143 additions & 0 deletions src/features/options/fetch/fetchOptionsSagas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ describe('fetchOptionsSagas', () => {
dataMapping: {
some_field: 'some_url_parm',
},
fixedQueryParameters: undefined,
secure: undefined,
})
.run();
Expand Down Expand Up @@ -140,13 +141,15 @@ describe('fetchOptionsSagas', () => {
.fork(fetchSpecificOptionSaga, {
optionsId: 'fylke',
dataMapping: undefined,
fixedQueryParameters: undefined,
secure: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: {
'FlytteFra.Fylke': 'fylke',
},
fixedQueryParameters: undefined,
secure: undefined,
})
.run();
Expand Down Expand Up @@ -200,19 +203,159 @@ describe('fetchOptionsSagas', () => {
dataMapping: {
'FlytteFra.Fylke': 'fylke',
},
fixedQueryParameters: undefined,
secure: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: {
'FlytteTil.Fylke': 'fylke',
},
fixedQueryParameters: undefined,
secure: undefined,
})
.run();
});
});

describe('Fixed query parameters', () => {
it('should include static query parameters and mapping', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);
const formLayout: ILayouts = {
formLayout: [
{
id: 'fylke',
type: 'Dropdown',
textResourceBindings: {
title: 'fylke',
},
dataModelBindings: {
simpleBinding: 'FlytteFra.Fylke',
},
optionsId: 'fylke',
required: true,
queryParameters: {
level: '1',
},
},
{
id: 'kommune',
type: 'Dropdown',
textResourceBindings: {
title: 'kommune',
},
dataModelBindings: {
simpleBinding: 'FlytteTil.Kommune',
},
optionsId: 'kommune',
required: true,
mapping: {
'FlytteTil.Fylke': 'fylke',
},
queryParameters: {
level: '2',
},
},
],
};

return expectSaga(fetchOptionsSaga)
.provide([
[selectNotNull(formLayoutSelector), formLayout],
[selectNotNull(repeatingGroupsSelector), {}],
[select(instanceIdSelector), 'someId'],
])
.fork(fetchSpecificOptionSaga, {
optionsId: 'fylke',
dataMapping: undefined,
fixedQueryParameters: { level: '1' },
secure: undefined,
})
.fork(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: {
'FlytteTil.Fylke': 'fylke',
},
fixedQueryParameters: { level: '2' },
secure: undefined,
})
.run();
});

it('should include static query parameters in url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
};

return expectSaga(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: undefined,
fixedQueryParameters: { level: '1' },
secure: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1')
.run();
});

it('should include mapping in url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
};

return expectSaga(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: {
'FlytteTil.Fylke': 'fylke',
},
fixedQueryParameters: undefined,
secure: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(networking.httpGet, 'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&fylke=Oslo')
.run();
});

it('should include static query parameters and mapping in request url', () => {
jest.spyOn(networking, 'httpGet').mockResolvedValue([]);

const formData = {
'FlytteTil.Fylke': 'Oslo',
};

return expectSaga(fetchSpecificOptionSaga, {
optionsId: 'kommune',
dataMapping: {
'FlytteTil.Fylke': 'fylke',
},
fixedQueryParameters: { level: '1' },
secure: undefined,
})
.provide([
[select(formDataSelector), formData],
[select(staticUseLanguageFromState), { selectedLanguage: 'nb' }],
[select(instanceIdSelector), 'someId'],
])
.call(
networking.httpGet,
'https://local.altinn.cloud/ttd/test/api/options/kommune?language=nb&level=1&fylke=Oslo',
)
.run();
});
});

describe('instanceIdSelector', () => {
it('should return instance id if present', () => {
const state = {
Expand Down
25 changes: 18 additions & 7 deletions src/features/options/fetch/fetchOptionsSagas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ export function* fetchOptionsSaga(): SagaIterator {
const optionsWithIndexIndicators: IOptionsMetaData[] = [];
for (const layoutId of Object.keys(layouts)) {
for (const element of layouts[layoutId] || []) {
const { optionsId, mapping, secure } = element as ISelectionComponentProps;
const { optionsId, mapping, queryParameters, secure } = element as ISelectionComponentProps;

// if we have index indicators we get up the lookup keys for existing indexes
const { keys, keyWithIndexIndicator } =
(optionsId &&
getOptionLookupKeys({
id: optionsId,
mapping,
fixedQueryParameters: queryParameters,
secure,
repeatingGroups,
})) ||
Expand All @@ -81,12 +82,13 @@ export function* fetchOptionsSaga(): SagaIterator {
}

for (const optionObject of keys) {
const { id, mapping, secure } = optionObject;
const lookupKey = getOptionLookupKey({ id, mapping });
const { id, mapping, fixedQueryParameters, secure } = optionObject;
const lookupKey = getOptionLookupKey({ id, mapping, fixedQueryParameters });
if (optionsId && !fetchedOptions.includes(lookupKey)) {
yield fork(fetchSpecificOptionSaga, {
optionsId,
dataMapping: mapping,
fixedQueryParameters: queryParameters,
secure,
});
fetchedOptions.push(lookupKey);
Expand All @@ -104,13 +106,19 @@ export function* fetchOptionsSaga(): SagaIterator {
);
}

export function* fetchSpecificOptionSaga({ optionsId, dataMapping, secure }: IFetchSpecificOptionSaga): SagaIterator {
const key = getOptionLookupKey({ id: optionsId, mapping: dataMapping });
export function* fetchSpecificOptionSaga({
optionsId,
dataMapping,
fixedQueryParameters,
secure,
}: IFetchSpecificOptionSaga): SagaIterator {
const key = getOptionLookupKey({ id: optionsId, mapping: dataMapping, fixedQueryParameters });
const instanceId = yield select(instanceIdSelector);
try {
const metaData: IOptionsMetaData = {
id: optionsId,
mapping: dataMapping,
fixedQueryParameters,
secure,
};
yield put(OptionsActions.fetching({ key, metaData }));
Expand All @@ -122,6 +130,7 @@ export function* fetchSpecificOptionSaga({ optionsId, dataMapping, secure }: IFe
formData,
language,
dataMapping,
fixedQueryParameters,
secure,
instanceId,
});
Expand All @@ -138,7 +147,7 @@ export function* checkIfOptionsShouldRefetchSaga({ payload: { field } }: Payload
const optionsWithIndexIndicators = yield select(optionsWithIndexIndicatorsSelector);
let foundInExistingOptions = false;
for (const optionsKey of Object.keys(options)) {
const { mapping, id, secure } = options[optionsKey] || {};
const { mapping, fixedQueryParameters, id, secure } = options[optionsKey] || {};
if (!id) {
continue;
}
Expand All @@ -148,6 +157,7 @@ export function* checkIfOptionsShouldRefetchSaga({ payload: { field } }: Payload
yield fork(fetchSpecificOptionSaga, {
optionsId: id,
dataMapping: mapping,
fixedQueryParameters,
secure,
});
}
Expand All @@ -159,7 +169,7 @@ export function* checkIfOptionsShouldRefetchSaga({ payload: { field } }: Payload
}

for (const option of optionsWithIndexIndicators) {
const { mapping, id, secure } = option;
const { mapping, fixedQueryParameters, id, secure } = option;
if (
mapping &&
Object.keys(mapping)
Expand All @@ -175,6 +185,7 @@ export function* checkIfOptionsShouldRefetchSaga({ payload: { field } }: Payload
yield fork(fetchSpecificOptionSaga, {
optionsId: id,
dataMapping: newDataMapping,
fixedQueryParameters,
secure,
});
}
Expand Down
6 changes: 4 additions & 2 deletions src/hooks/useGetOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { IDataSources } from 'src/types/shared';
interface IUseGetOptionsParams {
optionsId: string | undefined;
mapping?: IMapping;
queryParameters?: Record<string, string>;
source?: IOptionSource;
}

Expand All @@ -19,7 +20,7 @@ export interface IOptionResources {
helpText?: ITextResource;
}

export const useGetOptions = ({ optionsId, mapping, source }: IUseGetOptionsParams) => {
export const useGetOptions = ({ optionsId, mapping, queryParameters, source }: IUseGetOptionsParams) => {
const relevantFormData = useAppSelector(
(state) => (source && getRelevantFormDataForOptionSource(state.formData.formData, source)) || {},
shallowEqual,
Expand All @@ -42,7 +43,7 @@ export const useGetOptions = ({ optionsId, mapping, source }: IUseGetOptionsPara

useEffect(() => {
if (optionsId) {
const key = getOptionLookupKey({ id: optionsId, mapping });
const key = getOptionLookupKey({ id: optionsId, mapping, fixedQueryParameters: queryParameters });
setOptions(optionState[key]?.options);
}

Expand Down Expand Up @@ -83,6 +84,7 @@ export const useGetOptions = ({ optionsId, mapping, source }: IUseGetOptionsPara
relevantTextResources.label,
relevantTextResources.description,
relevantTextResources.helpText,
queryParameters,
]);

return options;
Expand Down
3 changes: 2 additions & 1 deletion src/layout/Checkboxes/CheckboxesContainerComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,13 @@ export const CheckboxContainerComponent = ({
layout,
readOnly,
mapping,
queryParameters,
source,
textResourceBindings,
required,
labelSettings,
} = node.item;
const apiOptions = useGetOptions({ optionsId, mapping, source });
const apiOptions = useGetOptions({ optionsId, mapping, queryParameters, source });
const calculatedOptions = apiOptions || options || defaultOptions;
const hasSelectedInitial = React.useRef(false);
const optionsHasChanged = useHasChangedIgnoreUndefined(apiOptions);
Expand Down
5 changes: 4 additions & 1 deletion src/layout/Dropdown/DropdownComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,14 @@ export function DropdownComponent({ node, formData, handleDataChange, isValid, o
id,
readOnly,
mapping,
queryParameters,
source,
textResourceBindings,
} = node.item;
const { langAsString } = useLanguage();
const options = (useGetOptions({ optionsId, mapping, source }) || staticOptions)?.filter(duplicateOptionFilter);
const options = (useGetOptions({ optionsId, mapping, queryParameters, source }) || staticOptions)?.filter(
duplicateOptionFilter,
);
const lookupKey = optionsId && getOptionLookupKey({ id: optionsId, mapping });
const fetchingOptions = useAppSelector((state) => lookupKey && state.optionState.options[lookupKey]?.loading);
const hasSelectedInitial = React.useRef(false);
Expand Down
Loading

0 comments on commit bc4fb43

Please sign in to comment.