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 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.5",
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",
@@ -24,7 +24,7 @@
24
24
 
25
25
  padding: 0.4em @spacing;
26
26
 
27
- background: var(--cBgAccent);
27
+ background: var(--cBgActive);
28
28
  }
29
29
 
30
30
  .clickable {
@@ -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
+ }
@@ -39,7 +39,6 @@ export type Props = (
39
39
  ) & {
40
40
  choices: Choices | ChoicesFunc,
41
41
  enableSearch?: boolean,
42
- inline?: boolean,
43
42
  required?: boolean,
44
43
  noneSelection?: false | string,
45
44
  currentList: Choice[],
@@ -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/inputv3';
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 ? [] : initChoices );
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
- }, [initChoices, search.keywords]);
123
-
124
- const currentList: Choice[] = current === undefined
125
- ? []
126
- : (Array.isArray(current)
127
- ? current
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
- return <>
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} onClick={() => refInputSearch.current?.focus()}>
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}{required && (
195
+ <label>{title}{isRequired && (
148
196
  <span class="fg error">&nbsp;*</span>
149
197
  )}</label>
150
198
 
151
199
  <div class="row al-left wrap sp-05">
200
+ {SelectedItems}
152
201
 
153
- {/*!isRequired && (
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
- {(enableSearch && choices.length !== 0 && search.keywords.length !== 0) && (
183
- <ul class="row al-left wrap sp-05 pd-1" style={{
184
- maxHeight: '30vh',
185
- overflowY: 'auto'
186
- }}>
187
- {choices.map( choice => (
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: (content.props.className || '')
109
- + ' card popover pd-1'
110
- + (position ? ' pos_' + position.cote : ''),
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)
@@ -23,6 +23,12 @@
23
23
  height: 100%;
24
24
  }
25
25
 
26
+ .preview {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ }
31
+
26
32
  input[type="file"] {
27
33
  cursor: pointer;
28
34
  opacity: 0;
@@ -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
- console.log( LogPrefix, "Go to", url);
165
- history?.replace( url );
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.set is not available on server side.");
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: TDomainsList,
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
- const slackPos = path.indexOf('/');
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
- public async fetchData() {
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
- this.fetchers = dataFetcher({
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));
@@ -1,4 +1,4 @@
1
1
  export { default as Schema } from './schema';
2
- export type { TSchemaFields } from './schema';
2
+ export type { TSchemaFields, TValidatedData } from './schema';
3
3
  export { default as Validators } from './validators';
4
4
  export { default as Validator } from './validator';
@@ -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
- return new Validator<any[]>('array', (items, input, output, corriger) => {
77
+ // Type
78
+ if (!Array.isArray(items))
79
+ throw new InputError("This value must be a list.");
78
80
 
79
- // Type
80
- if (!Array.isArray(items))
81
- throw new InputError("This value must be a list.");
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
- // Items number
84
- if ((min !== undefined && items.length < min))
85
- throw new InputError(`Please select at least ${min} items.`);
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
- // Verif each item
90
- if (subtype !== undefined) {
91
- if (subtype instanceof Schema) {
91
+ items = items.map( item =>
92
+ subtype.validate( item, item, item, { }, []).values
93
+ )
92
94
 
93
- items = items.map( item =>
94
- subtype.validate( item, item, item, { }, []).values
95
- )
95
+ } else {
96
96
 
97
- } else {
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
- return items;
107
- }, {
108
- ...opts,
109
- //multiple: true, // Sélection multiple
110
- //subtype
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
- new Validator<any>('choice', (val, input, output) => {
111
+ public choice = (choices?: any[], { multiple, ...opts }: TValidator<any> & {
112
+ multiple?: boolean
113
+ } = {}) => new Validator<any>('choice', (val, input, output) => {
116
114
 
117
- // Choice object
118
- if (typeof val === 'object' && ('value' in val) && typeof val.value !== 'object')
119
- val = val.value;
115
+ // Empty array = undefined if not required
116
+ if (val.length === 0 && opts.opt)
117
+ return undefined;
120
118
 
121
- if (choices !== undefined) {
122
- const isValid = choices.some(v => v.value === val);
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
- return val;
122
+ const checkChoice = ( choice: any ) => {
128
123
 
129
- }, opts, { choices })
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
- - CHAINES
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
- if (val === '')
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
- // Espaces blancs
145
- val = trim(val);
134
+ }
146
135
 
147
- // Taille min
148
- if (min !== undefined && val.length < min)
149
- throw new InputError(`Must be at least ` + min + ' characters');
136
+ // Check every choice
137
+ if (Array.isArray( val ))
138
+ val = val.map(checkChoice)
139
+ else
140
+ val = checkChoice( val );
150
141
 
151
- // Taille max
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
- return val;
159
-
160
- }, opts)
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 (this.app.env.profile === 'dev' || ['warn', 'error'].includes( logLevel ))
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 (Array.isArray(data))
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
 
@@ -120,6 +120,7 @@ export default class HttpServer {
120
120
  expressStaticGzip( Container.path.root + '/bin/public', {
121
121
  enableBrotli: true,
122
122
  serveStatic: {
123
+ dotfiles: 'deny',
123
124
  setHeaders: function setCustomCacheControl(res, path) {
124
125
 
125
126
  const dontCache = [
@@ -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
  ----------------------------------*/