Custom UI Field Component on Add Network to Location user action
Authors:
Randy Chan, Cameron Johns
Changed on:
25 Feb 2025
Key Points
- This component simplifies the addition of networks to locations by providing an easy to use selector of available networks
- The basic principles can be used in other scenarios where complex fields and values may be required
Prerequisites
Steps
Before you start creating a custom UI Component
- Download the Component SDK to build a new component.
- Ensure you have a good knowledge of the following:
- Fluent OMS framework
- Front End development knowledge, including REACT JS and Typescript
- Download the ES Starter Pack and install it in your sandbox. More info can be found in Partner-hub
- After the ES starter pack is installed in the sandbox, you can check the following:
- Ensure the plugin is uploaded and activated in your sandbox.
`locationupsert`
- Ensure the ruleset is in the location workflow.
`AddNetworkToLocation`
- Ensure the
- Log in to the OMS web app and go to the location detail page. You should see the ADD NETWORK user action at the top right-hand corner.
- Then you are good to start creating the new UI component!

Test out the ADD NETWORK user action
First, try out the ADD NETWORK user action is working as expected, before creating a new UI Component.

Enter the Network Ref in the provided textfield and click the submit button.

After submitting, check the NETWORKS section, the new network should be added to the list:

Purpose of our new UI field
As you can see, the `AddNetworkToLocation`
The outcome will look like the below screenshot:

To acheive this we will follow these steps:
- Create a new component file and component code
- Register the new field in
`index.tsx`
- Change the attribute type in the ruleset (so that we can show our new field)
- Utilise the hook provided in the SDK to get the list of available networks
`useQuery()`
- Further utilise the hook to get the current location's networks and filter them from the available networks list
`useQuery()`
- Sort the output network list (sorting can be customised per your needs)
- Leverage the available design system to provide consistent style and layout
- Test the new Component
Create a network selector tsx file
Open the OMX component SDK, create a new file in
`src/fields/location`

For this article, I will name the file
`NetworkSelector.tsx`

Open your IDE of choice (VS Code recommended) and enter the following code:
1import { FormFieldProps } from 'mystique/registry/FieldRegistry';
2import { FC } from 'react';
3
4export const NetworkSelector: FC<FormFieldProps<string>> = () => {
5 return (
6 <div>Network Selector component</div>
7 );
8};
9
Language: typescript
Name: NetworkSelector.tsx initial code
Description:
The above code is the basis of our new field. For testing it will only output a string
`Network Selector component`
Save the file.
Register the new field in index.tsx
Now go to index.tsx file and add the following to register the component:
- import { NetworkSelector } from './fields/location/NetworkSelector';
- FieldRegistry.register(['NetworkSelector'], NetworkSelector);
Save the index.tsx
Registering a component in the
`FieldRegistry`
`NetworkSelector`
Change the attribute type in the ruleset and validate output
This step is to update the ruleset to use the new UI field component. Load the workflow (or use workflow modeller) and update the ruleset -> useractions -> attributes -> ref type from
`string`
`NetworkSelector`
1{
2 "name": "AddNetworkToLocation",
3 "description": "Add network to a location",
4 "type": "LOCATION",
5 "subtype": "STORE",
6 "eventType": "NORMAL",
7 "rules": [
8 {
9 "name": "{AccountID}.locationupsert.AddNetworkToLocation",
10 "props": null
11 }
12 ],
13 "triggers": [
14 {
15 "status": "ACTIVE"
16 },
17 {
18 "status": "INACTIVE"
19 },
20 {
21 "status": "CREATED"
22 }
23 ],
24 "userActions": [
25 {
26 "eventName": "AddNetworkToLocation",
27 "context": [
28 {
29 "label": "Add Network",
30 "type": "PRIMARY",
31 "modules": [
32 "adminconsole"
33 ],
34 "confirm": true
35 }
36 ],
37 "attributes": [
38 {
39 "name": "ref",
40 "label": "Network Ref",
41 "type": "NetworkSelector",
42 "source": "",
43 "defaultValue": "",
44 "mandatory": true
45 }
46 ]
47 }
48 ]
49 },
Language: json
Name: AddNetworkToLocation ruleset in Location workflow
Description:
change ref type from
`string`
`FieldRegistry`
`NetworkSelector`
After saving the workflow, refresh the OMS webapp and click on the ADD NETWORK button. The drawer should be showing the text from the custom component:


