5htp-core 0.6.0 → 0.6.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.
Files changed (48) hide show
  1. package/client/app/component.tsx +1 -0
  2. package/client/assets/css/colors.less +46 -25
  3. package/client/assets/css/components/button.less +14 -5
  4. package/client/assets/css/components/card.less +5 -10
  5. package/client/assets/css/components/mantine.less +6 -5
  6. package/client/assets/css/components/table.less +1 -1
  7. package/client/assets/css/text/icons.less +1 -1
  8. package/client/assets/css/text/text.less +4 -0
  9. package/client/assets/css/utils/borders.less +1 -1
  10. package/client/assets/css/utils/layouts.less +8 -5
  11. package/client/components/Button.tsx +20 -17
  12. package/client/components/Checkbox.tsx +6 -1
  13. package/client/components/ConnectedInput.tsx +34 -0
  14. package/client/components/DropDown.tsx +21 -4
  15. package/client/components/Input.tsx +2 -2
  16. package/client/components/Rte/Editor.tsx +23 -9
  17. package/client/components/Rte/ToolbarPlugin/ElementFormat.tsx +1 -1
  18. package/client/components/Rte/ToolbarPlugin/index.tsx +272 -183
  19. package/client/components/Rte/currentEditor.ts +31 -2
  20. package/client/components/Rte/index.tsx +3 -0
  21. package/client/components/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +4 -1
  22. package/client/components/Select.tsx +29 -16
  23. package/client/components/Table/index.tsx +27 -11
  24. package/client/components/containers/Popover/index.tsx +21 -4
  25. package/client/components/index.ts +4 -2
  26. package/client/services/router/index.tsx +7 -5
  27. package/common/errors/index.tsx +27 -3
  28. package/common/router/index.ts +4 -1
  29. package/common/utils/rte.ts +183 -0
  30. package/package.json +3 -2
  31. package/server/app/container/console/index.ts +62 -42
  32. package/server/app/container/index.ts +4 -0
  33. package/server/app/service/index.ts +4 -2
  34. package/server/services/auth/index.ts +28 -14
  35. package/server/services/auth/router/index.ts +1 -1
  36. package/server/services/auth/router/request.ts +4 -4
  37. package/server/services/email/index.ts +8 -51
  38. package/server/services/prisma/Facet.ts +118 -0
  39. package/server/services/prisma/index.ts +24 -0
  40. package/server/services/router/http/index.ts +0 -2
  41. package/server/services/router/index.ts +220 -86
  42. package/server/services/router/response/index.ts +0 -15
  43. package/server/utils/rte.ts +21 -132
  44. package/types/global/utils.d.ts +4 -22
  45. package/types/icons.d.ts +1 -1
  46. package/server/services/email/service.json +0 -6
  47. package/server/services/email/templates.ts +0 -49
  48. package/server/services/email/transporter.ts +0 -31
@@ -22,7 +22,8 @@ import Popover, { Props as PopoverProps } from '@client/components/containers/Po
22
22
  ----------------------------------*/
23
23
 
24
24
  export type Props = SelectProps & InputBaseProps<ComboboxItem> & {
25
- popoverProps?: PopoverProps
25
+ popoverProps?: PopoverProps,
26
+ buttonProps?: ButtonProps,
26
27
  }
27
28
 
28
29
  export type Choice = ComboboxItem;
@@ -63,7 +64,7 @@ export default (initProps: Props) => {
63
64
  onChange, value: current,
64
65
  required
65
66
  }, {
66
- multiple, choices: initChoices, enableSearch, popoverProps,
67
+ multiple, choices: initChoices, enableSearch, popoverProps, buttonProps,
67
68
  ...props
68
69
  }] = useMantineInput<Props, string|number>(initProps);
69
70
 
