Author:
Christopher Tse
Changed on:
14 Oct 2024
To generate a schema for your setting, we recommend using an online tool that converts JSON into a JSON schema. You can retrieve an example here.
Here’s how to use it:
Once you have the schema, store it as its own setting. The setting type should be JSON, and we recommend storing it at the account level to ensure global accessibility.
Example:
Let’s use the RETURN_REASON setting as an example. This setting is made up of label-value pairs, represented in schema terms as an array of objects. Each object contains two strings: one for the label and one for the value. Using a JSON schema converter, you should expect a schema that looks something like this:
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "title": "Generated schema for Root",
4 "type": "array",
5 "items": {
6 "type": "object",
7 "properties": {
8 "label": {
9 "type": "string"
10 },
11 "value": {
12 "type": "string"
13 }
14 },
15 "required": [
16 "label",
17 "value"
18 ]
19 }
20}
Language: json
Name: Sample JSON schema
Description:
[Warning: empty required content area]As explained above, you can now take the generated schema and create a new setting with the following details:
Name | RETURN_REASON_SCHEMA |
Context | ACCOUNT |
Context ID | 0 |
Type | JSON |
Value | {{schema from above}} |
In the schema, you can assign titles to each field using the
`title`
`title`
`label`
Depending on the JSON schema generator you used, titles may already be present in your schema. In this example, only the top-level title was added initially, but we also need titles for the
`label`
`value`
`object`
1{
2 "title": "Return Reasons",
3 "type": "array",
4 "items": {
5 "type": "object",
6 "properties": {
7 "label": {
8 "title": "Label",
9 "type": "string"
10 },
11 "value": {
12 "title": "Value",
13 "type": "string"
14 }
15 }
16 }
17}
Language: json
Name: Modified Schema
Description:
[Warning: empty required content area](Note: i18n strings are currently not supported)
For schemas that already include titles, you can use this step to update them with more appropriate labels. Ensure that each field has a clear and descriptive title.
We will later demonstrate how these titles are reflected in the settings component when we review how it is laid out on a page.
To add the settings component to a page, you need to open the manifest fragment for that page.
In this example, we will incorporate the return reason setting into a new tab called Settings under the Retailer detail page in OMS.
The retailer page is located in the
`fc.mystique.manifest.oms.fragment.admin`
Once you’ve created the setting, the next step is to edit the Retailer details page to split it into two tabs. The first tab will be named Details and will retain the existing layout currently defined in the manifest. The second tab, called Settings, will host the new component for editing the return reasons.
1{
2 "path": "retailers/:id",
3 "type": "page",
4 "component": "fc.page",
5 "data": {
6 "query": "query ($id: ID!) { retailerById(id: $id) { id ref tradingName createdOn websiteUrlName websiteUrl supportContactName supportEmail primaryEmail supportPhone summary updatedOn }}",
7 "variables": {
8 "id": "{{params.id}}"
9 }
10 },
11 "props": {
12 "title": "Retailer - {{retailerById.ref}}",
13 "backButtons": [
14 {
15 "path": "retailers",
16 "menuLabel": "i18n:fc.admin.retailers.detail.breadcrumb.backToRetailers"
17 }
18 ]
19 },
20 "descendants": [
21 {
22 "component": "fc.tabs",
23 "props": {
24 "layouts": [
25 {
26 "label": "Summary"
27 },
28 {
29 "label": "Settings"
30 }
31 ]
32 },
33 "descendants": [
34 {
35 "component": "fc.page.section",
36 "descendants": [
37 {
38 "component": "fc.card.attribute",
39 "props": {
40 "title": "i18n:fc.admin.retailers.detail.card.summary.title",
41 "width": "half",
42 "dataSource": "retailerById",
43 "attributes": [
44 {
45 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.id.label",
46 "template": "{{id}}"
47 },
48 {
49 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.ref.label",
50 "template": "{{ref}}"
51 },
52 {
53 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.creationDate.label",
54 "template": "{{dateStringFormatter createdOn}} ({{dateRelative createdOn}})"
55 },
56 {
57 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.updatedDate.label",
58 "template": "{{dateStringFormatter updatedOn}} ({{dateRelative updatedOn}})"
59 }
60 ]
61 }
62 },
63 {
64 "component": "fc.card.attribute",
65 "props": {
66 "title": "i18n:fc.admin.retailers.detail.card.details.title",
67 "width": "half",
68 "dataSource": "retailerById",
69 "attributes": [
70 {
71 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.tradingName.label",
72 "template": "{{tradingName}}"
73 },
74 {
75 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.websiteUrl.label",
76 "template": "{{websiteUrl}}",
77 "link_template": "{{websiteUrl}}",
78 "condition": "{{and websiteUrl}}"
79 },
80 {
81 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportPhone.label",
82 "template": "{{supportPhone}}"
83 },
84 {
85 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportEmail.label",
86 "template": "{{supportEmail}}"
87 }
88 ]
89 }
90 }
91 ]
92 },
93 {
94 "component": "fc.page.section",
95 "descendants": [
96 {
97 "component": "fc.settingForm",
98 "props": {
99 "context": "RETAILER",
100 "contextId": "1",
101 "setting": "RETURN_REASON",
102 "schema": "RETURN_REASON_SCHEMA"
103 }
104 }
105 ]
106 }
107 ]
108 }
109 ]
110}
Language: json
Name: Manifest Snippet
Description:
[Warning: empty required content area]Now that you have added the settings component to the page, it doesn't look very appealing on its own, as it currently occupies the entire screen while the form only takes up about half of that space. To enhance the appearance, you can place the settings component within an Accordion Component, adjust its width, and use empty components to center the Accordion.
1{
2 "path": "retailers/:id",
3 "type": "page",
4 "component": "fc.page",
5 "data": {
6 "query": "query ($id: ID!) { retailerById(id: $id) { id ref tradingName createdOn websiteUrlName websiteUrl supportContactName supportEmail primaryEmail supportPhone summary updatedOn }}",
7 "variables": {
8 "id": "{{params.id}}"
9 }
10 },
11 "props": {
12 "title": "Retailer - {{retailerById.ref}}",
13 "backButtons": [
14 {
15 "path": "retailers",
16 "menuLabel": "i18n:fc.admin.retailers.detail.breadcrumb.backToRetailers"
17 }
18 ]
19 },
20 "descendants": [
21 {
22 "component": "fc.tabs",
23 "props": {
24 "layouts": [
25 {
26 "label": "Summary"
27 },
28 {
29 "label": "Settings"
30 }
31 ]
32 },
33 "descendants": [
34 {
35 "component": "fc.page.section",
36 "descendants": [
37 {
38 "component": "fc.card.attribute",
39 "props": {
40 "title": "i18n:fc.admin.retailers.detail.card.summary.title",
41 "width": "half",
42 "dataSource": "retailerById",
43 "attributes": [
44 {
45 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.id.label",
46 "template": "{{id}}"
47 },
48 {
49 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.ref.label",
50 "template": "{{ref}}"
51 },
52 {
53 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.creationDate.label",
54 "template": "{{dateStringFormatter createdOn}} ({{dateRelative createdOn}})"
55 },
56 {
57 "label": "i18n:fc.admin.retailers.detail.card.summary.attribute.updatedDate.label",
58 "template": "{{dateStringFormatter updatedOn}} ({{dateRelative updatedOn}})"
59 }
60 ]
61 }
62 },
63 {
64 "component": "fc.card.attribute",
65 "props": {
66 "title": "i18n:fc.admin.retailers.detail.card.details.title",
67 "width": "half",
68 "dataSource": "retailerById",
69 "attributes": [
70 {
71 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.tradingName.label",
72 "template": "{{tradingName}}"
73 },
74 {
75 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.websiteUrl.label",
76 "template": "{{websiteUrl}}",
77 "link_template": "{{websiteUrl}}",
78 "condition": "{{and websiteUrl}}"
79 },
80 {
81 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportPhone.label",
82 "template": "{{supportPhone}}"
83 },
84 {
85 "label": "i18n:fc.admin.retailers.detail.card.details.attribute.supportEmail.label",
86 "template": "{{supportEmail}}"
87 }
88 ]
89 }
90 }
91 ]
92 },
93 {
94 "component": "fc.page.section",
95 "descendants": [
96 {
97 "component": "fc.page.section.column",
98 "props": {
99 "width": 2
100 }
101 },
102 {
103 "component": "fc.accordion",
104 "props": {
105 "width": 8,
106 "summary": "Return Reasons",
107 "details": {
108 "direction": "column",
109 "components": [
110 {
111 "component": "fc.settingForm",
112 "props": {
113 "context": "RETAILER",
114 "contextId": "1",
115 "setting": "RETURN_REASON",
116 "schema": "RETURN_REASON_SCHEMA"
117 }
118 }
119 ]
120 }
121 }
122 },
123 {
124 "component": "fc.page.section.column",
125 "props": {
126 "width": 2
127 }
128 }
129 ]
130 }
131 ]
132 }
133 ]
134}
Language: json
Name: Manifest Snippet
Description:
[Warning: empty required content area]In the snippet above, we've enclosed the settings component in an Accordion with a width set to 8. Additionally, we've added empty column components on either side of the Accordion. This arrangement allows us to center the Accordion, resulting in a more aesthetically pleasing layout.
Now it looks much better.
At this point, the page is functional for business users to edit return reasons.
However, there's some room for improvement here. For example, there's the fact that return reasons consist of a label and a value. The setting structure was designed this way so that the dropdown label could be changed without changing the value sent to the backend.
Since our audience are business users, they are primarily concerned with editing the label, which affects what is displayed in the dropdown menu. This raises the question: can we configure the form so that users only need to input the label, and the value auto-populates? The answer is yes, and we can achieve this by using a custom field.
1. Components SDK
First, ensure you have access to the Components SDK. If you don't already have it, you can download it from the Components SDK page.
2. Create the Custom Field
We will design a field that outputs a JSON object similar to the following:
1{
2 "label": "string",
3 "value": "string"
4}
Language: plain_text
Name: Contract for the Field's Output
Description:
[Warning: empty required content area]You can use the following boilerplate code to get started on creating the new custom field:
1import { FormFieldProps } from 'mystique/registry/FieldRegistry';
2import { FC } from 'react';
3
4
5interface LabelValuePair {
6 label: string
7 value: string
8}
9
10export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({onChange}) => {
11
12 return <></>;
13};
Language: plain_text
Name: Basic Framework for the field
Description:
[Warning: empty required content area]3. Register the Custom Field
To register this field in the field registry, add the following line to your
`index.tsx`
1FieldRegistry.register(['labelValuePair'], LabelValuePairField);
Language: plain_text
Name: Registering the field in index.html
Description:
[Warning: empty required content area]4. Use the Custom Field in the Schema
Now you can utilize this custom field in your schema. To do this, add the
`fcField`
`name`
1{
2 "$schema": "http://json-schema.org/draft-07/schema#",
3 "title": "Return Reasons",
4 "type": "array",
5 "items": {
6 "title": "Return Reason",
7 "type": "object",
8 "fcField": {
9 "name": "labelValuePair"
10 },
11 "properties": {
12 "label": {
13 "title": "Label",
14 "description": "Put the label you want the business user to see here",
15 "type": "string"
16 },
17 "value": {
18 "title": "Value",
19 "description": "The internal code for a reason. This should stay constant whiel thelabel can change.",
20 "type": "string"
21 }
22 }
23 }
24}
Language: json
Name: RETURN_REASON_SCHEMA
Description:
[Warning: empty required content area]5. Add UI to Your Field
Let's add some UI to the field. You can use the label prop, which gets passed the value of the item's
`title`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange}) => {
12 const boxCss = css`
13 display: flex;
14 align-items: center;
15 `;
16 const textFieldCss = css`
17 padding-left: 16px;
18 flex-grow: 1;
19 `;
20
21 return <Box css={boxCss}>
22 {label} <TextField css={textFieldCss} />
23 </Box>;
24};
Language: typescript
Name: Field Code
Description:
[Warning: empty required content area]6. Handle Value and Label Logic
Next, you need to pass both the label and value back to the form via the
`onChange`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue}) => {
12 const boxCss = css`
13 display: flex;
14 align-items: center;
15 `;
16 const textFieldCss = css`
17 padding-left: 16px;
18 flex-grow: 1;
19 `;
20
21 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
22 onChange({
23 label: e.target.value,
24 value: ''
25 });
26 };
27
28 return <Box css={boxCss}>
29 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
30 </Box>;
31};
Language: typescript
Name: Code
Description:
[Warning: empty required content area]7. Managing Value State
For the value, we want it to equal the label when creating a new entry, but it shouldn't change if we're editing an existing return reason. To achieve this, we'll use state to track whether the value should be the label or the previous value.
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC, useState } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue}) => {
12 const [value] = useState(defaultValue?.value);
13 const boxCss = css`
14 display: flex;
15 align-items: center;
16 `;
17 const textFieldCss = css`
18 padding-left: 16px;
19 flex-grow: 1;
20 `;
21
22 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
23 onChange({
24 label: e.target.value,
25 value: value ? value : e.target.value,
26 });
27 };
28
29 return <Box css={boxCss}>
30 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
31 </Box>;
32};
Language: typescript
Name: Code
Description:
[Warning: empty required content area]Conclusion
By following these steps, you have created a custom field that simplifies the editing process for return reasons, allowing users to focus on changing the label while automatically managing the value. This approach enhances the usability of your form for business users, aligning the technical functionality with user-friendly design.
Feel free to expand or adjust this implementation based on user feedback or additional requirements!
We now have a custom field for return reasons that can be utilized wherever a label-value pair is needed, with the user only able to modify the label.
However, what if we want the value field's functionality to change dynamically based on the label? In some instances, we may want the value to automatically update whenever the label is changed.
To accommodate this, we can introduce an extension to the field that allows toggling this behavior on and off. Here’s how we can implement this.
Extensions can be added through the
`fcField`
`extension`
`extension`
`extension`
Let's modify our field to include an
`alwaysChangeValue`
1import { css } from '@emotion/react';
2import { Box, TextField } from '@material-ui/core';
3import { FormFieldProps } from 'mystique/registry/FieldRegistry';
4import { FC, useState } from 'react';
5
6interface LabelValuePair {
7 label: string
8 value: string
9}
10
11export const LabelValuePairField : FC<FormFieldProps<LabelValuePair>> = ({label, onChange, defaultValue, extensions}) => {
12 const [value] = useState(defaultValue?.value);
13 const boxCss = css`
14 display: flex;
15 align-items: center;
16 `;
17 const textFieldCss = css`
18 padding-left: 16px;
19 flex-grow: 1;
20 `;
21
22 const onTextChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => {
23 const oldValue = value ? value : e.target.value;
24 const newValue = extensions?.alwaysChangeValue ? e.target.value : oldValue;
25 onChange({
26 label: e.target.value,
27 value: newValue,
28 });
29 };
30
31 return <Box css={boxCss}>
32 {label} <TextField css={textFieldCss} onChange={onTextChange} defaultValue={defaultValue?.label || ''}/>
33 </Box>;
34};
Language: typescript
Name: New Field
Description:
[Warning: empty required content area]And here’s how the schema would look:
1{
2 "title": "Return Reason:",
3 "type": "array",
4 "items": {
5 "title": "Return Reason",
6 "type": "object",
7 "fcField": {
8 "name": "labelvaluepair",
9 "extensions": {
10 "alwaysChangeValue": true
11 }
12 },
13 "properties": {
14 "label": {
15 "title": "Label",
16 "type": "string"
17 },
18 "value": {
19 "title": "Value",
20 "type": "string"
21 }
22 }
23 }
24}
Language: json
Name: Schema
Description:
[Warning: empty required content area]Now we have a field that can adapt its behavior based on the
`alwaysChangeValue`
Copyright © 2024 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.