5htp-core 0.3.5 → 0.3.6
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 +1 -1
- package/src/client/assets/css/components/other.less +1 -1
- package/src/client/components/Form.ts +19 -0
- package/src/client/components/Select/ChoiceElement.tsx +63 -0
- package/src/client/components/Select/ChoiceSelector.tsx +0 -1
- package/src/client/components/Select/index.tsx +111 -93
- package/src/client/components/containers/Popover/index.tsx +3 -3
- package/src/client/components/inputv3/file/index.less +6 -0
- package/src/client/components/inputv3/index.tsx +16 -2
- package/src/client/services/router/index.tsx +8 -2
- package/src/client/services/router/request/api.ts +1 -1
- package/src/common/router/index.ts +5 -3
- package/src/common/router/request/api.ts +2 -0
- package/src/common/router/response/page.ts +11 -3
- package/src/common/validation/index.ts +1 -1
- package/src/common/validation/validators.ts +83 -66
- package/src/server/services/console/index.ts +5 -1
- package/src/server/services/database/index.ts +2 -4
- package/src/server/services/router/http/index.ts +1 -0
- package/src/server/services/router/request/api.ts +8 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,18 +29,22 @@ type FieldsAttrs<TFormData extends {}> = {
|
|
|
29
29
|
|
|
30
30
|
export type Form<TFormData extends {} = {}> = {
|
|
31
31
|
|
|
32
|
+
// Data
|
|
32
33
|
fields: FieldsAttrs<TFormData>,
|
|
33
34
|
data: TFormData,
|
|
34
35
|
options: TFormOptions<TFormData>,
|
|
35
36
|
autosavedData?: Partial<TFormData>,
|
|
36
37
|
|
|
38
|
+
// Actions
|
|
37
39
|
validate: (data: Partial<TFormData>) => TValidationResult<{}>,
|
|
38
40
|
set: (data: Partial<TFormData>) => void,
|
|
39
41
|
submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
|
|
42
|
+
|
|
40
43
|
} & FormState
|
|
41
44
|
|
|
42
45
|
type FormState = {
|
|
43
46
|
isLoading: boolean,
|
|
47
|
+
hasChanged: boolean,
|
|
44
48
|
errorsCount: number,
|
|
45
49
|
errors: { [fieldName: string]: string[] },
|
|
46
50
|
}
|
|
@@ -58,6 +62,8 @@ export default function useForm<TFormData extends {}>(
|
|
|
58
62
|
/*----------------------------------
|
|
59
63
|
- INIT
|
|
60
64
|
----------------------------------*/
|
|
65
|
+
|
|
66
|
+
// Autosaving data
|
|
61
67
|
let autosavedData: TFormData | undefined;
|
|
62
68
|
if (options.autoSave && typeof window !== 'undefined') {
|
|
63
69
|
const autosaved = localStorage.getItem('form.' + options.autoSave.id);
|
|
@@ -73,9 +79,11 @@ export default function useForm<TFormData extends {}>(
|
|
|
73
79
|
|
|
74
80
|
const initialData: Partial<TFormData> = options.data || {};
|
|
75
81
|
|
|
82
|
+
// States
|
|
76
83
|
const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
|
|
77
84
|
const [data, setData] = React.useState< Partial<TFormData> >(initialData);
|
|
78
85
|
const [state, setState] = React.useState<FormState>({
|
|
86
|
+
hasChanged: false,
|
|
79
87
|
isLoading: false,
|
|
80
88
|
errorsCount: 0,
|
|
81
89
|
errors: {}
|
|
@@ -139,6 +147,12 @@ export default function useForm<TFormData extends {}>(
|
|
|
139
147
|
if (options.autoSave)
|
|
140
148
|
localStorage.removeItem('form.' + options.autoSave.id);
|
|
141
149
|
|
|
150
|
+
// Update state
|
|
151
|
+
setState( current => ({
|
|
152
|
+
...current,
|
|
153
|
+
hasChanged: false
|
|
154
|
+
}));
|
|
155
|
+
|
|
142
156
|
return submitResult;
|
|
143
157
|
}
|
|
144
158
|
|
|
@@ -177,6 +191,11 @@ export default function useForm<TFormData extends {}>(
|
|
|
177
191
|
: val
|
|
178
192
|
}
|
|
179
193
|
})
|
|
194
|
+
|
|
195
|
+
setState(current => ({
|
|
196
|
+
...current,
|
|
197
|
+
hasChanged: true
|
|
198
|
+
}));
|
|
180
199
|
},
|
|
181
200
|
|
|
182
201
|
// Submit on press enter
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
6
|
+
import React from 'react';
|
|
7
|
+
|
|
8
|
+
// Specific
|
|
9
|
+
import type {
|
|
10
|
+
Choice,
|
|
11
|
+
} from './ChoiceSelector';
|
|
12
|
+
|
|
13
|
+
import type { Props } from '.';
|
|
14
|
+
|
|
15
|
+
/*----------------------------------
|
|
16
|
+
- TYPE
|
|
17
|
+
----------------------------------*/
|
|
18
|
+
|
|
19
|
+
/*----------------------------------
|
|
20
|
+
- COMPONENT
|
|
21
|
+
----------------------------------*/
|
|
22
|
+
export default ({ choice, currentList, onChange, multiple, includeCurrent }: {
|
|
23
|
+
choice: Choice,
|
|
24
|
+
currentList: Choice[],
|
|
25
|
+
includeCurrent: boolean
|
|
26
|
+
} & Pick<Props, 'onChange'|'multiple'>) => {
|
|
27
|
+
|
|
28
|
+
const isCurrent = currentList.some(c => c.value === choice.value);
|
|
29
|
+
if (isCurrent && !includeCurrent) return null;
|
|
30
|
+
|
|
31
|
+
const showRemoveButton = multiple;
|
|
32
|
+
|
|
33
|
+
return isCurrent ? (
|
|
34
|
+
<li class={"badge bg primary"+ (showRemoveButton ? ' pdr-05' : '')}>
|
|
35
|
+
{choice.label}
|
|
36
|
+
|
|
37
|
+
{showRemoveButton && (
|
|
38
|
+
<span class="badge xs clickable" onClick={(e) => {
|
|
39
|
+
e.stopPropagation();
|
|
40
|
+
onChange( current => current.filter( c => c.value !== choice.value))
|
|
41
|
+
return false;
|
|
42
|
+
}}>
|
|
43
|
+
x
|
|
44
|
+
</span>
|
|
45
|
+
)}
|
|
46
|
+
</li>
|
|
47
|
+
) : (
|
|
48
|
+
<li class={"badge clickable"} onClick={() => {
|
|
49
|
+
onChange( current => multiple
|
|
50
|
+
? [...(current || []), choice]
|
|
51
|
+
: choice
|
|
52
|
+
);
|
|
53
|
+
}}>
|
|
54
|
+
{/*search.keywords ? (
|
|
55
|
+
<span>
|
|
56
|
+
|
|
57
|
+
<strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
|
|
58
|
+
|
|
59
|
+
</span>
|
|
60
|
+
) : */choice.label}
|
|
61
|
+
</li>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -7,7 +7,7 @@ import React from 'react';
|
|
|
7
7
|
|
|
8
8
|
// Core
|
|
9
9
|
import { Props as DropdownProps } from '@client/components/dropdown';
|
|
10
|
-
import Input from '@client/components
|
|
10
|
+
import { Popover, Button, Input } from '@client/components';
|
|
11
11
|
|
|
12
12
|
// Specific
|
|
13
13
|
import {
|
|
@@ -15,53 +15,20 @@ import {
|
|
|
15
15
|
Choice,
|
|
16
16
|
} from './ChoiceSelector';
|
|
17
17
|
|
|
18
|
+
import ChoiceElement from './ChoiceElement';
|
|
19
|
+
|
|
18
20
|
/*----------------------------------
|
|
19
21
|
- TYPES
|
|
20
22
|
----------------------------------*/
|
|
21
23
|
|
|
22
24
|
export type Props = DropdownProps & SelectorProps & {
|
|
25
|
+
dropdown: boolean,
|
|
23
26
|
title: string,
|
|
24
27
|
errors?: string[],
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
export type { Choice } from './ChoiceSelector';
|
|
28
31
|
|
|
29
|
-
const ChoiceElement = ({ choice, currentList, onChange, multiple, includeCurrent }: {
|
|
30
|
-
choice: Choice,
|
|
31
|
-
currentList: Choice[],
|
|
32
|
-
includeCurrent: boolean
|
|
33
|
-
} & Pick<Props, 'onChange'|'multiple'>) => {
|
|
34
|
-
|
|
35
|
-
const isCurrent = currentList.some(c => c.value === choice.value);
|
|
36
|
-
if (isCurrent && !includeCurrent) return null;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<li class={"badge clickable " + (isCurrent ? 'bg primary' : '')} onClick={() => {
|
|
40
|
-
onChange( current => {
|
|
41
|
-
|
|
42
|
-
return multiple
|
|
43
|
-
? (isCurrent
|
|
44
|
-
? current.filter(c => c.value !== choice.value)
|
|
45
|
-
: [...(current || []), choice]
|
|
46
|
-
)
|
|
47
|
-
: (isCurrent
|
|
48
|
-
? undefined
|
|
49
|
-
: choice
|
|
50
|
-
)
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
}}>
|
|
54
|
-
{/*search.keywords ? (
|
|
55
|
-
<span>
|
|
56
|
-
|
|
57
|
-
<strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
|
|
58
|
-
|
|
59
|
-
</span>
|
|
60
|
-
) : */choice.label}
|
|
61
|
-
</li>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
32
|
/*----------------------------------
|
|
66
33
|
- COMONENT
|
|
67
34
|
----------------------------------*/
|
|
@@ -79,8 +46,8 @@ export default ({
|
|
|
79
46
|
enableSearch,
|
|
80
47
|
value: current,
|
|
81
48
|
onChange,
|
|
82
|
-
inline,
|
|
83
49
|
multiple,
|
|
50
|
+
dropdown,
|
|
84
51
|
...otherProps
|
|
85
52
|
}: Props) => {
|
|
86
53
|
|
|
@@ -88,6 +55,8 @@ export default ({
|
|
|
88
55
|
- INIT
|
|
89
56
|
----------------------------------*/
|
|
90
57
|
|
|
58
|
+
const popoverState = React.useState(false);
|
|
59
|
+
|
|
91
60
|
const choicesViaFunc = typeof initChoices === 'function';
|
|
92
61
|
if (choicesViaFunc && enableSearch === undefined)
|
|
93
62
|
enableSearch = true;
|
|
@@ -96,17 +65,39 @@ export default ({
|
|
|
96
65
|
|
|
97
66
|
let className: string = 'input select txt-left';
|
|
98
67
|
|
|
99
|
-
const isRequired = required || validator?.options.min;
|
|
100
|
-
|
|
101
68
|
const [search, setSearch] = React.useState<{
|
|
102
69
|
keywords: string,
|
|
103
|
-
loading: boolean
|
|
70
|
+
loading: boolean,
|
|
71
|
+
focused: boolean
|
|
104
72
|
}>({
|
|
105
73
|
keywords: '',
|
|
106
|
-
loading: choicesViaFunc
|
|
74
|
+
loading: choicesViaFunc,
|
|
75
|
+
focused: true
|
|
107
76
|
});
|
|
108
77
|
|
|
109
|
-
const [choices, setChoices] = React.useState<Choice[]>( choicesViaFunc
|
|
78
|
+
const [choices, setChoices] = React.useState<Choice[]>( choicesViaFunc
|
|
79
|
+
? []
|
|
80
|
+
: initChoices
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const displayChoices = (
|
|
84
|
+
enableSearch
|
|
85
|
+
&&
|
|
86
|
+
choices.length !== 0
|
|
87
|
+
&&
|
|
88
|
+
search.keywords.length !== 0
|
|
89
|
+
&&
|
|
90
|
+
search.focused
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
const isRequired = required || validator?.options.min;
|
|
94
|
+
|
|
95
|
+
const currentList: Choice[] = current === undefined
|
|
96
|
+
? []
|
|
97
|
+
: (Array.isArray(current)
|
|
98
|
+
? current
|
|
99
|
+
: [current]
|
|
100
|
+
);
|
|
110
101
|
|
|
111
102
|
/*----------------------------------
|
|
112
103
|
- ACTIONS
|
|
@@ -119,22 +110,79 @@ export default ({
|
|
|
119
110
|
setChoices(searchResults);
|
|
120
111
|
})
|
|
121
112
|
}
|
|
122
|
-
}, [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
:
|
|
127
|
-
|
|
128
|
-
: [current]
|
|
129
|
-
);
|
|
113
|
+
}, [
|
|
114
|
+
search.keywords,
|
|
115
|
+
// When initChoices is a function, React considers it's always different
|
|
116
|
+
// It avoids the choices are fetched everytimle the parent component is re-rendered
|
|
117
|
+
typeof initChoices === 'function' ? true : initChoices
|
|
118
|
+
]);
|
|
130
119
|
|
|
131
120
|
/*----------------------------------
|
|
132
121
|
- RENDER
|
|
133
122
|
----------------------------------*/
|
|
134
|
-
|
|
123
|
+
|
|
124
|
+
const SelectedItems = ( enableSearch ? currentList : choices ).map( choice => (
|
|
125
|
+
<ChoiceElement choice={choice}
|
|
126
|
+
currentList={currentList}
|
|
127
|
+
onChange={onChange}
|
|
128
|
+
multiple={multiple}
|
|
129
|
+
includeCurrent
|
|
130
|
+
/>
|
|
131
|
+
))
|
|
132
|
+
|
|
133
|
+
const Search = enableSearch && (
|
|
134
|
+
<Input
|
|
135
|
+
placeholder="Type your search here"
|
|
136
|
+
value={search.keywords}
|
|
137
|
+
onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
|
|
138
|
+
inputRef={refInputSearch}
|
|
139
|
+
/>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
const SearchResults = displayChoices && (
|
|
143
|
+
<ul class="row al-left wrap sp-05" style={{
|
|
144
|
+
maxHeight: '30vh',
|
|
145
|
+
overflowY: 'auto'
|
|
146
|
+
}}>
|
|
147
|
+
{choices.map( choice => (
|
|
148
|
+
<ChoiceElement choice={choice}
|
|
149
|
+
currentList={currentList}
|
|
150
|
+
onChange={onChange}
|
|
151
|
+
multiple={multiple}
|
|
152
|
+
/>
|
|
153
|
+
))}
|
|
154
|
+
</ul>
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return dropdown ? (
|
|
158
|
+
<Popover content={(
|
|
159
|
+
<div class="card col" style={{ width: '300px' }}>
|
|
160
|
+
|
|
161
|
+
<div class="col">
|
|
162
|
+
|
|
163
|
+
{SelectedItems.length !== 0 && (
|
|
164
|
+
<div class="row wrap">
|
|
165
|
+
{SelectedItems}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{Search}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{SearchResults}
|
|
173
|
+
</div>
|
|
174
|
+
)} state={popoverState}>
|
|
175
|
+
<Button icon={icon} iconR="chevron-down" {...otherProps}>
|
|
176
|
+
{title} {(multiple && currentList.length > 0)
|
|
177
|
+
? <span class="badge s bg accent">{currentList.length}</span>
|
|
178
|
+
: null
|
|
179
|
+
}
|
|
180
|
+
</Button>
|
|
181
|
+
</Popover>
|
|
182
|
+
) : (
|
|
135
183
|
|
|
136
184
|
<div class="col sp-05">
|
|
137
|
-
<div class={className}
|
|
185
|
+
<div class={className} onMouseDown={() => refInputSearch.current?.focus()}>
|
|
138
186
|
|
|
139
187
|
<div class="row al-left wrap pd-1">
|
|
140
188
|
|
|
@@ -144,54 +192,25 @@ export default ({
|
|
|
144
192
|
|
|
145
193
|
<div class="col al-left sp-05">
|
|
146
194
|
|
|
147
|
-
<label>{title}{
|
|
195
|
+
<label>{title}{isRequired && (
|
|
148
196
|
<span class="fg error"> *</span>
|
|
149
197
|
)}</label>
|
|
150
198
|
|
|
151
199
|
<div class="row al-left wrap sp-05">
|
|
200
|
+
{SelectedItems}
|
|
152
201
|
|
|
153
|
-
{
|
|
154
|
-
<span class={"badge clickable " + (currentList.length === 0 ? 'bg primary' : '')}
|
|
155
|
-
onClick={() => onChange(multiple ? [] : undefined)}>
|
|
156
|
-
{noneSelection || 'None'}
|
|
157
|
-
</span>
|
|
158
|
-
)*/}
|
|
159
|
-
|
|
160
|
-
{( enableSearch ? currentList : choices ).map( choice => (
|
|
161
|
-
<ChoiceElement choice={choice}
|
|
162
|
-
currentList={currentList}
|
|
163
|
-
onChange={onChange}
|
|
164
|
-
multiple={multiple}
|
|
165
|
-
includeCurrent
|
|
166
|
-
/>
|
|
167
|
-
))}
|
|
168
|
-
|
|
169
|
-
{enableSearch && (
|
|
170
|
-
<Input
|
|
171
|
-
placeholder="Type your search here"
|
|
172
|
-
value={search.keywords}
|
|
173
|
-
onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
|
|
174
|
-
inputRef={refInputSearch}
|
|
175
|
-
/>
|
|
176
|
-
)}
|
|
202
|
+
{Search}
|
|
177
203
|
</div>
|
|
178
204
|
</div>
|
|
179
205
|
|
|
180
206
|
</div>
|
|
181
207
|
|
|
182
|
-
{
|
|
183
|
-
<
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
<ChoiceElement choice={choice}
|
|
189
|
-
currentList={currentList}
|
|
190
|
-
onChange={onChange}
|
|
191
|
-
multiple={multiple}
|
|
192
|
-
/>
|
|
193
|
-
))}
|
|
194
|
-
</ul>
|
|
208
|
+
{SearchResults && (
|
|
209
|
+
<div class="pd-1">
|
|
210
|
+
|
|
211
|
+
{SearchResults}
|
|
212
|
+
|
|
213
|
+
</div>
|
|
195
214
|
)}
|
|
196
215
|
|
|
197
216
|
</div>
|
|
@@ -201,6 +220,5 @@ export default ({
|
|
|
201
220
|
</div>
|
|
202
221
|
)}
|
|
203
222
|
</div>
|
|
204
|
-
|
|
205
|
-
</>
|
|
223
|
+
)
|
|
206
224
|
}
|
|
@@ -105,9 +105,9 @@ export default (props: Props) => {
|
|
|
105
105
|
renderedContent = React.cloneElement(
|
|
106
106
|
content,
|
|
107
107
|
{
|
|
108
|
-
className:
|
|
109
|
-
+ '
|
|
110
|
-
+
|
|
108
|
+
className: 'card popover pd-1'
|
|
109
|
+
+ (position ? ' pos_' + position.cote : '')
|
|
110
|
+
+ ' ' + (content.props.className || ''),
|
|
111
111
|
|
|
112
112
|
ref: (ref: any) => {
|
|
113
113
|
if (ref !== null)
|
|
@@ -62,6 +62,13 @@ export default ({
|
|
|
62
62
|
|
|
63
63
|
// Trigger onchange oly when finished typing
|
|
64
64
|
const refCommit = React.useRef<NodeJS.Timeout | null>(null);
|
|
65
|
+
|
|
66
|
+
const refInput = inputRef || React.useRef<HTMLInputElement>();
|
|
67
|
+
|
|
68
|
+
/*----------------------------------
|
|
69
|
+
- ACTIONS
|
|
70
|
+
----------------------------------*/
|
|
71
|
+
|
|
65
72
|
React.useEffect(() => {
|
|
66
73
|
|
|
67
74
|
if (refCommit.current !== null)
|
|
@@ -70,6 +77,15 @@ export default ({
|
|
|
70
77
|
refCommit.current = setTimeout(commitValue, 500);
|
|
71
78
|
|
|
72
79
|
}, [value]);
|
|
80
|
+
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
|
|
83
|
+
if (focus && props.onFocus)
|
|
84
|
+
props.onFocus(null);
|
|
85
|
+
else if (!focus && props.onBlur)
|
|
86
|
+
props.onBlur(null);
|
|
87
|
+
|
|
88
|
+
}, [focus]);
|
|
73
89
|
|
|
74
90
|
const updateValue = v => {
|
|
75
91
|
if (type === 'number') {
|
|
@@ -82,8 +98,6 @@ export default ({
|
|
|
82
98
|
} else
|
|
83
99
|
setValue(v);
|
|
84
100
|
}
|
|
85
|
-
|
|
86
|
-
const refInput = inputRef || React.useRef<HTMLInputElement>();
|
|
87
101
|
|
|
88
102
|
/*----------------------------------
|
|
89
103
|
- ATTRIBUTES
|
|
@@ -160,9 +160,15 @@ export default class ClientRouter<
|
|
|
160
160
|
buildUrl(path, params, this.config.domains, absolute);
|
|
161
161
|
|
|
162
162
|
public go( url: string ) {
|
|
163
|
+
|
|
163
164
|
url = this.url(url, {}, false);
|
|
164
|
-
|
|
165
|
-
history
|
|
165
|
+
|
|
166
|
+
// Same domain = history url replacement
|
|
167
|
+
if (url[0] === '/')
|
|
168
|
+
history?.replace( url );
|
|
169
|
+
// Different domain = hard navigation
|
|
170
|
+
else
|
|
171
|
+
windows.location.href = url;
|
|
166
172
|
}
|
|
167
173
|
|
|
168
174
|
/*----------------------------------
|
|
@@ -80,7 +80,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
80
80
|
public reload( ids?: string | string[], params?: TObjetDonnees ) {
|
|
81
81
|
|
|
82
82
|
if (!('context' in this.router))
|
|
83
|
-
throw new Error("api.
|
|
83
|
+
throw new Error("api.reload is not available on server side.");
|
|
84
84
|
|
|
85
85
|
const page = this.router.context.page;
|
|
86
86
|
|
|
@@ -122,8 +122,8 @@ export const defaultOptions = {
|
|
|
122
122
|
----------------------------------*/
|
|
123
123
|
export const buildUrl = (
|
|
124
124
|
path: string,
|
|
125
|
-
params: {
|
|
126
|
-
domains:
|
|
125
|
+
params: {[alias: string]: any},
|
|
126
|
+
domains: {[alias: string]: string},
|
|
127
127
|
absolute: boolean
|
|
128
128
|
) => {
|
|
129
129
|
|
|
@@ -135,7 +135,9 @@ export const buildUrl = (
|
|
|
135
135
|
|
|
136
136
|
// Extract domain ID from path
|
|
137
137
|
let domainId: string;
|
|
138
|
-
|
|
138
|
+
let slackPos = path.indexOf('/');
|
|
139
|
+
if (slackPos === -1)
|
|
140
|
+
slackPos = path.length;
|
|
139
141
|
domainId = path.substring(1, slackPos);
|
|
140
142
|
path = path.substring(slackPos);
|
|
141
143
|
|
|
@@ -74,6 +74,8 @@ export default abstract class ApiClient {
|
|
|
74
74
|
|
|
75
75
|
public abstract set( newData: TObjetDonnees );
|
|
76
76
|
|
|
77
|
+
public abstract reload( ids?: string | string[], params?: TObjetDonnees );
|
|
78
|
+
|
|
77
79
|
/*----------------------------------
|
|
78
80
|
- LOW LEVEL
|
|
79
81
|
----------------------------------*/
|
|
@@ -82,18 +82,26 @@ export default abstract class PageResponse<TRouter extends ClientOrServerRouter
|
|
|
82
82
|
) {
|
|
83
83
|
|
|
84
84
|
this.chunkId = context.route.options["id"];
|
|
85
|
+
|
|
86
|
+
this.fetchers = this.createFetchers();
|
|
85
87
|
|
|
86
88
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
+
|
|
90
|
+
private createFetchers() {
|
|
89
91
|
|
|
90
92
|
// Load the fetchers list to load data if needed
|
|
91
93
|
const dataFetcher = this.route.options.data;
|
|
92
94
|
if (dataFetcher)
|
|
93
|
-
|
|
95
|
+
return dataFetcher({
|
|
94
96
|
...this.context,
|
|
95
97
|
data: this.context.request.data
|
|
96
98
|
});
|
|
99
|
+
else
|
|
100
|
+
return {}
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public async fetchData() {
|
|
97
105
|
|
|
98
106
|
// Execute the fetchers for missing data
|
|
99
107
|
debug && console.log(`[router][page] Fetching api data:` + Object.keys(this.fetchers));
|
|
@@ -72,92 +72,109 @@ export default class SchemaValidators {
|
|
|
72
72
|
choice?: any[],
|
|
73
73
|
min?: number,
|
|
74
74
|
max?: number
|
|
75
|
-
} = {}) => {
|
|
75
|
+
} = {}) => new Validator<any[]>('array', (items, input, output, corriger) => {
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
// Type
|
|
78
|
+
if (!Array.isArray(items))
|
|
79
|
+
throw new InputError("This value must be a list.");
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
// Items number
|
|
82
|
+
if ((min !== undefined && items.length < min))
|
|
83
|
+
throw new InputError(`Please select at least ${min} items.`);
|
|
84
|
+
if ((max !== undefined && items.length > max))
|
|
85
|
+
throw new InputError(`Please select maximum ${max} items.`);
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
if ((max !== undefined && items.length > max))
|
|
87
|
-
throw new InputError(`Please select maximum ${max} items.`);
|
|
87
|
+
// Verif each item
|
|
88
|
+
if (subtype !== undefined) {
|
|
89
|
+
if (subtype instanceof Schema) {
|
|
88
90
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
items = items.map( item =>
|
|
92
|
+
subtype.validate( item, item, item, { }, []).values
|
|
93
|
+
)
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
subtype.validate( item, item, item, { }, []).values
|
|
95
|
-
)
|
|
95
|
+
} else {
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
items = items.map( item =>
|
|
98
|
+
subtype.validate( item, items, items, corriger )
|
|
99
|
+
)
|
|
98
100
|
|
|
99
|
-
items = items.map( item =>
|
|
100
|
-
subtype.validate( item, items, items, corriger )
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
}
|
|
104
101
|
}
|
|
102
|
+
}
|
|
105
103
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
104
|
+
return items;
|
|
105
|
+
}, {
|
|
106
|
+
...opts,
|
|
107
|
+
//multiple: true, // Sélection multiple
|
|
108
|
+
//subtype
|
|
109
|
+
})
|
|
113
110
|
|
|
114
|
-
public choice = (choices?: any[], opts: TValidator<any> & {
|
|
115
|
-
|
|
111
|
+
public choice = (choices?: any[], { multiple, ...opts }: TValidator<any> & {
|
|
112
|
+
multiple?: boolean
|
|
113
|
+
} = {}) => new Validator<any>('choice', (val, input, output) => {
|
|
116
114
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
115
|
+
// Empty array = undefined if not required
|
|
116
|
+
if (val.length === 0 && opts.opt)
|
|
117
|
+
return undefined;
|
|
120
118
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (!isValid)
|
|
124
|
-
throw new InputError("Invalid value. Must be: " + choices.map(v => v.value).join(', '));
|
|
125
|
-
}
|
|
119
|
+
// Normalize for verifications
|
|
120
|
+
const choicesValues = choices?.map(v => v.value)
|
|
126
121
|
|
|
127
|
-
|
|
122
|
+
const checkChoice = ( choice: any ) => {
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
// Choice object = extract value
|
|
125
|
+
if (typeof choice === 'object' && ('value' in choice) && typeof choice.value !== 'object')
|
|
126
|
+
choice = choice.value;
|
|
130
127
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
public string = ({ min, max, ...opts }: TValidator<string> & { min?: number, max?: number } = {}) =>
|
|
135
|
-
new Validator<string>('string', (val, input, output, corriger?: boolean) => {
|
|
128
|
+
// If choices list rpovided, check if the choice is in the choices list
|
|
129
|
+
if (choicesValues !== undefined && !choicesValues.includes(choice))
|
|
130
|
+
throw new InputError("Invalid value: " + choice + ". Must be: " + choicesValues.join(', '));
|
|
136
131
|
|
|
137
|
-
|
|
138
|
-
return undefined;
|
|
139
|
-
else if (typeof val === 'number')
|
|
140
|
-
return val.toString();
|
|
141
|
-
else if (typeof val !== 'string')
|
|
142
|
-
throw new InputError("This value must be a string.");
|
|
132
|
+
return choice;
|
|
143
133
|
|
|
144
|
-
|
|
145
|
-
val = trim(val);
|
|
134
|
+
}
|
|
146
135
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
// Check every choice
|
|
137
|
+
if (Array.isArray( val ))
|
|
138
|
+
val = val.map(checkChoice)
|
|
139
|
+
else
|
|
140
|
+
val = checkChoice( val );
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
if (max !== undefined && val.length > max)
|
|
153
|
-
if (corriger)
|
|
154
|
-
val = val.substring(0, max);
|
|
155
|
-
else
|
|
156
|
-
throw new InputError(`Must be up to ` + max + ' characters');
|
|
142
|
+
return val;
|
|
157
143
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
}, opts, { choices, multiple })
|
|
145
|
+
|
|
146
|
+
/*----------------------------------
|
|
147
|
+
- CHAINES
|
|
148
|
+
----------------------------------*/
|
|
149
|
+
public string = ({ min, max, ...opts }: TValidator<string> & {
|
|
150
|
+
min?: number,
|
|
151
|
+
max?: number
|
|
152
|
+
} = {}) => new Validator<string>('string', (val, input, output, corriger?: boolean) => {
|
|
153
|
+
|
|
154
|
+
if (val === '')
|
|
155
|
+
return undefined;
|
|
156
|
+
else if (typeof val === 'number')
|
|
157
|
+
return val.toString();
|
|
158
|
+
else if (typeof val !== 'string')
|
|
159
|
+
throw new InputError("This value must be a string.");
|
|
160
|
+
|
|
161
|
+
// Espaces blancs
|
|
162
|
+
val = trim(val);
|
|
163
|
+
|
|
164
|
+
// Taille min
|
|
165
|
+
if (min !== undefined && val.length < min)
|
|
166
|
+
throw new InputError(`Must be at least ` + min + ' characters');
|
|
167
|
+
|
|
168
|
+
// Taille max
|
|
169
|
+
if (max !== undefined && val.length > max)
|
|
170
|
+
if (corriger)
|
|
171
|
+
val = val.substring(0, max);
|
|
172
|
+
else
|
|
173
|
+
throw new InputError(`Must be up to ` + max + ' characters');
|
|
174
|
+
|
|
175
|
+
return val;
|
|
176
|
+
|
|
177
|
+
}, opts)
|
|
161
178
|
|
|
162
179
|
public url = (opts: TValidator<string> & {
|
|
163
180
|
normalize?: NormalizeUrlOptions
|
|
@@ -139,6 +139,9 @@ export default class Console extends Service<Config, Hooks, Application, Service
|
|
|
139
139
|
|
|
140
140
|
const origLog = console.log
|
|
141
141
|
|
|
142
|
+
const envConfig = this.config[ this.app.env.profile === 'prod' ? 'prod' : 'dev' ];
|
|
143
|
+
const minLogLevel = logLevels[ envConfig.level ];
|
|
144
|
+
|
|
142
145
|
this.logger = new Logger({
|
|
143
146
|
// Use to improve performance in production
|
|
144
147
|
hideLogPositionForProduction: this.app.env.profile === 'prod',
|
|
@@ -173,10 +176,11 @@ export default class Console extends Service<Config, Hooks, Application, Service
|
|
|
173
176
|
return;
|
|
174
177
|
|
|
175
178
|
for (const logLevel in logLevels) {
|
|
179
|
+
const levelNumber = logLevels[ logLevel ];
|
|
176
180
|
console[ logLevel ] = (...args: any[]) => {
|
|
177
181
|
|
|
178
182
|
// Dev mode = no care about performance = rich logging
|
|
179
|
-
if (
|
|
183
|
+
if (levelNumber >= minLogLevel)
|
|
180
184
|
//this.logger[ logLevel ](...args);
|
|
181
185
|
origLog(...args);
|
|
182
186
|
// Prod mode = minimal logging
|
|
@@ -176,9 +176,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
|
|
|
176
176
|
// TODO: do it via datatypes.ts
|
|
177
177
|
if (typeof data === 'object' && data !== null) {
|
|
178
178
|
|
|
179
|
-
if (
|
|
180
|
-
data = data.join(',')
|
|
181
|
-
else if (data.constructor.name === "Object")
|
|
179
|
+
if (data.constructor.name === "Object")
|
|
182
180
|
data = safeStringify(data);
|
|
183
181
|
}
|
|
184
182
|
|
|
@@ -516,8 +514,8 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
|
|
|
516
514
|
let valuesNamesToUpdate: (keyof TData)[] = [];
|
|
517
515
|
if (colsToUpdate === '*') {
|
|
518
516
|
|
|
519
|
-
console.log(LogPrefix, `Automatic upsert into ${table.chemin} using ${table.pk.join(', ')} as pk`);
|
|
520
517
|
valuesNamesToUpdate = Object.keys(table.colonnes);// table.columnNamesButPk;
|
|
518
|
+
console.log(LogPrefix, `Automatic upsert into ${table.chemin} using ${table.pk.join(', ')} as pk: ${valuesNamesToUpdate.join(', ')}`);
|
|
521
519
|
// We don't take columnNamesButPk, because if all the columns are pks, we don't have yny value for the ON DUPLICATE KEY
|
|
522
520
|
// Meaning
|
|
523
521
|
|
|
@@ -47,10 +47,18 @@ export default class ApiClientRequest extends RequestService implements ApiClien
|
|
|
47
47
|
public delete = <TData extends unknown = unknown>(path: string, data?: TObjetDonnees, opts?: TApiFetchOptions) =>
|
|
48
48
|
this.createFetcher<TData>('DELETE', path, data, opts);
|
|
49
49
|
|
|
50
|
+
/*----------------------------------
|
|
51
|
+
- PLACEHOLDERS
|
|
52
|
+
----------------------------------*/
|
|
53
|
+
|
|
50
54
|
public set( newData: TObjetDonnees ) {
|
|
51
55
|
throw new Error("api.set is not available on server side.");
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
public reload( ids?: string | string[], params?: TObjetDonnees ) {
|
|
59
|
+
throw new Error("api.set is not available on server side.");
|
|
60
|
+
}
|
|
61
|
+
|
|
54
62
|
/*----------------------------------
|
|
55
63
|
- API CALLS FROM SERVER
|
|
56
64
|
----------------------------------*/
|