Add useQuery() to get the list of available and current networks
The next step is to add
`useQuery()`
Update our component with the following code
1import { useQuery } from 'mystique/hooks/useQuery';
2import { FormFieldProps } from 'mystique/registry/FieldRegistry';
3import { Connection } from 'mystique/types/common';
4import { FC } from 'react';
5
6interface NetworkNode {
7 id: number;
8 ref: string;
9}
10
11interface NetworkResult {
12 networks: Connection<NetworkNode>;
13}
14
15interface LocationNode {
16 id: string;
17 ref: string;
18 status: string;
19 networks: Connection<NetworkNode>;
20}
21
22interface LocationResult {
23 locations: Connection<LocationNode>;
24}
25
26const locationQuery = `
27query getLocations($locationRef:[String]){
28 locations(ref:$locationRef){
29 edges{
30 node{
31 id
32 ref
33 status
34 networks(first:100){
35 edges{
36 node{
37 id
38 ref
39 }
40 }
41 }
42
43 }
44 }
45 }
46}`;
47
48const networkQuery = `
49query getNetworks{
50 networks(first:100){
51 edges{
52 node{
53 id
54 ref
55 }
56 }
57 }
58}`;
59export const NetworkSelector: FC<FormFieldProps<string>> = ({ entityContext }) => {
60 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
61 locationRef: entityContext?.[0].entity.ref,
62 });
63 const [networkList] = useQuery<NetworkResult>(networkQuery);
64
65 console.log({ currentLocation, networkList });
66
67 return <div>Network Selector component</div>;
68};
69
Language: typescript
Name: Updated code with interfaces and queries to return networks
Description:
There's a little going on here so lets break it down.
First we declare a number of interfaces to strongly type the results coming back from our queries. Next we define our two queries to get available and current networks. Finally in our component we make the two queries and output them to the browser console.
After saving the changes, refresh the OMS webapp and click on the ADD NETWORK button. You should see the output from the queries in the console:

Filter and sort the available networks
Now that we have the queries working lets filter and sort the available networks from the current
1import { useQuery } from 'mystique/hooks/useQuery';
2import { FormFieldProps } from 'mystique/registry/FieldRegistry';
3import { Connection, Edge } from 'mystique/types/common';
4import { FC } from 'react';
5
6interface NetworkNode {
7 id: number;
8 ref: string;
9}
10
11interface NetworkResult {
12 networks: Connection<NetworkNode>;
13}
14
15interface LocationNode {
16 id: string;
17 ref: string;
18 status: string;
19 networks: Connection<NetworkNode>;
20}
21
22interface LocationResult {
23 locations: Connection<LocationNode>;
24}
25
26const locationQuery = `
27query getLocations($locationRef:[String]){
28 locations(ref:$locationRef){
29 edges{
30 node{
31 id
32 ref
33 status
34 networks(first:100){
35 edges{
36 node{
37 id
38 ref
39 }
40 }
41 }
42
43 }
44 }
45 }
46}`;
47
48const networkQuery = `
49query getNetworks{
50 networks(first:100){
51 edges{
52 node{
53 id
54 ref
55 }
56 }
57 }
58}`;
59export const NetworkSelector: FC<FormFieldProps<string>> = ({ entityContext }) => {
60 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
61 locationRef: entityContext?.[0].entity.ref,
62 });
63 const [networkList] = useQuery<NetworkResult>(networkQuery);
64
65 const compare = (a: Edge<NetworkNode>, b: Edge<NetworkNode>) => {
66 if (a.node.ref < b.node.ref) {
67 return -1;
68 }
69 if (a.node.ref > b.node.ref) {
70 return 1;
71 }
72 return 0;
73 };
74
75 const currentNetworks = currentLocation.data?.locations.edges[0].node.networks.edges
76 .map((currentNetwork) => currentNetwork.node.ref)
77 .sort();
78
79 const filteredNetworks = networkList.data?.networks.edges
80 .filter((availableNetwork) => !currentNetworks?.includes(availableNetwork.node.ref))
81 .sort(compare);
82
83 console.log({ currentNetworks, filteredNetworks });
84
85 return <div>Network Selector component</div>;
86};
87
Language: typescript
Name: Code to enable sorting and filtering of our network lists
Description:
In this change we've added our compare function to support sorting our networks and filtered the available networks to remove any current ones.
With the above code added save the file and refresh the OMS. You should see output in the console like this:

