Throughout these documentation I've referred to SimpleSearch. This is our first WidgetType, and it's designed to do simple lookup and display of information from backends implementing the same query style as ERM.
I'm now going to walk you through some of the setup we have for SimpleSearch, in order to help illustrate some of this.
WidgetType
This is going to dig a bit deeper into the type JSON for SimpleSearch. The WidgetType is quite large, so I'm going to break it into component sections and talk about what they do and how you can use them to define a WidgetDefinition which conforms to this Type.
At the top level, this schema (this does not include the fields at the level above for name/version) looks like the following:
{ "$schema": "http://json-schema.org/draft-07/schema#", "title": "SimpleSearch widget", "type": "object", "description": "SimpleSearch widget type", "additionalProperties": false, "required": ["baseUrl", "results"], "properties": { ... } }
This specifies some simple information about the type, as well as any required information a WidgetDefinition must contain in order to be a valid Definition for this Type. This is where anything the frontend absolutely requires for basic functionality has to be contained.
Let's look at some of the properties:
"properties": { "baseUrl": { "type": "string", "description": "The base url queries built with this widget will go to" }, "results": { "$ref": "#/$defs/results" }, "filters": { "$ref": "#/$defs/filters" }, "sort": { "$ref": "#/$defs/sort" }, "configurableProperties": { "$ref": "#/$defs/configurableProperties" } }
As you can see, some of the properties may be very simple fields, such as baseUrl, and some of the properties may require more complicated objects. In this case, most of the top-level properties require more detailed information, so to keep the JSON schema as efficient and flat as possible, this information is split out into a "$defs" section. Let's look next at the "results" property:
"results": { "type": "object", "title": "Results", "description": "Contains all the information the dashboard needs to fetch and parse results", "additionalProperties": false, "required": ["columns"], "properties": { "columns": { "type": "array", "items": { "$ref": "#/$defs/resultColumn" } } } }, "resultColumn": { "type": "object", "title": "Result column", "description": "Describes the columns to be made available in the output", "additionalProperties": false, "required": ["accessPath", "name", "valueType"], "properties": { "accessPath": { "type": "string", "description": "a string defining the path to the specified object property" }, "label": { "type": "string", "description": "an optional string prescribing the display label of the field" }, "name": { "type": "string", "description": "a string defining the name of the property for this column" }, "valueType": { "type": "string", "enum": ["String", "Integer", "Float", "Boolean", "Date"], "description": "a string defining the type of property we are displaying" } } }
All of this information is to specify that the shape for this property must look like the following:
results: { columns: [ { accessPath: "path.to.property.on.object", name: "name of property", label: "display label of field, can be overwritten", valueType: "String" }, ... ] }
(here we've only filled in one index of the columns array, normally we'd expect more options).
The frontend can then take this information and, as part of the configuration form, offer the options to users as potential fields they can display as part of their results table. Based on the valueType, we can display the results using a formatter, like FormattedUTCDate for Dates, or ✓/✖ for Boolean values.
SimpleSearch also has similar fields for `filters` and `sort`, determining the fields which a user can define filters on, and sort on. The valueType in the filter column will be used to decide what kind of field entry the user will see in the widget creation form. For a Date they will see a FOLIO datepicker, for a String they will see a TextField etc. Finally lets look at some of the other options the SimpleSearch WidgetType gives us for the Definitions:
"configurableProperties": { "type": "object", "title": "Other properties", "description": "A collection of other properties which can be configured/made configurable", "additionalProperties": false, "properties": { "urlLink": { "$ref": "#/$defs/urlLink" }, "numberOfRows": { "$ref": "#/$defs/numberOfRows" } } }
We use these as a place to hold other configurable fields, where we know things such as what type of data we're expecting. The urlLink will be used to display a link at the bottom of the simpleSearch widget, which the user can configure to take them to the same query in the SASQ screen of whatever app is relevant. The numberOfRows is how we specify a maximum number of rows for the fetch to return. See below for how this would be configured in a definition.
WidgetDefinition
A WidgetDefinition ready to be accepted into the database for this type might look like the following:
{ "type": { "name": "SimpleSearch", "version": "v1.0.0" }, "version": "v1.0.0", "name":"ERM Agreements", "definition": { ... } }
(This top level shape will be shared by definitions for ALL widget types)
We will get to the definition in a moment, but first notice that we use text fields here for the "type" field. We will then use these to attempt to resolve which type this definition conforms to (If in fact after resolution we do not find the relevant type, or the "definition" field fails validation then this will error out).
We also have a "version" field. This will be used to resolve definitions in case breaking changes are made to a widgetDefinition, eg columns removed or API paths changing. The idea is that widgets created using the old version of the widgetDefinition will not immediately fail.
(The concept of what we should do here, perhaps rendering a red "attention needed" widget in place of out of date ones or a simple wanring MessageBanner, will be later determined by user use cases and stories)
The definition section is what contains the meat of the WidgetDefinition, the actual JSON conforming to the Type.
{ "baseUrl":"/erm/sas", "results": { "columns": [ { "name":"agreementName", "label": "Agreement name", "accessPath":"name", "valueType": "String" }, { "name":"startDate", "label": "Start date", "accessPath":"startDate", "valueType": "Date" }, { "name":"endDate", "label": "End date", "accessPath":"endDate", "valueType": "Date" }, { "name":"agreementStatus", "label": "Status", "accessPath":"agreementStatus.value", "valueType": "String" } ] }, "filters": { "columns": [ { "name":"agreementName", "label": "Agreement name", "filterPath":"name", "valueType": "String", "comparators": ["=~", "!~"] }, { "name":"agreementStatus", "label": "Agreement status", "filterPath":"agreementStatus.value", "valueType": "Enum", "enumValues": [ {"value": "active", "label": "Active"}, {"value": "closed", "label": "Closed"}, {"value": "in_negotiation", "label": "In negotiation"} ], "comparators": ["==", "!="] }, { "name":"startDate", "label": "Start date", "filterPath":"startDate", "valueType": "Date", "comparators": ["==", "!=", ">", ">=", "<", "<="] }, { "name":"endDate", "label": "End date", "filterPath":"endDate", "valueType": "Date", "comparators": ["==", "!=", ">", ">=", "<", "<=", "isNull", "isNotNull"] }, { "name":"internalContact", "label": "Internal contact", "filterPath":"contacts.user", "valueType": "UUID", "comparators": ["==", "!="] } ] }, "sort": { "columns": [ { "name":"id", "sortPath":"id", "sortTypes": ["asc"] }, { "name":"agreementName", "sortPath":"name", "sortTypes": ["asc", "desc"] }, { "name":"startDate", "sortPath":"startDate", "sortTypes": ["asc", "desc"] }, { "name":"endDate", "sortPath":"endDate", "sortTypes": ["asc", "desc"] } ] }, "configurableProperties": { "urlLink": { "configurable": true }, "numberOfRows": { "configurable": false, "defValue": 5 } } }
There's a lot of information there, so I'm going to break down some of the headlines.
Results
This section is all about display in the final table. To that end the value types allowed are all from the following list
["String", "Integer", "Float", "Boolean", "Date"]
Notice for example that the valueType for agreementStatus is "String", whereas later on the same path is described as an "Enum". We will look more at the filters later, but for right now all you need to know is that a valueType String means that the resulting field will be displayed as simple text in the resulting table.
Likewise we use "Boolean" and "Date" types to drive display of either a FormattedUTCDate, or a tick/cross for true/false.
These columns are then used to populate a component which allows us to pick one or several of these to form the columns for our display table in the output. The label will be the default label displayed for that column, although this can be overwritten.
Filters
This section drives both how we dynamically create the filter sections as well as how we build the filter part of the final query. As mentioned above lets look first at the agreementStatus entry:
{ "name":"agreementStatus", "label": "Agreement status", "filterPath":"agreementStatus.value", "valueType": "Enum", "enumValues": [ {"value": "active", "label": "Active"}, {"value": "closed", "label": "Closed"}, {"value": "in_negotiation", "label": "In negotiation"} ], "comparators": ["==", "!="] }
As before we have a name and label, used to help the user while they build their widget configuration in the form. Here we have specified that the agreement status has type "Enum". This is because agreementStatus is not a string, but actually a closed list of possible values. This setup will mean that the user will see a dropdown list of the specified values.
At some point we may offer a more complicated WidgetType (or version of SimpleSearch) that can look these values up at the point where the user is building their widget.
Since we're just saving this to a JSON string eventually it wouldn't break anything to specify this as a "String" type. However that would necessitate that your users know each of the possible values for this field, as well as any differences between the filter value and how it's displayed to them usually. In fact it wouldn't break anything to put type Date, but it would then render a datePicker component and return strings of form "2021-03-19", so wouldn't likely be so useful.
However there's nothing stopping you from having an "Enum" for n set Dates in a date field, say:
"enumValues": [ {"value": "2020-12-25", "label": "Christmas 2020"}, {"value": "2019-12-25", "label": "Christmas 2019"}, {"value": "2018-12-25", "label": "Christmas 2018"}, ],
The point of stating all this is just to illustrate that the actual type of the underlying data isn't what you're specifying here, it's all relating to the configurable form and display.
The final part of this section is the comparators. For SimpleSearch these can be chosen from the list (although this is at present not enforced):
["==", "!=", "=~", "!~", ">", ">=", "<", "<=", "isNull", "isNotNull"]
since those are the comparators understood by KIWT style endpoints. Obviously not all of these make sense in all circumstances, but the Definition does not care, and will dutifully offer all of the comparators specified without question, even though "status <= 'abc'" is clearly nonsense.
Sort
The sort block is the simplest of the three,
{ "name":"endDate", "sortPath":"endDate", "sortTypes": ["asc", "desc"] }
This simply needs the path, and whether we're offering ascending/descending options. If there is only one sortType for a single sortEntry then no options are offered to the user in the form and that single value is used instead.
Configurable Properties
These are a bit different from the above, instead being for individual configurable fields with specific purposes. The way that SimpleSearch type is set up is that these can either be set to be user-configurable or not in the WidgetDefinition.
"configurableProperties": { "urlLink": { "configurable": true }, "numberOfRows": { "configurable": false, "defValue": 5 } }
Here you can see that urlLink is set to be configurable, whereas numberOfRows is non-configruable, and has a defaultValue. This will remove that entry on the dynamic widget form. The Type schema is set up to enforce defValue if configurable is set to false. In the case where a defValue is set and configurable is set to true this will manifest as a default value in the initial form creation.