Fluent Commerce Logo
Sign In

Custom UI Field Component on Add Network to Location user action

How-to Guide
Extend

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

Steps

Step arrow right iconBefore 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 `locationupsert` plugin is uploaded and activated in your sandbox.
    • Ensure the ruleset `AddNetworkToLocation` is in the location workflow.
  • 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!
No alt provided


Step arrow right iconTest out the ADD NETWORK user action

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

No alt provided

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

No alt provided

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

No alt provided


Step arrow right iconPurpose of our new UI field

As you can see, the `AddNetworkToLocation` only provides the textfield for the user to enter the networkRef to add it to the .  To improve the user experience, we will create a new field that displays both a dropdown showing a list of networks that have not been added to this and a list of networks currently added. The user can select a from the list and submit it to the .

The outcome will look like the below screenshot:

No alt provided

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 `useQuery()` hook provided in the SDK to get the list of available networks
  • Further utilise the `useQuery()` hook to get the current location's networks and filter them from the available networks list
  • 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



Step arrow right iconCreate a network selector tsx file

Open the OMX component SDK, create a new file in `src/fields/location`

No alt provided

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

No alt provided

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

Save the file.

Step arrow right iconRegister 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` enables the field to be automatically selected when a requesting type (from a , mutation, query etc) matches the registered string. In this case `NetworkSelector`

Step arrow right iconChange the attribute type in the ruleset and validate output

This step is to update the to use the new UI field component. Load the (or use modeller) and update the -> useractions -> attributes -> ref type from `string` to `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        },

After saving the , refresh the OMS webapp and click on the ADD button. The drawer should be showing the text from the custom component:

No alt providedNo alt provided

Step arrow right iconAdd useQuery() to get the list of available and current networks

The next step is to add `useQuery()` to get both the available and current networks and output them to the console.

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

After saving the changes, refresh the OMS webapp and click on the ADD button. You should see the output from the queries in the console:

No alt provided

Step arrow right iconFilter 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

With the above code added save the file and refresh the OMS. You should see output in the console like this:

No alt provided

Step arrow right iconCreate 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

With the above code saved and OMS refreshed you should see this when selecting add :

No alt provided

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

The new component should look like this:

No alt provided

Step arrow right iconFinal 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

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

With these changes saved and the OMS refreshed you should see the final output like this:

No alt provided

Step arrow right iconTest the new component with the user action flow

No alt providedNo alt provided
Randy Chan

Randy Chan

Contributors:
Cameron Johns