Create the dropdown and populate with available networks
Utilising the available design system (MUIV4) we will now create and populate the dropdown selector. Start by updating the file to the below snippet.
1import { Grid, MenuItem, Select, Typography } from '@material-ui/core';
2import { useQuery } from 'mystique/hooks/useQuery';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { Connection, Edge } from 'mystique/types/common';
5import { FC } from 'react';
6
7interface NetworkNode {
8 id: number;
9 ref: string;
10}
11
12interface NetworkResult {
13 networks: Connection<NetworkNode>;
14}
15
16interface LocationNode {
17 id: string;
18 ref: string;
19 status: string;
20 networks: Connection<NetworkNode>;
21}
22
23interface LocationResult {
24 locations: Connection<LocationNode>;
25}
26
27const locationQuery = `
28query getLocations($locationRef:[String]){
29 locations(ref:$locationRef){
30 edges{
31 node{
32 id
33 ref
34 status
35 networks(first:100){
36 edges{
37 node{
38 id
39 ref
40 }
41 }
42 }
43
44 }
45 }
46 }
47}`;
48
49const networkQuery = `
50query getNetworks{
51 networks(first:100){
52 edges{
53 node{
54 id
55 ref
56 }
57 }
58 }
59}`;
60export const NetworkSelector: FC<FormFieldProps<string>> = ({ onChange, entityContext, label }) => {
61 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
62 locationRef: entityContext?.[0].entity.ref,
63 });
64 const [networkList] = useQuery<NetworkResult>(networkQuery);
65
66 const compare = (a: Edge<NetworkNode>, b: Edge<NetworkNode>) => {
67 if (a.node.ref < b.node.ref) {
68 return -1;
69 }
70 if (a.node.ref > b.node.ref) {
71 return 1;
72 }
73 return 0;
74 };
75
76 const currentNetworks = currentLocation.data?.locations.edges[0].node.networks.edges.map(
77 (currentNetwork) => currentNetwork.node.ref,
78 );
79 const filteredNetworks = networkList.data?.networks.edges
80 .filter((availableNetwork) => !currentNetworks?.includes(availableNetwork.node.ref))
81 .sort(compare);
82
83 return (
84 <Grid container>
85 <Grid item>
86 <Typography>{label}</Typography>
87 </Grid>
88 <Grid item>
89 <Select onChange={(event) => onChange(event.target.value as string)}>
90 {filteredNetworks?.map((network) => {
91 const ID = network.node.ref;
92 return (
93 <MenuItem value={ID} key={ID}>
94 {ID}
95 </MenuItem>
96 );
97 })}
98 </Select>
99 </Grid>
100 </Grid>
101 );
102};
103
Language: typescript
Name: Basic selector with available networks
Description:
[Warning: empty required content area]With the above code saved and OMS refreshed you should see this when selecting add network:

