Skip to content

Commit

Permalink
Add select_one and select_multiple types (#96)
Browse files Browse the repository at this point in the history
* feat(#55): add select_one and select_multiple

* feat(#55): remove gender validator file

* feat(#55): update readme

* fix(#55): address feedback

* fix(#55): address feedback part 2

* test(#55): update test cases

* fix(#55): address feedback part 3

* fix(#55): address final feedback

* 1.3.5

---------

Co-authored-by: Kenn Sippell <[email protected]>
Co-authored-by: paulpascal <[email protected]>
  • Loading branch information
3 people authored Mar 29, 2024
1 parent 81115b7 commit ef9f593
Show file tree
Hide file tree
Showing 22 changed files with 186 additions and 125 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,10 @@ The `ConfigPropertyType` defines a property's validation rules and auto-formatti
| name | Must be defined | Same as string + title case + `parameter` behavior | One or more regexes which are removed from the value when matched (eg. `"parameter": ["\\sCHU"]` will format `this CHU` into `This`) |
| regex | Must match the `regex` captured by `parameter` | Same as `string` | A regex which must be matched to pass validation (eg. `"parameter": "^\\d{6}$"` will accept only 6 digit numbers) |
| phone | A valid phone number for the specified locality | Auto formatting provided by [libphonenumber](https://github.com/google/libphonenumber) | Two letter country code specifying the locality of phone number (eg. `"parameter": "KE"`) |
| none | None | None | None |
| gender | A binary gender (eg. `Male`, `Woman`, `M`) | Formats to either `Male` or `Female` | None |
| generated | None. No user inputs. | Uses [LiquidJS](https://liquidjs.com) templates to generate data | None | [Details](#The-Generated-ConfigPropertyType)
| select_one | Single choice from a list of options | Same as `string` | None | Dictionary where the keys are the option values and the values are the corresponding labels |
| select_multiple | Multiple choice from a list of options | Same as `string` | None | Same as `select_one`
| none | None | None | None |

#### The Generated ConfigPropertyType
ContactProperties with `type: "generated"` use the [LiquidJS](https://liquidjs.com) template engine to populate a property with data. Here is an example of some configuration properties which use `"type": "generated"`:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cht-user-management",
"version": "1.3.4",
"version": "1.3.5",
"main": "dist/index.js",
"dependencies": {
"@fastify/autoload": "^5.8.0",
Expand Down
14 changes: 11 additions & 3 deletions src/config/chis-tg/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@
{
"friendly_name": "Gender",
"property_name": "sex",
"type": "gender",
"type": "select_one",
"parameter": {
"male": "Masculin",
"female": "Feminin"
},
"required": true
},
{
Expand Down Expand Up @@ -155,9 +159,13 @@
"required": true
},
{
"friendly_name": "Sex",
"friendly_name": "Gender",
"property_name": "sex",
"type": "gender",
"type": "select_one",
"parameter": {
"male": "Masculin",
"female": "Feminin"
},
"required": true
},
{
Expand Down
6 changes: 5 additions & 1 deletion src/config/chis-ug/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,11 @@
{
"friendly_name": "Sex",
"property_name": "sex",
"type": "gender",
"type": "select_one",
"parameter": {
"male": "Male",
"female": "Female"
},
"required": true
}
]
Expand Down
17 changes: 12 additions & 5 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type HierarchyConstraint = {
property_name: string;
type: string;
required: boolean;
parameter? : string | string[];
parameter? : string | string[] | object;
errorDescription? : string;

contact_type: string;
Expand All @@ -43,7 +43,7 @@ export type ContactProperty = {
property_name: string;
type: string;
required: boolean;
parameter? : string | string[];
parameter? : string | string[] | object;
errorDescription? : string;
};

Expand Down Expand Up @@ -103,12 +103,19 @@ export class Config {
}

public static getUserRoleConfig(contactType: ContactType): ContactProperty {
const parameter = contactType.user_role.reduce(
(acc: { [key: string]: string }, curr: string) => {
acc[curr] = curr;
return acc;
}, {}
);

return {
friendly_name: 'Role(s)',
friendly_name: 'Roles',
property_name: 'role',
type: 'select_role',
type: 'select_multiple',
required: true,
parameter: contactType.user_role,
parameter,
};
}

Expand Down
8 changes: 4 additions & 4 deletions src/lib/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ import RemotePlaceResolver from './remote-place-resolver';
import { RemotePlace } from './cht-api';

import ValidatorDateOfBirth from './validator-dob';
import ValidatorGender from './validator-gender';
import ValidatorGenerated from './validator-generated';
import ValidatorName from './validator-name';
import ValidatorPhone from './validator-phone';
import ValidatorRegex from './validator-regex';
import ValidatorSelectMultiple from './validator-select-multiple';
import ValidatorSelectOne from './validator-select-one';
import ValidatorSkip from './validator-skip';
import ValidatorString from './validator-string';
import ValidatorRole from './validator-role';

export type ValidationError = {
property_name: string;
Expand All @@ -31,14 +31,14 @@ type ValidatorMap = {

const TypeValidatorMap: ValidatorMap = {
dob: new ValidatorDateOfBirth(),
gender: new ValidatorGender(),
generated: new ValidatorGenerated(),
name: new ValidatorName(),
none: new ValidatorSkip(),
phone: new ValidatorPhone(),
regex: new ValidatorRegex(),
select_role: new ValidatorRole(),
string: new ValidatorString(),
select_one: new ValidatorSelectOne(),
select_multiple: new ValidatorSelectMultiple(),
};

export class Validation {
Expand Down
29 changes: 0 additions & 29 deletions src/lib/validator-gender.ts

This file was deleted.

7 changes: 6 additions & 1 deletion src/lib/validator-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { IValidator } from './validation';
import ValidatorString from './validator-string';

export default class ValidatorName implements IValidator {
isValid(input: string) : boolean | string {
isValid(input: string, property : ContactProperty) : boolean | string {
// Verify property.parameter is always array
if (property.parameter && !Array.isArray(property.parameter)) {
throw Error(`Property '${property.friendly_name}' of type 'name' expects 'parameter' to be an array.`);
}

return !!input;
}

Expand Down
4 changes: 4 additions & 0 deletions src/lib/validator-phone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export default class ValidatorPhone implements IValidator {
throw Error(`property of type phone on ${property.friendly_name} missing parameter with locale`);
}

if (typeof property.parameter !== 'string') {
throw Error(`property '${property.friendly_name}' of type 'phone' expects 'parameter' to be a string.`);
}

try {
const isValid = isValidNumberForRegion(input, property.parameter as CountryCode);
if (isValid) {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/validator-regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ export default class ValidatorRegex implements IValidator {
throw Error(`property of type regex - ${property.friendly_name} is missing parameter`);
}

if (Array.isArray(property.parameter)) {
throw Error(`property of type regex - 'parameter' should not be an array`);
if (typeof property.parameter !== 'string') {
throw Error(`property '${property.friendly_name}' of type 'regex' expects 'parameter' to be a string.`);
}

const regex = new RegExp(property.parameter);
const regex = new RegExp(property.parameter.toString());
const validatorStr = new ValidatorString();
const altered = validatorStr.format(input);
const match = altered.match(regex);
Expand Down
33 changes: 0 additions & 33 deletions src/lib/validator-role.ts

This file was deleted.

54 changes: 54 additions & 0 deletions src/lib/validator-select-multiple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {ContactProperty} from '../config';
import {IValidator} from './validation';
import ValidatorString from './validator-string';
import ValidatorSelectOne from './validator-select-one';

const DELIMITER = ' ';

export default class ValidatorSelectMultiple implements IValidator {

isValid(input: string, property: ContactProperty): boolean | string {
// Verify property.parameter is an object and is not null
if (!property?.parameter || typeof property.parameter !== 'object') {
throw new TypeError(`Expected attribute "parameter" on property ${property.property_name} to be an object.`);
}

const selectOneValidator = new ValidatorSelectOne();
const stringValidator = new ValidatorString();

const selectedValues = this.parseInput(input, stringValidator);
const invalidValues = selectedValues.filter(
value => !selectOneValidator.isValid(value, property)
);

if (invalidValues.length > 0) {
return `Invalid values for property "${property.friendly_name}": ${invalidValues.join(', ')}`;
}

// Check if any values are missing and property is required
if (selectedValues.length === 0 && property.required) {
return 'Value is required';
}

return true;
}

format(input: string): string {
return Array.isArray(input) ? input.join(DELIMITER) : input;
}

get defaultError(): string {
return `Invalid input. Please use 'space' as delimiter.`;
}

private parseInput(input: string|string[], stringValidator: ValidatorString): string[] {
if (Array.isArray(input)) {
return input;
}

return input
.split(DELIMITER)
.map(value => stringValidator.format(value))
.filter(Boolean);
}
}
29 changes: 29 additions & 0 deletions src/lib/validator-select-one.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {ContactProperty} from '../config';
import {IValidator} from './validation';
import ValidatorString from './validator-string';

export default class ValidatorSelectOne implements IValidator {
isValid(input: string, property: ContactProperty): boolean|string {
const stringValidator = new ValidatorString();
const trimmedInput = stringValidator.format(input);

if (trimmedInput.length === 0 && property.required) {
return `Value is required`;
}
// Verify property.parameter is an object
if (!property?.parameter || typeof property.parameter !== 'object') {
throw new TypeError(`Expected attribute "parameter" on property ${property.property_name} to be an object.`);
}

const validValues = Object.keys(property.parameter);
return validValues.includes(trimmedInput);
}

format(input: string): string {
return input;
}

get defaultError(): string {
return 'Invalid value selected';
}
}
2 changes: 1 addition & 1 deletion src/lib/validator-string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export default class ValidatorString implements IValidator {
}

format(input : string) : string {
input = input.replace(/[^^a-zA-Z0-9À-ÖØ-öø-ÿ ()@./\-']/gu, '');
input = input.replace(/[^^a-zA-Z0-9À-ÖØ-öø-ÿ ()@./\-_']/gu, '');
input = input.replace(/\s\s+/g, ' ');
return input.trim();
}
Expand Down
23 changes: 16 additions & 7 deletions src/liquid/components/contact_type_property.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,22 @@
{{include.prop.friendly_name}}
</label>
<div class="control">
<input
name="{{ prop_name }}"
type="{% if include.prop.type == 'dob' %}date{% else %}text{% endif %}"
class="input"
{% if false and include.prop.type == 'regex' %} pattern="{{ include.prop.parameter }}" {% endif %}
{% if data[prop_name] %} value="{{ data[prop_name] }}" {% endif %}
/>
{% if include.prop.type == 'select_one' or include.prop.type == 'select_multiple' %}
{%
include "components/contact_type_select.html"
prop_name=prop_name
prop=include.prop
data=data
%}
{% else %}
<input
name="{{ prop_name }}"
type="{% if include.prop.type == 'dob' %}date{% else %}text{% endif %}"
class="input"
{% if false and include.prop.type == 'regex' %} pattern="{{ include.prop.parameter }}" {% endif %}
{% if data[prop_name] %} value="{{ data[prop_name] }}" {% endif %}
/>
{% endif %}
</div>
</div>
{% endif %}
10 changes: 10 additions & 0 deletions src/liquid/components/contact_type_select.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% assign is_multiple = include.prop.type == 'select_multiple' %}
<div class="select is-fullwidth{% if is_multiple %} is-multiple{% endif %}">
<select name="{{ prop_name }}" {% if is_multiple %}multiple{% endif %}>
{% for params in include.prop.parameter %}
<option value="{{ params[0] }}" {% if data[prop_name] == params[0] or data[prop_name] contains params[0] %}selected{% endif %}>
{{ params[1] }}
</option>
{% endfor %}
</select>
</div>
1 change: 0 additions & 1 deletion src/liquid/components/list_cell.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

<td
id="{{ include.propertyName }}"
{% if place.validationErrors[include.propertyName] %}
Expand Down
Loading

0 comments on commit ef9f593

Please sign in to comment.