@@ -95,7 +96,8 @@ export default (initProps: Props) => {
95
96
  /*----------------------------------
96
97
  - ACTIONS
97
98
  ----------------------------------*/
98
-
99
+
100
+ // Load search results
99
101
  React.useEffect(() => {
100
102
 
101
103
  if (choicesViaFunc && opened) {
@@ -113,12 +115,16 @@ export default (initProps: Props) => {
113
115
 
114
116
  }, [
115
117
  opened,
116
- search.keywords,
117
- // When initChoices is a function, React considers it's always different
118
- // It avoids the choices are fetched everytimle the parent component is re-rendered
119
- typeof initChoices === 'function' ? true : initChoices
118
+ search.keywords
120
119
  ]);
121
120
 
121
+ // When initChoices is not a function and has changed
122
+ React.useEffect(() => {
123
+ if (!choicesViaFunc) {
124
+ setChoices(initChoices);
125
+ }
126
+ }, [initChoices]);
127
+
122
128
  /*----------------------------------
123
129
  - RENDER
124
130
  ----------------------------------*/
@@ -172,16 +178,22 @@ export default (initProps: Props) => {
172
178
  <Button key={choice.value}
173
179
  size="s"
174
180
  suffix={isSelected ? <i src="check" /> : null}
175
- onClick={() => onChange( multiple
176
- ? (isSelected
177
- ? current.filter(c => c.value !== choice.value)
178
- : [...(current || []), choice]
179
- )
180
- : ((isSelected && !required)
181
- ? null
182
- : choice
181
+ onClick={() => {
182
+ onChange( multiple
183
+ ? (isSelected
184
+ ? current.filter(c => c.value !== choice.value)
185
+ : [...(current || []), choice]
186
+ )
187
+ : ((isSelected && !required)
188
+ ? null
189
+ : choice
190
+ )
183
191
  )
184
- )}>
192
+
193
+ if (!multiple)
194
+ setOpened(false);
195
+
196
+ }}>
185
197
  {choice.label}
186
198
  </Button>
187
199
  )
@@ -189,6 +201,7 @@ export default (initProps: Props) => {
189
201
  </div>
190
202
  )}>
191
203
  <Button
204
+ {...buttonProps}
192
205
  prefix={(
193
206
  (multiple && current?.length) ? (
194
207
  <span class="badge bg info s">
@@ -32,8 +32,7 @@ export type Props<TRow> = {
32
32
  empty?: ComponentChild | false,
33
33
 
34
34
  // Interactions
35
- sort?: TSortOptions,
36
- onSort?: (columnId: string | null, order: TSortOptions["order"]) => void,
35
+ sortState?: [TSortOptions, React.SetStateAction<TSortOptions>],
37
36
  onCellClick?: (row: TRow) => void,
38
37
 
39
38
  selection?: [TRow[], React.SetStateAction<TRow[]>],
@@ -57,7 +56,7 @@ type TSortOptions = {
57
56
  - COMPOSANTS
58
57
  ----------------------------------*/
59
58
  export default function Liste<TRow extends TDonneeInconnue>({
60
- stickyHeader, onSort, sort: sorted,
59
+ stickyHeader, sortState,
61
60
  data: rows, setData, empty,
62
61
  onCellClick,
63
62
  selection: selectionState, maxSelection,
@@ -84,6 +83,23 @@ export default function Liste<TRow extends TDonneeInconnue>({
84
83
  </div>
85
84
  );
86
85
 
86
+ const sortBy = (columnId: string | null, defaultOrder: 'asc' | 'desc') => {
87
+
88
+ if (!sortState) return;
89
+
90
+ const [sort, setSort] = sortState;
91
+
92
+ if (columnId === sort.id) {
93
+ setSort({
94
+ id: columnId,
95
+ order: defaultOrder === 'asc' ? 'desc' : 'asc'
96
+ });
97
+ } else {
98
+ setSort({ columnId, order: defaultOrder });
99
+ }
100
+
101
+ }
102
+
87
103
  /*----------------------------------
88
104
  - RENDU COLONNES / LIGNES
89
105
  ----------------------------------*/
@@ -99,7 +115,7 @@ export default function Liste<TRow extends TDonneeInconnue>({
99
115
  <td>
100
116
  <Checkbox
101
117
  id={"selectionner" + iDonnee}
102
- value={selection.current.some(s => s.id === row.id)}
118
+ value={selection.current.some(s => s?.id === row?.id)}
103
119
  onChange={(isSelected: boolean) => {
104
120
  selection.set(current => isSelected
105
121
  // Ajoute
@@ -141,15 +157,15 @@ export default function Liste<TRow extends TDonneeInconnue>({
141
157
  if (iDonnee === 0) {
142
158
 
143
159
  const headerProps = { className: '', ...cellProps };
144
- const isCurrentlySorted = sort && sorted && sorted.id === sort.id;
145
- const isSortable = sort && onSort;
146
- if (isSortable) {
160
+ const isCurrentlySorted = sort && sort.id === sort.id;
161
+ const isSortable = sort;
162
+ if (isSortable && sortState[1]) {
147
163
  headerProps.className += ' clickable';
148
164
  headerProps.onClick = () => {
149
165
  if (isCurrentlySorted)
150
- onSort(null, sort.order);
166
+ sortBy(null, sort.order);
151
167
  else
152
- onSort(sort.id, sort.order);
168
+ sortBy(sort.id, sort.order);
153
169
  }
154
170
  }
155
171
 
@@ -203,7 +219,7 @@ export default function Liste<TRow extends TDonneeInconnue>({
203
219
  render = JSON.stringify(cell);
204
220
 
205
221
  return (
206
- <td class={classe} {...cellProps}>
222
+ <td {...cellProps} className={classe}>
207
223
  {render}
208
224
  </td>
209
225
  )
@@ -230,7 +246,7 @@ export default function Liste<TRow extends TDonneeInconnue>({
230
246
  <Checkbox
231
247
  value={selection.current.length >= rows.length}
232
248
  onChange={(status: boolean) => {
233
- selection.set(status ? rows : []);
249
+ selection.set(status ? rows.filter(r => r?.id) : []);
234
250
  }}
235
251
  />
236
252
  </th>
@@ -20,13 +20,16 @@ export type Props = JSX.HTMLAttributes<HTMLDivElement> & {
20
20
  id?: string,
21
21
 
22
22
  // Display
23
+ mode: 'hide' | 'remove',
23
24
  content?: ComponentChild | JSX.Element
24
25
  state?: [boolean, StateUpdater<boolean>],
25
26
  width?: number | string,
26
27
  disable?: boolean
28
+
27
29
  // Position
28
30
  frame?: HTMLElement,
29
31
  side?: TSide,
32
+
30
33
  // Tag
31
34
  children: JSX.Element | [JSX.Element],
32
35
  tag?: string,
@@ -45,7 +48,7 @@ export default (props: Props) => {
45
48
  let {
46
49
  id,
47
50
 
48
- content, state, width, disable,
51
+ mode = 'remove', content, state, width, disable,
49
52
 
50
53
  frame, side = 'bottom',
51
54
 
@@ -108,7 +111,7 @@ export default (props: Props) => {
108
111
  const Tag = tag || 'div';
109
112
 
110
113
  let renderedContent: ComponentChild;
111
- if (active) {
114
+ if (active || mode === 'hide') {
112
115
  //content = typeof content === 'function' ? React.createElement(content) : content;
113
116
  renderedContent = React.cloneElement(
114
117
  content,
@@ -124,13 +127,24 @@ export default (props: Props) => {
124
127
 
125
128
  style: {
126
129
  ...(content.props.style || {}),
130
+
131
+ ...(!active && mode === 'hide' ? {
132
+ display: 'none'
133
+ } : {}),
134
+
135
+ // Positionning
127
136
  ...(position ? {
128
137
  top: position.css.top,
129
138
  left: position.css.left,
130
139
  right: position.css.right,
131
140
  bottom: position.css.bottom,
132
- } : {}),
133
- ...(width !== undefined ? { width: typeof width === 'number' ? width + 'rem' : width } : {})
141
+ } : {}),
142
+
143
+ ...(width !== undefined ? {
144
+ width: typeof width === 'number'
145
+ ? width + 'rem'
146
+ : width
147
+ } : {})
134
148
  }
135
149
  }
136
150
  )
@@ -156,6 +170,9 @@ export default (props: Props) => {
156
170
  {React.cloneElement( children, {
157
171
  onClick: (e) => {
158
172
  show(isShown => !isShown);
173
+ e.stopPropagation();
174
+ e.preventDefault();
175
+ return false;
159
176
  }
160
177
  })}
161
178
 
@@ -22,7 +22,9 @@ export { default as Date } from './Date';
22
22
  export { default as Select } from './Select';
23
23
  export { default as Checkbox } from './Checkbox';
24
24
  export { InputWrapper } from './utils';
25
- export { default as DropDown } from './DropDown';
25
+ export { default as DropDown } from './DropDown';
26
+
27
+ export { default as ConnectedInput } from './ConnectedInput';
26
28
 
27
29
  // Mantine
28
30
  export {
@@ -59,7 +61,7 @@ export {
59
61
  //Button,
60
62
  //Card,
61
63
  Center,
62
- //Checkbox,
64
+ Checkbox as CheckboxMantine,
63
65
  Chip,
64
66
  Code,
65
67
  ColorPicker,
@@ -168,7 +168,7 @@ export default class ClientRouter<
168
168
 
169
169
  // Error code
170
170
  if (typeof url === 'number') {
171
- this.createResponse( this.errors[url], this.context.request ).then(( page ) => {
171
+ this.createResponse( this.errors[url], this.context.request, data ).then(( page ) => {
172
172
  this.navigate(page, data);
173
173
  })
174
174
  return;
@@ -335,10 +335,10 @@ export default class ClientRouter<
335
335
 
336
336
  };
337
337
 
338
- console.log("404 error page not found.", this.errors, this.routes);
339
-
340
338
  const notFoundRoute = this.errors[404];
341
- return await this.createResponse(notFoundRoute, request);
339
+ return await this.createResponse(notFoundRoute, request, {
340
+ error: new Error("Page not found")
341
+ });
342
342
  }
343
343
 
344
344
  private async load(route: TUnresolvedNormalRoute): Promise<TRoute>;
@@ -504,7 +504,9 @@ export default class ClientRouter<
504
504
  // Listener remover
505
505
  return () => {
506
506
  debug && console.info(LogPrefix, `De-register hook ${hookName} (index ${cbIndex})`);
507
- delete (callbacks as THookCallback<this>[])[cbIndex];
507
+ this.hooks[hookName] = this.hooks[hookName]?.filter(
508
+ (_, index) => index !== cbIndex
509
+ );
508
510
  }
509
511
 
510
512
  }
@@ -20,9 +20,16 @@ export type TJsonError = {
20
20
  } & TErrorDetails
21
21
 
22
22
  type TErrorDetails = {
23
+
23
24
  // Allow to identify the error catched (ex: displaying custop content, running custom actions, ...)
24
25
  id?: string,
25
26
  data?: {},
27
+
28
+ cta?: {
29
+ label: string,
30
+ link: string,
31
+ },
32
+
26
33
  // For debugging
27
34
  stack?: string,
28
35
  origin?: string,
@@ -55,11 +62,13 @@ export type ServerBug = {
55
62
  },
56
63
 
57
64
  // Error
58
- error: Error,
59
- stacktrace: string,
60
- logs: TJsonLog[],
65
+ title?: string,
66
+ stacktraces: string[],
67
+ context: object[],
61
68
  }
62
69
 
70
+ export type TCatchedError = Error | CoreError | Anomaly;
71
+
63
72
  /*----------------------------------
64
73
  - ERREURS
65
74
  ----------------------------------*/
@@ -152,6 +161,21 @@ export class AuthRequired extends CoreError {
152
161
  public http = 401;
153
162
  public title = "Authentication Required";
154
163
  public static msgDefaut = "Please Login to Continue.";
164
+
165
+ public constructor(
166
+ message: string,
167
+ public motivation?: string,
168
+ details?: TErrorDetails
169
+ ) {
170
+ super(message, details);
171
+ }
172
+
173
+ public json(): TJsonError & { motivation?: string } {
174
+ return {
175
+ ...super.json(),
176
+ motivation: this.motivation,
177
+ }
178
+ }
155
179
  }
156
180
 
157
181
  export class UpgradeRequired extends CoreError {
@@ -98,7 +98,10 @@ export type TRouteOptions = {
98
98
  redirectLogged?: string, // Redirect to this route if auth: false and user is logged
99
99
 
100
100
  // Rendering
101
- static?: boolean,
101
+ static?: {
102
+ refresh?: string,
103
+ urls: string[]
104
+ },
102
105
  canonicalParams?: string[], // For SEO + unique ID for static cache
103
106
  layout?: false | string, // The nale of the layout
104
107
 
@@ -0,0 +1,183 @@
1
+
2
+ import type Driver from '@server/services/disks/driver';
3
+ import { Anomaly } from '@common/errors';
4
+
5
+ export type LexicalNode = {
6
+ version: number,
7
+ type: string,
8
+ children?: LexicalNode[],
9
+ // Attachement
10
+ src?: string;
11
+ // Headhing
12
+ text?: string;
13
+ anchor?: string;
14
+ tag?: string;
15
+ }
16
+
17
+ export type LexicalState = {
18
+ root: LexicalNode
19
+ }
20
+
21
+ export type TRenderOptions = {
22
+
23
+ format?: 'html' | 'text', // Default = html
24
+ transform?: RteUtils["transformNode"],
25
+
26
+ render?: (
27
+ node: LexicalNode,
28
+ parent: LexicalNode | null,
29
+ options: TRenderOptions
30
+ ) => Promise<LexicalNode>,
31
+
32
+ attachements?: {
33
+ disk: Driver,
34
+ directory: string,
35
+ prevVersion?: string | LexicalState | null,
36
+ }
37
+ }
38
+
39
+ export type TSkeleton = {
40
+ id: string,
41
+ title: string,
42
+ level: number,
43
+ childrens: TSkeleton
44
+ }[];
45
+
46
+ export type TContentAssets = {
47
+ attachements: string[],
48
+ skeleton: TSkeleton
49
+ }
50
+
51
+ export default abstract class RteUtils {
52
+
53
+ public async render(
54
+ content: string | LexicalState,
55
+ options: TRenderOptions = {}
56
+ ): Promise<TContentAssets & {
57
+ html: string | null,
58
+ json: string | LexicalState,
59
+ }> {
60
+
61
+ // Transform content
62
+ const assets: TContentAssets = {
63
+ attachements: [],
64
+ skeleton: []
65
+ }
66
+
67
+ // Parse content if string
68
+ let json = this.parseState(content);
69
+ if (json === false)
70
+ return { html: '', json: content, ...assets }
71
+
72
+ // Parse prev version if string
73
+ if (typeof options?.attachements?.prevVersion === 'string') {
74
+ try {
75
+ options.attachements.prevVersion = JSON.parse(options.attachements.prevVersion) as LexicalState;
76
+ } catch (error) {
77
+ throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
78
+ }
79
+ }
80
+
81
+ const root = await this.processContent(json.root, null, async (node, parent) => {
82
+ return await this.transformNode(node, parent, assets, options);
83
+ });
84
+
85
+ json = { ...json, root };
86
+
87
+ // Delete unused attachements
88
+ const attachementOptions = options?.attachements;
89
+ if (attachementOptions && attachementOptions.prevVersion !== undefined) {
90
+
91
+ await this.processContent(root, null, async (node) => {
92
+ return await this.deleteUnusedFile(node, assets, attachementOptions);
93
+ });
94
+ }
95
+
96
+ // Convert json to HTML
97
+ let html: string | null;
98
+ if (options.format === 'text')
99
+ html = await this.jsonToText( json.root );
100
+ else
101
+ html = await this.jsonToHtml( json, options );
102
+
103
+ return { html, json: content, ...assets };
104
+ }
105
+
106
+ private parseState( content: string | LexicalState ): LexicalState | false {
107
+
108
+ if (typeof content === 'string' && content.trim().startsWith('{')) {
109
+ try {
110
+ return JSON.parse(content) as LexicalState;
111
+ } catch (error) {
112
+ throw new Anomaly("Invalid JSON format for the given JSON RTE content.");
113
+ }
114
+ } else if (content && typeof content === 'object' && content.root)
115
+ return content;
116
+ else
117
+ return false;
118
+
119
+ }
120
+
121
+ protected jsonToText(root: LexicalNode): string {
122
+ let result = '';
123
+
124
+ function traverse(node: LexicalNode) {
125
+ switch (node.type) {
126
+ case 'text':
127
+ // Leaf text node
128
+ result += node.text ?? '';
129
+ break;
130
+ case 'linebreak':
131
+ // Explicit line break node
132
+ result += '\n';
133
+ break;
134
+ default:
135
+ // Container or block node: dive into children if any
136
+ if (node.children) {
137
+ node.children.forEach(traverse);
138
+ }
139
+ // After finishing a block-level node, append newline
140
+ if (isBlockNode(node.type)) {
141
+ result += '\n';
142
+ }
143
+ break;
144
+ }
145
+ }
146
+
147
+ // Heuristic: treat these as blocks
148
+ function isBlockNode(type: string): boolean {
149
+ return [
150
+ 'root',
151
+ 'paragraph',
152
+ 'heading',
153
+ 'listitem',
154
+ 'unorderedlist',
155
+ 'orderedlist',
156
+ 'quote',
157
+ 'codeblock',
158
+ 'table',
159
+ ].includes(type);
160
+ }
161
+
162
+ traverse(root);
163
+
164
+ // Trim trailing whitespace/newlines
165
+ return result.replace(/\s+$/, '');
166
+ }
167
+
168
+ public abstract jsonToHtml( json: LexicalState, options: TRenderOptions ): Promise<string | null>;
169
+
170
+ protected abstract processContent(
171
+ node: LexicalNode,
172
+ parent: LexicalNode | null,
173
+ callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
174
+ ): Promise<LexicalNode>;
175
+
176
+ protected abstract transformNode( node: LexicalNode, parent: LexicalNode | null, assets: TContentAssets, options: TRenderOptions ): Promise<LexicalNode>;
177
+
178
+ protected abstract deleteUnusedFile(
179
+ node: LexicalNode,
180
+ assets: TContentAssets,
181
+ options: NonNullable<TRenderOptions["attachements"]>
182
+ ): Promise<LexicalNode>;
183
+ }
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.0",
4
+ "version": "0.6.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",
@@ -63,7 +63,6 @@
63
63
  "md5": "^2.3.0",
64
64
  "mime-types": "^2.1.35",
65
65
  "module-alias": "^2.2.2",
66
- "morgan": "^1.10.0",
67
66
  "mysql2": "^2.3.0",
68
67
  "object-sizeof": "^1.6.3",
69
68
  "path-to-regexp": "^6.2.0",
@@ -73,9 +72,11 @@
73
72
  "prettier": "^3.3.3",
74
73
  "react-scrollbars-custom": "^4.0.27",
75
74
  "react-slider": "^2.0.1",
75
+ "react-textarea-autosize": "^8.5.9",
76
76
  "regenerator-runtime": "^0.13.9",
77
77
  "request": "^2.88.2",
78
78
  "slugify": "^1.6.6",
79
+ "source-map-support": "^0.5.21",
79
80
  "sql-formatter": "^4.0.2",
80
81
  "stopword": "^3.1.1",
81
82
  "tslog": "^4.9.1",