This is functional but it doesn't look great so lets fix that.
We'll leverage the built in props of the MUI Select and Grid components
1import { Grid, MenuItem, Select, Typography } from '@material-ui/core';
2import { useQuery } from 'mystique/hooks/useQuery';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { Connection, Edge } from 'mystique/types/common';
5import { FC } from 'react';
6
7interface NetworkNode {
8 id: number;
9 ref: string;
10}
11
12interface NetworkResult {
13 networks: Connection<NetworkNode>;
14}
15
16interface LocationNode {
17 id: string;
18 ref: string;
19 status: string;
20 networks: Connection<NetworkNode>;
21}
22
23interface LocationResult {
24 locations: Connection<LocationNode>;
25}
26
27const locationQuery = `
28query getLocations($locationRef:[String]){
29 locations(ref:$locationRef){
30 edges{
31 node{
32 id
33 ref
34 status
35 networks(first:100){
36 edges{
37 node{
38 id
39 ref
40 }
41 }
42 }
43
44 }
45 }
46 }
47}`;
48
49const networkQuery = `
50query getNetworks{
51 networks(first:100){
52 edges{
53 node{
54 id
55 ref
56 }
57 }
58 }
59}`;
60export const NetworkSelector: FC<FormFieldProps<string>> = ({ onChange, entityContext, label }) => {
61 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
62 locationRef: entityContext?.[0].entity.ref,
63 });
64 const [networkList] = useQuery<NetworkResult>(networkQuery);
65
66 const compare = (a: Edge<NetworkNode>, b: Edge<NetworkNode>) => {
67 if (a.node.ref < b.node.ref) {
68 return -1;
69 }
70 if (a.node.ref > b.node.ref) {
71 return 1;
72 }
73 return 0;
74 };
75
76 const currentNetworks = currentLocation.data?.locations.edges[0].node.networks.edges.map(
77 (currentNetwork) => currentNetwork.node.ref,
78 );
79 const filteredNetworks = networkList.data?.networks.edges
80 .filter((availableNetwork) => !currentNetworks?.includes(availableNetwork.node.ref))
81 .sort(compare);
82
83 return (
84 // Add direction="column" here to change the flow from horizontal to vertical
85 <Grid container direction="column">
86 <Grid item>
87 <Typography>{label}</Typography>
88 </Grid>
89 <Grid item>
90 {/* Add fullWidth here to force the selector to grow to fill the available space */}
91 <Select fullWidth onChange={(event) => onChange(event.target.value as string)}>
92 {filteredNetworks?.map((network) => {
93 const ID = network.node.ref;
94 return (
95 <MenuItem value={ID} key={ID}>
96 {ID}
97 </MenuItem>
98 );
99 })}
100 </Select>
101 </Grid>
102 </Grid>
103 );
104};
105
Language: typescript
Name: Updated selector code with style props applied to both the root Grid and Select components
Description:
[Warning: empty required content area]The new component should look like this:

