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 NETWORK user action is working as expected, before creating a new UI Component.  

No alt provided

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

No alt provided

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

No alt provided


Step arrow right iconPurpose of our new UI field

As you can see, the

`AddNetworkToLocation`
ruleset only provides the textfield for the user to enter the networkRef to add it to the location.  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 location and a list of networks currently added. The user can select a network from the list and submit it to trigger the action.

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 article, 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

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`
. In later steps we will extend this to the full output shown earlier.

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 user action, 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 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`
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        },

Language: json

Name: AddNetworkToLocation ruleset in Location workflow

Description:

change ref type from

`string`
to the new component type registered in the
`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:

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

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:

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

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:

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

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:

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

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:

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

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:

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

Copyright © 2025 Fluent Retail Pty Ltd (trading as Fluent Commerce). All rights reserved. No materials on this docs.fluentcommerce.com site may be used in any way and/or for any purpose without prior written authorisation from Fluent Commerce. Current customers and partners shall use these materials strictly in accordance with the terms and conditions of their written agreements with Fluent Commerce or its affiliates.

Fluent Logo