Configurable Dropdown
1. GENERAL DESCRIPTION
The Administrator can create UI elements of the “Dropdown” type with custom behavior, and add them to views. The customization consists in that the Administrator can define, in the form of an API v2 request, the rule by which the Dropdown generates the list of values that it shows when the User clicks on it.
The typical use case of the Configurable Dropdown is having a “dependent selector”. This means having a Dropdown which shows a _limited _list of the respective field’s values, depending on values set for other fields of the same entity. This prevents the User from having to scroll through a long list of all possible values.
Example 1: When the User is linking a Feature to a Program Increment (PI), they want the “Program Increment” Dropdown to list only the PIs of the particular Agile Release Train (ART) assigned to this Feature. The User doesn’t want to scroll through, and guess among, all the ARTs’ PIs.
Example 2: When the User is linking a work item to a Key Result, they want the “Key Result” Dropdown to list only the Key Results of the Objective this work item is linked to. The User doesn’t want to scroll through all the Key Results in the System.
2. HOW TO CONFIGURE AND ADD DROPDOWNS
2.1. What Is a Configuration?
A single particular case of customization of the Dropdown’s behavior is called a “Configuration” of the Dropdown. The Configurable Dropdown currently supports, and its any Configuration can be used in:
- Detailed views, as a Dropdown control element,
- List views and Inner Lists, as a unit.
Every Configuration can be associated with only one field of an Entity type (types). This field can be a link to a default (native domain) Entity, a link to an Extendable Domain Entity, or a Custom Field of "Targetprocess Entity" type.
A Configuration is in fact a piece of code, a request to the Configurable Controls API which contains a set of attributes. The attributes are explained in the Configuration Attributes section.
2.2. Creating a Dropdown Configuration
A Configuration can be created with a mashup.
Let us create a sample Configuration to solve the problem from Example 1, above.
With the SAFe data model, our Account has, among other things, the following entity types:
- Agile Release Train, or ART. This is basically a “team of teams”. It is implemented as an Extendable Domain entity type and is parent to the Team entity type.
- Program Increment, or PI. This is basically an iteration of an ART’s work, i.e. a time period linked to an ART. It is a renamed Release, which is a default entity type.
By default, the “Program Increment” Dropdown shows a list of PIs of all the ARTs present in the Account, even if a particular ART has been assigned to the entity in question. It can be difficult for the User to scan through all the PIs most of which are irrelevant. This screenshot illustrates the problem:
Now let’s add a mashup with a Configuration which alleviates the problem – see the code below. This Configuration makes the Dropdown of the “Program Increment” field show only the Program Increments of the ART which is assigned to this particular entity (e.g. Feature). In other words, it prevents the User from having to select from the PIs of all the ARTs in the Account.
This Configuration also sorts the list of PIs in the Dropdown by name in descending order. Given the naming conventions of this particular demo Account, this means that the newest PIs will appear at the top of the Dropdown’s list.
Sample code:
tau.mashups
.addDependency('tau/api/configurable-controls/controls/v1')
.addMashup(( controlsApiV1 ) => {
controlsApiV1.addConfiguration('dropdown', {
id: 'dd_conf_01',
name: 'Program Increments of the assigned ART',
label: 'Program Increment',
supportedEntityTypes: ['feature'],
requiredEntityFields: ['id', 'name', 'release'],
sampleData: {
release: { name: 'Program Increment' }
},
entityClickBehavior: 'openEntity',
field: 'release',
dropdownItemsSource: {
type: 'interpolatedQuery',
query: "release?where=(agileReleaseTrain.id==${{agileReleaseTrain.id}}$)&orderBy=name desc"
}
})
})
When setting up the mashup, in the “Placeholder(s)” field you can use any global placeholder, e.g. footerPlaceholder
.
After the mashup has been added, you can use this Configuration of the Dropdown in views. Note that you can add any number of Configurations in a single mashup.
(To learn more about how this mashup works, refer to the Configuration Attributes section.)
2.3. Adding a Dropdown Configuration to Detailed Views
On the Detailed view layout editor page, you can find a snippet of the necessary Configuration for use in your layout:
The snippet should be added as a new property component, or placed instead of an existing property component, in the target entity type’s Detailed view layout:
Now in Detailed views, our example Program Increment Dropdown only shows the PIs of the ART which is assigned to the entity:
2.4. Adding a Dropdown Configuration to a List View
In List views, the Configuration of the Dropdown is reflected in a custom Unit for the target field. Add this custom Unit to the view setup at the “Customize Cards” tab (you’ll probably want to substitute the default unit):
Now in this List view, the example Program Increment Dropdown only shows the PIs of the ART which is assigned to this particular entity:
2.5. Adding a Dropdown Configuration to Quick Add
To add configurable Dropdown fields in Quick Add, the first step is to create a configuration. Make sure that "allowedLocations" contains "quickAdd" (see configuration attributes below) or is not set at all.
tau.mashups
.addDependency('tau/api/configurable-controls/controls/v1')
.addMashup(( controlsApiV1 ) => {
controlsApiV1.addConfiguration('dropdown', {
id: 'dd_conf_01',
name: 'Program Increments of the assigned ART',
label: 'Program Increment',
supportedEntityTypes: ['feature'],
requiredEntityFields: ['id', 'name', 'release'],
allowReset:false,
sampleData: {
release: { name: 'Program Increment' }
},
entityClickBehavior: 'openEntity',
field: 'release',
dropdownItemsSource: {
type: 'custom',
getItems: async ({entity}) => {
const artId = entity.agilereleasetrain || (entity.agileReleaseTrain && entity.agileReleaseTrain.id) || null
const response = await fetch(`/api/v2/release?where=(agileReleaseTrain.id==${artId})&take=1000&orderBy=name desc`);
const {items} = await response.json();
return items;
}
//type: 'interpolatedQuery',
//query: "release?where=(agileReleaseTrain.id==${{agileReleaseTrain.id}}$)&orderBy=name desc"
}
})
})
Now go to "Settings", choose "Quick Add" on the left hand side and choose your desired entity.
Choose "Add Field" and select previously configured field. All configurable dropdown fields will be visible at the bottom.
After selecting a field, it will be added to the list of configured fields. "Required" flag is controlled by "allowReset" flag in the configuration.
Now open Quick Add popup by clicking on the button in the top-right corner and choose your configured entity. You should see a configurable dropdown field in the list of entity fields.
3. CONFIGURATION ATTRIBUTES
All the possible attributes of a Configuration of a Dropdown are listed and explained below.
3.1. id
Required? Yes.
Text ID specified by the mashup creator. Must be unique among controls of the same type (i.e. “dropdown”).
3.2. name
Required? No.
Friendly text name. Use this to help people understand what this Configuration does.
3.3. label
Required? Yes.
Text rendered as the Dropdown’s label in Detailed views, and as a column header in Lists.
3.4. supportedEntityTypes
Required? No.
Targetprocess entity type names (in lowercase) that support rendering this particular control. If none are provided, the control is considered to be universal.
Example snippet:
supportedEntityTypes: ['userstory', 'bug']
3.5. allowedLocations
Required? No.
Allows to use this Dropdown Configuration in specific view types only, e.g. only Detailed views but not Lists.
Example snippet:
allowedLocations: {
listUnit: false,
detailedView: true,
quickAdd: true,
}
By default a Configuration can be used in all view types currently supported by the Configurable Dropdown.
3.6. requiredEntityFields
Required? No (Yes - for Entity custom field).
A list of fields that should be available in “ConfigurableControlInfo” (see below) and must be requested for this control to render. The fields can belong to the very entity this Dropdown Configuration will be used with (the “target” entity), as well as to entities linked to it.
The fields are aggregated into a single API v2 selector, so it's possible to use field names (i.e. "effort") or aliased API v2 selectors (e.g. "userStoryId:userStory.id" – an ID of a User Story linked to the target entity).
Field names should be written in camel case. E.g. userStory
.
Example snippet:
requiredEntityFields: [
'id',
'name',
'feature:{id:feature.id,name:feature.name,projectId:feature.project.id}',
'tasks:tasks.select({id,name})',
'customFieldName: {id:CustomValues.Entity("Custom Field Name").id, name:CustomValues.Entity("Custom Field Name").name, resourceType:"entityTypeName"}'
]
3.7. sampleData
Required? No.
A list of dummy values for all fields that are required by the "Required Entity Fields" attribute. These dummy values will be used to render the control in all editors (like Customize Cards) where entity data isn't available, to show a meaningful preview.
Example snippet:
sampleData: {
id: 5,
name: 'Some name',
feature: {id: 10, name: 'Some feature name', projectId: 50},
tasks: [
{id: 18, name: 'Task 1'},
{id: 19, name: 'Task 2'}
]
}
3.8. isEnabled
Required? No.
A callback that consists in a particular check to determine if the control must be enabled or disabled (i.e. read only).
Template:
isEnabled?: {
callback: (info: ConfigurableControlInfo) => boolean | PromiseLike<boolean>;
rerunOnRequiredFieldsChanged: string[];
}
Example snippet 1 – the control is only enabled if the target entity’s name starts with an 'S' or 's':
isEnabled: {
callback: info => info.entity.name.toLowerCase().startsWith('s'),
rerunOnRequiredFieldsChanged: ['name']
}
Example snippet 2 – the control is always disabled (read only):
isEnabled: {
callback: () => false,
rerunOnRequiredFieldsChanged: []
}
The callback is executed a) prior to the control’s initialization, and b) when one of the entity’s fields listed in "rerunOnRequiredFieldsChanged" is updated.
On initial render, if the callback returns a promise-like, the control will be disabled and will remain disabled until the promise-like resolves with a truthy value; if it rejects or resolves with a falsy value, the control will remain disabled.
On a re-render of the Dropdown, the "rerunOnRequiredFieldsChanged" collection will be checked against the fields that are present in requiredEntityFields and have been updated. If any of them has been modified, the callback is executed.
If the callback returns a promise-like, for UX reasons, the control retains its previous "disabledness" visually (but not functionally) until the promise-like resolves, to prevent flashing on every modification.
NOTE that when registering the control as a unit (i.e. in Lists), this callback will be executed for every visible card. So if the callback performs some heavy and/or asynchronous work to verify visibility (like a network request), this may negatively affect performance when used as a unit.
If "rerunOnRequiredFieldsChanged" is not provided or empty, the callback will only be executed for the initial render.
If isEnabled is not provided, the control is always enabled.
3.9. entityClickBehavior
Required? Yes.
Determines what happens when the Dropdown is not open and the user clicks on the name of the entity selected for this field. "openDropdown" ignores the entity link and opens the Dropdown; "openEntity" opens the entity’s Detailed view.
Example snippet:
entityClickBehavior: 'openEntity'
3.10. field
Required? Yes.
The name of the entity’s field for which the Dropdown is to be used. For Custom Fields, Custom Field name is specified. For non-custom fields, one of the fields present in requiredEntityFields is specified. Field behavior must satisfy the following conditions:
- It is
null
when no value is set. - It has
id
andname
parameters (both required), and an optionalresourceType
parameter that's used to produce the correct entity label color.
The field will be used both for displaying the value and for updating the value when new dropdown option is chosen.
The field name should be written in camel case. E.g. userStory
.
3.11. allowReset
Required? No.
A boolean attribute that determines whether the Dropdown supports an empty value. Is “true” by default.
3.12. dropdownItemsSource
Required? Yes.
A subset of attributes that determines where the items in the Dropdown’s list come from. The items can be provided either by an interpolated query or by a custom function.
Template:
dropdownItemsSource:
| {
type: 'interpolatedQuery';
query: string;
}
| {
type: 'custom';
getItems: (info: ConfigurableControlInfo) => PromiseLike<{id: any; name: string}[]>;
};
An interpolated query is a templated request to API v2. It can reference the target entity’s fields with template syntax: ${{}}$
(dollar sign, double open curly braces, expression, double closed curly braces, dollar sign), for example:
"feature?where=(epic.id=='${{epic.id}}$)"
Which means “show the Features that are linked to the same Epic this target entity is linked to”.
In the case of the type: 'custom'
option, list items are provided by a custom function which takes the info about the Dropdown’s state (including the requiredEntityFields values) as input, and asynchronously returns an array of objects with 'id' and 'name' fields. In principle, this custom function can make requests to any destination or even return some hardcoded data.
Example:
getItems: async info => {
const response = await fetch(`https://my-service.com/api/getPossibleProjects?id=${info.entity.id}`);
const items = await response.json();
return items;
}
This custom function makes a request to a particular service for an array of entities that satisfy a particular condition, then returns the array.
3.13. showSearch
Required? No.
A boolean attribute that determines whether the Dropdown is always searching. Is “false” by default.
If not set dropdown alows search for more than 10 items.
4. CONFIGURATIONS LIBRARY
This section is an aggregation of useful Dropdown Configuration examples from real Customer Accounts. You can take these examples and modify them to create your new Configurations.
As new useful examples are available, they will be added here.
4.1. Show Value Streams Relevant for the Assigned ARTs
With this Configuration, the Value Stream Dropdown will only show Value Streams relevant for the ARTs assigned to the target entity. If no ARTs are assigned, then the Dropdown shows all the Value Streams in the Account.
tau.mashups
.addDependency('tau/api/configurable-controls/controls/v1')
.addMashup(controlsApiV1 => {
controlsApiV1.addConfiguration('dropdown', {
id: 'feature_valuestream_filtered_by_assigned_art',
name: 'Value Stream filtered by ART',
label: 'Value Stream',
supportedEntityTypes: ['feature'],
requiredEntityFields: ['valueStream'],
sampleData: {
valueStream: { resourceType: 'valuestream', name: 'Value Stream' }
},
entityClickBehavior: 'openDropdown',
field: 'valueStream',
dropdownItemsSource: {
type: 'interpolatedQuery',
query: "valuestream?where=(valueStreamAgileReleaseTrains.count(${{agileReleaseTrain.id}}$ == null or agileReleaseTrain.id == ${{agileReleaseTrain.id}}$) > 0)&take=999"
}
})
})
4.2. Show Program Increments with Their Corresponding Projects
With this Configuration, the Program Increment (or Release) Dropdown will show its list of possible values, each of them combined with the name of its corresponding Project entity. This is implemented with a custom items source, as an interpolated query won’t work.
Note that the list will only contain the currently running Program Increments (via the &where=(IsCurrent == True)
condition).
tau.mashups
.addDependency('tau/api/configurable-controls/controls/v1')
.addDependency('tau/configurator')
.addMashup(( controlsApiV1, configurator ) => {
const store2 = configurator.getStore2();
controlsApiV1.addConfiguration('dropdown', {
id: 'dd_conf_03',
name: 'PI with Project',
label: 'PI w/ Project',
supportedEntityTypes: ['teamiteration'],
requiredEntityFields: ['release'],
sampleData: {
release: {name: 'Program Increment', resourceType: 'release'}
},
entityClickBehavior: 'openEntity',
field: 'release',
dropdownItemsSource: {
type: 'custom',
getItems: async () => {
const releasesWithProjects = await store2.findAll('release?select={id,name,projectName:project.name}&where=(IsCurrent == True)&orderBy=name desc');
return releasesWithProjects.map(release => ({
id: release.id,
name: `${release.name} (${release.projectName})`
}));
}
}
});
})
4.3. Show Only the Suitable and Available Users for a Work Allocation
This Configuration limits the Dropdown list of Connected Users in a Work Allocation entity only to those Users who:
- have at least one Skill required in this Work Allocation;
- have Availabilities in all the Timeframes where this Work Allocation has Demands, and these Availabilities have available capacity equal or greater than the FTE required by this Work Allocation.
In other words, the Dropdown will only show the Users who have a necessary Skill and enough unallocated capacity in the necessary periods.
tau.mashups
.addDependency('tau/api/configurable-controls/controls/v1')
.addMashup(( controlsApiV1 ) => {
controlsApiV1.addConfiguration('dropdown', {
id: 'dd_conf_04',
name: 'Connected User',
label: 'Connected User',
supportedEntityTypes: ['WorkAllocation'],
requiredEntityFields: ['connectedUser:connectedUser==null?null:{id:connectedUser.id,name:connectedUser.fullName,resourceType:connectedUser.resourceType}'],
sampleData: {
ConnectedUser: { name: 'John Smith' }
},
entityClickBehavior: 'openEntity',
field: 'connectedUser',
dropdownItemsSource: {
type: 'interpolatedQuery',
query: "user?select={id, fullname as name}&where=(Skills.Where(name==\"${{Skill.Name}}$\").Count()>0 and Availabilities.Where(Timeframe.Id in [${{Demands.Select(Timeframe.Id)}}$]).Count()==${{Demands.Count()}}$ and Availabilities.Where(Timeframe.Id in [${{Demands.Select(Timeframe.Id)}}$]).Where((AvailableCapacity/100)<${{CustomValues.Get(\"FTE\").Value}}$).Count()==0)"
}
})
})
For the requiredEntityFields attribute in this particular example it's not possible to use a connectedUser
reference in the standard way (connectedUser: {id: connectedUser.id, name: connectedUser.name, resourceType: connectedUser.resourceType}
). The selector should be modified because it references the User entity that does not have a name
field. For this reason, we alias fullName
as name
: {id: connectedUser.id, name: connectedUser.fullName, resourceType: connectedUser.resourceType}
. However, using this selector alone is now not sufficient, because connectedUser
being null
will produce an empty object (connectedUser: {}
) instead of null
. To avoid this, we add an additional check using the ternary operator to evaluate to null
whenever the entity's connectedUser
also has a null
value. For additional reference on API v2 selectors syntax, see the REST API v2 Overview and related documents.
NOTE that Work Allocations, Skills, Availabilities, Timeframes, and Demands are extendable domain entities that might not be present in your particular Targetprocess Account.
Updated 9 months ago