Final touches
The component is now perfectly functional but we can add a couple of final touches to make it a little more useful
Lets start by adding a loading indicator should the queries take a while to load
1import { Grid, MenuItem, Select, Typography } from '@material-ui/core';
2import { Loading } from 'mystique/components/Loading';
3import { useQuery } from 'mystique/hooks/useQuery';
4import { FormFieldProps } from 'mystique/registry/FieldRegistry';
5import { Connection, Edge } from 'mystique/types/common';
6import { FC } from 'react';
7
8interface NetworkNode {
9 id: number;
10 ref: string;
11}
12
13interface NetworkResult {
14 networks: Connection<NetworkNode>;
15}
16
17interface LocationNode {
18 id: string;
19 ref: string;
20 status: string;
21 networks: Connection<NetworkNode>;
22}
23
24interface LocationResult {
25 locations: Connection<LocationNode>;
26}
27
28const locationQuery = `
29query getLocations($locationRef:[String]){
30 locations(ref:$locationRef){
31 edges{
32 node{
33 id
34 ref
35 status
36 networks(first:100){
37 edges{
38 node{
39 id
40 ref
41 }
42 }
43 }
44
45 }
46 }
47 }
48}`;
49
50const networkQuery = `
51query getNetworks{
52 networks(first:100){
53 edges{
54 node{
55 id
56 ref
57 }
58 }
59 }
60}`;
61export const NetworkSelector: FC<FormFieldProps<string>> = ({ onChange, entityContext, label }) => {
62 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
63 locationRef: entityContext?.[0].entity.ref,
64 });
65 const [networkList] = useQuery<NetworkResult>(networkQuery);
66
67 const compare = (a: Edge<NetworkNode>, b: Edge<NetworkNode>) => {
68 if (a.node.ref < b.node.ref) {
69 return -1;
70 }
71 if (a.node.ref > b.node.ref) {
72 return 1;
73 }
74 return 0;
75 };
76
77 const currentNetworks = currentLocation.data?.locations.edges[0].node.networks.edges.map(
78 (currentNetwork) => currentNetwork.node.ref,
79 );
80 const filteredNetworks = networkList.data?.networks.edges
81 .filter((availableNetwork) => !currentNetworks?.includes(availableNetwork.node.ref))
82 .sort(compare);
83
84 // This line will show a loading indicator if either of the queries are fetching
85 if (networkList.fetching || currentLocation.fetching) return <Loading />;
86
87 return (
88 <Grid container direction="column">
89 <Grid item>
90 <Typography variant="body1">{label}</Typography>
91 </Grid>
92 <Grid item>
93 <Select fullWidth onChange={(event) => onChange(event.target.value as string)}>
94 {filteredNetworks?.map((network) => {
95 const ID = network.node.ref;
96 return (
97 <MenuItem value={ID} key={ID}>
98 {ID}
99 </MenuItem>
100 );
101 })}
102 </Select>
103 </Grid>
104 </Grid>
105 );
106};
107
Language: tsx
Name: Add the loading indicator and show when either of the queries are fetching
Description:
[Warning: empty required content area]Next we will add a list of currently added networks as a separate list
1import { Grid, MenuItem, Select, Typography } from '@material-ui/core';
2import { Loading } from 'mystique/components/Loading';
3import { useQuery } from 'mystique/hooks/useQuery';
4import { FormFieldProps } from 'mystique/registry/FieldRegistry';
5import { Connection, Edge } from 'mystique/types/common';
6import { FC } from 'react';
7
8interface NetworkNode {
9 id: number;
10 ref: string;
11}
12
13interface NetworkResult {
14 networks: Connection<NetworkNode>;
15}
16
17interface LocationNode {
18 id: string;
19 ref: string;
20 status: string;
21 networks: Connection<NetworkNode>;
22}
23
24interface LocationResult {
25 locations: Connection<LocationNode>;
26}
27
28const locationQuery = `
29query getLocations($locationRef:[String]){
30 locations(ref:$locationRef){
31 edges{
32 node{
33 id
34 ref
35 status
36 networks(first:100){
37 edges{
38 node{
39 id
40 ref
41 }
42 }
43 }
44
45 }
46 }
47 }
48}`;
49
50const networkQuery = `
51query getNetworks{
52 networks(first:100){
53 edges{
54 node{
55 id
56 ref
57 }
58 }
59 }
60}`;
61export const NetworkSelector: FC<FormFieldProps<string>> = ({ onChange, entityContext, label }) => {
62 const [currentLocation] = useQuery<LocationResult>(locationQuery, {
63 locationRef: entityContext?.[0].entity.ref,
64 });
65 const [networkList] = useQuery<NetworkResult>(networkQuery);
66
67 const compare = (a: Edge<NetworkNode>, b: Edge<NetworkNode>) => {
68 if (a.node.ref < b.node.ref) {
69 return -1;
70 }
71 if (a.node.ref > b.node.ref) {
72 return 1;
73 }
74 return 0;
75 };
76
77 const currentNetworks = currentLocation.data?.locations.edges[0].node.networks.edges.map(
78 (currentNetwork) => currentNetwork.node.ref,
79 );
80 const filteredNetworks = networkList.data?.networks.edges
81 .filter((availableNetwork) => !currentNetworks?.includes(availableNetwork.node.ref))
82 .sort(compare);
83
84 if (networkList.fetching || currentLocation.fetching) return <Loading />;
85
86 return (
87 <Grid container direction="column">
88 <Grid item>
89 <Typography variant="body1">{label}</Typography>
90 </Grid>
91 <Grid item>
92 <Select fullWidth onChange={(event) => onChange(event.target.value as string)}>
93 {filteredNetworks?.map((network) => {
94 const ID = network.node.ref;
95 return (
96 <MenuItem value={ID} key={ID}>
97 {ID}
98 </MenuItem>
99 );
100 })}
101 </Select>
102 </Grid>
103 <Grid item>
104 <Typography variant="caption">Current networks:</Typography>
105 </Grid>
106 <Grid item>
107 <Typography variant="caption">{currentNetworks?.join(', ')}</Typography>
108 </Grid>
109 </Grid>
110 );
111};
112
Language: typescript
Name: Add list of currently added networks
Description:
[Warning: empty required content area]With these changes saved and the OMS refreshed you should see the final output like this:

Test the new component with the user action flow

