5htp-core 0.6.1 → 0.6.2-1
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/client/components/File/index.tsx +4 -19
- package/client/components/Input.tsx +2 -2
- package/client/services/router/components/router.tsx +2 -2
- package/client/services/router/request/api.ts +27 -8
- package/client/services/router/request/multipart.ts +0 -5
- package/common/errors/index.tsx +1 -0
- package/common/router/index.ts +2 -1
- package/common/router/request/api.ts +3 -3
- package/package.json +4 -4
- package/server/app/container/console/index.ts +33 -23
- package/server/app/index.ts +2 -0
- package/server/app/service/index.ts +1 -1
- package/server/services/email/index.ts +1 -1
- package/server/services/router/http/multipart.ts +5 -22
- package/server/services/router/index.ts +38 -24
- package/server/services/router/request/index.ts +1 -2
- package/{common → server/services/router/request}/validation/validators.ts +3 -4
- package/server/services/router/request/validation/zod.ts +97 -0
- package/server/services/schema/index.ts +2 -4
- package/server/services/schema/request.ts +1 -1
- package/types/icons.d.ts +1 -1
- package/client/components/File/FileToUpload.ts +0 -34
- package/client/components/Form.ts +0 -275
- /package/{common → server/services/router/request}/validation/index.ts +0 -0
- /package/{common → server/services/router/request}/validation/schema.ts +0 -0
- /package/{common → server/services/router/request}/validation/validator.ts +0 -0
|
@@ -12,16 +12,12 @@ import Button, { Props as BtnProps } from '@client/components/Button';
|
|
|
12
12
|
import { InputWrapper } from '../utils';
|
|
13
13
|
import useContext from '@/client/context';
|
|
14
14
|
|
|
15
|
-
// specific
|
|
16
|
-
import FileToUpload from './FileToUpload';
|
|
17
|
-
|
|
18
15
|
// Ressources
|
|
19
16
|
import './index.less';
|
|
20
17
|
|
|
21
18
|
/*----------------------------------
|
|
22
19
|
- OUTILS
|
|
23
20
|
----------------------------------*/
|
|
24
|
-
export { default as FileToUpload } from './FileToUpload';
|
|
25
21
|
|
|
26
22
|
export const createImagePreview = (file: Blob) => new Promise((resolve, reject) => {
|
|
27
23
|
|
|
@@ -44,15 +40,6 @@ export const createImagePreview = (file: Blob) => new Promise((resolve, reject)
|
|
|
44
40
|
reader.readAsDataURL(file);
|
|
45
41
|
});
|
|
46
42
|
|
|
47
|
-
// Instanciate FileToUpload from browser side File
|
|
48
|
-
const normalizeFile = (file: File) => new FileToUpload({
|
|
49
|
-
name: file.name,
|
|
50
|
-
type: file.type,
|
|
51
|
-
size: file.size,
|
|
52
|
-
data: file,
|
|
53
|
-
//original: file
|
|
54
|
-
})
|
|
55
|
-
|
|
56
43
|
/*----------------------------------
|
|
57
44
|
- TYPES
|
|
58
45
|
----------------------------------*/
|
|
@@ -61,7 +48,7 @@ export type Props = {
|
|
|
61
48
|
|
|
62
49
|
// Input
|
|
63
50
|
title: string,
|
|
64
|
-
value?: string |
|
|
51
|
+
value?: string | File, // string = already registered
|
|
65
52
|
|
|
66
53
|
// Display
|
|
67
54
|
emptyText?: ComponentChild,
|
|
@@ -70,7 +57,7 @@ export type Props = {
|
|
|
70
57
|
button?: boolean | BtnProps,
|
|
71
58
|
|
|
72
59
|
// Actions
|
|
73
|
-
onChange: (file:
|
|
60
|
+
onChange: (file: File | undefined) => void
|
|
74
61
|
remove?: () => Promise<void>,
|
|
75
62
|
}
|
|
76
63
|
|
|
@@ -115,9 +102,7 @@ export default (props: Props) => {
|
|
|
115
102
|
const selectedfile = fileSelectEvent.target.files[0] as File;
|
|
116
103
|
if (selectedfile) {
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
onChange(fileToUpload);
|
|
105
|
+
onChange(selectedfile);
|
|
121
106
|
}
|
|
122
107
|
}
|
|
123
108
|
|
|
@@ -125,7 +110,7 @@ export default (props: Props) => {
|
|
|
125
110
|
|
|
126
111
|
// Image = decode & display preview
|
|
127
112
|
if (file !== undefined && typeof file === 'object' && file.type.startsWith('image/'))
|
|
128
|
-
createImagePreview(file
|
|
113
|
+
createImagePreview(file).then(setPreviewUrl);
|
|
129
114
|
else
|
|
130
115
|
setPreviewUrl(undefined);
|
|
131
116
|
|
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
|
|
14
14
|
// Core libs
|
|
15
15
|
import { InputBaseProps, useMantineInput } from './utils';
|
|
16
|
-
import { default as Validator } from '../../
|
|
17
|
-
import type { SchemaValidators } from '@
|
|
16
|
+
import { default as Validator } from '../../server/services/router/request/validation/validator';
|
|
17
|
+
import type { SchemaValidators } from '@server/services/router/request/validation/validators';
|
|
18
18
|
|
|
19
19
|
/*----------------------------------
|
|
20
20
|
- TYPES
|
|
@@ -151,8 +151,8 @@ export default ({ service: clientRouter, loaderComponent }: TProps) => {
|
|
|
151
151
|
// But when we call setLayout, the style of the previous layout are still oaded and applied
|
|
152
152
|
// Find a way to unload the previous layout / page resources before to load the new one
|
|
153
153
|
console.log(LogPrefix, `Changing layout. Before:`, curLayout, 'New layout:', newLayout);
|
|
154
|
-
window.location.replace( request ? request.url : window.location.href );
|
|
155
|
-
return page; // Don't spread since it's an instance
|
|
154
|
+
/*window.location.replace( request ? request.url : window.location.href );
|
|
155
|
+
return page; // Don't spread since it's an instance*/
|
|
156
156
|
|
|
157
157
|
context.app.setLayout(newLayout);
|
|
158
158
|
}
|
|
@@ -15,7 +15,6 @@ import ApiClientService, {
|
|
|
15
15
|
// Specific
|
|
16
16
|
import type { default as Router, Request } from '..';
|
|
17
17
|
import { toMultipart } from './multipart';
|
|
18
|
-
import FileToUpload from '@client/components/File/FileToUpload';
|
|
19
18
|
|
|
20
19
|
/*----------------------------------
|
|
21
20
|
- TYPES
|
|
@@ -118,16 +117,36 @@ export default class ApiClient implements ApiClientService {
|
|
|
118
117
|
// For async calls: api.post(...).then((data) => ...)
|
|
119
118
|
then: (callback: (data: any) => void) => this.fetchAsync<TData>(...args)
|
|
120
119
|
.then(callback)
|
|
121
|
-
.catch( e =>
|
|
120
|
+
.catch( e => {
|
|
121
|
+
this.app.handleError(e);
|
|
122
|
+
|
|
123
|
+
// Don't run what is next
|
|
124
|
+
return {
|
|
125
|
+
then: () => {},
|
|
126
|
+
catch: () => {},
|
|
127
|
+
finally: (callback: () => void) => {
|
|
128
|
+
callback();
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
}),
|
|
122
132
|
|
|
123
133
|
// Default error behavior only if not handled before by the app
|
|
124
|
-
catch: (callback: (data: any) => void) => this.fetchAsync<TData>(...args)
|
|
134
|
+
catch: (callback: (data: any) => false | void) => this.fetchAsync<TData>(...args)
|
|
125
135
|
.catch((e) => {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
this.app.handleError(e)
|
|
136
|
+
|
|
137
|
+
const shouldThrow = callback(e);
|
|
138
|
+
if (shouldThrow)
|
|
139
|
+
this.app.handleError(e);
|
|
140
|
+
|
|
141
|
+
// Don't run what is next
|
|
142
|
+
return {
|
|
143
|
+
then: () => {},
|
|
144
|
+
catch: () => {},
|
|
145
|
+
finally: (callback: () => void) => {
|
|
146
|
+
callback();
|
|
147
|
+
},
|
|
130
148
|
}
|
|
149
|
+
|
|
131
150
|
}),
|
|
132
151
|
|
|
133
152
|
finally: (callback: () => void) => this.fetchAsync<TData>(...args)
|
|
@@ -206,7 +225,7 @@ export default class ApiClient implements ApiClientService {
|
|
|
206
225
|
|
|
207
226
|
// If file included in data, need to use multipart
|
|
208
227
|
// TODO: deep check
|
|
209
|
-
const hasFile = Object.values(data).some((value) => value instanceof
|
|
228
|
+
const hasFile = Object.values(data).some((value) => value instanceof File);
|
|
210
229
|
if (hasFile) {
|
|
211
230
|
// GET request = Can't send files
|
|
212
231
|
if (method === "GET")
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
// Core
|
|
6
6
|
import { TPostData } from '@common/router/request/api';
|
|
7
|
-
import { FileToUpload } from '@client/components/File';
|
|
8
7
|
|
|
9
8
|
/*----------------------------------
|
|
10
9
|
- TYPES
|
|
@@ -75,10 +74,6 @@ function convertRecursively(
|
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
76
|
|
|
78
|
-
// Exract the file object from value
|
|
79
|
-
if (typeof value === 'object' && value instanceof FileToUpload)
|
|
80
|
-
value = value.data;
|
|
81
|
-
|
|
82
77
|
if (isArray(value) || isJsonObject(value)) {
|
|
83
78
|
|
|
84
79
|
convertRecursively(value, options, formData, propName);
|
package/common/errors/index.tsx
CHANGED
package/common/router/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
----------------------------------*/
|
|
4
4
|
|
|
5
5
|
// Npm
|
|
6
|
-
import zod from 'zod';
|
|
6
|
+
import type zod from 'zod';
|
|
7
7
|
|
|
8
8
|
// types
|
|
9
9
|
import type {
|
|
@@ -102,6 +102,7 @@ export type TRouteOptions = {
|
|
|
102
102
|
refresh?: string,
|
|
103
103
|
urls: string[]
|
|
104
104
|
},
|
|
105
|
+
whenStatic?: boolean, // If true, the route is only executed even if the page is cached
|
|
105
106
|
canonicalParams?: string[], // For SEO + unique ID for static cache
|
|
106
107
|
layout?: false | string, // The nale of the layout
|
|
107
108
|
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import type { HttpMethod } from '@server/services/router';
|
|
6
6
|
|
|
7
|
-
import type { FileToUpload } from '@client/components/File';
|
|
8
|
-
|
|
9
7
|
/*----------------------------------
|
|
10
8
|
- TYPES
|
|
11
9
|
----------------------------------*/
|
|
@@ -18,6 +16,8 @@ export type TFetcher<TData extends any = unknown> = {
|
|
|
18
16
|
|
|
19
17
|
// For async calls: api.post(...).then((data) => ...)
|
|
20
18
|
then: (callback: (data: TData) => void) => Promise<TData>,
|
|
19
|
+
catch: (callback: (data: any) => false | void) => Promise<TData>,
|
|
20
|
+
finally: (callback: () => void) => Promise<TData>,
|
|
21
21
|
run: () => Promise<TData>,
|
|
22
22
|
|
|
23
23
|
method: HttpMethod,
|
|
@@ -42,7 +42,7 @@ export type TApiFetchOptions = {
|
|
|
42
42
|
|
|
43
43
|
export type TPostData = TPostDataWithFile
|
|
44
44
|
|
|
45
|
-
export type TPostDataWithFile = { [key: string]: PrimitiveValue
|
|
45
|
+
export type TPostDataWithFile = { [key: string]: PrimitiveValue }
|
|
46
46
|
|
|
47
47
|
export type TPostDataWithoutFile = { [key: string]: PrimitiveValue }
|
|
48
48
|
|
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.6.1",
|
|
4
|
+
"version": "0.6.2-1",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -67,8 +67,8 @@
|
|
|
67
67
|
"object-sizeof": "^1.6.3",
|
|
68
68
|
"path-to-regexp": "^6.2.0",
|
|
69
69
|
"picomatch": "^2.3.1",
|
|
70
|
-
"preact": "^10.
|
|
71
|
-
"preact-render-to-string": "^6.
|
|
70
|
+
"preact": "^10.27.1",
|
|
71
|
+
"preact-render-to-string": "^6.6.1",
|
|
72
72
|
"prettier": "^3.3.3",
|
|
73
73
|
"react-scrollbars-custom": "^4.0.27",
|
|
74
74
|
"react-slider": "^2.0.1",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"yargs-parser": "^21.1.1",
|
|
89
89
|
"youch": "^3.3.3",
|
|
90
90
|
"youch-terminal": "^2.2.3",
|
|
91
|
-
"zod": "^
|
|
91
|
+
"zod": "^4.1.5"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@types/cookie": "^0.4.1",
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// Node
|
|
6
6
|
import { serialize } from 'v8';
|
|
7
7
|
import { formatWithOptions } from 'util';
|
|
8
|
+
import md5 from 'md5';
|
|
9
|
+
import dayjs from 'dayjs';
|
|
8
10
|
import Youch from 'youch';
|
|
9
11
|
import forTerminal from 'youch-terminal';
|
|
10
12
|
|
|
@@ -147,6 +149,12 @@ export default class Console {
|
|
|
147
149
|
public logger!: Logger<ILogObj>;
|
|
148
150
|
// Buffers
|
|
149
151
|
public logs: TJsonLog[] = [];
|
|
152
|
+
private reported: {
|
|
153
|
+
[hash: string]: {
|
|
154
|
+
times: number,
|
|
155
|
+
last: Date,
|
|
156
|
+
}
|
|
157
|
+
} = {};
|
|
150
158
|
|
|
151
159
|
/*----------------------------------
|
|
152
160
|
- LIFECYCLE
|
|
@@ -282,35 +290,12 @@ export default class Console {
|
|
|
282
290
|
// We don't prevent duplicates because we want to receive all variants of the same error
|
|
283
291
|
public async createBugReport( error: TCatchedError, request?: ServerRequest ) {
|
|
284
292
|
|
|
285
|
-
/*const youchRes = new Youch(error, {});
|
|
286
|
-
const jsonResponse = await youchRes.toJSON()
|
|
287
|
-
console.log( forTerminal(jsonResponse, {
|
|
288
|
-
// Defaults to false
|
|
289
|
-
displayShortPath: false,
|
|
290
|
-
|
|
291
|
-
// Defaults to single whitspace
|
|
292
|
-
prefix: ' ',
|
|
293
|
-
|
|
294
|
-
// Defaults to false
|
|
295
|
-
hideErrorTitle: false,
|
|
296
|
-
|
|
297
|
-
// Defaults to false
|
|
298
|
-
hideMessage: false,
|
|
299
|
-
|
|
300
|
-
// Defaults to false
|
|
301
|
-
displayMainFrameOnly: false,
|
|
302
|
-
|
|
303
|
-
// Defaults to 3
|
|
304
|
-
framesMaxLimit: 3,
|
|
305
|
-
}) );*/
|
|
306
|
-
|
|
307
293
|
const application = this.container.application;
|
|
308
294
|
if (application === undefined)
|
|
309
295
|
return console.error(LogPrefix, "Can't send bug report because the application is not instanciated");
|
|
310
296
|
|
|
311
297
|
// Get context
|
|
312
298
|
const now = new Date();
|
|
313
|
-
const hash = uuid();
|
|
314
299
|
const { channelType, channelId } = this.getChannel();
|
|
315
300
|
|
|
316
301
|
// On envoi l'email avant l'insertion dans bla bdd
|
|
@@ -353,10 +338,35 @@ export default class Console {
|
|
|
353
338
|
: undefined
|
|
354
339
|
}
|
|
355
340
|
|
|
341
|
+
// Genertae unique error hash
|
|
342
|
+
const hash = md5( stacktraces.join('\n') );
|
|
343
|
+
|
|
344
|
+
// Don't send the same error twice in a row (avoid email spamming)
|
|
345
|
+
const lastReport = this.reported[hash];
|
|
346
|
+
let isDuplicate = false;
|
|
347
|
+
if (lastReport === undefined) {
|
|
348
|
+
|
|
349
|
+
this.reported[hash] = {
|
|
350
|
+
times: 0,
|
|
351
|
+
last: new Date()
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// If error older than 1 day
|
|
355
|
+
} else if (dayjs(now).diff( dayjs(lastReport.last), 'day' ) > 1) {
|
|
356
|
+
|
|
357
|
+
lastReport.times++;
|
|
358
|
+
lastReport.last = now;
|
|
359
|
+
|
|
360
|
+
} else {
|
|
361
|
+
|
|
362
|
+
isDuplicate = true;
|
|
363
|
+
}
|
|
364
|
+
|
|
356
365
|
const bugReport: ServerBug = {
|
|
357
366
|
|
|
358
367
|
// Context
|
|
359
368
|
hash: hash,
|
|
369
|
+
isDuplicate,
|
|
360
370
|
date: now,
|
|
361
371
|
channelType,
|
|
362
372
|
channelId,
|
package/server/app/index.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type { TServiceMetas } from './container';
|
|
|
9
9
|
import type { TControllerDefinition, TRoute } from '../../services/router';
|
|
10
10
|
import { Anomaly } from "@common/errors";
|
|
11
11
|
|
|
12
|
-
export {
|
|
12
|
+
export { schema } from '../../services/router/request/validation/zod';
|
|
13
13
|
|
|
14
14
|
/*----------------------------------
|
|
15
15
|
- TYPES: OPTIONS
|
|
@@ -171,7 +171,7 @@ export default abstract class Email<TConfig extends Config>
|
|
|
171
171
|
|
|
172
172
|
});
|
|
173
173
|
|
|
174
|
-
console.info(LogPrefix, `Sending ${emailsToSend.length} emails
|
|
174
|
+
console.info(LogPrefix, `Sending ${emailsToSend.length} emails:`, emailsToSend[0].subject);
|
|
175
175
|
|
|
176
176
|
// Pas d'envoi d'email quand local
|
|
177
177
|
if (this.app.env.name === 'local' && this.config.simulateWhenLocal === true) {
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import mime from 'mime-types';
|
|
7
7
|
|
|
8
8
|
// Core
|
|
9
|
-
import FileToUpload from '@client/components/File/FileToUpload';
|
|
10
9
|
import { InputError } from '@common/errors';
|
|
11
10
|
|
|
12
11
|
/*----------------------------------
|
|
@@ -82,7 +81,11 @@ export const traiterMultipart = (...canaux: any[]) => {
|
|
|
82
81
|
&&
|
|
83
82
|
donnee.data instanceof Buffer
|
|
84
83
|
){
|
|
85
|
-
donnee =
|
|
84
|
+
donnee = new File(donnee.data, donnee.name, {
|
|
85
|
+
type: donnee.mimetype,
|
|
86
|
+
lastModified: Date.now(),
|
|
87
|
+
//size: donnee.size,
|
|
88
|
+
});
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
brancheA[ cle ] = donnee;
|
|
@@ -91,24 +94,4 @@ export const traiterMultipart = (...canaux: any[]) => {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
return sortie;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const normalizeFile = (file: UploadedFile) => {
|
|
97
|
-
|
|
98
|
-
const ext = mime.extension(file.mimetype);
|
|
99
|
-
|
|
100
|
-
if (ext === false)
|
|
101
|
-
throw new InputError(`We couldn't determine the type of the CV file you sent. Please encure it's not corrupted and try again.`);
|
|
102
|
-
|
|
103
|
-
return new FileToUpload({
|
|
104
|
-
|
|
105
|
-
name: file.name,
|
|
106
|
-
type: file.mimetype,
|
|
107
|
-
size: file.size,
|
|
108
|
-
|
|
109
|
-
data: file.data,
|
|
110
|
-
|
|
111
|
-
md5: file.md5,
|
|
112
|
-
ext: ext
|
|
113
|
-
})
|
|
114
97
|
}
|
|
@@ -221,38 +221,37 @@ export default class ServerRouter
|
|
|
221
221
|
- ACTIONS
|
|
222
222
|
----------------------------------*/
|
|
223
223
|
|
|
224
|
-
|
|
225
|
-
|
|
224
|
+
public async renderStatic(
|
|
225
|
+
url: string,
|
|
226
226
|
options: TRouteOptions["static"],
|
|
227
227
|
rendered?: any
|
|
228
228
|
) {
|
|
229
229
|
|
|
230
230
|
// Wildcard: tell that the newly rendered pages should be cached
|
|
231
|
-
if (
|
|
231
|
+
if (url === '*' || !url)
|
|
232
232
|
return;
|
|
233
233
|
|
|
234
234
|
if (!rendered) {
|
|
235
235
|
|
|
236
|
-
const fullUrl = this.url(
|
|
237
|
-
console.log('[router] renderStatic', fullUrl);
|
|
238
|
-
|
|
236
|
+
const fullUrl = this.url(url, {}, true);
|
|
239
237
|
const response = await got( fullUrl, {
|
|
240
238
|
method: 'GET',
|
|
241
239
|
headers: {
|
|
242
|
-
'Accept': 'text/html'
|
|
240
|
+
'Accept': 'text/html',
|
|
241
|
+
'bypasscache': '1'
|
|
243
242
|
},
|
|
244
243
|
throwHttpErrors: false,
|
|
245
244
|
});
|
|
246
245
|
|
|
247
246
|
if (response.statusCode !== 200) {
|
|
248
|
-
console.error(
|
|
247
|
+
console.error("[router] renderStatic: page returned code", response.statusCode, fullUrl);
|
|
249
248
|
return;
|
|
250
249
|
}
|
|
251
250
|
|
|
252
251
|
rendered = response.body;
|
|
253
252
|
}
|
|
254
253
|
|
|
255
|
-
this.cache[
|
|
254
|
+
this.cache[url] = {
|
|
256
255
|
rendered: rendered,
|
|
257
256
|
options: options,
|
|
258
257
|
expire: typeof options === 'object'
|
|
@@ -266,11 +265,11 @@ export default class ServerRouter
|
|
|
266
265
|
|
|
267
266
|
console.log('[router] refreshStaticPages');
|
|
268
267
|
|
|
269
|
-
for (const
|
|
270
|
-
const page = this.cache[
|
|
271
|
-
if (page.
|
|
268
|
+
for (const pageUrl in this.cache) {
|
|
269
|
+
const page = this.cache[pageUrl];
|
|
270
|
+
if (page.expire && page.expire < Date.now()) {
|
|
272
271
|
|
|
273
|
-
this.renderStatic(
|
|
272
|
+
this.renderStatic(pageUrl, page.options);
|
|
274
273
|
|
|
275
274
|
}
|
|
276
275
|
}
|
|
@@ -499,15 +498,12 @@ export default class ServerRouter
|
|
|
499
498
|
"no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
500
499
|
);
|
|
501
500
|
|
|
502
|
-
// Static pages
|
|
503
|
-
if (this.cache[req.path]) {
|
|
504
|
-
console.log('[router] Get static page from cache', req.path);
|
|
505
|
-
res.send( this.cache[req.path].rendered );
|
|
506
|
-
return;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
501
|
// Create request
|
|
510
502
|
let requestId = uuid();
|
|
503
|
+
const cachedPage = req.headers['bypasscache']
|
|
504
|
+
? undefined
|
|
505
|
+
: this.cache[req.path];
|
|
506
|
+
|
|
511
507
|
const request = new ServerRequest(
|
|
512
508
|
requestId,
|
|
513
509
|
|
|
@@ -533,13 +529,25 @@ export default class ServerRouter
|
|
|
533
529
|
return await this.resolveApiBatch(request.data.fetchers, request);
|
|
534
530
|
|
|
535
531
|
} else {
|
|
536
|
-
response = await this.resolve(
|
|
532
|
+
response = await this.resolve(
|
|
533
|
+
request,
|
|
534
|
+
// If cached page, we only run routes with priority >= 10
|
|
535
|
+
cachedPage ? true : false
|
|
536
|
+
);
|
|
537
537
|
}
|
|
538
538
|
} catch (e) {
|
|
539
539
|
response = await this.handleError(e, request);
|
|
540
540
|
}
|
|
541
541
|
|
|
542
542
|
if (!res.headersSent) {
|
|
543
|
+
|
|
544
|
+
// Static pages
|
|
545
|
+
if (cachedPage) {
|
|
546
|
+
console.log('[router] Get static page from cache', req.path);
|
|
547
|
+
res.send( cachedPage.rendered );
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
543
551
|
// Status
|
|
544
552
|
res.status(response.statusCode);
|
|
545
553
|
// Headers
|
|
@@ -572,7 +580,10 @@ export default class ServerRouter
|
|
|
572
580
|
return contextServices;
|
|
573
581
|
}
|
|
574
582
|
|
|
575
|
-
public resolve = (
|
|
583
|
+
public resolve = (
|
|
584
|
+
request: ServerRequest<this>,
|
|
585
|
+
isStatic?: boolean
|
|
586
|
+
) => new Promise<ServerResponse<this>>((resolve, reject) => {
|
|
576
587
|
|
|
577
588
|
// Create request context so we can access request context across all the request-triggered libs
|
|
578
589
|
context.run({
|
|
@@ -613,6 +624,9 @@ export default class ServerRouter
|
|
|
613
624
|
// Classic routes
|
|
614
625
|
for (route of this.routes) {
|
|
615
626
|
|
|
627
|
+
if (isStatic && !route.options.whenStatic)
|
|
628
|
+
continue;
|
|
629
|
+
|
|
616
630
|
// Match Method
|
|
617
631
|
if (request.method !== route.method && route.method !== '*')
|
|
618
632
|
continue;
|
|
@@ -710,11 +724,11 @@ export default class ServerRouter
|
|
|
710
724
|
request.res.json(responseData);
|
|
711
725
|
}
|
|
712
726
|
|
|
713
|
-
private async handleError( e: Error |CoreError | ZodError, request: ServerRequest<ServerRouter> ) {
|
|
727
|
+
private async handleError( e: Error | CoreError | ZodError, request: ServerRequest<ServerRouter> ) {
|
|
714
728
|
|
|
715
729
|
if (e instanceof ZodError)
|
|
716
730
|
e = new InputError(
|
|
717
|
-
e.
|
|
731
|
+
e.issues.map((e) => e.path.join('.') + ': ' + e.message).join(', ')
|
|
718
732
|
);
|
|
719
733
|
|
|
720
734
|
const code = 'http' in e ? e.http : 500;
|
|
@@ -10,7 +10,6 @@ import Bowser from "bowser";
|
|
|
10
10
|
|
|
11
11
|
// Core
|
|
12
12
|
import BaseRequest from '@common/router/request';
|
|
13
|
-
import type FileToUpload from '@client/components/File/FileToUpload';
|
|
14
13
|
|
|
15
14
|
// Specific
|
|
16
15
|
import type {
|
|
@@ -40,7 +39,7 @@ const localeFilter = (input: any) => {
|
|
|
40
39
|
return lang.toUpperCase();
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
export type UploadedFile =
|
|
42
|
+
export type UploadedFile = File
|
|
44
43
|
|
|
45
44
|
/*----------------------------------
|
|
46
45
|
- CONTEXTE
|
|
@@ -13,7 +13,6 @@ import normalizeUrl, { Options as NormalizeUrlOptions } from 'normalize-url';
|
|
|
13
13
|
|
|
14
14
|
// Core
|
|
15
15
|
import { InputError } from '@common/errors';
|
|
16
|
-
import FileToUpload from '@client/components/File/FileToUpload';
|
|
17
16
|
|
|
18
17
|
// Speciific
|
|
19
18
|
import Schema, { TSchemaFields } from './schema'
|
|
@@ -23,7 +22,7 @@ import Validator, { TValidatorOptions, EXCLUDE_VALUE, TValidatorDefinition } fro
|
|
|
23
22
|
- TYPES
|
|
24
23
|
----------------------------------*/
|
|
25
24
|
|
|
26
|
-
export type TFileValidator = TValidatorOptions<
|
|
25
|
+
export type TFileValidator = TValidatorOptions<File> & {
|
|
27
26
|
type?: string[], // Raccourci, ou liste de mimetype
|
|
28
27
|
taille?: number,
|
|
29
28
|
disk?: string, // Disk to upload files to
|
|
@@ -447,14 +446,14 @@ export class SchemaValidators {
|
|
|
447
446
|
----------------------------------*/
|
|
448
447
|
public file = ({ type, taille, ...opts }: TFileValidator & {
|
|
449
448
|
|
|
450
|
-
} = {}) => new Validator<
|
|
449
|
+
} = {}) => new Validator<File>('file', (val, options, path) => {
|
|
451
450
|
|
|
452
451
|
// Chaine = url ancien fichier = exclusion de la valeur pour conserver l'ancien fichier
|
|
453
452
|
// NOTE: Si la valeur est présente mais undefined, alors on supprimera le fichier
|
|
454
453
|
if (typeof val === 'string')
|
|
455
454
|
return EXCLUDE_VALUE;
|
|
456
455
|
|
|
457
|
-
if (!(val instanceof
|
|
456
|
+
if (!(val instanceof File))
|
|
458
457
|
throw new InputError(`Must be a File (${typeof val} received)`);
|
|
459
458
|
|
|
460
459
|
// MIME
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { InputError } from '@common/errors';
|
|
2
|
+
import zod from 'zod';
|
|
3
|
+
|
|
4
|
+
export type TRichTextValidatorOptions = {
|
|
5
|
+
attachements?: boolean
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Recursive function to validate each node
|
|
9
|
+
function validateLexicalNode(node: any, opts: TRichTextValidatorOptions ) {
|
|
10
|
+
|
|
11
|
+
// Each node should be an object with a `type` property
|
|
12
|
+
if (typeof node !== 'object' || !node.type || typeof node.type !== 'string')
|
|
13
|
+
throw new InputError("Invalid rich text value (3).");
|
|
14
|
+
|
|
15
|
+
// Validate text nodes
|
|
16
|
+
if (node.type === 'text') {
|
|
17
|
+
|
|
18
|
+
if (typeof node.text !== 'string')
|
|
19
|
+
throw new InputError("Invalid rich text value (4).");
|
|
20
|
+
|
|
21
|
+
// Validate paragraph, heading, or other structural nodes that may contain children
|
|
22
|
+
} else if (['paragraph', 'heading', 'list', 'listitem'].includes(node.type)) {
|
|
23
|
+
|
|
24
|
+
if (!Array.isArray(node.children) || !node.children.every(children => validateLexicalNode(children, opts))) {
|
|
25
|
+
throw new InputError("Invalid rich text value (5).");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Files upload
|
|
29
|
+
} else if (node.type === 'image') {
|
|
30
|
+
|
|
31
|
+
// Check if allowed
|
|
32
|
+
/*if (opts.attachements === undefined)
|
|
33
|
+
throw new InputError("Image attachments not allowed in this rich text field.");*/
|
|
34
|
+
|
|
35
|
+
// TODO: check mime
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
// Upload file
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const schema = {
|
|
47
|
+
...zod,
|
|
48
|
+
|
|
49
|
+
file: (builder: (file: zod.ZodType<File>) => any) => schema.custom(val => {
|
|
50
|
+
|
|
51
|
+
// Chaine = url ancien fichier = exclusion de la valeur pour conserver l'ancien fichier
|
|
52
|
+
// NOTE: Si la valeur est présente mais undefined, alors on supprimera le fichier
|
|
53
|
+
if (typeof val === 'string')
|
|
54
|
+
return true;
|
|
55
|
+
|
|
56
|
+
// Default file validation
|
|
57
|
+
const fileInstance = zod.file();
|
|
58
|
+
|
|
59
|
+
return builder(fileInstance).parse(val);
|
|
60
|
+
}),
|
|
61
|
+
|
|
62
|
+
richText: (opts: TRichTextValidatorOptions = {}) => schema.custom(val => {
|
|
63
|
+
|
|
64
|
+
if (typeof val !== 'string') {
|
|
65
|
+
console.error("Invalid rich text format.", val);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// We get a stringified json as input since the editor workds with JSON string
|
|
70
|
+
try {
|
|
71
|
+
val = JSON.parse(val);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("Failed to parse rich text json:", error, val);
|
|
74
|
+
return false;//throw new InputError("Invalid rich text format.");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Check that the root exists and has a valid type
|
|
78
|
+
if (!val || typeof val !== 'object' || typeof val.root !== 'object' || val.root.type !== 'root') {
|
|
79
|
+
console.error("Invalid rich text value (1).", val);
|
|
80
|
+
return false;//throw new InputError("Invalid rich text value (1).");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if root has children array
|
|
84
|
+
if (!Array.isArray(val.root.children)) {
|
|
85
|
+
console.error("Invalid rich text value (2).", val);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate each child node in root
|
|
90
|
+
for (const child of val.root.children) {
|
|
91
|
+
if (!validateLexicalNode(child, opts))
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
})
|
|
97
|
+
}
|
|
@@ -6,10 +6,8 @@
|
|
|
6
6
|
import type { Application } from '@server/app';
|
|
7
7
|
|
|
8
8
|
// Specific
|
|
9
|
-
import { SchemaValidators, TFileValidator } from '@
|
|
10
|
-
import Validator, { TValidatorOptions } from '@
|
|
11
|
-
|
|
12
|
-
import type FileToUpload from '@client/components/File/FileToUpload';
|
|
9
|
+
import { SchemaValidators, TFileValidator } from '@server/services/router/request/validation/validators';
|
|
10
|
+
import Validator, { TValidatorOptions } from '@server/services/router/request/validation/validator';
|
|
13
11
|
|
|
14
12
|
/*----------------------------------
|
|
15
13
|
- TYPES
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
default as Router, RequestService, Request as ServerRequest
|
|
8
8
|
} from '@server/services/router';
|
|
9
9
|
|
|
10
|
-
import Schema, { TSchemaFields, TValidatedData } from '@
|
|
10
|
+
import Schema, { TSchemaFields, TValidatedData } from '@server/services/router/request/validation/schema';
|
|
11
11
|
|
|
12
12
|
// Specific
|
|
13
13
|
import ServerSchemaValidator from '.';
|
package/types/icons.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type TIcones = "solid/spinner-third"|"long-arrow-right"|"times-circle"|"brands/whatsapp"|"times"|"search"|"user"|"rocket"|"globe"|"bullhorn"|"briefcase"|"chart-line"|"handshake"|"ellipsis-h"|"brands/google"|"brands/reddit-alien"|"brands/linkedin-in"|"brands/github"|"robot"|"comments"|"user-friends"|"
|
|
1
|
+
export type TIcones = "solid/spinner-third"|"long-arrow-right"|"times-circle"|"brands/whatsapp"|"times"|"search"|"angle-down"|"user"|"rocket"|"globe"|"bullhorn"|"briefcase"|"chart-line"|"handshake"|"ellipsis-h"|"brands/google"|"brands/reddit-alien"|"brands/linkedin-in"|"brands/github"|"robot"|"comments"|"user-friends"|"mouse-pointer"|"thumbs-up"|"dollar-sign"|"info-circle"|"check-circle"|"exclamation-circle"|"home"|"user-circle"|"newspaper"|"plus-circle"|"brands/linkedin"|"brands/twitter"|"brands/facebook"|"comment-alt"|"heart"|"lock"|"eye"|"credit-card"|"at"|"key"|"chart-bar"|"power-off"|"bars"|"font"|"tag"|"compress"|"bolt"|"puzzle-piece"|"planet-ringed"|"seedling"|"palette"|"car"|"plane"|"university"|"hard-hat"|"graduation-cap"|"cogs"|"film"|"leaf"|"tshirt"|"utensils"|"map-marked-alt"|"dumbbell"|"stethoscope"|"concierge-bell"|"book"|"shield-alt"|"gavel"|"industry"|"square-root-alt"|"pills"|"medal"|"capsules"|"balance-scale"|"praying-hands"|"shopping-cart"|"flask"|"futbol"|"microchip"|"satellite-dish"|"shipping-fast"|"passport"|"tools"|"exclamation"|"solid/download"|"database"|"solid/fire"|"usd-circle"|"lightbulb"|"solid/dollar-sign"|"download"|"code"|"solid/clock"|"paper-plane"|"check"|"long-arrow-left"|"angle-left"|"angle-right"|"trash"|"arrow-left"|"arrow-right"|"meh-rolling-eyes"|"unlink"|"pen"|"link"|"file"|"bold"|"italic"|"underline"|"strikethrough"|"subscript"|"superscript"|"plus"|"empty-set"|"horizontal-rule"|"page-break"|"image"|"table"|"poll"|"columns"|"sticky-note"|"caret-right"|"align-left"|"align-center"|"align-right"|"align-justify"|"indent"|"outdent"|"list-ul"|"check-square"|"h1"|"h2"|"h3"|"h4"|"list-ol"|"paragraph"|"quote-left"
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Normalize file between browser and nodejs side
|
|
2
|
-
export default class FileToUpload {
|
|
3
|
-
|
|
4
|
-
public name: string;
|
|
5
|
-
public size: number;
|
|
6
|
-
public type: string;
|
|
7
|
-
|
|
8
|
-
public data: File;
|
|
9
|
-
|
|
10
|
-
// Retrieved on backend only
|
|
11
|
-
public md5?: string;
|
|
12
|
-
public ext?: string;
|
|
13
|
-
|
|
14
|
-
public constructor(opts: {
|
|
15
|
-
name: string,
|
|
16
|
-
size: number,
|
|
17
|
-
type: string,
|
|
18
|
-
|
|
19
|
-
data: File,
|
|
20
|
-
|
|
21
|
-
md5?: string,
|
|
22
|
-
ext?: string,
|
|
23
|
-
}) {
|
|
24
|
-
|
|
25
|
-
this.name = opts.name;
|
|
26
|
-
this.size = opts.size;
|
|
27
|
-
this.type = opts.type;
|
|
28
|
-
|
|
29
|
-
this.data = opts.data;
|
|
30
|
-
|
|
31
|
-
this.md5 = opts.md5;
|
|
32
|
-
this.ext = opts.ext;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
/*----------------------------------
|
|
2
|
-
- DEPENDANCES
|
|
3
|
-
----------------------------------*/
|
|
4
|
-
|
|
5
|
-
// npm
|
|
6
|
-
import React from 'react';
|
|
7
|
-
|
|
8
|
-
// Core
|
|
9
|
-
import { InputErrorSchema } from '@common/errors';
|
|
10
|
-
import type { Schema } from '@common/validation';
|
|
11
|
-
import type { TValidationResult, TValidateOptions } from '@common/validation/schema';
|
|
12
|
-
import useContext from '@/client/context';
|
|
13
|
-
|
|
14
|
-
// Exports
|
|
15
|
-
export type { TValidationResult, TSchemaData } from '@common/validation/schema';
|
|
16
|
-
|
|
17
|
-
/*----------------------------------
|
|
18
|
-
- TYPES
|
|
19
|
-
----------------------------------*/
|
|
20
|
-
export type TFormOptions<TFormData extends {}> = {
|
|
21
|
-
data?: Partial<TFormData>,
|
|
22
|
-
submit?: (data: TFormData) => Promise<void>,
|
|
23
|
-
autoValidateOnly?: (keyof TFormData)[],
|
|
24
|
-
autoSave?: {
|
|
25
|
-
id: string
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type FieldsAttrs<TFormData extends {}> = {
|
|
30
|
-
[fieldName in keyof TFormData]: {}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type Form<TFormData extends {} = {}> = {
|
|
34
|
-
|
|
35
|
-
// Data
|
|
36
|
-
fields: FieldsAttrs<TFormData>,
|
|
37
|
-
data: TFormData,
|
|
38
|
-
options: TFormOptions<TFormData>,
|
|
39
|
-
backup?: Partial<TFormData>,
|
|
40
|
-
|
|
41
|
-
// Actions
|
|
42
|
-
setBackup: (backup: Partial<TFormData>) => void,
|
|
43
|
-
validate: (data: Partial<TFormData>, validateAll?: boolean) => TValidationResult<{}>,
|
|
44
|
-
set: (data: Partial<TFormData>, merge?: boolean) => void,
|
|
45
|
-
submit: (additionnalData?: Partial<TFormData>) => Promise<any>,
|
|
46
|
-
|
|
47
|
-
} & FormState
|
|
48
|
-
|
|
49
|
-
type FormState<TFormData extends {} = {}> = {
|
|
50
|
-
isLoading: boolean,
|
|
51
|
-
hasChanged: boolean,
|
|
52
|
-
errorsCount: number,
|
|
53
|
-
errors: { [fieldName: string]: string[] },
|
|
54
|
-
backup?: Partial<TFormData>,
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/*----------------------------------
|
|
58
|
-
- HOOK
|
|
59
|
-
----------------------------------*/
|
|
60
|
-
export default function useForm<TFormData extends {}>(
|
|
61
|
-
schema: Schema<TFormData>,
|
|
62
|
-
options: TFormOptions<TFormData> = {}
|
|
63
|
-
): [ Form<TFormData>, FieldsAttrs<TFormData> ] {
|
|
64
|
-
|
|
65
|
-
const context = useContext();
|
|
66
|
-
|
|
67
|
-
/*----------------------------------
|
|
68
|
-
- INIT
|
|
69
|
-
----------------------------------*/
|
|
70
|
-
|
|
71
|
-
const [state, setState] = React.useState<FormState<TFormData>>({
|
|
72
|
-
hasChanged: false,//options.data !== undefined,
|
|
73
|
-
isLoading: false,
|
|
74
|
-
errorsCount: 0,
|
|
75
|
-
errors: {},
|
|
76
|
-
backup: undefined
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const initialData: Partial<TFormData> = options.data || {};
|
|
80
|
-
|
|
81
|
-
// States
|
|
82
|
-
const fields = React.useRef<FieldsAttrs<TFormData> | null>(null);
|
|
83
|
-
const [data, setData] = React.useState< Partial<TFormData> >(initialData);
|
|
84
|
-
|
|
85
|
-
// When typed data changes
|
|
86
|
-
React.useEffect(() => {
|
|
87
|
-
|
|
88
|
-
// Validate
|
|
89
|
-
validate(data, { ignoreMissing: true });
|
|
90
|
-
|
|
91
|
-
// Autosave
|
|
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);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
}, []);
|
|
119
|
-
|
|
120
|
-
/*----------------------------------
|
|
121
|
-
- ACTIONS
|
|
122
|
-
----------------------------------*/
|
|
123
|
-
const validate = (allData: Partial<TFormData> = data, opts: TValidateOptions<TFormData> = {}) => {
|
|
124
|
-
|
|
125
|
-
const validated = schema.validateWithDetails(allData, allData, {}, {
|
|
126
|
-
// Ignore the fields where the vlaue has not been changed
|
|
127
|
-
// if the validation was triggered via onChange
|
|
128
|
-
ignoreMissing: false,
|
|
129
|
-
// The list of fields we should only validate
|
|
130
|
-
only: options.autoValidateOnly,
|
|
131
|
-
// Custom options
|
|
132
|
-
...opts
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Update errors
|
|
136
|
-
if (validated.errorsCount !== state.errorsCount) {
|
|
137
|
-
rebuildFieldsAttrs({
|
|
138
|
-
errorsCount: validated.errorsCount,
|
|
139
|
-
errors: validated.erreurs,
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return validated;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const submit = (additionnalData: Partial<TFormData> = {}) => {
|
|
147
|
-
|
|
148
|
-
const allData = { ...data, ...additionnalData }
|
|
149
|
-
|
|
150
|
-
// Validation
|
|
151
|
-
const validated = validate(allData);
|
|
152
|
-
if (validated.errorsCount !== 0) {
|
|
153
|
-
throw new InputErrorSchema(validated.erreurs);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const afterSubmit = (responseData?: any) => {
|
|
157
|
-
|
|
158
|
-
// Reset autosaved data
|
|
159
|
-
if (options.autoSave)
|
|
160
|
-
localStorage.removeItem('form.' + options.autoSave.id);
|
|
161
|
-
|
|
162
|
-
// Update state
|
|
163
|
-
setState( current => ({
|
|
164
|
-
...current,
|
|
165
|
-
hasChanged: false
|
|
166
|
-
}));
|
|
167
|
-
|
|
168
|
-
return responseData;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Callback
|
|
172
|
-
if (options.submit)
|
|
173
|
-
return options.submit(allData as TFormData).then(afterSubmit);
|
|
174
|
-
else {
|
|
175
|
-
afterSubmit();
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
const rebuildFieldsAttrs = (newState: Partial<FormState> = {}) => {
|
|
181
|
-
// Force rebuilding the fields definition on the next state change
|
|
182
|
-
fields.current = null;
|
|
183
|
-
// Force state change
|
|
184
|
-
setState(old => ({
|
|
185
|
-
...old,
|
|
186
|
-
...newState
|
|
187
|
-
}));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const saveLocally = (data: Partial<TFormData>, id: string) => {
|
|
191
|
-
console.log('[form] Autosave data for form:', id, ':', data);
|
|
192
|
-
localStorage.setItem('form.' + id, JSON.stringify(data));
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Rebuild the fields attrs when the schema changes
|
|
196
|
-
if (fields.current === null || Object.keys(schema).join(',') !== Object.keys(fields.current).join(',')) {
|
|
197
|
-
fields.current = {} as FieldsAttrs<TFormData>
|
|
198
|
-
for (const fieldName in schema.fields) {
|
|
199
|
-
|
|
200
|
-
const validator = schema.getFieldValidator(fieldName);
|
|
201
|
-
|
|
202
|
-
fields.current[fieldName] = {
|
|
203
|
-
|
|
204
|
-
// Value control
|
|
205
|
-
value: data[fieldName],
|
|
206
|
-
onChange: (val) => {
|
|
207
|
-
setData(old => {
|
|
208
|
-
return {
|
|
209
|
-
...old,
|
|
210
|
-
[fieldName]: typeof val === 'function'
|
|
211
|
-
? val(old[fieldName])
|
|
212
|
-
: val
|
|
213
|
-
}
|
|
214
|
-
})
|
|
215
|
-
|
|
216
|
-
setState(current => ({
|
|
217
|
-
...current,
|
|
218
|
-
hasChanged: true
|
|
219
|
-
}));
|
|
220
|
-
},
|
|
221
|
-
|
|
222
|
-
// Submit on press enter
|
|
223
|
-
onKeyDown: e => {
|
|
224
|
-
if (e.key === 'Enter' || (e.keyCode || e.which) === 13) {
|
|
225
|
-
submit({ [fieldName]: e.target.value } as Partial<TFormData>);
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
// Error
|
|
230
|
-
errors: state.errors[fieldName],
|
|
231
|
-
|
|
232
|
-
// Component attributes
|
|
233
|
-
...validator.componentAttributes
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
/*----------------------------------
|
|
239
|
-
- EXPOSE
|
|
240
|
-
----------------------------------*/
|
|
241
|
-
|
|
242
|
-
const form: Form<TFormData> = {
|
|
243
|
-
|
|
244
|
-
fields: fields.current,
|
|
245
|
-
data,
|
|
246
|
-
set: (data, merge = true) => {
|
|
247
|
-
|
|
248
|
-
setState( current => ({
|
|
249
|
-
...current,
|
|
250
|
-
hasChanged: true
|
|
251
|
-
}));
|
|
252
|
-
|
|
253
|
-
setData( merge
|
|
254
|
-
? c => ({ ...c, ...data })
|
|
255
|
-
: data
|
|
256
|
-
);
|
|
257
|
-
},
|
|
258
|
-
|
|
259
|
-
validate,
|
|
260
|
-
submit,
|
|
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
|
-
|
|
271
|
-
...state
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return [form, fields.current]
|
|
275
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|