@colisweb/rescript-toolkit 5.4.1 → 5.5.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/package.json +3 -1
- package/rescript.json +2 -1
- package/src/primitives/Toolkit__Primitives.res +2 -9
- package/src/request/Request.res +205 -0
- package/src/ui/Toolkit__Ui_MultiSelect.res +3 -18
- package/src/ui/Toolkit__Ui_MultiSelectWithValidation.res +2 -7
- package/src/ui/Toolkit__Ui_SelectWithValidation.res +1 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colisweb/rescript-toolkit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"clean": "rescript clean",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@colisweb/bs-react-intl-extractor-bin": "0.12.2",
|
|
31
31
|
"@datadog/browser-rum": "5.8.0",
|
|
32
|
+
"@dck/rescript-ky": "^2.0.0",
|
|
32
33
|
"@dck/rescript-promise": "1.1.0",
|
|
33
34
|
"@dck/restorative": "1.1.0",
|
|
34
35
|
"@greenlabs/ppx-spice": "0.2.1",
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
"copy-to-clipboard": "3.3.3",
|
|
44
45
|
"date-fns": "3.2.0",
|
|
45
46
|
"dedent": "0.7.0",
|
|
47
|
+
"ky": "^1.2.4",
|
|
46
48
|
"lenses-ppx": "6.1.10",
|
|
47
49
|
"list-selectors": "2.0.1",
|
|
48
50
|
"lodash": "4.17.21",
|
package/rescript.json
CHANGED
|
@@ -5,13 +5,6 @@ module String = {
|
|
|
5
5
|
@ocaml.doc(" TODO: remove ")
|
|
6
6
|
let join = joinNonEmty
|
|
7
7
|
|
|
8
|
-
let includes = (str1, str2) => {
|
|
9
|
-
str1
|
|
10
|
-
->Js.String2.toLowerCase
|
|
11
|
-
->Js.String2.normalizeByForm("NFD")
|
|
12
|
-
->Js.String2.includes(str2->Js.String2.toLowerCase->Js.String2.normalizeByForm("NFD"))
|
|
13
|
-
}
|
|
14
|
-
|
|
15
8
|
/**
|
|
16
9
|
*Normalize NFD*: replace by re remove split by commponents the accent letters
|
|
17
10
|
and deletes the accent component only, leaving the un-accented letter
|
|
@@ -23,8 +16,8 @@ module String = {
|
|
|
23
16
|
->Js.String2.replaceByRe(%re("/[\u0300-\u036f]/g"), "")
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
let
|
|
27
|
-
str->normalizeForSearch->Js.String2.includes(search)
|
|
19
|
+
let normalizedIncludes = (str, search) => {
|
|
20
|
+
str->normalizeForSearch->Js.String2.includes(search->normalizeForSearch)
|
|
28
21
|
}
|
|
29
22
|
}
|
|
30
23
|
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
type error<'apiError> =
|
|
2
|
+
| DecodeError(Spice.decodeError)
|
|
3
|
+
| Unknown(Ky.error<Js.Json.t>)
|
|
4
|
+
| Custom('apiError)
|
|
5
|
+
|
|
6
|
+
type requestConfig<'apiError, 'response> = {
|
|
7
|
+
path: string,
|
|
8
|
+
requestOptions: Ky.requestOptions<Js.Json.t, Js.Json.t, Js.Json.t, 'response>,
|
|
9
|
+
key?: array<string>,
|
|
10
|
+
customError?: Ky.error<Js.Json.t> => Promise.t<error<'apiError>>,
|
|
11
|
+
mapPromise?: Js.Json.t => result<'response, error<'apiError>>,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
%%private(
|
|
15
|
+
let fetch = (
|
|
16
|
+
~instance,
|
|
17
|
+
~path,
|
|
18
|
+
~requestOptions,
|
|
19
|
+
~mapPromise=?,
|
|
20
|
+
~customError=?,
|
|
21
|
+
~response_decode,
|
|
22
|
+
) => {
|
|
23
|
+
// TODO :
|
|
24
|
+
// - parseJson
|
|
25
|
+
// - abort controller signal
|
|
26
|
+
Ky.Instance.asCallable(instance)(path, ~options=requestOptions)
|
|
27
|
+
->Ky.Response.json()
|
|
28
|
+
->Promise.Js.fromBsPromise
|
|
29
|
+
->Promise.Js.toResult
|
|
30
|
+
->Promise.flatMap(response => {
|
|
31
|
+
switch response {
|
|
32
|
+
| Error(err) => {
|
|
33
|
+
let error: Ky.error<'a> = err->Obj.magic
|
|
34
|
+
customError->Option.mapWithDefault(Promise.resolved(Error(Unknown(error))), fn =>
|
|
35
|
+
fn(error)->Promise.map(e => Error(e))
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
| Ok(response) =>
|
|
39
|
+
switch mapPromise {
|
|
40
|
+
| None =>
|
|
41
|
+
Promise.resolved(
|
|
42
|
+
switch response->response_decode {
|
|
43
|
+
| Ok(_) as ok => ok
|
|
44
|
+
| Error(decodeError) => Error(DecodeError(decodeError))
|
|
45
|
+
},
|
|
46
|
+
)
|
|
47
|
+
| Some(fn) => Promise.resolved(fn(response))
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
module type Config = {
|
|
54
|
+
type argument
|
|
55
|
+
type response
|
|
56
|
+
type error
|
|
57
|
+
let instance: Ky.Instance.t
|
|
58
|
+
let response_decode: Js.Json.t => result<response, Spice.decodeError>
|
|
59
|
+
let config: argument => requestConfig<error, response>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let fetchAPI = (
|
|
63
|
+
type argument response err,
|
|
64
|
+
config: module(Config with
|
|
65
|
+
type argument = argument
|
|
66
|
+
and type response = response
|
|
67
|
+
and type error = err
|
|
68
|
+
),
|
|
69
|
+
argument: argument,
|
|
70
|
+
): Promise.t<result<response, error<err>>> => {
|
|
71
|
+
let module(C) = config
|
|
72
|
+
let requestConfig = C.config(argument)
|
|
73
|
+
|
|
74
|
+
fetch(
|
|
75
|
+
~instance=C.instance,
|
|
76
|
+
~path=requestConfig.path,
|
|
77
|
+
~response_decode=C.response_decode,
|
|
78
|
+
~requestOptions=requestConfig.requestOptions,
|
|
79
|
+
~customError=?requestConfig.customError,
|
|
80
|
+
~mapPromise=?requestConfig.mapPromise,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
let useFetcher = (
|
|
84
|
+
type argument response error,
|
|
85
|
+
~options: option<Swr.fetcherOptions>=?,
|
|
86
|
+
config: module(Config with
|
|
87
|
+
type argument = argument
|
|
88
|
+
and type response = response
|
|
89
|
+
and type error = error
|
|
90
|
+
),
|
|
91
|
+
argument: option<argument>,
|
|
92
|
+
): Toolkit__Hooks.fetcher<response> => {
|
|
93
|
+
let module(C) = config
|
|
94
|
+
|
|
95
|
+
Toolkit__Hooks.useFetcher(
|
|
96
|
+
~options?,
|
|
97
|
+
argument->Option.flatMap(argument => {
|
|
98
|
+
let requestConfig = C.config(argument)
|
|
99
|
+
|
|
100
|
+
switch requestConfig.key->Obj.magic {
|
|
101
|
+
| None =>
|
|
102
|
+
Js.Exn.raiseError(
|
|
103
|
+
`You are using a config without a key for this path ${requestConfig.path}`,
|
|
104
|
+
)
|
|
105
|
+
| Some(key) => key
|
|
106
|
+
}
|
|
107
|
+
}),
|
|
108
|
+
() => {
|
|
109
|
+
fetchAPI(config, argument->Option.getExn)->Promise.Js.fromResult
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let useOptionalFetcher = (
|
|
115
|
+
type argument response error,
|
|
116
|
+
~options: option<Swr.fetcherOptions>=?,
|
|
117
|
+
config: module(Config with
|
|
118
|
+
type argument = argument
|
|
119
|
+
and type response = response
|
|
120
|
+
and type error = error
|
|
121
|
+
),
|
|
122
|
+
argument: option<argument>,
|
|
123
|
+
): Toolkit__Hooks.fetcher<response> => {
|
|
124
|
+
let module(C) = config
|
|
125
|
+
|
|
126
|
+
Toolkit__Hooks.useOptionalFetcher(
|
|
127
|
+
~options?,
|
|
128
|
+
argument->Option.flatMap(argument => {
|
|
129
|
+
let requestConfig = C.config(argument)
|
|
130
|
+
|
|
131
|
+
switch requestConfig.key->Obj.magic {
|
|
132
|
+
| None =>
|
|
133
|
+
Js.Exn.raiseError(
|
|
134
|
+
`You are using a config without a key for this path ${requestConfig.path}`,
|
|
135
|
+
)
|
|
136
|
+
| Some(key) => key
|
|
137
|
+
}
|
|
138
|
+
}),
|
|
139
|
+
() => {
|
|
140
|
+
fetchAPI(config, argument->Option.getExn)->Promise.Js.fromResult
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type state<'data, 'error> =
|
|
146
|
+
| NotAsked
|
|
147
|
+
| Loading
|
|
148
|
+
| Done(result<'data, 'error>)
|
|
149
|
+
|
|
150
|
+
%%private(
|
|
151
|
+
let minInt = -999999999
|
|
152
|
+
let maxInt = 1000000000
|
|
153
|
+
|
|
154
|
+
let increment = (num: int): int => num !== maxInt ? num + 1 : minInt
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
let useManualRequest = (
|
|
158
|
+
type argument response error,
|
|
159
|
+
config: module(Config with
|
|
160
|
+
type argument = argument
|
|
161
|
+
and type response = response
|
|
162
|
+
and type error = error
|
|
163
|
+
),
|
|
164
|
+
) => {
|
|
165
|
+
let module(Config) = config
|
|
166
|
+
|
|
167
|
+
let lastCallId = React.useRef(0)
|
|
168
|
+
let canceled = React.useRef(false)
|
|
169
|
+
let (state, set) = React.useState(() => NotAsked)
|
|
170
|
+
let isMounted = ReactUse.useMountedState(.)
|
|
171
|
+
|
|
172
|
+
let trigger = argument => {
|
|
173
|
+
lastCallId.current = lastCallId.current->increment
|
|
174
|
+
let callId = lastCallId.current
|
|
175
|
+
|
|
176
|
+
set(_ => Loading)
|
|
177
|
+
|
|
178
|
+
canceled.current = false
|
|
179
|
+
|
|
180
|
+
fetchAPI(module(Config), argument)->Promise.map(result => {
|
|
181
|
+
let isCanceled = callId !== lastCallId.current || canceled.current
|
|
182
|
+
|
|
183
|
+
if isMounted() && !isCanceled {
|
|
184
|
+
set(_ => Done(result))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
(result, isCanceled)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let cancel = React.useCallback(() => canceled.current = true, [])
|
|
192
|
+
|
|
193
|
+
(state, trigger, cancel)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
external exnToError: Js.Exn.t => error<'a> = "%identity"
|
|
197
|
+
|
|
198
|
+
let decodeResponseError = (responseError, decoder) => {
|
|
199
|
+
responseError
|
|
200
|
+
->Ky.Response.json()
|
|
201
|
+
->Promise.Js.fromBsPromise
|
|
202
|
+
->Promise.Js.toResult
|
|
203
|
+
->Promise.mapError(Obj.magic)
|
|
204
|
+
->Promise.flatMapOk(json => json->decoder->Promise.resolved)
|
|
205
|
+
}
|
|
@@ -46,12 +46,7 @@ let make = (
|
|
|
46
46
|
let filterOptionsBySearch = (~options, ~search) => {
|
|
47
47
|
options->Array.keep(({label}) =>
|
|
48
48
|
// normalize nfd -> replace by re remove split by commponents the accent letters and deletes the accent component only, leaving the un-accented letter
|
|
49
|
-
search == "" ||
|
|
50
|
-
label
|
|
51
|
-
->Js.String2.toLowerCase
|
|
52
|
-
->Js.String2.normalizeByForm("NFD")
|
|
53
|
-
->Js.String2.replaceByRe(%re("/[\u0300-\u036f]/g"), "")
|
|
54
|
-
->Js.String2.includes(search)
|
|
49
|
+
search == "" || Toolkit__Primitives.String.normalizedIncludes(label, search)
|
|
55
50
|
)
|
|
56
51
|
}
|
|
57
52
|
|
|
@@ -99,12 +94,7 @@ let make = (
|
|
|
99
94
|
onChange={event => {
|
|
100
95
|
let target = event->ReactEvent.Form.currentTarget
|
|
101
96
|
// normalize nfd -> replace by re remove split by commponents the accent letters and deletes the accent component only, leaving the un-accented letter
|
|
102
|
-
setSearch(_ =>
|
|
103
|
-
target["value"]
|
|
104
|
-
->Js.String2.toLowerCase
|
|
105
|
-
->Js.String2.normalizeByForm("NFD")
|
|
106
|
-
->Js.String2.replaceByRe(%re("/[\u0300-\u036f]/g"), "")
|
|
107
|
-
)
|
|
97
|
+
setSearch(_ => target["value"])
|
|
108
98
|
}}
|
|
109
99
|
/>
|
|
110
100
|
</div>
|
|
@@ -112,12 +102,7 @@ let make = (
|
|
|
112
102
|
{options
|
|
113
103
|
->Array.keep(({label}) =>
|
|
114
104
|
// normalize nfd -> replace by re remove split by commponents the accent letters and deletes the accent component only, leaving the un-accented letter
|
|
115
|
-
search == "" ||
|
|
116
|
-
label
|
|
117
|
-
->Js.String2.toLowerCase
|
|
118
|
-
->Js.String2.normalizeByForm("NFD")
|
|
119
|
-
->Js.String2.replaceByRe(%re("/[\u0300-\u036f]/g"), "")
|
|
120
|
-
->Js.String2.includes(search)
|
|
105
|
+
search == "" || Toolkit__Primitives.String.normalizedIncludes(label, search)
|
|
121
106
|
)
|
|
122
107
|
->Array.mapWithIndex((i, item) => {
|
|
123
108
|
let {label, value} = item
|
|
@@ -121,12 +121,7 @@ let make = (
|
|
|
121
121
|
let filterOptionsBySearch = (~options, ~search) => {
|
|
122
122
|
options->Array.keep(({label}) =>
|
|
123
123
|
// normalize nfd -> replace by re remove split by commponents the accent letters and deletes the accent component only, leaving the un-accented letter
|
|
124
|
-
search == "" ||
|
|
125
|
-
label
|
|
126
|
-
->Js.String2.toLowerCase
|
|
127
|
-
->Js.String2.normalizeByForm("NFD")
|
|
128
|
-
->Js.String2.replaceByRe(%re("/[\u0300-\u036f]/g"), "")
|
|
129
|
-
->Js.String2.includes(search)
|
|
124
|
+
search == "" || Toolkit__Primitives.String.normalizedIncludes(label, search)
|
|
130
125
|
)
|
|
131
126
|
}
|
|
132
127
|
|
|
@@ -173,7 +168,7 @@ let make = (
|
|
|
173
168
|
onChange={event => {
|
|
174
169
|
let target = event->ReactEvent.Form.currentTarget
|
|
175
170
|
|
|
176
|
-
setSearch(_ => target["value"]
|
|
171
|
+
setSearch(_ => target["value"])
|
|
177
172
|
}}
|
|
178
173
|
/>
|
|
179
174
|
</div>
|
|
@@ -50,8 +50,7 @@ module Options = {
|
|
|
50
50
|
let make = (~options, ~deferredSearch, ~itemClassName, ~setSelectedOption, ~selectedOption) => {
|
|
51
51
|
options
|
|
52
52
|
->Array.keep(({label}) =>
|
|
53
|
-
deferredSearch == "" ||
|
|
54
|
-
label->Toolkit__Primitives.String.normalizeForSearch->Js.String2.includes(deferredSearch)
|
|
53
|
+
deferredSearch == "" || Toolkit__Primitives.String.normalizedIncludes(label, deferredSearch)
|
|
55
54
|
)
|
|
56
55
|
->Array.mapWithIndex((i, item) => {
|
|
57
56
|
let {label, value} = item
|