5htp-core 0.2.8 → 0.2.9

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 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.2.8",
4
+ "version": "0.2.9",
5
5
  "author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
6
6
  "repository": "git://github.com/gaetanlegac/5htp-core.git",
7
7
  "license": "MIT",
@@ -49,7 +49,32 @@
49
49
  }
50
50
 
51
51
  &.col {
52
- padding: @cardPaddingLong @cardPadding;
52
+ padding: @cardPadding @cardPaddingLong;
53
+
54
+ &.inside {
55
+
56
+ padding: 0;
57
+ gap: 0;
58
+ align-items: stretch;
59
+
60
+ > * {
61
+ padding: @cardPaddingLong @cardPadding;
62
+ &:first-child {
63
+ border-top-left-radius: inherit;
64
+ border-top-right-radius: inherit;
65
+ }
66
+ &:last-child {
67
+ border-bottom-left-radius: inherit;
68
+ border-bottom-right-radius: inherit;
69
+ }
70
+ }
71
+ }
72
+
73
+ &.sep {
74
+ > * + * {
75
+ border-top: solid 1px #eee;
76
+ }
77
+ }
53
78
  }
54
79
 
55
80
  &.selected {
@@ -32,7 +32,8 @@
32
32
 
33
33
  .card,
34
34
  .btn,
35
- .table,
35
+ .input.text,
36
+ .input.select,
36
37
  i.solid {
37
38
 
38
39
  .build-theme-bg( #fff, #8E8E8E);
@@ -48,16 +49,6 @@ i.solid {
48
49
  }
49
50
 
50
51
  .input.text {
51
- border: solid 0.25em #eee;
52
-
53
- &:hover {
54
- border-color: #ddd;
55
- }
56
-
57
- &.focus {
58
- border-color: @c1;
59
- }
60
-
61
52
  &.error {
62
53
  border-color: @cError;
63
54
  }
@@ -111,7 +111,7 @@ header > strong {
111
111
  }
112
112
 
113
113
  em {
114
- font-style: normal;
114
+ /*font-style: normal;
115
115
  position: relative;
116
116
  display: inline-block;
117
117
  color: inherit;
@@ -129,7 +129,7 @@ em {
129
129
  bottom: 0.05em;
130
130
  left: -0.1em;
131
131
  right: -0.1em;
132
- }
132
+ }*/
133
133
  }
134
134
 
135
135
  pre {
@@ -148,6 +148,8 @@ pre {
148
148
  @readingMargin: 2.4rem;
149
149
  .reading {
150
150
 
151
+ --cTxtBase: #555;
152
+
151
153
  .card > & {
152
154
 
153
155
  padding: 3vh 0 !important;
@@ -165,20 +167,21 @@ pre {
165
167
  max-width: var(--focusWidth);
166
168
  margin: 0 auto;
167
169
  width: 100%;
168
- padding: 0 @readingMargin;
170
+ //padding: 0 @readingMargin;
169
171
  }
170
172
 
171
173
  &,
172
174
  p {
173
175
  font-family: 'Lato', sans-serif;
174
176
  font-size: 1.1rem;
175
- line-height: 1.8em;
177
+ line-height: 2em;
176
178
  color: var(--cTxtBase);
177
179
  text-align: justify;
178
180
  }
179
181
 
180
182
  h2, h3, h4 {
181
183
  text-align: left;
184
+ margin: @spacing * 2 0;
182
185
  }
183
186
 
184
187
  h2 {
@@ -28,10 +28,13 @@ type FieldsAttrs<TFormData extends {}> = {
28
28
  }
29
29
 
30
30
  export type Form<TFormData extends {} = {}> = {
31
+
31
32
  fields: FieldsAttrs<TFormData>,
32
33
  data: TFormData,
33
34
  options: TFormOptions<TFormData>,
34
- validate: (data: TFormData) => TValidationResult<{}>,
35
+ autosavedData?: Partial<TFormData>,
36
+
37
+ validate: (data: Partial<TFormData>) => TValidationResult<{}>,
35
38
  set: (data: Partial<TFormData>) => void,
36
39
  submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
37
40
  } & FormState
@@ -48,30 +51,30 @@ type FormState = {
48
51
  export default function useForm<TFormData extends {}>(
49
52
  schema: Schema<TFormData>,
50
53
  options: TFormOptions<TFormData>
51
- ) {
54
+ ): [ Form, FieldsAttrs<TFormData> ] {
52
55
 
53
56
  const context = useContext();
54
57
 
55
58
  /*----------------------------------
56
59
  - INIT
57
60
  ----------------------------------*/
58
- let initialData: any;
61
+ let autosavedData: TFormData | undefined;
59
62
  if (options.autoSave && typeof window !== 'undefined') {
60
63
  const autosaved = localStorage.getItem('form.' + options.autoSave.id);
61
64
  if (autosaved !== null) {
62
65
  try {
63
66
  console.log('[form] Parse autosaved from json:', autosaved);
64
- initialData = JSON.parse(autosaved);
67
+ autosavedData = JSON.parse(autosaved);
65
68
  } catch (error) {
66
69
  console.error('[form] Failed to decode autosaved data from json:', autosaved);
67
70
  }
68
71
  }
69
72
  }
70
- if (initialData === undefined)
71
- initialData = options.data || {};
72
73
 
73
- const fields = React.useRef<FieldsAttrs<TFormData>>(null);
74
- const [data, setData] = React.useState<TFormData>(initialData);
74
+ const initialData: Partial<TFormData> = options.data || {};
75
+
76
+ const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
77
+ const [data, setData] = React.useState< Partial<TFormData> >(initialData);
75
78
  const [state, setState] = React.useState<FormState>({
76
79
  isLoading: false,
77
80
  errorsCount: 0,
@@ -93,7 +96,7 @@ export default function useForm<TFormData extends {}>(
93
96
  /*----------------------------------
94
97
  - ACTIONS
95
98
  ----------------------------------*/
96
- const validate = (allData: TFormData = data, validateAll: boolean = true) => {
99
+ const validate = (allData: Partial<TFormData> = data, validateAll: boolean = true) => {
97
100
 
98
101
  const validated = schema.validate(allData, allData, {}, {
99
102
  // Ignore the fields where the vlaue has not been changed
@@ -130,7 +133,7 @@ export default function useForm<TFormData extends {}>(
130
133
  // Callback
131
134
  let submitResult: any;
132
135
  if (options.submit)
133
- submitResult = await options.submit(allData);
136
+ submitResult = await options.submit(allData as TFormData);
134
137
 
135
138
  // Reset autosaved data
136
139
  if (options.autoSave)
@@ -149,14 +152,14 @@ export default function useForm<TFormData extends {}>(
149
152
  }));
150
153
  }
151
154
 
152
- const saveLocally = (data: TFormData, id: string) => {
155
+ const saveLocally = (data: Partial<TFormData>, id: string) => {
153
156
  console.log('[form] Autosave data for form:', id, ':', data);
154
157
  localStorage.setItem('form.' + id, JSON.stringify(data));
155
158
  }
156
159
 
157
160
  // Rebuild the fields attrs when the schema changes
158
161
  if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')) {
159
- fields.current = {}
162
+ fields.current = {} as FieldsAttrs<TFormData>
160
163
  for (const fieldName in schema.fields) {
161
164
 
162
165
  const validator = schema.fields[fieldName];
@@ -202,6 +205,7 @@ export default function useForm<TFormData extends {}>(
202
205
  validate,
203
206
  submit,
204
207
  options,
208
+ autosavedData,
205
209
  ...state
206
210
  }
207
211
 
@@ -69,36 +69,7 @@ export default React.forwardRef<HTMLDivElement, Props>(({
69
69
  ...otherProps
70
70
  }: Props, ref) => {
71
71
 
72
- /*----------------------------------
73
- - INIT
74
- ----------------------------------*/
75
-
76
- const choicesViaFunc = typeof initChoices === 'function';
77
- if (choicesViaFunc && enableSearch === undefined)
78
- enableSearch = true;
79
-
80
- const [search, setSearch] = React.useState<{
81
- keywords: string,
82
- loading: boolean
83
- }>({
84
- keywords: '',
85
- loading: choicesViaFunc
86
- });
87
-
88
- const [choices, setChoices] = React.useState<Choices>( choicesViaFunc ? [] : initChoices );
89
-
90
- /*----------------------------------
91
- - ACTIONS
92
- ----------------------------------*/
93
-
94
- React.useEffect(() => {
95
- if (choicesViaFunc) {
96
- initChoices(search.keywords).then((searchResults) => {
97
- setSearch(s => ({ ...s, loading: false }))
98
- setChoices(searchResults);
99
- })
100
- }
101
- }, [initChoices, search.keywords]);
72
+
102
73
 
103
74
  /*----------------------------------
104
75
  - RENDER
@@ -7,6 +7,7 @@ import React from 'react';
7
7
 
8
8
  // Core
9
9
  import Dropdown, { TDropdownControl, Props as DropdownProps } from '@client/components/dropdown';
10
+ import Input from '@client/components/inputv3';
10
11
 
11
12
  // Specific
12
13
  import ChoiceSelector, {
@@ -23,60 +24,178 @@ export type Props = DropdownProps & SelectorProps & {
23
24
  errors?: string[],
24
25
  }
25
26
 
27
+ const ChoiceElement = ({ choice, currentList, onChange, multiple, includeCurrent }: {
28
+ choice: Choice,
29
+ currentList: Choice[],
30
+ includeCurrent: boolean
31
+ } & Pick<Props, 'onChange'|'multiple'>) => {
32
+
33
+ const isCurrent = currentList.some(c => c.value === choice.value);
34
+ if (isCurrent && !includeCurrent) return null;
35
+
36
+ return (
37
+ <li class={"badge clickable " + (isCurrent ? 'bg primary' : '')} onClick={() => {
38
+ onChange( current => {
39
+
40
+ return multiple
41
+ ? (isCurrent
42
+ ? current.filter(c => c.value !== choice.value)
43
+ : [...(current || []), choice]
44
+ )
45
+ : (isCurrent
46
+ ? undefined
47
+ : choice
48
+ )
49
+ });
50
+
51
+ }}>
52
+ {/*search.keywords ? (
53
+ <span>
54
+
55
+ <strong>{search.keywords}</strong>{choice.label.slice( search.keywords.length )}
56
+
57
+ </span>
58
+ ) : */choice.label}
59
+ </li>
60
+ )
61
+ }
62
+
26
63
  /*----------------------------------
27
64
  - COMONENT
28
65
  ----------------------------------*/
29
66
  export default ({
67
+ // Input basics
30
68
  title,
31
69
  errors,
32
- ...props
70
+ icon,
71
+ required,
72
+ validator,
73
+
74
+ // Choice selection
75
+ choices: initChoices,
76
+ noneSelection,
77
+ enableSearch,
78
+ value: current,
79
+ onChange,
80
+ inline,
81
+ multiple,
82
+ ...otherProps
33
83
  }: Props) => {
34
84
 
35
85
  /*----------------------------------
36
86
  - INIT
37
87
  ----------------------------------*/
38
88
 
39
- const refDropdown = React.useRef<TDropdownControl>(null);
89
+ const choicesViaFunc = typeof initChoices === 'function';
90
+ if (choicesViaFunc && enableSearch === undefined)
91
+ enableSearch = true;
92
+
93
+ const refInputSearch = React.useRef<HTMLInputElement | null>(null);
94
+
95
+ let className: string = 'input select txt-left';
96
+
97
+ const isRequired = required || validator?.options.min;
98
+
99
+ const [search, setSearch] = React.useState<{
100
+ keywords: string,
101
+ loading: boolean
102
+ }>({
103
+ keywords: '',
104
+ loading: choicesViaFunc
105
+ });
106
+
107
+ const [choices, setChoices] = React.useState<Choice[]>( choicesViaFunc ? [] : initChoices );
40
108
 
41
109
  /*----------------------------------
42
110
  - ACTIONS
43
111
  ----------------------------------*/
44
112
 
45
- const currentList: Choice[] = props.value === undefined
113
+ React.useEffect(() => {
114
+ if (choicesViaFunc && (search.keywords || !enableSearch)) {
115
+ initChoices(search.keywords).then((searchResults) => {
116
+ setSearch(s => ({ ...s, loading: false }))
117
+ setChoices(searchResults);
118
+ })
119
+ }
120
+ }, [initChoices, search.keywords]);
121
+
122
+ const currentList: Choice[] = current === undefined
46
123
  ? []
47
- : (Array.isArray(props.value)
48
- ? props.value
49
- : [props.value]
124
+ : (Array.isArray(current)
125
+ ? current
126
+ : [current]
50
127
  );
51
128
 
52
129
  /*----------------------------------
53
130
  - RENDER
54
131
  ----------------------------------*/
55
132
  return <>
56
- {props.inline ? (
57
- <ChoiceSelector {...props} currentList={currentList} />
58
- ) : (
59
- <Dropdown {...props} content={(
60
- <ChoiceSelector {...props} currentList={currentList} refDropdown={refDropdown} />
61
- )} iconR="chevron-down" refDropdown={refDropdown}>
62
-
63
- {currentList.length === 0
64
- ? title
65
- : currentList.map(
66
- choice => (
67
- <span class="badge">
68
- {choice.label}
69
- </span>
70
- )
133
+
134
+ <div class="col sp-05">
135
+ <div class={className} onClick={() => refInputSearch.current?.focus()}>
136
+
137
+ <div class="row al-left wrap pd-1">
138
+
139
+ {icon !== undefined && (
140
+ <i src={icon} />
71
141
  )}
72
142
 
73
- </Dropdown>
74
- )}
143
+ <div class="col al-left sp-05">
144
+
145
+ <label>{title}{required && (
146
+ <span class="fg error">&nbsp;*</span>
147
+ )}</label>
148
+
149
+ <div class="row al-left wrap sp-05">
75
150
 
76
- {errors?.length && (
77
- <div class="fg error txt-left">
78
- {errors.join('. ')}
151
+ {/*!isRequired && (
152
+ <span class={"badge clickable " + (currentList.length === 0 ? 'bg primary' : '')}
153
+ onClick={() => onChange(multiple ? [] : undefined)}>
154
+ {noneSelection || 'None'}
155
+ </span>
156
+ )*/}
157
+
158
+ {( enableSearch ? currentList : choices ).map( choice => (
159
+ <ChoiceElement choice={choice}
160
+ currentList={currentList}
161
+ onChange={onChange}
162
+ multiple={multiple}
163
+ includeCurrent
164
+ />
165
+ ))}
166
+
167
+ {enableSearch && (
168
+ <Input
169
+ placeholder="Type your search here"
170
+ value={search.keywords}
171
+ onChange={keywords => setSearch(s => ({ ...s, loading: true, keywords }))}
172
+ inputRef={refInputSearch}
173
+ />
174
+ )}
175
+ </div>
176
+ </div>
177
+
178
+ </div>
179
+
180
+ {(enableSearch && choices.length !== 0 && search.keywords.length !== 0) && (
181
+ <ul class="row al-left wrap sp-05 pd-1">
182
+ {choices.map( choice => (
183
+ <ChoiceElement choice={choice}
184
+ currentList={currentList}
185
+ onChange={onChange}
186
+ multiple={multiple}
187
+ />
188
+ ))}
189
+ </ul>
190
+ )}
191
+
79
192
  </div>
80
- )}
193
+ {errors?.length && (
194
+ <div class="fg error txt-left">
195
+ {errors.join('. ')}
196
+ </div>
197
+ )}
198
+ </div>
199
+
81
200
  </>
82
201
  }
@@ -71,7 +71,7 @@ export default (props: Props) => {
71
71
  refContent.current,
72
72
  false,
73
73
  side,
74
- frame || document.getElementById('page') || undefined
74
+ /*frame || document.getElementById('page') || */undefined
75
75
  )
76
76
  );
77
77
 
@@ -65,6 +65,7 @@ export default ({
65
65
  setValue( !value );
66
66
  }}
67
67
  checked={value}
68
+ {...fieldProps}
68
69
  />
69
70
 
70
71
  <label htmlFor={id} class="col-1 txt-left">
@@ -1,4 +1,4 @@
1
- .champ.slider {
1
+ .input.slider {
2
2
  @hSlider: 2.5rem;
3
3
  @hTrack: 0.5rem;
4
4
 
@@ -1,9 +1,16 @@
1
1
  /*----------------------------------
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
+
5
+ // Npm
4
6
  import React from 'react';
7
+ import { VNode, JSX } from 'preact';
5
8
  import Slider from 'react-slider';
6
- import Champ from '../Base';
9
+
10
+ // Core libs
11
+ import { useInput, InputBaseProps } from '../../inputv3/base';
12
+ import { default as Validator } from '@common/validation/validator';
13
+ import type SchemaValidators from '@common/validation/validators';
7
14
 
8
15
  /*----------------------------------
9
16
  - TYPES
@@ -14,44 +21,78 @@ type TValeurDefaut = typeof valeurDefaut;
14
21
  type TValeurOut = string;
15
22
 
16
23
  export type Props = {
17
- valeur: TValeur,
18
- step?: number,
19
24
 
25
+ value: TValeur,
26
+
27
+ step?: number,
20
28
  min?: number,
21
29
  max?: number,
22
30
 
23
- formater?: (valeur: number) => string
31
+ format?: (value: number) => string
24
32
  }
25
33
 
34
+
26
35
  /*----------------------------------
27
36
  - COMPOSANT
28
37
  ----------------------------------*/
29
38
  import './index.less';
30
- export default Champ<Props, TValeurDefaut, TValeurOut>('slider', { valeurDefaut, saisieManuelle: false }, ({
31
- suffixeLabel, attrsChamp,
32
- step, min, max, formater
33
- }, { valeur, state, setState }, rendre) => {
34
-
35
- if (suffixeLabel === undefined)
36
- suffixeLabel = <strong>{formater ? formater(valeur) : valeur}</strong>
37
-
38
- // On copie les attributs pour ne pas modifer l'objet attrsChamp et provoquer des comportements imprévus
39
- let attributs = { ...attrsChamp };
40
-
41
- return rendre((
42
- <Slider {...attributs}
43
- step={step}
44
- min={min}
45
- max={max}
46
-
47
- value={valeur}
48
- onChange={(valeur: number) => {
49
- setState({ valeur });
50
- }}
51
-
52
- className="champ slider"
53
- thumbClassName="thumb"
54
- trackClassName="track"
55
- />
56
- ), { suffixeLabel });
57
- });
39
+ export default ({
40
+ // Decoration
41
+ icon, prefix, required,
42
+ step, min, max,
43
+ // State
44
+ errors,
45
+ // Behavior
46
+ type,
47
+ ...props
48
+ }: Props & InputBaseProps<number> & Omit<JSX.HTMLAttributes<HTMLInputElement>, 'onChange'>) => {
49
+
50
+ /*----------------------------------
51
+ - INIT
52
+ ----------------------------------*/
53
+
54
+ const [{ value, focus, fieldProps }, setValue, commitValue, setState] = useInput(props, 0);
55
+
56
+
57
+ /*----------------------------------
58
+ - ATTRIBUTES
59
+ ----------------------------------*/
60
+
61
+ let className: string = 'input slider';
62
+
63
+
64
+
65
+ /*----------------------------------
66
+ - VALIDATION
67
+ ----------------------------------*/
68
+
69
+
70
+ /*----------------------------------
71
+ - RENDER
72
+ ----------------------------------*/
73
+ return <>
74
+ <div class={className}>
75
+ <div class="contValue">
76
+ <Slider
77
+ step={step}
78
+ min={min}
79
+ max={max}
80
+
81
+ value={value}
82
+ onChange={(value: number) => {
83
+ setValue(value)
84
+ }}
85
+
86
+ className="champ slider"
87
+ thumbClassName="thumb"
88
+ trackClassName="track"
89
+ />
90
+ </div>
91
+ </div>
92
+ {errors?.length && (
93
+ <div class="fg error txt-left">
94
+ {errors.join('. ')}
95
+ </div>
96
+ )}
97
+ </>
98
+ }
@@ -1,4 +1,5 @@
1
1
  @hInput: @sizeComponent;
2
+ @labelH: 0.6em;
2
3
 
3
4
  // TODO: Adapter textarea à input/basev2
4
5
  .contChamp textarea {
@@ -9,56 +10,93 @@
9
10
  padding-top: @spacing * 2 !important;
10
11
  }
11
12
 
13
+ .input {
14
+ &.text,
15
+ &.select {
16
+
17
+ > i {
18
+ color: var(--cTxtDesc);
19
+ }
20
+
21
+ label {
22
+ display: flex;
23
+ align-items: center;
24
+ color: var(--cTxtImportant);
25
+ font-size: 0.9em;
26
+ font-weight: 600;
27
+ transition: all .1s ease-out;
28
+ }
29
+ }
30
+ }
31
+
32
+ .input.select {
33
+
34
+ padding: 0;
35
+
36
+ > * {
37
+ + * {
38
+ border-top: solid 1px var(--cLine);;
39
+ }
40
+ }
41
+
42
+ .input.text {
43
+ border: none;
44
+ box-shadow: none;
45
+ padding: 0;
46
+ height: auto;
47
+ width: 150px;
48
+ }
49
+ }
50
+
12
51
  .input.text {
13
-
52
+
14
53
  display: flex;
15
54
  align-items: center;
55
+ align-items: flex-start;
16
56
  padding: @spacing;
17
- gap: @spacing;
57
+ gap: 1em;
18
58
 
19
59
  // Inputs don't have to have the same height as the buttons,
20
60
  // Since they are not supposed to be on the same row (mobile first design)
21
61
  height: @hInput;
22
- border-radius: @radius;
62
+ align-items: center;
23
63
 
24
64
  --cBg2: #eee;
25
65
  //box-shadow: 1px 3px 0 fade(#000,4%), 0 1px 2px 0 fade(#000,2%);
26
66
 
27
- > i {
28
- color: var(--cTxtDesc);
29
- }
30
-
31
- .contValue {
32
- position: relative;
33
- flex: 1;
34
- &, * {
35
- cursor: text;
36
- }
37
- }
38
-
39
67
  @gapAround: (@hInput - @labelH) / 2;
40
68
  > .btn:first-child { margin-left: 0 - @gapAround / 2; }
41
69
 
42
- @labelH: 0.6em;
43
70
  label {
44
71
  position: absolute;
45
72
  left: 0; width: 100%;
46
73
  top: 0;
47
- display: flex;
48
- align-items: center;
49
-
50
74
  height: @labelH;
51
75
  font-size: 0.8em;
52
76
  line-height: 0.8em;
53
- color: var(--cTxtDesc);
54
- transition: all .1s ease-out;
77
+
78
+ color: var(--cTxtImportant);
55
79
  }
56
80
 
57
81
  &.empty:not(.focus) {
58
82
  label {
59
83
  height: 100%;
60
84
  font-size: 1em;
61
- //color: var(--cTxtImportant);
85
+ color: var(--cTxtBase);
86
+ }
87
+ }
88
+
89
+ .contValue {
90
+ position: relative;
91
+ flex: 1;
92
+
93
+ display: flex;
94
+ flex-direction: column;
95
+ align-items: flex-start;
96
+ gap: @spacing / 2;
97
+
98
+ &, * {
99
+ cursor: text;
62
100
  }
63
101
  }
64
102
 
@@ -69,10 +107,19 @@
69
107
  font-family: inherit;
70
108
  width: 100%;
71
109
  padding: @labelH 0 0 0;
110
+ font-size: 1rem;
111
+ }
112
+
113
+ input {
72
114
  height: 2.5em;
73
115
  }
74
116
 
117
+ &.multiline {
118
+ height: auto;
119
+ }
120
+
75
121
  textarea {
76
122
  padding: @labelH + @spacing 0 @spacing 0;
123
+ min-height: 4em;
77
124
  }
78
125
  }
@@ -20,7 +20,7 @@ export type InputBaseProps<TValue> = {
20
20
  errors?: string[],
21
21
 
22
22
  value: TValue,
23
- onChange?: StateUpdater<TValue>,
23
+ onChange?: (value: TValue) => void,
24
24
  }
25
25
 
26
26
  export type TInputState<TValue> = {
@@ -35,7 +35,7 @@ export type TInputState<TValue> = {
35
35
  - HOOKS
36
36
  ----------------------------------*/
37
37
  export function useInput<TValue>(
38
- { value: externalValue, onChange }: InputBaseProps<TValue>,
38
+ { value: externalValue, onChange, ...otherProps }: InputBaseProps<TValue>,
39
39
  defaultValue: TValue,
40
40
  autoCommit: boolean = false
41
41
  ): [
@@ -48,7 +48,7 @@ export function useInput<TValue>(
48
48
  const [state, setState] = useState<TInputState<TValue>>({
49
49
  value: externalValue !== undefined ? externalValue : defaultValue,
50
50
  valueSource: 'external',
51
- fieldProps: {},
51
+ fieldProps: otherProps,
52
52
  focus: false,
53
53
  changed: false
54
54
  });
@@ -57,7 +57,7 @@ export function useInput<TValue>(
57
57
  setState({ value, valueSource: 'internal', changed: true });
58
58
 
59
59
  if (autoCommit)
60
- commitValue(value);
60
+ commitValue();
61
61
  };
62
62
 
63
63
  const commitValue = () => {
@@ -71,6 +71,8 @@ export default ({
71
71
 
72
72
  }, [value]);
73
73
 
74
+ const updateValue = v => setValue( type === 'number' ? parseFloat(v) : v );
75
+
74
76
  const refInput = inputRef || React.useRef<HTMLInputElement>();
75
77
 
76
78
  /*----------------------------------
@@ -98,7 +100,8 @@ export default ({
98
100
  } else if (type === 'longtext') {
99
101
 
100
102
  prefix = prefix || <i src="text" />;
101
- Tag = 'textarea'//TextareaAutosize;
103
+ Tag = 'textarea';
104
+ className += ' multiline';
102
105
 
103
106
  } else if (type === 'number') {
104
107
 
@@ -139,42 +142,44 @@ export default ({
139
142
  /*----------------------------------
140
143
  - RENDER
141
144
  ----------------------------------*/
142
- return <>
143
- <div class={className} onClick={() => refInput.current?.focus()}>
144
-
145
- {prefix}
146
-
147
- <div class="contValue">
148
-
149
- <Tag {...fieldProps}
150
- // @ts-ignore: Property 'ref' does not exist on type 'IntrinsicAttributes'
151
- ref={refInput}
152
- value={value}
153
-
154
- onFocus={() => setState({ focus: true })}
155
- onBlur={() => setState({ focus: false })}
156
- onChange={(e) => setValue(e.target.value)}
157
-
158
- onKeyDown={(e) => {
159
- if (onPressEnter && e.key === 'Enter' && value !== undefined) {
160
- commitValue();
161
- onPressEnter(value)
162
- }
163
- }}
164
- />
165
-
166
- <label>{props.title}{required && (
167
- <span class="fg error">&nbsp;*</span>
168
- )}</label>
145
+ return (
146
+ <div class="col sp-05">
147
+ <div class={className} onClick={() => refInput.current?.focus()}>
148
+
149
+ {prefix}
150
+
151
+ <div class="contValue">
152
+
153
+ <Tag {...fieldProps}
154
+ // @ts-ignore: Property 'ref' does not exist on type 'IntrinsicAttributes'
155
+ ref={refInput}
156
+ value={value}
157
+
158
+ onFocus={() => setState({ focus: true })}
159
+ onBlur={() => setState({ focus: false })}
160
+ onChange={(e) => updateValue(e.target.value)}
161
+
162
+ onKeyDown={(e) => {
163
+ if (onPressEnter && e.key === 'Enter' && value !== undefined) {
164
+ commitValue();
165
+ onPressEnter(value)
166
+ }
167
+ }}
168
+ />
169
+
170
+ <label>{props.title}{required && (
171
+ <span class="fg error">&nbsp;*</span>
172
+ )}</label>
173
+ </div>
174
+
175
+ {suffix}
176
+
169
177
  </div>
170
-
171
- {suffix}
172
-
178
+ {errors?.length && (
179
+ <div class="fg error txt-left">
180
+ {errors.join('. ')}
181
+ </div>
182
+ )}
173
183
  </div>
174
- {errors?.length && (
175
- <div class="fg error txt-left">
176
- {errors.join('. ')}
177
- </div>
178
- )}
179
- </>
184
+ )
180
185
  }
@@ -147,6 +147,7 @@ export default class ClientRouter<
147
147
  }
148
148
 
149
149
  public go( url: string ) {
150
+ console.log( LogPrefix, "Go to", url);
150
151
  history?.replace(url);
151
152
  }
152
153
 
@@ -17,8 +17,6 @@ import { history } from '@client/services/router/request/history';
17
17
  export type TDataProvider<TProvidedData extends TFetcherList = {}> =
18
18
  (context: TClientOrServerContext/* & TUrlData */) => TProvidedData
19
19
 
20
-
21
-
22
20
  // The function that renders routes
23
21
  export type TFrontRenderer<
24
22
  TProvidedData extends TFetcherList = {},
@@ -43,6 +41,8 @@ export type TPageResource = {
43
41
  preload?: boolean
44
42
  })
45
43
 
44
+ const debug = false;
45
+
46
46
  /*----------------------------------
47
47
  - CLASS
48
48
  ----------------------------------*/
@@ -81,7 +81,7 @@ export default class PageResponse<TRouter extends ClientOrServerRouter = ClientO
81
81
  this.fetchers = this.dataProvider({ ...this.context, ...this.context.request.data });
82
82
 
83
83
  // Execute the fetchers for missing data
84
- console.log(`[router][page] Fetching api data:` + Object.keys(this.fetchers));
84
+ debug && console.log(`[router][page] Fetching api data:` + Object.keys(this.fetchers));
85
85
  this.data = await this.context.request.api.fetchSync( this.fetchers, this.data );
86
86
  return this.data;
87
87
  }
@@ -23,9 +23,6 @@ export type TValidator<TValue> = {
23
23
  activer?: (donnees: TObjetDonnees) => boolean,
24
24
  onglet?: string, // Sert juste d'identifiant secondaire. Ex: nom onglet correspondant
25
25
 
26
- // Restrict to a specific set of values
27
- in?: TValue[],
28
-
29
26
  // Executé après le validateur propre au type
30
27
  dependances?: string[],
31
28
  opt?: true,
@@ -15,6 +15,7 @@ import { InputError } from '@common/errors';
15
15
  import FileToUpload from '@client/components/inputv3/file/FileToUpload';
16
16
 
17
17
  // Speciific
18
+ import Schema from './schema'
18
19
  import Validator, { TValidator } from './validator'
19
20
 
20
21
  // Components
@@ -63,15 +64,14 @@ export default class SchemaValidators {
63
64
  return val;
64
65
  }, opts)
65
66
 
66
- public array = (subtype?: Validator<any>, { choice, min, max, ...opts }: TValidator<any[]> & {
67
+ public array = ( subtype?: Validator<any> | Schema<{}>, {
68
+ choice, min, max, ...opts
69
+ }: TValidator<any[]> & {
67
70
  choice?: any[],
68
71
  min?: number,
69
72
  max?: number
70
73
  } = {}) => {
71
74
 
72
- if (subtype !== undefined)
73
- subtype.options.in = choice;
74
-
75
75
  return new Validator<any[]>('array', (items, input, output, corriger) => {
76
76
 
77
77
  // Type
@@ -86,32 +86,41 @@ export default class SchemaValidators {
86
86
 
87
87
  // Verif each item
88
88
  if (subtype !== undefined) {
89
- if (false/*subtype instanceof Schema*/) {
89
+ if (subtype instanceof Schema) {
90
90
 
91
- console.log('TODO: VALIDER VIA SOUS SCHEMA');
91
+ items = items.map( item =>
92
+ subtype.validate( item, item, item, { }, []).values
93
+ )
92
94
 
93
95
  } else {
94
96
 
95
97
  items = items.map( item =>
96
98
  subtype.validate( item, items, items, corriger )
97
99
  )
100
+
98
101
  }
99
102
  }
100
103
 
101
104
  return items;
102
105
  }, {
103
106
  ...opts,
104
- in: choice,
105
107
  //multiple: true, // Sélection multiple
106
108
  //subtype
107
109
  })
108
110
  }
109
111
 
110
- public choice = (values: any[], opts: TValidator<any> & {} = {}) =>
112
+ public choice = (values?: any[], opts: TValidator<any> & {} = {}) =>
111
113
  new Validator<any>('choice', (val, input, output) => {
112
114
 
113
- if (!values.includes(val))
114
- throw new InputError("Invalid value. Must be: " + values.join(', '));
115
+ // Choice object
116
+ if (typeof val === 'object' && ('value' in val) && typeof val.value !== 'object')
117
+ val = val.value;
118
+
119
+ if (values !== undefined) {
120
+ const isValid = values.some(v => v.value === val);
121
+ if (!isValid)
122
+ throw new InputError("Invalid value. Must be: " + values.map(v => v.value).join(', '));
123
+ }
115
124
 
116
125
  return val;
117
126
 
@@ -123,10 +132,6 @@ export default class SchemaValidators {
123
132
  public string = ({ min, max, ...opts }: TValidator<string> & { min?: number, max?: number } = {}) =>
124
133
  new Validator<string>('string', (val, input, output, corriger?: boolean) => {
125
134
 
126
- // Choice value from Select component
127
- if (typeof val === 'object' && val.value)
128
- val = val.value;
129
-
130
135
  if (val === '')
131
136
  return undefined;
132
137
  else if (typeof val === 'number')
@@ -173,9 +178,30 @@ export default class SchemaValidators {
173
178
  if (!isEmail(val))
174
179
  throw new InputError("Please enter a valid email address.");
175
180
 
176
- const retour = normalizeEmail(val);
177
-
178
- return retour;
181
+ // Disable normalzation !!! We should keep the email as it was entered by the user
182
+ /*const normalizedEmail = normalizeEmail(val, {
183
+ all_lowercase: true,
184
+ gmail_lowercase: true,
185
+ gmail_remove_dots: false,
186
+ gmail_remove_subaddress: true,
187
+ gmail_convert_googlemaildotcom: true,
188
+
189
+ outlookdotcom_lowercase: true,
190
+ outlookdotcom_remove_subaddress: true,
191
+
192
+ yahoo_lowercase: true,
193
+ yahoo_remove_subaddress: true,
194
+
195
+ yandex_lowercase: true,
196
+
197
+ icloud_lowercase: true,
198
+ icloud_remove_subaddress: true,
199
+ });*/
200
+
201
+ const normalizedEmail = val.toLowerCase();
202
+ console.log("validate email, inou", val, normalizedEmail);
203
+
204
+ return normalizedEmail;
179
205
  }, opts)
180
206
 
181
207
  /*----------------------------------
@@ -94,8 +94,7 @@ export default abstract class Application extends Service<Config, Hooks, /* TODO
94
94
 
95
95
  public constructor() {
96
96
 
97
- // @ts-ignore: can't pass this to super
98
- super();
97
+ super({}, {});
99
98
 
100
99
  // Gestion crash
101
100
  process.on('unhandledRejection', (error: any, promise: any) => {
@@ -24,6 +24,10 @@ type THookOptions = {
24
24
 
25
25
  export type TPriority = -2 | -1 | 0 | 1 | 2
26
26
 
27
+ type TServiceConfig = {
28
+ debug?: boolean
29
+ }
30
+
27
31
  /*----------------------------------
28
32
  - CONFIG
29
33
  ----------------------------------*/
@@ -34,7 +38,7 @@ const LogPrefix = '[service]';
34
38
  - CLASS
35
39
  ----------------------------------*/
36
40
  export default abstract class Service<
37
- TConfig extends {},
41
+ TConfig extends TServiceConfig,
38
42
  THooks extends THooksList,
39
43
  TApplication extends Application
40
44
  > {
@@ -88,7 +92,7 @@ export default abstract class Service<
88
92
  if (!callbacks)
89
93
  return console.info(LogPrefix, `No ${name} hook defined in the current service instance.`);
90
94
 
91
- console.info(`[hook] Run all ${name} hook (${callbacks.length}).`);
95
+ this.config.debug && console.info(`[hook] Run all ${name} hook (${callbacks.length}).`);
92
96
  return Promise.all(
93
97
  callbacks.map(
94
98
  cb => cb(...args).catch(e => {
@@ -98,7 +102,7 @@ export default abstract class Service<
98
102
  })
99
103
  )
100
104
  ).then(() => {
101
- console.info(`[hook] Hooks ${name} executed with success.`);
105
+ this.config.debug && console.info(`[hook] Hooks ${name} executed with success.`);
102
106
  })
103
107
  }
104
108
 
@@ -37,6 +37,7 @@ type ConnectionConfig = {
37
37
  }
38
38
 
39
39
  export type DatabaseServiceConfig = {
40
+ debug: boolean,
40
41
  connections: ConnectionConfig[]
41
42
  }
42
43
 
@@ -207,7 +208,7 @@ export default class DatabaseManager extends Service<DatabaseServiceConfig, THoo
207
208
 
208
209
  let jsTypeName = mysqlToJs[ mysqlType.name ];
209
210
  if (jsTypeName === undefined) {
210
- console.warn(`The mySQL data type « ${mysqlType.name} » has not been associated with a JS equivalent in mysqlToJs. Using any instead.`);
211
+ this.config.debug && console.warn(`Column "${field.table}.${field.name}": The mySQL data type « ${mysqlType.name} » has not been associated with a JS equivalent in mysqlToJs. Using any instead.`);
211
212
  jsTypeName = mysqlToJs['UNKNOWN'];
212
213
  }
213
214
 
@@ -201,9 +201,9 @@ export default class SQL extends Service<Config, Hooks, Application> {
201
201
 
202
202
  // SQL query
203
203
  } else if (typeof value === 'function' && value.string !== undefined)
204
- value = value.string;
204
+ value = ' ' + value.string;
205
205
  else
206
- value = mysql.escape(value);
206
+ value = ' ' + mysql.escape(value);
207
207
 
208
208
  stringBefore += value;
209
209
 
@@ -310,7 +310,7 @@ export default class SQL extends Service<Config, Hooks, Application> {
310
310
  return this.database.query(
311
311
  data.map( record =>
312
312
  this.update(tableName, record, where, { ...opts, returnQuery: true })
313
- ).join(';\n')
313
+ ).join('\n')
314
314
  )
315
315
 
316
316
  // Automatic where based on pks
@@ -298,7 +298,7 @@ export default class MySQLMetasParser {
298
298
 
299
299
  }
300
300
 
301
- private parseJsType( name: string, mysqlType: TMySQLType, comment: string | null, isOptional?: boolean ): TJsType {
301
+ private parseJsType( colName: string, mysqlType: TMySQLType, comment: string | null, isOptional?: boolean ): TJsType {
302
302
 
303
303
  let typeName: TJsType["name"] | undefined;
304
304
  let params: TJsType["params"];
@@ -324,7 +324,7 @@ export default class MySQLMetasParser {
324
324
 
325
325
  // Equivalent not found
326
326
  if (typeName === undefined) {
327
- console.warn(`The mySQL data type « ${mysqlType.name} » has not been associated with a JS equivalent in mysqlToJs. Using any instead.`);
327
+ this.database.config.debug && console.warn(`Column "${colName}": The mySQL data type « ${mysqlType.name} » has not been associated with a JS equivalent in mysqlToJs. Using any instead.`);
328
328
  typeName = mysqlToJs['UNKNOWN'];
329
329
  }
330
330
  }
@@ -334,7 +334,7 @@ export default class MySQLMetasParser {
334
334
  if (!jsTypeUtils)
335
335
  throw new Error(`Unable to find the typescript print funvction for js type "${typeName}"`);
336
336
 
337
- const raw = name + (isOptional ? '?' : '') + ': ' + jsTypeUtils.print( mysqlType.params );
337
+ const raw = colName + (isOptional ? '?' : '') + ': ' + jsTypeUtils.print( mysqlType.params );
338
338
 
339
339
  return { name: typeName, params, raw }
340
340
  }
@@ -564,7 +564,7 @@ declare type Routes = {
564
564
  e.message = "We encountered an internal error, and our team has just been notified. Sorry for the inconvenience.";
565
565
 
566
566
  // Pour déboguer les erreurs HTTP
567
- } else if (this.app.env.profile === "dev")
567
+ } else if (code !== 404 && this.app.env.profile === "dev")
568
568
  console.warn(e);
569
569
 
570
570
  if (request.accepts("html"))