5htp-core 0.5.0 → 0.5.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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/client/assets/css/core.less +5 -3
  3. package/src/client/assets/css/text/icons.less +4 -1
  4. package/src/client/assets/css/text/text.less +1 -1
  5. package/src/client/assets/css/text/titres.less +4 -3
  6. package/src/client/assets/css/theme.less +2 -1
  7. package/src/client/assets/css/utils/layouts.less +1 -0
  8. package/src/client/assets/css/utils/sizing.less +15 -15
  9. package/src/client/components/Dialog/Manager.tsx +1 -3
  10. package/src/client/components/Dialog/index.less +6 -9
  11. package/src/client/components/Form.ts +62 -31
  12. package/src/client/components/Select/index.tsx +1 -1
  13. package/src/client/components/Table/index.tsx +40 -6
  14. package/src/client/components/button.tsx +36 -23
  15. package/src/client/components/containers/Popover/getPosition.ts +48 -28
  16. package/src/client/components/containers/Popover/index.tsx +9 -3
  17. package/src/client/components/inputv3/Rte/Editor.tsx +64 -5
  18. package/src/client/components/inputv3/Rte/ToolbarPlugin/BlockFormat.tsx +1 -1
  19. package/src/client/components/inputv3/Rte/index.tsx +11 -76
  20. package/src/client/components/inputv3/Rte/plugins/DraggableBlockPlugin/index.css +1 -1
  21. package/src/client/components/inputv3/Rte/style.less +1 -25
  22. package/src/client/components/inputv3/base.tsx +1 -1
  23. package/src/client/components/inputv3/index.tsx +7 -1
  24. package/src/client/services/router/components/router.tsx +4 -3
  25. package/src/client/services/router/index.tsx +2 -1
  26. package/src/client/utils/dom.ts +1 -1
  27. package/src/common/errors/index.tsx +18 -6
  28. package/src/common/router/index.ts +2 -0
  29. package/src/server/services/auth/index.ts +0 -9
  30. package/src/server/services/database/index.ts +2 -2
  31. package/src/server/services/router/http/index.ts +5 -9
  32. package/src/server/services/router/index.ts +26 -49
  33. package/src/server/services/router/request/index.ts +11 -0
  34. package/src/server/services/router/response/index.ts +21 -0
  35. package/src/server/services/router/response/page/document.tsx +1 -4
  36. package/src/server/services/router/response/page/index.tsx +4 -0
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",
4
+ "version": "0.5.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",
@@ -1,13 +1,11 @@
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
9
7
  .bg {
10
- background: var(--cBg);
8
+ background-color: var(--cBg);
11
9
  color: var(--cTxtBase);
12
10
 
13
11
  &.img {
@@ -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';
@@ -46,7 +46,6 @@ i {
46
46
  flex: 0 0 @sizeComponent * 0.75;
47
47
  height: @sizeComponent * 0.75;
48
48
  line-height: @sizeComponent * 0.75;
49
- //font-size: 0.9em;
50
49
  }
51
50
 
52
51
  &.unit {
@@ -88,6 +87,10 @@ i.logo {
88
87
  border: none; // For img``
89
88
 
90
89
  background-color: var(--cBg);
90
+
91
+ &.circle {
92
+ border-radius: 50%;
93
+ }
91
94
  }
92
95
 
93
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
 
@@ -51,7 +51,8 @@
51
51
  --cBgActive: @bgActive;
52
52
  --cBgPressed: @bgPressed;
53
53
  & when (@apply = true) {
54
- background: var(--cBg);
54
+ // Don't overflow the other background image props with background:
55
+ background-color: var(--cBg);
55
56
  }
56
57
 
57
58
  // Accent
@@ -253,6 +253,7 @@
253
253
 
254
254
  &.sep-1 { // Bordure interne
255
255
 
256
+ align-items: stretch;
256
257
  background-color: var(--cLine2);
257
258
  gap: 1px;
258
259
  &.card {
@@ -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
  )
@@ -53,19 +53,13 @@
53
53
  display: flex;
54
54
  flex-direction: column;
55
55
  align-items: center;
56
- justify-content: flex-end;
56
+ justify-content: center;
57
57
  gap: @spacing;
58
58
  z-index: @modal-zindex;
59
59
 
60
60
  background: fade(#000, 20%);
61
61
  border-radius: @radius;
62
62
 
63
- // Desktop = vertically center the modal
64
- @media (min-width: 900px) {
65
- justify-content: center;
66
- //padding: @spacing;
67
- }
68
-
69
63
  // Pour les animations (ex: conffetis
70
64
  > canvas {
71
65
  position: absolute;
@@ -79,7 +73,9 @@
79
73
 
80
74
  position: relative;
81
75
  min-width: 300px;
82
- max-height: 90vh;
76
+ max-height: 99vh;
77
+ max-width: 99vw;
78
+ width: 400px; // Default width
83
79
  box-shadow: none;
84
80
  overflow-y: auto;
85
81
 
@@ -148,5 +144,6 @@
148
144
 
149
145
  // Selecteur moins profond pour que les clases utilitaires (w-a-x) soient prioritaires
150
146
  .modal > .card {
151
- max-width: 500px;
147
+ // Modal content should always be whiteys adapt from content width
148
+ //max-width: 500px;
152
149
  }
@@ -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 => (
@@ -20,14 +20,20 @@ import Checkbox from '../inputv3/Checkbox';
20
20
  export type TDonneeInconnue = { id: any } & {[cle: string]: any};
21
21
 
22
22
  export type Props<TRow> = {
23
-
24
- data: TRow[],
25
- columns: (row: TRow, rows: TRow[], index: number) => TColumn[];
23
+
24
+ // Appearence
26
25
  stickyHeader?: boolean,
26
+ className?: string,
27
27
 
28
+ // Data
29
+ data: TRow[],
28
30
  setData?: (rows: TRow[]) => void,
31
+ columns: (row: TRow, rows: TRow[], index: number) => TColumn[];
29
32
  empty?: ComponentChild | false,
30
- className?: string,
33
+
34
+ // Interactions
35
+ sort?: TSortOptions,
36
+ onSort?: (columnId: string | null, order: TSortOptions["order"]) => void,
31
37
 
32
38
  selection?: [TRow[], React.SetStateAction<TRow[]>],
33
39
  maxSelection?: number,
@@ -38,13 +44,19 @@ export type TColumn = JSX.HTMLAttributes<HTMLElement> & {
38
44
  cell: ComponentChild,
39
45
  raw?: number | string | boolean,
40
46
  stick?: boolean,
47
+ sort?: TSortOptions
48
+ }
49
+
50
+ type TSortOptions = {
51
+ id: string,
52
+ order: 'desc' | 'asc'
41
53
  }
42
54
 
43
55
  /*----------------------------------
44
56
  - COMPOSANTS
45
57
  ----------------------------------*/
46
58
  export default function Liste<TRow extends TDonneeInconnue>({
47
- stickyHeader,
59
+ stickyHeader, onSort, sort: sorted,
48
60
  data: rows, setData, empty,
49
61
  selection: selectionState, maxSelection,
50
62
  columns, ...props
@@ -99,6 +111,7 @@ export default function Liste<TRow extends TDonneeInconnue>({
99
111
 
100
112
  {columns(row, rows, iDonnee).map(({
101
113
  label, cell, class: className, raw,
114
+ sort,
102
115
  stick, width, ...cellProps
103
116
  }) => {
104
117
 
@@ -123,9 +136,30 @@ export default function Liste<TRow extends TDonneeInconnue>({
123
136
  }
124
137
  }
125
138
 
139
+ const isCurrentlySorted = sort && sorted && sorted.id === sort.id;
140
+ const isSortable = sort && onSort;
141
+ if (isSortable) {
142
+ classe += ' clickable';
143
+ cellProps.onClick = () => {
144
+ if (isCurrentlySorted)
145
+ onSort(null, sort.order);
146
+ else
147
+ onSort(sort.id, sort.order);
148
+ }
149
+ }
150
+
126
151
  if (iDonnee === 0) renduColonnes.push(
127
152
  <th class={classe} {...cellProps}>
128
- {label}
153
+ <div class="row sp-btw">
154
+
155
+ {isSortable ? (
156
+ <a>{label}</a>
157
+ ) : label}
158
+
159
+ {isCurrentlySorted && (
160
+ <i src={sort.order === "asc" ? "caret-up" : "caret-down"} />
161
+ )}
162
+ </div>
129
163
  </th>
130
164
  );
131
165
 
@@ -23,7 +23,6 @@ export type Props = {
23
23
  iconR?: ComponentChild,
24
24
 
25
25
  prefix?: ComponentChild,
26
- children?: ComponentChild | ComponentChild[],
27
26
  suffix?: ComponentChild,
28
27
 
29
28
  tag?: "a" | "button",
@@ -31,7 +30,6 @@ export type Props = {
31
30
  shape?: 'default' | 'icon' | 'tile' | 'pill',
32
31
  size?: TComponentSize,
33
32
  class?: string,
34
- title?: string,
35
33
 
36
34
  state?: [string, React.StateUpdater<string>],
37
35
  active?: boolean,
@@ -45,7 +43,14 @@ export type Props = {
45
43
  submenu?: ComponentChild,
46
44
  nav?: boolean | 'exact'
47
45
 
48
- } & (TButtonProps | TLinkProps)
46
+ // SEO: if icon only, should provinde a hint (aria-label)
47
+ } & ({
48
+ hint: string,
49
+ children?: ComponentChild | ComponentChild[],
50
+ } | {
51
+ children: ComponentChild | ComponentChild[],
52
+ hint?: string,
53
+ }) & (TButtonProps | TLinkProps)
49
54
 
50
55
  export type TButtonProps = {
51
56
 
@@ -84,6 +89,7 @@ export default ({
84
89
  iconR, suffix,
85
90
  submenu,
86
91
  nav,
92
+ hint,
87
93
 
88
94
  // Style
89
95
  class: className,
@@ -125,6 +131,12 @@ export default ({
125
131
  props.onClick = () => setActive(id);
126
132
  }
127
133
 
134
+ // Hint
135
+ if (hint !== undefined) {
136
+ props['aria-label'] = hint;
137
+ props.title = hint;
138
+ }
139
+
128
140
  // Shape classes
129
141
  const classNames: string[] = ['btn'];
130
142
  if (className)
@@ -171,35 +183,36 @@ export default ({
171
183
  // Render
172
184
  if ('link' in props || Tag === "a") {
173
185
 
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';
186
+ // Link (only if enabled)
187
+ if (!disabled) {
179
188
 
180
- if (props.target === undefined) {
189
+ props.href = props.link;
190
+
191
+ // External = open in new tab by default
192
+ if (props.href && (props.href[0] !== '/' || props.href.startsWith('//')))
193
+ props.target = '_blank';
194
+ }
181
195
 
182
- if (nav) {
196
+ // Nav
197
+ if (nav && props.target === undefined) {
183
198
 
184
- const checkIfCurrentUrl = (url: string) =>
185
- isCurrentUrl(url, props.link, nav === 'exact');
199
+ const checkIfCurrentUrl = (url: string) =>
200
+ isCurrentUrl(url, props.link, nav === 'exact');
186
201
 
187
- React.useEffect(() => {
202
+ React.useEffect(() => {
188
203
 
189
- // Init
190
- if (checkIfCurrentUrl(ctx.request.path))
191
- setIsActive(true);
204
+ // Init
205
+ if (checkIfCurrentUrl(ctx.request.path))
206
+ setIsActive(true);
192
207
 
193
- // On location change
194
- return history?.listen(({ location }) => {
208
+ // On location change
209
+ return history?.listen(({ location }) => {
195
210
 
196
- setIsActive( checkIfCurrentUrl(location.pathname) );
197
-
198
- })
211
+ setIsActive( checkIfCurrentUrl(location.pathname) );
199
212
 
200
- }, []);
201
- }
213
+ })
202
214
 
215
+ }, []);
203
216
  }
204
217
 
205
218
  Tag = 'a';