@colisweb/rescript-toolkit 2.67.0 → 2.68.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.gitlab-ci.yml +3 -2
- package/.yarn/cache/boolbase-npm-1.0.0-965fe9af6d-3e25c80ef6.zip +0 -0
- package/.yarn/cache/camelcase-npm-3.0.0-0c65af0c7f-ae4fe1c17c.zip +0 -0
- package/.yarn/cache/comma-separated-tokens-npm-1.0.8-00dbbf3418-0adcb07174.zip +0 -0
- package/.yarn/cache/css-selector-parser-npm-1.4.1-b8c642c4c5-31948754e5.zip +0 -0
- package/.yarn/cache/fault-npm-2.0.1-c462630f58-c9b30f47d9.zip +0 -0
- package/.yarn/cache/format-npm-0.2.2-679f3acc64-646a60e133.zip +0 -0
- package/.yarn/cache/github-slugger-npm-2.0.0-3afba76e6c-250375cde2.zip +0 -0
- package/.yarn/cache/hast-util-has-property-npm-1.0.4-a09b607810-23025cee66.zip +0 -0
- package/.yarn/cache/hast-util-has-property-npm-2.0.1-aa6919669c-cc909b7e29.zip +0 -0
- package/.yarn/cache/hast-util-heading-rank-npm-2.1.1-0d71da5801-a49233e9ac.zip +0 -0
- package/.yarn/cache/hast-util-is-element-npm-1.1.0-be10e62fa7-30fad3f65e.zip +0 -0
- package/.yarn/cache/hast-util-is-element-npm-2.1.3-3051d610ff-9d988f6839.zip +0 -0
- package/.yarn/cache/hast-util-select-npm-1.0.1-115974f390-9cdb20850f.zip +0 -0
- package/.yarn/cache/hast-util-to-string-npm-2.0.0-c6108aa2b8-0c087f8dee.zip +0 -0
- package/.yarn/cache/hast-util-to-text-npm-3.1.2-922eb1f623-d17cf3344c.zip +0 -0
- package/.yarn/cache/hast-util-whitespace-npm-1.0.4-43bb1ff3d0-b7f4a1942b.zip +0 -0
- package/.yarn/cache/{husky-npm-8.0.2-46c70b41ed-e101656fcb.zip → husky-npm-8.0.3-b0b59c5127-837bc7e441.zip} +0 -0
- package/.yarn/cache/lowlight-npm-2.8.0-c65abb6cac-c45a91e715.zip +0 -0
- package/.yarn/cache/not-npm-0.1.0-a1712708cd-8043bb53bc.zip +0 -0
- package/.yarn/cache/nth-check-npm-1.0.2-3f6d0d22eb-59e115fdd7.zip +0 -0
- package/.yarn/cache/property-information-npm-3.2.0-ae434c241d-ed2614520d.zip +0 -0
- package/.yarn/cache/rehype-add-classes-npm-1.0.0-ddf6b4e74d-25c0e2dbf5.zip +0 -0
- package/.yarn/cache/rehype-autolink-headings-npm-6.1.1-fe8058cc11-60782fb2e5.zip +0 -0
- package/.yarn/cache/rehype-highlight-npm-6.0.0-5179257139-5a70e7ad45.zip +0 -0
- package/.yarn/cache/rehype-slug-npm-5.1.0-ae08840ba8-2a7c17fd74.zip +0 -0
- package/.yarn/cache/space-separated-tokens-npm-1.1.5-2352c83473-8ef68f1cfa.zip +0 -0
- package/.yarn/cache/unist-util-find-after-npm-4.0.0-11b3b7fb4f-8381ef0bad.zip +0 -0
- package/.yarn/cache/zwitch-npm-1.0.5-5911cef6ce-28a1bebaca.zip +0 -0
- package/.yarn/install-state.gz +0 -0
- package/package.json +8 -5
- package/playground/{App.res → PlaygroundApp.res} +14 -7
- package/playground/PlaygroundComponents.res +114 -0
- package/playground/PlaygroundLocales.res +1 -0
- package/playground/PlaygroundRouter.res +23 -0
- package/playground/{CodeBlock.res → Playground_CodeBlock.res} +12 -16
- package/playground/components/Playground_Alert.res +25 -0
- package/playground/components/Playground_Button.res +75 -0
- package/playground/components/Playground_Label.res +16 -0
- package/playground/components/Playground_Modal.res +53 -0
- package/playground/components/Playground_MultiSelect.res +91 -0
- package/playground/components/Playground_Switch.res +2 -2
- package/playground/custom.css +4 -1
- package/playground/{stories/IntroductionColors.stories.mdx → design/DesignSystem_Colors.mdx} +0 -0
- package/playground/{stories/IntroductionFonts.stories.mdx → design/DesignSystem_Fonts.mdx} +0 -0
- package/playground/{stories/IntroductionMediaQueries.stories.mdx → design/DesignSystem_MediaQueries.mdx} +0 -0
- package/playground/design/Playground_DesignSystem.res +12 -0
- package/playground/{stories/TailwindConfigBreakpoints.js → design/TailwindConfigBreakpoints.jsx} +1 -1
- package/playground/{stories/TailwindConfigColorsPreview.js → design/TailwindConfigColorsPreview.jsx} +1 -1
- package/playground/{stories/TailwindConfigFontsPreview.js → design/TailwindConfigFontsPreview.jsx} +1 -1
- package/playground/docs/ApiDecoding.mdx +70 -0
- package/playground/docs/Form.mdx +109 -0
- package/playground/docs/Identifiers.mdx +38 -0
- package/playground/{Doc.res → docs/Playground_Docs.res} +8 -8
- package/playground/docs/Primitives.mdx +53 -0
- package/playground/docs/Request.mdx +281 -0
- package/playground/docs/Router.mdx +43 -0
- package/playground/docs/Setup.mdx +51 -0
- package/playground/docs/Unleash.mdx +53 -0
- package/playground/main.jsx +1 -1
- package/src/intl/Toolkit__Intl.res +5 -9
- package/src/intl/Toolkit__Intl.resi +1 -0
- package/src/logger/Toolkit__BrowserLogger.res +15 -15
- package/src/router/Toolkit__Router.res +2 -1
- package/src/tailwind/tailwind.config.cjs +3 -5
- package/src/ui/Toolkit__Ui_MultiSelect.res +21 -0
- package/src/ui/styles.css +6 -3
- package/vite.config.js +44 -1
- package/.yarn/cache/cosmiconfig-npm-7.0.0-b9d0d7d156-6801feaa02.zip +0 -0
- package/.yarn/cache/import-fresh-npm-3.3.0-3e34265ca9-2cacfad06e.zip +0 -0
- package/.yarn/cache/klona-npm-2.0.4-6bc4e7cd86-abc6690882.zip +0 -0
- package/.yarn/cache/postcss-loader-npm-4.2.0-f01fec2503-c45ec1ca1b.zip +0 -0
- package/.yarn/cache/semver-npm-7.3.4-4c3baf0ead-96451bfd7c.zip +0 -0
- package/playground/Playground_Component.res +0 -33
- package/playground/Playground_ComponentDetails.res +0 -25
- package/playground/Playground_ComponentsList.res +0 -43
- package/playground/stories/Toolkit__UI_AlertStory.res +0 -111
- package/playground/stories/Toolkit__UI_LabelStory.res +0 -39
- package/playground/stories/Toolkit__UI_ModalStory.res +0 -211
- package/playground/stories/Toolkit__UI_MultiSelectStory.res +0 -132
- package/playground/stories/Toolkit__Ui_ButtonStory.res +0 -324
package/playground/custom.css
CHANGED
package/playground/{stories/IntroductionColors.stories.mdx → design/DesignSystem_Colors.mdx}
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Fonts = {
|
|
2
|
+
@module("../../../../playground/design/DesignSystem_Fonts.mdx") @react.component
|
|
3
|
+
external make: unit => React.element = "default"
|
|
4
|
+
}
|
|
5
|
+
module Colors = {
|
|
6
|
+
@module("../../../../playground/design/DesignSystem_Colors.mdx") @react.component
|
|
7
|
+
external make: unit => React.element = "default"
|
|
8
|
+
}
|
|
9
|
+
module MediaQueries = {
|
|
10
|
+
@module("../../../../playground/design/DesignSystem_MediaQueries.mdx") @react.component
|
|
11
|
+
external make: unit => React.element = "default"
|
|
12
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Decode content from API result
|
|
2
|
+
|
|
3
|
+
## API
|
|
4
|
+
|
|
5
|
+
### Decode an enumeration
|
|
6
|
+
|
|
7
|
+
```rescript
|
|
8
|
+
module LiftEnum = {
|
|
9
|
+
@deriving(jsConverter)
|
|
10
|
+
type enum = [
|
|
11
|
+
| #withLift
|
|
12
|
+
| #withoutLift
|
|
13
|
+
| #unknown
|
|
14
|
+
]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module Lift = Toolkit.Decoder.Enum(LiftEnum)
|
|
18
|
+
|
|
19
|
+
@decco
|
|
20
|
+
type response = {lift: Lift.t}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Decode a date
|
|
24
|
+
|
|
25
|
+
```rescript
|
|
26
|
+
@decco
|
|
27
|
+
type response = {
|
|
28
|
+
createdAt: Toolkit.Decoder.Date.t
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Decode unit measure
|
|
33
|
+
|
|
34
|
+
There are 2 options :
|
|
35
|
+
|
|
36
|
+
- the field has the unit like : `19 mm`
|
|
37
|
+
- the field has only the value
|
|
38
|
+
|
|
39
|
+
#### Automatic unit handling
|
|
40
|
+
|
|
41
|
+
```rescript
|
|
42
|
+
[@decco]
|
|
43
|
+
type response = {
|
|
44
|
+
length: Toolkit.Decoder.UnitMeasure.Dimension.WithUnit.t
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### Known unit
|
|
49
|
+
|
|
50
|
+
```rescript
|
|
51
|
+
[@decco]
|
|
52
|
+
type response = {
|
|
53
|
+
weight: Toolkit.Decoder.UnitMeasure.Weight.Kg.t
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Decode an Array written as a String
|
|
58
|
+
|
|
59
|
+
This codec is intended for encoding the array params of API requests of scala services.
|
|
60
|
+
|
|
61
|
+
```rescript
|
|
62
|
+
@decco
|
|
63
|
+
type params = {
|
|
64
|
+
lengths: Toolkit.Decoder.StringArray<Toolkit.Decoder.UnitMeasure.Dimension.WithUnit.t>
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let params = {lengths:[#cm(1),#m(40), #km(3)]};
|
|
68
|
+
let encodedParams = params->params_encode; // "1.00 cm,40.00 m,3.00 km"
|
|
69
|
+
let decodedParams = encodedParams->params_decode; // Ok([#cm(1),#m(40),#km(3)])
|
|
70
|
+
```
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Handling forms
|
|
2
|
+
|
|
3
|
+
We have created a fork of [reform@9.8.0](https://github.com/Astrocoders/reform) we had some features :
|
|
4
|
+
|
|
5
|
+
- `onSubmit` must return a `Promise.promise(result('a, 'e))`
|
|
6
|
+
- `SubmitFailed(option('error'))` to the `formState('error)` variant
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
### Define a FormState
|
|
11
|
+
|
|
12
|
+
The module must contains an `error` type and an include of `state` type with `%lenses`
|
|
13
|
+
|
|
14
|
+
```rescript
|
|
15
|
+
module FormState = {
|
|
16
|
+
type error = ColiswebApi.V5.Store.Contact.CreateContact.Request.error;
|
|
17
|
+
include [%lenses
|
|
18
|
+
type state = {
|
|
19
|
+
firstName: string,
|
|
20
|
+
lastName: string,
|
|
21
|
+
email: string,
|
|
22
|
+
phone1: string,
|
|
23
|
+
phone2: string,
|
|
24
|
+
}
|
|
25
|
+
];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module FormApi = Toolkit.Form.Make(FormState);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Use the form in React
|
|
32
|
+
|
|
33
|
+
```rescript
|
|
34
|
+
// ...
|
|
35
|
+
@react.component
|
|
36
|
+
let make = () => {
|
|
37
|
+
|
|
38
|
+
let initialState =
|
|
39
|
+
FormState.{
|
|
40
|
+
firstName: "",
|
|
41
|
+
lastName: "",
|
|
42
|
+
phone1: "",
|
|
43
|
+
phone2: "",
|
|
44
|
+
email: "",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
let schema =
|
|
48
|
+
FormApi.Form.Validation.Schema([
|
|
49
|
+
StringNonEmpty(FirstName),
|
|
50
|
+
StringNonEmpty(LastName),
|
|
51
|
+
Email(Email),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
let onSubmit = (form: FormApi.Form.onSubmitAPI) => {
|
|
55
|
+
let {values}: FormApi.Form.state = form.state;
|
|
56
|
+
|
|
57
|
+
ColiswebApi.V5.Store.Contact.CreateContact.Config.exec((
|
|
58
|
+
clientId,
|
|
59
|
+
storeId,
|
|
60
|
+
{
|
|
61
|
+
firstName: values.firstName->OptionUtils.fromString,
|
|
62
|
+
lastName: values.lastName->OptionUtils.fromString,
|
|
63
|
+
email: values.email,
|
|
64
|
+
phone1: values.phone1->OptionUtils.fromString,
|
|
65
|
+
phone2: values.phone2->OptionUtils.fromString,
|
|
66
|
+
},
|
|
67
|
+
))
|
|
68
|
+
->Promise.flatMapOk(result =>
|
|
69
|
+
reloadContact()->Promise.map(_ => Ok(result))
|
|
70
|
+
)
|
|
71
|
+
->Promise.tapOk(_ => hide())
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
let form = FormApi.Form.use(~initialState, ~schema, ~onSubmit, ());
|
|
75
|
+
|
|
76
|
+
<FormApi.Form.Provider value=form>
|
|
77
|
+
<form onSubmit={event => {
|
|
78
|
+
ReactEvent.Synthetic.preventDefault(event);
|
|
79
|
+
form.submit();
|
|
80
|
+
}}>
|
|
81
|
+
|
|
82
|
+
<Form.Field
|
|
83
|
+
field
|
|
84
|
+
render={({handleChange, error, value, validate}) => {
|
|
85
|
+
let isInvalid = error->Option.isSome;
|
|
86
|
+
<>
|
|
87
|
+
<input value onChange={BsReform.Helpers.handleChange(handleChange)} />
|
|
88
|
+
|
|
89
|
+
{
|
|
90
|
+
error->Option.mapWithDefault(React.null, e => <p>e->React.string</p>)
|
|
91
|
+
}
|
|
92
|
+
</>;
|
|
93
|
+
}}
|
|
94
|
+
/>;
|
|
95
|
+
|
|
96
|
+
<Button
|
|
97
|
+
type_="submit"
|
|
98
|
+
color=#primary
|
|
99
|
+
isLoading={form.state.formState === Submitting}
|
|
100
|
+
onClick={_ => form.submit()}>
|
|
101
|
+
<FormattedMessage
|
|
102
|
+
id="forms.controls.confirm"
|
|
103
|
+
defaultMessage="Confirm"
|
|
104
|
+
/>
|
|
105
|
+
</Button>
|
|
106
|
+
</form>
|
|
107
|
+
</FormApi.Form.Provider>;
|
|
108
|
+
}
|
|
109
|
+
```
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Avoid string type with unique identifiers
|
|
2
|
+
|
|
3
|
+
Convert a string or an int in an opaque type with a decco decoder.
|
|
4
|
+
|
|
5
|
+
### `string` identifier
|
|
6
|
+
|
|
7
|
+
```rescript
|
|
8
|
+
module DeliveryId = Toolkit.Identifier.MakeString({});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
somewhere
|
|
12
|
+
**/
|
|
13
|
+
@decco
|
|
14
|
+
type response = {
|
|
15
|
+
deliveryId: DeliveryId.t
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let deliveryId = DeliveryId.make("some-id");
|
|
19
|
+
deliveryId->DeliveryId.toString // "some-id"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### `int` identifier
|
|
23
|
+
|
|
24
|
+
```rescript
|
|
25
|
+
module DeliveryId = Toolkit.Identifier.MakeInt({});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
somewhere
|
|
29
|
+
**/
|
|
30
|
+
@decco
|
|
31
|
+
type response = {
|
|
32
|
+
deliveryId: DeliveryId.t
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let deliveryId = DeliveryId.make(12);
|
|
36
|
+
deliveryId->DeliveryId.toString; // "12"
|
|
37
|
+
deliveryId->DeliveryId.toInt; // 12
|
|
38
|
+
```
|
|
@@ -1,32 +1,32 @@
|
|
|
1
1
|
module ApiDecoding = {
|
|
2
|
-
@module("
|
|
2
|
+
@module("../../../../playground/docs/ApiDecoding.mdx") @react.component
|
|
3
3
|
external make: unit => React.element = "default"
|
|
4
4
|
}
|
|
5
5
|
module Form = {
|
|
6
|
-
@module("
|
|
6
|
+
@module("../../../../playground/docs/Form.mdx") @react.component
|
|
7
7
|
external make: unit => React.element = "default"
|
|
8
8
|
}
|
|
9
9
|
module Identifiers = {
|
|
10
|
-
@module("
|
|
10
|
+
@module("../../../../playground/docs/Identifiers.mdx") @react.component
|
|
11
11
|
external make: unit => React.element = "default"
|
|
12
12
|
}
|
|
13
13
|
module Primitives = {
|
|
14
|
-
@module("
|
|
14
|
+
@module("../../../../playground/docs/Primitives.mdx") @react.component
|
|
15
15
|
external make: unit => React.element = "default"
|
|
16
16
|
}
|
|
17
17
|
module Requests = {
|
|
18
|
-
@module("
|
|
18
|
+
@module("../../../../playground/docs/Request.mdx") @react.component
|
|
19
19
|
external make: unit => React.element = "default"
|
|
20
20
|
}
|
|
21
21
|
module Setup = {
|
|
22
|
-
@module("
|
|
22
|
+
@module("../../../../playground/docs/Setup.mdx") @react.component
|
|
23
23
|
external make: unit => React.element = "default"
|
|
24
24
|
}
|
|
25
25
|
module Router = {
|
|
26
|
-
@module("
|
|
26
|
+
@module("../../../../playground/docs/Router.mdx") @react.component
|
|
27
27
|
external make: unit => React.element = "default"
|
|
28
28
|
}
|
|
29
29
|
module Unleash = {
|
|
30
|
-
@module("
|
|
30
|
+
@module("../../../../playground/docs/Unleash.mdx") @react.component
|
|
31
31
|
external make: unit => React.element = "default"
|
|
32
32
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Primitives
|
|
2
|
+
|
|
3
|
+
Utililties functions for primitives types :
|
|
4
|
+
|
|
5
|
+
- `option`
|
|
6
|
+
- `string`
|
|
7
|
+
- `array`
|
|
8
|
+
|
|
9
|
+
## Option
|
|
10
|
+
|
|
11
|
+
### string
|
|
12
|
+
|
|
13
|
+
- `fromString` : `string => option(string)`
|
|
14
|
+
- `toString` : `option(string) => string`
|
|
15
|
+
|
|
16
|
+
```rescript
|
|
17
|
+
let anEmptyString = "";
|
|
18
|
+
|
|
19
|
+
Toolkit.Primitives.Option.fromString(anEmptyString)
|
|
20
|
+
->Option.map(v => v ++ "test");
|
|
21
|
+
|
|
22
|
+
Toolkit.Primitives.Option.toString(Some("test")); // "test"
|
|
23
|
+
Toolkit.Primitives.Option.toString(None); // ""
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### boolean
|
|
27
|
+
|
|
28
|
+
- `fromBool` : `bool => option(bool)`
|
|
29
|
+
|
|
30
|
+
```rescript
|
|
31
|
+
Toolkit.Primitives.Option.fromBool(true); // Some(true)
|
|
32
|
+
Toolkit.Primitives.Option.fromBool(false); // None
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Array
|
|
36
|
+
|
|
37
|
+
```rescript
|
|
38
|
+
let variable = ["abc", "dce", ""];
|
|
39
|
+
|
|
40
|
+
Js.log(Primitives.Array.joinNonEmpty(anEmptyString)); // "abc, dce"
|
|
41
|
+
Js.log(Primitives.Array.joinNonEmpty(~separator="+", anEmptyString)); // "abc+dce"
|
|
42
|
+
Js.log(variable->Primitives.Array.tail); // ""
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Result
|
|
46
|
+
|
|
47
|
+
```rescript
|
|
48
|
+
let result = Ok("test");
|
|
49
|
+
let test = Error("error");
|
|
50
|
+
|
|
51
|
+
Js.log(Primitives.Result.get(result)); // Some("test")
|
|
52
|
+
Js.log(Primitives.Result.get(test)); // None
|
|
53
|
+
```
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# How to manage an API request
|
|
2
|
+
|
|
3
|
+
## 1. Create the request config
|
|
4
|
+
|
|
5
|
+
You have to declare 3 types :
|
|
6
|
+
|
|
7
|
+
- `argument` is what you give to the function
|
|
8
|
+
- `response` is what your API answer
|
|
9
|
+
- `error` is the potential custom errors that can occur (⚠️ this has to be handled manually)
|
|
10
|
+
|
|
11
|
+
And the `exec` method.
|
|
12
|
+
|
|
13
|
+
We use [decco](https://github.com/reasonml-labs/decco) to serialize / deserialize the data.
|
|
14
|
+
|
|
15
|
+
```rescript
|
|
16
|
+
let request = Axios.Instance.create(
|
|
17
|
+
Axios.makeConfig(~baseURL=envState.apiUrl,()),
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
module FetchUsers = {
|
|
21
|
+
module Config = {
|
|
22
|
+
type argument = unit;
|
|
23
|
+
|
|
24
|
+
@decco
|
|
25
|
+
type response = array<user>
|
|
26
|
+
@decco
|
|
27
|
+
and user = {
|
|
28
|
+
name: string
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type error = string;
|
|
32
|
+
|
|
33
|
+
let exec = () => {
|
|
34
|
+
request->Request.get("/", response_decode);
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
module Request = Toolkit.Request.Make(Config);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
FetchUsers.Request.exec();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1.2 Send data
|
|
44
|
+
|
|
45
|
+
We create a decodable `body` type that we give to the `argument` type.
|
|
46
|
+
This decodable `body` will be used in the exec method.
|
|
47
|
+
|
|
48
|
+
```rescript
|
|
49
|
+
module UpdateUser = {
|
|
50
|
+
module Config = {
|
|
51
|
+
type argument = body
|
|
52
|
+
@decco
|
|
53
|
+
and body = {
|
|
54
|
+
name: string,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
@decco
|
|
58
|
+
type response = unit;
|
|
59
|
+
|
|
60
|
+
type error = unit;
|
|
61
|
+
|
|
62
|
+
let exec = body => {
|
|
63
|
+
request->Request.postData("/update-user", response_decode, body->body_encode);
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
module Request = Toolkit_Request.Make(Config);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
UpdateUser.Request.exec({ name: "test" })
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 1.3 Handle errors
|
|
73
|
+
|
|
74
|
+
In order to handle special errors comming from the API, you can use the `mapError` parameter.
|
|
75
|
+
|
|
76
|
+
This parameter requires a callback which contains the **response** as argument and needs to return this type :
|
|
77
|
+
|
|
78
|
+
```rescript
|
|
79
|
+
option<result<'data, error<'a>>>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `result` is useful for some cases when an API error should not break the flow so you can handle the specific error and just return `Ok()`.
|
|
83
|
+
|
|
84
|
+
#### Request error type
|
|
85
|
+
|
|
86
|
+
```rescript
|
|
87
|
+
type undecodedResponse = {
|
|
88
|
+
data: undecodedData,
|
|
89
|
+
headers,
|
|
90
|
+
config,
|
|
91
|
+
status: int,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type noResponse = {
|
|
95
|
+
message: string,
|
|
96
|
+
config,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// We get this type in mapError callback argument
|
|
100
|
+
type failedResponse = {
|
|
101
|
+
message: string,
|
|
102
|
+
config,
|
|
103
|
+
response,
|
|
104
|
+
}
|
|
105
|
+
and response = {
|
|
106
|
+
data: failedResponseData,
|
|
107
|
+
headers,
|
|
108
|
+
status: int,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type error<'a> = [
|
|
112
|
+
| #noResponse(noResponse)
|
|
113
|
+
| #invalidResponse(failedResponse)
|
|
114
|
+
| #invalidResponseData(undecodedResponse, string)
|
|
115
|
+
| #invalidErrorData
|
|
116
|
+
| #unknown(Js.Promise.error)
|
|
117
|
+
| #custom('a)
|
|
118
|
+
]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```rescript
|
|
122
|
+
let exec = token => {
|
|
123
|
+
request->Request.get(
|
|
124
|
+
"/test",
|
|
125
|
+
~mapError=
|
|
126
|
+
error => {
|
|
127
|
+
// have to return option(Toolkit.Request.error('a'))
|
|
128
|
+
},
|
|
129
|
+
response_decode,
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`failedResponseData` can be anything depending on the API behavior. So you have to use `Obj.magic` to cast and use the correct field.
|
|
135
|
+
|
|
136
|
+
```rescript
|
|
137
|
+
let handleError = (error: failedResponse) => {
|
|
138
|
+
// the API return an object { code: string }
|
|
139
|
+
let code = error.response.data->Obj.magic##code;
|
|
140
|
+
|
|
141
|
+
// If code can be undefined
|
|
142
|
+
code
|
|
143
|
+
->Option.map(code => {
|
|
144
|
+
switch (code) {
|
|
145
|
+
| "some_code" => Error(#custom("message"))
|
|
146
|
+
| "other_code" => Error(#invalidErrorData)
|
|
147
|
+
| "its_ok" => Ok()
|
|
148
|
+
| _ => Error(#custom("unknown"))
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
};
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Often, we use decco in order to handle complex error (error with data). We create a custom Decoder for this kind of case
|
|
155
|
+
|
|
156
|
+
```rescript
|
|
157
|
+
module UpdateUser = {
|
|
158
|
+
module Config = {
|
|
159
|
+
type argument = body
|
|
160
|
+
@decco
|
|
161
|
+
and body = {name: string};
|
|
162
|
+
|
|
163
|
+
@decco
|
|
164
|
+
type response = unit;
|
|
165
|
+
|
|
166
|
+
module Error =
|
|
167
|
+
Decoders.Enum({
|
|
168
|
+
@deriving(jsConverter)
|
|
169
|
+
type enum = [
|
|
170
|
+
| #"unknown-user"
|
|
171
|
+
| #invalidData
|
|
172
|
+
| #unknownError
|
|
173
|
+
];
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
@decco
|
|
177
|
+
type error = Error.t;
|
|
178
|
+
|
|
179
|
+
let exec = body => {
|
|
180
|
+
request->Request.postData(
|
|
181
|
+
"/update-user",
|
|
182
|
+
response_decode,
|
|
183
|
+
body->body_encode,
|
|
184
|
+
~mapError=
|
|
185
|
+
({response}) => {
|
|
186
|
+
switch (response.data->Obj.magic->error_decode) {
|
|
187
|
+
| Ok(error) => Some(Error(`custom(error)))
|
|
188
|
+
| Error(err) =>
|
|
189
|
+
Js.log(err);
|
|
190
|
+
Some(Error(`custom(`unknownError)));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
);
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
module Request = Toolkit_Request.Make(Config);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
UpdateUser.Request.exec({ name: "test" })
|
|
200
|
+
->Promise.tapError(err => {
|
|
201
|
+
switch (err) {
|
|
202
|
+
| #custom(#"unknown-user") => Js.log("user not found")
|
|
203
|
+
| #custom(#unknownError) => Js.log("alert sentry")
|
|
204
|
+
| _ => Js.log("generic handler")
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## 2. Use request in React
|
|
210
|
+
|
|
211
|
+
### 2.1 Fetching data with swr
|
|
212
|
+
|
|
213
|
+
There are several ways to manage a request in React, we chose to use [swr](https://swr.vercel.app/) with [react-suspense](https://reactjs.org/docs/concurrent-mode-suspense.html) enabled.
|
|
214
|
+
We have a functor to generate automaticatly a swr fetcher based on the request created before :
|
|
215
|
+
|
|
216
|
+
```rescript
|
|
217
|
+
module GetUsers = {
|
|
218
|
+
module Config = {
|
|
219
|
+
type argument = unit;
|
|
220
|
+
|
|
221
|
+
@decco
|
|
222
|
+
type response = array<user>
|
|
223
|
+
@decco
|
|
224
|
+
and user = {
|
|
225
|
+
name: string
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
type error = string;
|
|
229
|
+
|
|
230
|
+
let exec = () => {
|
|
231
|
+
request->Request.get("/users", response_decode);
|
|
232
|
+
};
|
|
233
|
+
};
|
|
234
|
+
module Request = Toolkit.Request.Make();
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
module GetUsersFetcher =
|
|
239
|
+
Fetcher.Make({
|
|
240
|
+
module Request = GetUsers.Request;
|
|
241
|
+
let key = () => [|
|
|
242
|
+
"GetUsers",
|
|
243
|
+
|];
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Later in the react app
|
|
248
|
+
|
|
249
|
+
```rescript
|
|
250
|
+
module PageUsers = {
|
|
251
|
+
[@react.component]
|
|
252
|
+
let make = () => {
|
|
253
|
+
let (users, _, _) = GetUsersFetcher.use();
|
|
254
|
+
|
|
255
|
+
<div>
|
|
256
|
+
{
|
|
257
|
+
users
|
|
258
|
+
->Belt.Array.mapWithIndex((i, user) => {
|
|
259
|
+
<div key={i->Belt.Int.toString}>
|
|
260
|
+
{user.name->React.String}
|
|
261
|
+
</div>
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
</div>
|
|
265
|
+
};
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
[@react.component]
|
|
269
|
+
let make = () => {
|
|
270
|
+
<ErrorBoundary>
|
|
271
|
+
<React.Suspense fallback={<Spinner />}>
|
|
272
|
+
<PageUsers />
|
|
273
|
+
</React.Suspense>
|
|
274
|
+
</ErrorBoundary>
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
There are 2 functions generated by the fetcher functor :
|
|
279
|
+
|
|
280
|
+
- `use` can be used when the API will always return something
|
|
281
|
+
- `useOptional` return an `option<'a>` so it's used when the API can return nothing
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Router
|
|
2
|
+
|
|
3
|
+
Type-safe navigation
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```rescript
|
|
8
|
+
open Toolkit
|
|
9
|
+
|
|
10
|
+
module StoreId = Identifiers.MakeString()
|
|
11
|
+
|
|
12
|
+
module RouterConfig = {
|
|
13
|
+
type admin = Home
|
|
14
|
+
|
|
15
|
+
type t =
|
|
16
|
+
| Home
|
|
17
|
+
| StoreDetails(StoreId.t)
|
|
18
|
+
| Admin(admin)
|
|
19
|
+
| NotFound
|
|
20
|
+
|
|
21
|
+
let make = url =>
|
|
22
|
+
switch url.path {
|
|
23
|
+
| list{} => Home
|
|
24
|
+
| list{"stores", storeId} => StoreDetails(StoreId.make(storeId))
|
|
25
|
+
| list{"admin", ...rest} =>
|
|
26
|
+
switch rest {
|
|
27
|
+
| list{} => Admin(Home)
|
|
28
|
+
| _ => NotFound
|
|
29
|
+
}
|
|
30
|
+
| _ => NotFound
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let toString = route =>
|
|
34
|
+
switch route {
|
|
35
|
+
| Home => ""
|
|
36
|
+
| StoreDetails(storeId) => "/stores/" ++ storeId->StoreId.toString
|
|
37
|
+
| Admin(Home) => "/admin/"
|
|
38
|
+
| NotFound => "/404"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module Navigation = Router.Make(RouterConfig)
|
|
43
|
+
```
|