5htp-core 0.5.0-2 → 0.5.0-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.5.0-2",
4
+ "version": "0.5.0-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",
@@ -1,8 +1,6 @@
1
1
  @import (reference) '@/client/assets/vars.less';
2
2
 
3
3
  // Utils
4
- @import './utils/sizing.less';
5
- @import './utils/spacing.less';
6
4
  @import (reference) "./theme.less";
7
5
 
8
6
  // Apply the theme class
@@ -120,4 +118,8 @@ body {
120
118
  // Import components style (always after variables declaration)
121
119
  @import "@client/assets/css/components.less";
122
120
 
121
+ // Make utilisy classes priority compared to components
122
+ @import './utils/sizing.less';
123
+ @import './utils/spacing.less';
124
+
123
125
  @import '@/client/assets/theme.less';
@@ -87,6 +87,10 @@ i.logo {
87
87
  border: none; // For img``
88
88
 
89
89
  background-color: var(--cBg);
90
+
91
+ &.circle {
92
+ border-radius: 50%;
93
+ }
90
94
  }
91
95
 
92
96
  i.logo {
@@ -89,7 +89,7 @@ p, .p {
89
89
  .txt-base { color: var(--cTxtBase); }
90
90
  .txt-contenu { color: var(--cTxtPost); }
91
91
 
92
- strong,
92
+ //strong,
93
93
  .active,
94
94
  .txtImportant {
95
95
  color: var(--cTxtImportant);
@@ -20,9 +20,10 @@ h3 {
20
20
  color: var(--cTxtAccent);
21
21
  font-weight: inherit;
22
22
 
23
- .bg.img & {
24
- color: var(--cTxtImportant);
25
- }
23
+ // Targets the whte card inside bg images, which makes no sense
24
+ // .bg.img & {
25
+ // color: var(--cTxtImportant);
26
+ // }
26
27
  }
27
28
  }
28
29
 
@@ -47,52 +47,52 @@
47
47
 
48
48
  // Fixes
49
49
  .w-@{taille1} {
50
- width: @taille1 * @sizingUnit;
50
+ width: @taille1 * @sizingUnit !important;
51
51
  }
52
52
  .h-@{taille1} {
53
- height: @taille1 * @sizingUnit;
53
+ height: @taille1 * @sizingUnit !important;
54
54
  }
55
55
 
56
56
  .row > .w-@{taille1},
57
57
  .col > .h-@{taille1} {
58
- flex: 0 0 @taille1 * @sizingUnit;
58
+ flex: 0 0 @taille1 * @sizingUnit !important;
59
59
  }
60
60
 
61
61
  // Min - max
62
62
  .w-@{taille1}-a {
63
- min-width: @taille1 * @sizingUnit;
63
+ min-width: @taille1 * @sizingUnit !important;
64
64
  }
65
65
 
66
66
  .w-a-@{taille1} {
67
- max-width: @taille1 * @sizingUnit;
68
- width: 100%; // We take the maximum space wecan
67
+ max-width: @taille1 * @sizingUnit !important;
68
+ width: 100% !important; // We take the maximum space wecan
69
69
  }
70
70
 
71
71
  .h-@{taille1}-a {
72
- min-height: @taille1 * @sizingUnit;
72
+ min-height: @taille1 * @sizingUnit !important;
73
73
  }
74
74
 
75
75
  .h-a-@{taille1} {
76
- max-height: @taille1 * @sizingUnit;
77
- height: 100%; // We take the maximum space wecan
76
+ max-height: @taille1 * @sizingUnit !important;
77
+ height: 100% !important; // We take the maximum space wecan
78
78
  }
79
79
 
80
80
  // Ranges
81
81
  .taillesMax(@tailleMax2, @taille2: 0) when (@taille2 <= @tailleMax2) {
82
82
 
83
83
  .w-@{taille1}-@{taille2} {
84
- min-width: @taille1 * @sizingUnit;
85
- max-width: @taille2 * @sizingUnit;
86
- width: 100%; // We take the maximum space wecan
84
+ min-width: @taille1 * @sizingUnit !important;
85
+ max-width: @taille2 * @sizingUnit !important;
86
+ width: 100% !important; // We take the maximum space wecan
87
87
 
88
88
  .row > & {
89
89
  flex: 1;
90
90
  }
91
91
  }
92
92
  .h-@{taille1}-@{taille2} {
93
- min-height: @taille1 * @sizingUnit;
94
- max-height: @taille2 * @sizingUnit;
95
- height: 100%; // We take the maximum space wecan
93
+ min-height: @taille1 * @sizingUnit !important;
94
+ max-height: @taille2 * @sizingUnit !important;
95
+ height: 100% !important; // We take the maximum space wecan
96
96
 
97
97
  .col > & {
98
98
  flex: 1;
@@ -179,9 +179,7 @@ export const createDialog = (app: Application, isToast: boolean): DialogActions
179
179
 
180
180
  if (!isToast)
181
181
  render = (
182
- <div class="modal" onClick={e =>
183
- e.target.classList.contains('modal') && close(false)
184
- }>
182
+ <div class="modal">
185
183
  {render}
186
184
  </div>
187
185
  )
@@ -8,7 +8,7 @@ import React from 'react';
8
8
  // Core
9
9
  import { InputErrorSchema } from '@common/errors';
10
10
  import type { Schema } from '@common/validation';
11
- import type { TValidationResult } from '@common/validation/schema';
11
+ import type { TValidationResult, TValidateOptions } from '@common/validation/schema';
12
12
  import useContext from '@/client/context';
13
13
 
14
14
  // Exports
@@ -36,19 +36,22 @@ export type Form<TFormData extends {} = {}> = {
36
36
  fields: FieldsAttrs<TFormData>,
37
37
  data: TFormData,
38
38
  options: TFormOptions<TFormData>,
39
+ backup?: Partial<TFormData>,
39
40
 
40
41
  // Actions
41
- validate: (data: Partial<TFormData>) => TValidationResult<{}>,
42
- set: (data: Partial<TFormData>) => void,
42
+ setBackup: (backup: Partial<TFormData>) => void,
43
+ validate: (data: Partial<TFormData>, validateAll?: boolean) => TValidationResult<{}>,
44
+ set: (data: Partial<TFormData>, merge?: boolean) => void,
43
45
  submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
44
46
 
45
47
  } & FormState
46
48
 
47
- type FormState = {
49
+ type FormState<TFormData extends {} = {}> = {
48
50
  isLoading: boolean,
49
51
  hasChanged: boolean,
50
52
  errorsCount: number,
51
53
  errors: { [fieldName: string]: string[] },
54
+ backup?: Partial<TFormData>,
52
55
  }
53
56
 
54
57
  /*----------------------------------
@@ -57,7 +60,7 @@ type FormState = {
57
60
  export default function useForm<TFormData extends {}>(
58
61
  schema: Schema<TFormData>,
59
62
  options: TFormOptions<TFormData> = {}
60
- ): [ Form, FieldsAttrs<TFormData> ] {
63
+ ): [ Form<TFormData>, FieldsAttrs<TFormData> ] {
61
64
 
62
65
  const context = useContext();
63
66
 
@@ -65,11 +68,12 @@ export default function useForm<TFormData extends {}>(
65
68
  - INIT
66
69
  ----------------------------------*/
67
70
 
68
- const [state, setState] = React.useState<FormState>({
69
- hasChanged: options.data !== undefined,
71
+ const [state, setState] = React.useState<FormState<TFormData>>({
72
+ hasChanged: false,//options.data !== undefined,
70
73
  isLoading: false,
71
74
  errorsCount: 0,
72
- errors: {}
75
+ errors: {},
76
+ backup: undefined
73
77
  });
74
78
 
75
79
  const initialData: Partial<TFormData> = options.data || {};
@@ -78,43 +82,54 @@ export default function useForm<TFormData extends {}>(
78
82
  const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
79
83
  const [data, setData] = React.useState< Partial<TFormData> >(initialData);
80
84
 
81
- // Validate data when it changes
85
+ // When typed data changes
82
86
  React.useEffect(() => {
83
87
 
84
88
  // Validate
85
- validate(data, false);
89
+ validate(data, { ignoreMissing: true });
86
90
 
87
91
  // Autosave
88
- if (options.autoSave !== undefined) {
89
-
90
- if (state.hasChanged)
91
- saveLocally(data, options.autoSave.id);
92
- else {
93
- const autosaved = localStorage.getItem('form.' + options.autoSave.id);
94
- if (autosaved !== null) {
95
- try {
96
- console.log('[form] Parse autosaved from json:', autosaved);
97
- setData( JSON.parse(autosaved) );
98
- } catch (error) {
99
- console.error('[form] Failed to decode autosaved data from json:', autosaved);
100
- }
92
+ if (options.autoSave !== undefined && state.hasChanged) {
93
+ saveLocally(data, options.autoSave.id);
94
+ }
95
+
96
+ }, [data]);
97
+
98
+ // On start
99
+ React.useEffect(() => {
100
+
101
+ // Restore backup
102
+ if (options.autoSave !== undefined && !state.hasChanged) {
103
+
104
+ const autosaved = localStorage.getItem('form.' + options.autoSave.id);
105
+ if (autosaved !== null) {
106
+ try {
107
+ console.log('[form] Parse autosaved from json:', autosaved);
108
+ setState(c => ({
109
+ ...c,
110
+ backup: JSON.parse(autosaved)
111
+ }));
112
+ } catch (error) {
113
+ console.error('[form] Failed to decode autosaved data from json:', autosaved);
101
114
  }
102
115
  }
103
116
  }
104
117
 
105
- }, [data]);
118
+ }, []);
106
119
 
107
120
  /*----------------------------------
108
121
  - ACTIONS
109
122
  ----------------------------------*/
110
- const validate = (allData: Partial<TFormData> = data, validateAll: boolean = true) => {
123
+ const validate = (allData: Partial<TFormData> = data, opts: TValidateOptions<TFormData> = {}) => {
111
124
 
112
125
  const validated = schema.validateWithDetails(allData, allData, {}, {
113
126
  // Ignore the fields where the vlaue has not been changed
114
127
  // if the validation was triggered via onChange
115
- ignoreMissing: !validateAll,
128
+ ignoreMissing: false,
116
129
  // The list of fields we should only validate
117
- only: options.autoValidateOnly
130
+ only: options.autoValidateOnly,
131
+ // Custom options
132
+ ...opts
118
133
  });
119
134
 
120
135
  // Update errors
@@ -224,19 +239,35 @@ export default function useForm<TFormData extends {}>(
224
239
  - EXPOSE
225
240
  ----------------------------------*/
226
241
 
227
- const form = {
242
+ const form: Form<TFormData> = {
243
+
228
244
  fields: fields.current,
229
245
  data,
230
- set: data => {
231
- setState(current => ({
246
+ set: (data, merge = true) => {
247
+
248
+ setState( current => ({
232
249
  ...current,
233
250
  hasChanged: true
234
251
  }));
235
- setData(data);
252
+
253
+ setData( merge
254
+ ? c => ({ ...c, ...data })
255
+ : data
256
+ );
236
257
  },
258
+
237
259
  validate,
238
260
  submit,
239
261
  options,
262
+
263
+ setBackup: (backup: Partial<TFormData>) => {
264
+
265
+ setState(c => ({ ...c, backup }));
266
+
267
+ if (options.autoSave)
268
+ localStorage.setItem('form.' + options.autoSave.id, JSON.stringify(backup));
269
+ },
270
+
240
271
  ...state
241
272
  }
242
273
 
@@ -249,7 +249,7 @@ export default (props: Props) => {
249
249
 
250
250
  {Search}
251
251
 
252
- <ul class="row al-left wrap sp-05 scrollable mgt-1" style={{
252
+ <ul class="row al-left wrap sp-05 scrollable" style={{
253
253
  maxHeight: '30vh',
254
254
  }}>
255
255
  {selectedItems.map( choice => (
@@ -171,35 +171,36 @@ export default ({
171
171
  // Render
172
172
  if ('link' in props || Tag === "a") {
173
173
 
174
- props.href = props.link;
175
-
176
- // External = open in new tab by default
177
- if (props.href && (props.href[0] !== '/' || props.href.startsWith('//')))
178
- props.target = '_blank';
179
-
180
- if (props.target === undefined) {
174
+ // Link (only if enabled)
175
+ if (!disabled) {
176
+
177
+ props.href = props.link;
178
+
179
+ // External = open in new tab by default
180
+ if (props.href && (props.href[0] !== '/' || props.href.startsWith('//')))
181
+ props.target = '_blank';
182
+ }
181
183
 
182
- if (nav) {
184
+ // Nav
185
+ if (nav && props.target === undefined) {
183
186
 
184
- const checkIfCurrentUrl = (url: string) =>
185
- isCurrentUrl(url, props.link, nav === 'exact');
187
+ const checkIfCurrentUrl = (url: string) =>
188
+ isCurrentUrl(url, props.link, nav === 'exact');
186
189
 
187
- React.useEffect(() => {
190
+ React.useEffect(() => {
188
191
 
189
- // Init
190
- if (checkIfCurrentUrl(ctx.request.path))
191
- setIsActive(true);
192
+ // Init
193
+ if (checkIfCurrentUrl(ctx.request.path))
194
+ setIsActive(true);
192
195
 
193
- // On location change
194
- return history?.listen(({ location }) => {
196
+ // On location change
197
+ return history?.listen(({ location }) => {
195
198
 
196
- setIsActive( checkIfCurrentUrl(location.pathname) );
197
-
198
- })
199
+ setIsActive( checkIfCurrentUrl(location.pathname) );
199
200
 
200
- }, []);
201
- }
201
+ })
202
202
 
203
+ }, []);
203
204
  }
204
205
 
205
206
  Tag = 'a';
@@ -1,45 +1,50 @@
1
1
  export type TSide = "left" | "top" | "right" | "bottom";
2
2
 
3
- const debug = false;
3
+ // Margin from container
4
+ const containerMargin = 8;
5
+
6
+ // Margin from the screen/edges
7
+ const screenMargin = 10;
4
8
 
5
9
  export type TPosition = ReturnType<typeof corrigerPosition>;
6
10
 
7
11
  export default function corrigerPosition(
8
12
  container: HTMLElement, // button
9
- popover: HTMLElement, // popover
13
+ popover: HTMLElement, // popover
10
14
  preferredSide: TSide = "bottom",
11
- frame?: HTMLElement // body
15
+ frame?: HTMLElement | null // body or closest positioned ancestor
12
16
  ) {
13
- // Dimensions and bounding rectangles
14
- const popoverDims = { width: popover.offsetWidth, height: popover.offsetHeight };
17
+ // Dimensions
18
+ const popoverDims = {
19
+ width: popover.offsetWidth,
20
+ height: popover.offsetHeight,
21
+ };
15
22
  const containerRect = container.getBoundingClientRect();
16
23
 
17
- // Find the frame if not provided
24
+ // Find frame if not provided
18
25
  if (!frame) {
19
- // Find the closest relative-positioned parent
26
+ // Find the closest relative-positioned or sticky-positioned parent
20
27
  frame = container.parentElement;
21
28
  while (frame && !["relative", "sticky"].includes(getComputedStyle(frame).position)) {
22
29
  frame = frame.parentElement;
23
30
  }
24
-
25
31
  if (!frame) frame = document.body;
26
32
  }
27
33
 
28
- if (debug) console.log("frame", frame);
29
-
30
34
  const frameRect = frame.getBoundingClientRect();
35
+ const frameContRect = document.body.getBoundingClientRect();
31
36
  const frameOffsetTop = frame.scrollTop;
32
37
  const frameOffsetLeft = frame.scrollLeft;
33
38
 
34
- // Calculate available space in each direction relative to the frame
39
+ // Calculate available space (relative to the document body) around the container
35
40
  const space = {
36
- top: containerRect.top - frameRect.top,
37
- bottom: frameRect.bottom - containerRect.bottom,
38
- left: containerRect.left - frameRect.left,
39
- right: frameRect.right - containerRect.right,
41
+ top: containerRect.top - frameContRect.top,
42
+ bottom: frameContRect.bottom - containerRect.bottom,
43
+ left: containerRect.left - frameContRect.left,
44
+ right: frameContRect.right - containerRect.right,
40
45
  };
41
46
 
42
- // Helper function to check if there's enough space
47
+ // Helper to check if the popover can fit on a given side without clipping
43
48
  const canFit = (side: TSide) => {
44
49
  switch (side) {
45
50
  case "top":
@@ -53,7 +58,7 @@ export default function corrigerPosition(
53
58
  }
54
59
  };
55
60
 
56
- // Try preferred side first, then fallback
61
+ // Start with the preferred side; if it doesn't fit, pick the first side that fits
57
62
  let side: TSide = preferredSide;
58
63
  if (!canFit(preferredSide)) {
59
64
  if (canFit("top")) side = "top";
@@ -62,14 +67,17 @@ export default function corrigerPosition(
62
67
  else if (canFit("right")) side = "right";
63
68
  }
64
69
 
65
- // Calculate position based on side
70
+ // Calculate initial position (without screen-margin clamping)
66
71
  const position = { top: 0, left: 0 };
72
+
67
73
  if (side === "top") {
68
74
  position.top =
69
75
  containerRect.top -
70
76
  frameRect.top -
71
77
  popoverDims.height +
72
- frameOffsetTop;
78
+ frameOffsetTop -
79
+ containerMargin; // gap above container
80
+
73
81
  position.left =
74
82
  containerRect.left -
75
83
  frameRect.left +
@@ -79,7 +87,9 @@ export default function corrigerPosition(
79
87
  position.top =
80
88
  containerRect.bottom -
81
89
  frameRect.top +
82
- frameOffsetTop;
90
+ frameOffsetTop +
91
+ containerMargin; // gap below container
92
+
83
93
  position.left =
84
94
  containerRect.left -
85
95
  frameRect.left +
@@ -91,34 +101,44 @@ export default function corrigerPosition(
91
101
  frameRect.top +
92
102
  (containerRect.height - popoverDims.height) / 2 +
93
103
  frameOffsetTop;
104
+
94
105
  position.left =
95
106
  containerRect.left -
96
107
  frameRect.left -
97
108
  popoverDims.width +
98
- frameOffsetLeft;
109
+ frameOffsetLeft -
110
+ containerMargin; // gap to the left of container
99
111
  } else if (side === "right") {
100
112
  position.top =
101
113
  containerRect.top -
102
114
  frameRect.top +
103
115
  (containerRect.height - popoverDims.height) / 2 +
104
116
  frameOffsetTop;
117
+
105
118
  position.left =
106
119
  containerRect.right -
107
120
  frameRect.left +
108
- frameOffsetLeft;
121
+ frameOffsetLeft +
122
+ containerMargin; // gap to the right of container
109
123
  }
110
124
 
111
- // Adjust for overflow
125
+ // Clamp the final position to ensure a screenMargin from edges
112
126
  position.top = Math.max(
113
- frameOffsetTop,
114
- Math.min(frameRect.height - popoverDims.height + frameOffsetTop, position.top)
127
+ frameOffsetTop + screenMargin,
128
+ Math.min(
129
+ frameContRect.height - popoverDims.height + frameOffsetTop - screenMargin,
130
+ position.top
131
+ )
115
132
  );
116
133
  position.left = Math.max(
117
- frameOffsetLeft,
118
- Math.min(frameRect.width - popoverDims.width + frameOffsetLeft, position.left)
134
+ frameOffsetLeft + screenMargin,
135
+ Math.min(
136
+ frameContRect.width - popoverDims.width + frameOffsetLeft - screenMargin,
137
+ position.left
138
+ )
119
139
  );
120
140
 
121
- // Return result
141
+ // Return the final side and position
122
142
  return {
123
143
  side,
124
144
  css: {
@@ -185,7 +185,7 @@ export default ({ value, setValue, props }: {
185
185
  <div className="editor-inner">
186
186
  <RichTextPlugin
187
187
  contentEditable={
188
- <div className="editor pdh-2" ref={onRef}>
188
+ <div className="editor" ref={onRef}>
189
189
  <ContentEditable
190
190
  className="editor-input reading col"
191
191
  aria-placeholder={"Type text here ..."}
@@ -206,7 +206,7 @@ export default function BlockFormatDropDown({
206
206
 
207
207
  return (
208
208
  <DropDown disabled={disabled} icon={currentBlockType ? currentBlockType.icon : 'question'} size="s"
209
- label={currentBlockType ? currentBlockType.label : 'Unknown Block Type'}
209
+ //label={currentBlockType ? currentBlockType.label : 'Unknown Block Type'}
210
210
  popover={{ tag: 'li' }}
211
211
  >
212
212
  {blockTypes.map((block) => (
@@ -4,7 +4,7 @@
4
4
  cursor: grab;
5
5
  opacity: 0;
6
6
  position: absolute;
7
- left: 0;
7
+ left: -20px;
8
8
  top: 0;
9
9
  will-change: transform;
10
10
  }
@@ -2,31 +2,7 @@
2
2
 
3
3
  .preview {
4
4
  position: relative;
5
-
6
- &:before {
7
-
8
- content: 'Click here to edit';
9
-
10
- display: flex;
11
- justify-content: center;
12
- align-items: center;
13
-
14
- position: absolute;
15
- top: 0;
16
- left: 0;
17
- right: 0;
18
- bottom: 0;
19
-
20
- background: fade(#FFF, 50%);
21
- pointer-events: none;
22
- z-index: 1;
23
- opacity: 0;
24
- transition: all 0.3s linear;
25
- }
26
-
27
- &:hover:before {
28
- opacity: 1;
29
- }
5
+ cursor: text;
30
6
  }
31
7
 
32
8
  .other h2 {
@@ -41,7 +41,7 @@ export type TInputState<TValue> = {
41
41
  - HOOKS
42
42
  ----------------------------------*/
43
43
  export function useInput<TValue>(
44
- { value: externalValue, onChange, ...otherProps }: InputBaseProps<TValue>,
44
+ { value: externalValue, onChange, className, ...otherProps }: InputBaseProps<TValue>,
45
45
  defaultValue: TValue,
46
46
  autoCommit: boolean = false
47
47
  ): [
@@ -173,7 +173,13 @@ export default (props: Props & InputBaseProps<string> & TInputElementProps) => {
173
173
  ----------------------------------*/
174
174
  return (
175
175
  <InputWrapper {...props}>
176
- <div class={className} onClick={() => refInput.current?.focus()}>
176
+ <div class={className} onClick={(e) => {
177
+
178
+ const shouldFocus = props.onClick ? props.onClick() !== false : true;
179
+ if (shouldFocus)
180
+ refInput.current?.focus()
181
+
182
+ }}>
177
183
 
178
184
  {prefix}
179
185
 
@@ -71,7 +71,7 @@ export const focusContent = ( container: HTMLElement ) => {
71
71
 
72
72
  const toFocus = container.querySelector<HTMLInputElement | HTMLTextAreaElement | HTMLButtonElement>(
73
73
  'input, textarea, button.btn.primary, footer > button.btn'
74
- ) || container;
74
+ )// || container; // Is it useful ? Creating unwanted scroll issue on showing popover
75
75
 
76
76
  toFocus?.focus();
77
77
  }
@@ -162,6 +162,12 @@ export class NotFound extends CoreError {
162
162
  public static msgDefaut = "The resource you asked for was not found.";
163
163
  }
164
164
 
165
+ export class RateLimit extends CoreError {
166
+ public http = 429;
167
+ public title = "You're going too fast";
168
+ public static msgDefaut = "Please slow down a bit and retry again later.";
169
+ }
170
+
165
171
  export class Anomaly extends CoreError {
166
172
 
167
173
  public http = 500;
@@ -229,6 +235,8 @@ export const fromJson = ({ code, message, ...details }: TJsonError) => {
229
235
 
230
236
  case 404: return new NotFound( message, details );
231
237
 
238
+ case 429: return new RateLimit( message, details );
239
+
232
240
  default: return new Anomaly( message, details );
233
241
  }
234
242
 
@@ -143,7 +143,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
143
143
  query.then = (cb: (data: any) => void) => query().then(cb);
144
144
 
145
145
  query.first = <TRowData extends TObjetDonnees = {}>(opts: TSelectQueryOptions = {}) => this.first<TRowData>(string, opts);
146
- query.firstOrFail = (message?: string, opts: TQueryOptions = {}) => this.firstOrFail<TRowData>(string, message, opts);
146
+ query.firstOrFail = (message: string, opts: TQueryOptions = {}) => this.firstOrFail<TRowData>(string, message, opts);
147
147
 
148
148
  query.value = <TValue extends any = number>(opts: TQueryOptions = {}) => this.selectVal<TValue>(string, opts);
149
149
 
@@ -283,7 +283,7 @@ export default class SQL extends Service<Config, Hooks, Application, Services> {
283
283
  return resultatRequetes[0] || null;
284
284
  });
285
285
 
286
- public firstOrFail = <TRowData extends TObjetDonnees = {}>(query: string, message?: string, opts: TSelectQueryOptions = {}): Promise<TRowData> =>
286
+ public firstOrFail = <TRowData extends TObjetDonnees = {}>(query: string, message: string, opts: TSelectQueryOptions = {}): Promise<TRowData> =>
287
287
  this.select(query, opts).then((resultatRequetes: any) => {
288
288
 
289
289
  if (resultatRequetes.length === 0)
@@ -438,7 +438,7 @@ declare type Routes = {
438
438
  res.header(response.headers);
439
439
  // Data
440
440
  res.send(response.data);
441
-
441
+
442
442
  });
443
443
  }
444
444
 
@@ -558,9 +558,14 @@ declare type Routes = {
558
558
  if (this.app.env.profile === 'prod')
559
559
  e.message = "We encountered an internal error, and our team has just been notified. Sorry for the inconvenience.";
560
560
 
561
- // Pour déboguer les erreurs HTTP
562
- } else if (code !== 404 && this.app.env.profile === "dev")
563
- console.warn(e);
561
+ } else {
562
+
563
+ // For debugging HTTP errors
564
+ if (this.app.env.profile === "dev")
565
+ console.warn(e);
566
+
567
+ await this.app.runHook('error.' + code, e, request);
568
+ }
564
569
 
565
570
  // Return error based on the request format
566
571
  if (request.accepts("html")) {
@@ -135,6 +135,17 @@ export default class ServerRequest<
135
135
  return locale ? locale.toUpperCase() : 'EN'
136
136
  }
137
137
 
138
+ public cookie( key: string, consume: boolean = false ) {
139
+
140
+ const value = this.req.cookies[ key ];
141
+
142
+ if (consume)
143
+ this.res.clearCookie(key);
144
+
145
+ return value;
146
+
147
+ }
148
+
138
149
  /*----------------------------------
139
150
  - TESTS
140
151
  ----------------------------------*/
@@ -81,6 +81,7 @@ export default class ServerResponse<
81
81
  public statusCode: number = 200;
82
82
  public headers: {[cle: string]: string} = {}
83
83
  public cookie: express.Response["cookie"];
84
+ public clearCookie: express.Response["clearCookie"];
84
85
 
85
86
  // If data was provided by at lead one controller
86
87
  public wasProvided = false;
@@ -90,6 +91,7 @@ export default class ServerResponse<
90
91
  super(request);
91
92
 
92
93
  this.cookie = this.request.res.cookie.bind(this.request.res);
94
+ this.clearCookie = this.request.res.clearCookie.bind(this.request.res);
93
95
 
94
96
  this.router = request.router;
95
97
  this.app = this.router.app;
@@ -120,8 +120,12 @@ export default class ServerPage<TRouter extends Router = Router> extends PageRes
120
120
 
121
121
  private buildMetas() {
122
122
 
123
+ const shouldIndex = this.context.response.statusCode < 300;
124
+
123
125
  const metas = {
124
126
 
127
+ robots: shouldIndex ? 'index' : 'noindex',
128
+
125
129
  'og:type': 'website',
126
130
  'og:locale': this.app.identity.locale,
127
131
  'og:site_name': this.app.identity.web.title,