5htp-core 0.2.5 → 0.2.6-2

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 (33) hide show
  1. package/package.json +5 -4
  2. package/src/client/components/Dialog/Manager.tsx +4 -5
  3. package/src/client/components/Form.ts +88 -36
  4. package/src/client/components/Form_old/index.tsx +17 -17
  5. package/src/client/components/Form_old/index.tsx.old +17 -17
  6. package/src/client/components/Select/ChoiceSelector.tsx +172 -0
  7. package/src/client/components/Select/index.tsx +27 -138
  8. package/src/client/components/containers/Popover/getPosition.ts +14 -11
  9. package/src/client/components/containers/Popover/index.tsx +52 -35
  10. package/src/client/components/containers/Popover/popover.less +7 -1
  11. package/src/client/components/dropdown/index.tsx +1 -1
  12. package/src/client/components/index.ts +2 -1
  13. package/src/client/components/input/Number/index.tsx +2 -2
  14. package/src/client/components/inputv3/date/index.tsx +49 -0
  15. package/src/client/components/inputv3/date/react-calendar.less +143 -0
  16. package/src/client/components/inputv3/date/react-daterange-picker.less +112 -0
  17. package/src/client/components/inputv3/{string/index.tsx → index.tsx} +6 -2
  18. package/src/client/pages/_messages/403.tsx +1 -1
  19. package/src/client/pages/_messages/500.tsx +1 -1
  20. package/src/client/pages/bug.tsx +3 -3
  21. package/src/client/services/router/request/api.ts +0 -5
  22. package/src/client/services/router/request/multipart.ts +120 -9
  23. package/src/client/utils/dom.ts +12 -1
  24. package/src/common/validation/schema.ts +26 -27
  25. package/src/common/validation/validator.ts +14 -5
  26. package/src/server/app/index.ts +10 -1
  27. package/src/server/app/service.ts +1 -0
  28. package/src/server/services/console/index.ts +33 -32
  29. package/src/server/services/database/connection.ts +16 -13
  30. package/src/server/services/router/index.ts +2 -0
  31. package/src/server/services/schema/request.ts +0 -1
  32. package/src/client/components/input/Date/index.less +0 -167
  33. package/src/client/components/input/Date/index.tsx +0 -90
@@ -0,0 +1,112 @@
1
+ .react-daterange-picker {
2
+ display: inline-flex;
3
+ position: relative;
4
+ }
5
+
6
+ .react-daterange-picker,
7
+ .react-daterange-picker *,
8
+ .react-daterange-picker *:before,
9
+ .react-daterange-picker *:after {
10
+ -moz-box-sizing: border-box;
11
+ -webkit-box-sizing: border-box;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ .react-daterange-picker--disabled {
16
+ background-color: #f0f0f0;
17
+ color: #6d6d6d;
18
+ }
19
+
20
+ .react-daterange-picker__wrapper {
21
+ display: flex;
22
+ flex-grow: 1;
23
+ flex-shrink: 0;
24
+ align-items: center;
25
+ border: thin solid gray;
26
+ }
27
+
28
+ .react-daterange-picker__inputGroup {
29
+ min-width: calc((4px * 3) + 0.54em * 8 + 0.217em * 2);
30
+ height: 100%;
31
+ flex-grow: 1;
32
+ padding: 0 2px;
33
+ box-sizing: content-box;
34
+ }
35
+
36
+ .react-daterange-picker__inputGroup__divider {
37
+ padding: 1px 0;
38
+ white-space: pre;
39
+ }
40
+
41
+ .react-daterange-picker__inputGroup__divider,
42
+ .react-daterange-picker__inputGroup__leadingZero {
43
+ display: inline-block;
44
+ }
45
+
46
+ .react-daterange-picker__inputGroup__input {
47
+ min-width: 0.54em;
48
+ height: 100%;
49
+ position: relative;
50
+ padding: 0 1px;
51
+ border: 0;
52
+ background: none;
53
+ font: inherit;
54
+ box-sizing: content-box;
55
+ -webkit-appearance: textfield;
56
+ -moz-appearance: textfield;
57
+ appearance: textfield;
58
+ }
59
+
60
+ .react-daterange-picker__inputGroup__input::-webkit-outer-spin-button,
61
+ .react-daterange-picker__inputGroup__input::-webkit-inner-spin-button {
62
+ -webkit-appearance: none;
63
+ -moz-appearance: none;
64
+ appearance: none;
65
+ margin: 0;
66
+ }
67
+
68
+ .react-daterange-picker__inputGroup__input:invalid {
69
+ background: rgba(255, 0, 0, 0.1);
70
+ }
71
+
72
+ .react-daterange-picker__inputGroup__input--hasLeadingZero {
73
+ margin-left: -0.54em;
74
+ padding-left: calc(1px + 0.54em);
75
+ }
76
+
77
+ .react-daterange-picker__button {
78
+ border: 0;
79
+ background: transparent;
80
+ padding: 4px 6px;
81
+ }
82
+
83
+ .react-daterange-picker__button:enabled {
84
+ cursor: pointer;
85
+ }
86
+
87
+ .react-daterange-picker__button:enabled:hover .react-daterange-picker__button__icon,
88
+ .react-daterange-picker__button:enabled:focus .react-daterange-picker__button__icon {
89
+ stroke: #0078d7;
90
+ }
91
+
92
+ .react-daterange-picker__button:disabled .react-daterange-picker__button__icon {
93
+ stroke: #6d6d6d;
94
+ }
95
+
96
+ .react-daterange-picker__button svg {
97
+ display: inherit;
98
+ }
99
+
100
+ .react-daterange-picker__calendar {
101
+ width: 350px;
102
+ max-width: 100vw;
103
+ z-index: 1;
104
+ }
105
+
106
+ .react-daterange-picker__calendar--closed {
107
+ display: none;
108
+ }
109
+
110
+ .react-daterange-picker__calendar .react-calendar {
111
+ border-width: thin;
112
+ }
@@ -8,7 +8,7 @@ import { ComponentChild, JSX } from 'preact';
8
8
  import TextareaAutosize from 'react-textarea-autosize';
9
9
 
10
10
  // Core libs
11
- import { useInput, InputBaseProps } from '../base';
11
+ import { useInput, InputBaseProps } from './base';
12
12
 
13
13
  /*----------------------------------
14
14
  - TYPES
@@ -25,7 +25,7 @@ export type Props = {
25
25
  inputRef?: React.Ref<HTMLInputElement>
26
26
 
27
27
  // Behavior
28
- type?: 'email' | 'password' | 'longtext',
28
+ type?: 'email' | 'password' | 'longtext' | 'number',
29
29
  choice?: string[] | ((input: string) => Promise<string[]>),
30
30
 
31
31
  // Actions
@@ -93,6 +93,10 @@ export default ({
93
93
  prefix = prefix || <i src="text" />;
94
94
  Tag = 'textarea'//TextareaAutosize;
95
95
 
96
+ } else if (type === 'number') {
97
+
98
+ fieldProps.type = 'number';
99
+
96
100
  }
97
101
 
98
102
  // Auto suffix
@@ -23,7 +23,7 @@ router.error( 403, ({ message, modal }) => {
23
23
  });
24
24
 
25
25
  return (
26
- <div class="col pd-2">
26
+ <div class="card w-3-4 col al-center pd-2">
27
27
 
28
28
  <i src="times-circle" class="fg error xxl" />
29
29
 
@@ -23,7 +23,7 @@ router.error( 500, ({ message }) => {
23
23
  });
24
24
 
25
25
  return (
26
- <div class="col pd-2">
26
+ <div class="card w-3-4 col al-center pd-2">
27
27
 
28
28
  <i src="times-circle" class="fg error xxl" />
29
29
 
@@ -5,7 +5,7 @@
5
5
  import React from 'react';
6
6
 
7
7
  // Core components
8
- import { Button, String } from '@client/components';
8
+ import { Button, Input } from '@client/components';
9
9
  import Card, { Props as CardProps } from '@client/components/Dialog/card';
10
10
 
11
11
  // Core libs
@@ -49,11 +49,11 @@ export default ({ ...self }: {} & CardProps) => {
49
49
 
50
50
  <p>What's the problem ?</p>
51
51
 
52
- <String type="longtext" title="Description of the problem" value={observation} onChange={setObservation} />
52
+ <Input type="longtext" title="Description of the problem" value={observation} onChange={setObservation} />
53
53
 
54
54
  <p>What did you do just before the problem occurs ?</p>
55
55
 
56
- <String type="longtext" title="How the problem occured ?" value={before} onChange={setBefore} />
56
+ <Input type="longtext" title="How the problem occured ?" value={before} onChange={setBefore} />
57
57
 
58
58
  </Card>
59
59
  )
@@ -89,16 +89,11 @@ export default class ApiClient implements ApiClientService {
89
89
  fetcher.data = { ...(fetcher.data || {}), ...params };
90
90
 
91
91
  console.log("[api][reload]", id, fetcher.method, fetcher.path, fetcher.data);
92
- const indicator = this.toast.loading("Loading ...");
93
92
 
94
93
  this.fetchAsync(fetcher.method, fetcher.path, fetcher.data).then((data) => {
95
94
 
96
95
  this.set({ [id]: data });
97
96
 
98
- }).finally(() => {
99
-
100
- indicator.close(true);
101
-
102
97
  })
103
98
  }
104
99
  }
@@ -10,18 +10,129 @@ import { FileToUpload } from '@client/components/inputv3/file';
10
10
  - TYPES
11
11
  ----------------------------------*/
12
12
 
13
+ function mergeObjects(object1, object2) {
14
+ return [object1, object2].reduce(function (carry, objectToMerge) {
15
+ Object.keys(objectToMerge).forEach(function (objectKey) {
16
+ carry[objectKey] = objectToMerge[objectKey];
17
+ });
18
+ return carry;
19
+ }, {});
20
+ }
21
+
22
+ function isArray(val) {
23
+
24
+ return ({}).toString.call(val) === '[object Array]';
25
+ }
26
+
27
+ function isJsonObject(val) {
28
+
29
+ return !isArray(val) && typeof val === 'object' && !!val && !(val instanceof Blob) && !(val instanceof Date);
30
+ }
31
+
32
+ function isAppendFunctionPresent(formData) {
33
+
34
+ return typeof formData.append === 'function';
35
+ }
36
+
37
+ function isGlobalFormDataPresent() {
38
+
39
+ return typeof FormData === 'function';
40
+ }
41
+
42
+ function getDefaultFormData() {
43
+
44
+ if (isGlobalFormDataPresent()) {
45
+ return new FormData();
46
+ }
47
+ }
48
+
49
+ function convertRecursively(jsonObject, options, formData, parentKey) {
50
+
51
+ var index = 0;
52
+
53
+ for (var key in jsonObject) {
54
+
55
+ if (jsonObject.hasOwnProperty(key)) {
56
+
57
+ var propName = parentKey || key;
58
+ var value = options.mapping(jsonObject[key]);
59
+
60
+ if (parentKey && isJsonObject(jsonObject)) {
61
+ propName = parentKey + '[' + key + ']';
62
+ }
63
+
64
+ if (parentKey && isArray(jsonObject)) {
65
+
66
+ if (isArray(value) || options.showLeafArrayIndexes ) {
67
+ propName = parentKey + '[' + index + ']';
68
+ } else {
69
+ propName = parentKey + '[]';
70
+ }
71
+ }
72
+
73
+ // Exract the file object from value
74
+ if (typeof value === 'object' && value instanceof FileToUpload)
75
+ value = value.data;
76
+
77
+ if (isArray(value) || isJsonObject(value)) {
78
+
79
+ convertRecursively(value, options, formData, propName);
80
+
81
+ } else if (value instanceof FileList) {
82
+
83
+ for (var j = 0; j < value.length; j++) {
84
+ formData.append(propName + '[' + j + ']', value.item(j));
85
+ }
86
+ } else if (value instanceof Blob) {
87
+
88
+ formData.append(propName, value, value.name);
89
+
90
+ } else if (value instanceof Date) {
91
+
92
+ formData.append(propName, value.toISOString());
93
+
94
+ } else if (((value === null && options.includeNullValues) || value !== null) && value !== undefined) {
95
+
96
+ formData.append(propName, value);
97
+ }
98
+ }
99
+ index++;
100
+ }
101
+ return formData;
102
+ }
103
+
13
104
  /*----------------------------------
14
105
  - UTILS
15
106
  ----------------------------------*/
16
- export const toMultipart = (postData: TPostData) => {
17
-
18
- const formData = new FormData();
19
- for (const key in postData) {
20
- let data = postData[key];
21
- if (typeof data === 'object' && (data instanceof FileToUpload))
22
- data = data.data;
23
- formData.append(key, data);
107
+ /* Based on https://github.com/hyperatom/json-form-data
108
+ Changes:
109
+ - Add support for FileToUpload
110
+ */
111
+ export const toMultipart = (jsonObject: TPostData, options) => {
112
+
113
+ if (options && options.initialFormData) {
114
+
115
+ if (!isAppendFunctionPresent(options.initialFormData)) {
116
+ throw 'initialFormData must have an append function.';
117
+ }
118
+ } else if (!isGlobalFormDataPresent()) {
119
+
120
+ throw 'This environment does not have global form data. options.initialFormData must be specified.';
24
121
  }
25
122
 
26
- return formData;
123
+ var defaultOptions = {
124
+ initialFormData: getDefaultFormData(),
125
+ showLeafArrayIndexes: true,
126
+ includeNullValues: false,
127
+ mapping: function(value) {
128
+ if (typeof value === 'boolean') {
129
+ return +value ? '1': '0';
130
+ }
131
+ return value;
132
+ }
133
+ };
134
+
135
+ var mergedOptions = mergeObjects(defaultOptions, options || {});
136
+
137
+ return convertRecursively(jsonObject, mergedOptions, mergedOptions.initialFormData);
27
138
  }
@@ -47,7 +47,7 @@ export const blurable = (...args: [HTMLElement, Function][]) => {
47
47
 
48
48
  if (!deepContains([refElement], e.target))
49
49
  masquer();
50
-
50
+
51
51
  }
52
52
  }
53
53
 
@@ -65,4 +65,15 @@ export const blurable = (...args: [HTMLElement, Function][]) => {
65
65
  window.removeEventListener('mousedown', blur);
66
66
  unlisten();
67
67
  }
68
+ }
69
+
70
+ export const focusContent = ( container: HTMLElement ) => {
71
+
72
+ const toFocus = container.querySelector(
73
+ 'input, textarea, button.btn.primary, footer > button.btn'
74
+ ) || container;
75
+
76
+ console.log('Element to focus', toFocus);
77
+ // TODO: Type only docusable elemnts
78
+ toFocus?.focus();
68
79
  }
@@ -18,18 +18,18 @@ type TSchemaOptions = {
18
18
  opt?: boolean
19
19
  }
20
20
 
21
- type TOptsValider = {
21
+ export type TValidateOptions<TFields extends TSchemaFields = {}> = {
22
22
  debug?: boolean,
23
23
  throwError?: boolean,
24
-
25
- validateAll?: boolean,
24
+ ignoreMissing?: boolean,
25
+ only?: (keyof TFields)[],
26
26
  validateDeps?: boolean,
27
27
  autoCorrect?: boolean,
28
28
  }
29
29
 
30
30
  export type TValidationResult<TFields extends TSchemaFields> = {
31
31
  values: TValidatedData<TFields>,
32
- nbErreurs: number,
32
+ errorsCount: number,
33
33
  erreurs: TListeErreursSaisie
34
34
  }
35
35
 
@@ -62,7 +62,7 @@ export default class Schema<TFields extends TSchemaFields> {
62
62
  allData: TDonnees,
63
63
  output: TObjetDonnees = {},
64
64
 
65
- opts: TOptsValider = {},
65
+ opts: TValidateOptions<TFields> = {},
66
66
  chemin: string[] = []
67
67
 
68
68
  ): TValidationResult<TFields> {
@@ -70,45 +70,44 @@ export default class Schema<TFields extends TSchemaFields> {
70
70
  opts = {
71
71
  debug: false,
72
72
  throwError: false,
73
- validateAll: false,
74
73
  validateDeps: true,
75
74
  autoCorrect: false,
76
75
  ...opts,
77
76
  }
78
77
 
79
- const clesAvalider = Object.keys(opts.validateAll === true ? this.fields : dataToValidate);
80
-
81
78
  let outputSchema = output;
82
79
  for (const branche of chemin)
83
80
  outputSchema = outputSchema[branche];
81
+
82
+ const keysToValidate = opts.only || Object.keys(this.fields);
84
83
 
85
84
  // Validation de chacune d'entre elles
86
85
  let erreurs: TListeErreursSaisie = {};
87
- let nbErreurs = 0;
88
- for (const champ of clesAvalider) {
86
+ let errorsCount = 0;
87
+ for (const fieldName of keysToValidate) {
89
88
 
90
89
  // La donnée est répertoriée dans le schema
91
- const field = this.fields[champ];
90
+ const field = this.fields[fieldName];
92
91
  if (field === undefined) {
93
- opts.debug && console.warn(LogPrefix, '[' + champ + ']', 'Exclusion (pas présent dans le schéma)');
92
+ opts.debug && console.warn(LogPrefix, '[' + fieldName + ']', 'Exclusion (pas présent dans le schéma)');
94
93
  continue;
95
94
  }
96
95
 
97
- const cheminA = [...chemin, champ]
96
+ const cheminA = [...chemin, fieldName]
98
97
  const cheminAstr = cheminA.join('.')
99
98
 
100
99
  // Sous-schema
101
100
  if (field instanceof Schema) {
102
101
 
103
102
  // Initialise la structure pour permettre l'assignement d'outputSchema
104
- if (outputSchema[champ] === undefined)
105
- outputSchema[champ] = {}
103
+ if (outputSchema[fieldName] === undefined)
104
+ outputSchema[fieldName] = {}
106
105
 
107
106
  // The corresponding data should be an object
108
- const schemadata = dataToValidate[champ];
107
+ const schemadata = dataToValidate[fieldName];
109
108
  if (typeof schemadata !== 'object') {
110
109
  erreurs[ cheminAstr ] = [`Should be an object`];
111
- nbErreurs++;
110
+ errorsCount++;
112
111
  continue;
113
112
  }
114
113
 
@@ -123,30 +122,30 @@ export default class Schema<TFields extends TSchemaFields> {
123
122
  cheminA
124
123
  );
125
124
  erreurs = { ...erreurs, ...validationSchema.erreurs };
126
- nbErreurs += validationSchema.nbErreurs;
125
+ errorsCount += validationSchema.errorsCount;
127
126
 
128
127
  // Pas besoin d'assigner, car output est passé en référence
129
- //output[champ] = validationSchema.values;
128
+ //output[fieldName] = validationSchema.values;
130
129
 
131
130
 
132
131
  // I don't remind what is options.activer about
133
132
  /*} else if (field.activer !== undefined && field.activer(allData) === false) {
134
133
 
135
- delete outputSchema[champ];*/
134
+ delete outputSchema[fieldName];*/
136
135
 
137
136
  // Validator
138
137
  } else {
139
138
 
140
139
  // Champ composé de plusieurs values
141
140
  const valOrigine = field.options.as === undefined
142
- ? dataToValidate[champ]
143
- // Le champ regroupe plusieurs values (ex: Periode)
141
+ ? dataToValidate[fieldName]
142
+ // Le fieldName regroupe plusieurs values (ex: Periode)
144
143
  : field.options.as.map((nomVal: string) => dataToValidate[nomVal])
145
144
 
146
145
  // Validation
147
146
  try {
148
147
 
149
- const val = field.validate(valOrigine, allData, output, opts.autoCorrect);
148
+ const val = field.validate(valOrigine, allData, output, opts);
150
149
 
151
150
  // Exclusion seulement si explicitement demandé
152
151
  // IMPORTANT: Conserver les values undefined
@@ -155,7 +154,7 @@ export default class Schema<TFields extends TSchemaFields> {
155
154
  if (val === EXCLUDE_VALUE)
156
155
  opts.debug && console.log(LogPrefix, '[' + cheminA + '] Exclusion demandée');
157
156
  else
158
- outputSchema[champ] = val;
157
+ outputSchema[fieldName] = val;
159
158
 
160
159
  opts.debug && console.log(LogPrefix, '[' + cheminA + ']', valOrigine, '=>', val);
161
160
 
@@ -167,7 +166,7 @@ export default class Schema<TFields extends TSchemaFields> {
167
166
 
168
167
  // Référencement erreur
169
168
  erreurs[cheminAstr] = [error.message]
170
- nbErreurs++;
169
+ errorsCount++;
171
170
 
172
171
  } else
173
172
  throw error;
@@ -175,7 +174,7 @@ export default class Schema<TFields extends TSchemaFields> {
175
174
  }
176
175
  }
177
176
 
178
- if (nbErreurs !== 0 && opts.throwError === true) {
177
+ if (errorsCount !== 0 && opts.throwError === true) {
179
178
  throw new InputErrorSchema(erreurs);
180
179
  }
181
180
 
@@ -184,7 +183,7 @@ export default class Schema<TFields extends TSchemaFields> {
184
183
  return {
185
184
  values: output as TValidatedData<TFields>,
186
185
  erreurs,
187
- nbErreurs,
186
+ errorsCount,
188
187
  };
189
188
 
190
189
  }
@@ -8,6 +8,9 @@ import type { ComponentChild } from 'preact'
8
8
  // Core
9
9
  import { InputError } from '@common/errors';
10
10
 
11
+ // Specific
12
+ import type { TValidateOptions } from './schema';
13
+
11
14
  /*----------------------------------
12
15
  - TYPES
13
16
  ----------------------------------*/
@@ -41,12 +44,12 @@ type TValidationArgs<TValue, TAllValues extends {}> = [
41
44
  val: TNonEmptyValue,
42
45
  input: TAllValues,
43
46
  output: Partial<TAllValues>,
44
- corriger?: boolean
47
+ validateOptions?: TValidateOptions
45
48
  ]
46
49
 
47
50
  type TValidationFunction<TValue, TAllValues extends {} = {}> = (
48
51
  ...args: TValidationArgs<TValue, TAllValues>
49
- ) => TValue | typeof EXCLUDE_VALUE;
52
+ ) => TValue | typeof EXCLUDE_VALUE;
50
53
 
51
54
  type TValidateReturnType<
52
55
  TOptions extends TValidator<TValue>,
@@ -76,12 +79,18 @@ export default class Validator<TValue, TOptions extends TValidator<TValue> = TVa
76
79
 
77
80
  public isEmpty = (val: any) => val === undefined || val === '' || val === null
78
81
 
79
- public validate(...[ val, input, output, correct ]: TValidationArgs<TValue, {}>): TValidateReturnType<TOptions, TValue> {
82
+ public validate(...[
83
+ val, input, output, validateOptions
84
+ ]: TValidationArgs<TValue, {}>): TValidateReturnType<TOptions, TValue> {
80
85
 
81
86
  // Required value
82
87
  if (this.isEmpty(val)) {
83
88
  // Optionnel, on skip
84
- if (this.options.opt === true)
89
+ if (this.options.opt === true || (
90
+ validateOptions?.ignoreMissing === true
91
+ &&
92
+ val === undefined
93
+ ))
85
94
  return undefined as TValidateReturnType<TOptions, TValue>;
86
95
  // Requis
87
96
  else
@@ -89,7 +98,7 @@ export default class Validator<TValue, TOptions extends TValidator<TValue> = TVa
89
98
  }
90
99
 
91
100
  // Validate type
92
- return this.validateType(val, input, output, correct) as TValidateReturnType<TOptions, TValue>;
101
+ return this.validateType(val, input, output, validateOptions) as TValidateReturnType<TOptions, TValue>;
93
102
  }
94
103
 
95
104
  }
@@ -180,6 +180,7 @@ export default abstract class Application extends Service<Config, Hooks, /* TODO
180
180
  for (const service of this.servicesList) {
181
181
  const serviceClassName = service.constructor?.name;
182
182
  console.log(`[app] Start service`, serviceClassName);
183
+ service.status = 'starting';
183
184
 
184
185
  if (service.register)
185
186
  service.register();
@@ -191,8 +192,16 @@ export default abstract class Application extends Service<Config, Hooks, /* TODO
191
192
  // Start service
192
193
  if (service.start) {
193
194
  service.started = service.start();
194
- await service.started;
195
+ await service.started.catch(e => {
196
+ console.error("Catched error while starting service " + serviceClassName + '. Exiting process if mode production.', e);
197
+ if (this.env.profile === 'prod')
198
+ process.exit();
199
+ else
200
+ throw e;
201
+ })
195
202
  }
203
+
204
+ service.status = 'running';
196
205
  }
197
206
 
198
207
  console.log(`[app] All ${this.servicesList.length} services were started.`);
@@ -41,6 +41,7 @@ export default abstract class Service<
41
41
 
42
42
  public priority: TPriority = 0;
43
43
  public started?: Promise<void>;
44
+ public status: 'stopped' | 'starting' | 'running' | 'paused' = 'stopped';
44
45
 
45
46
  public commands?: Command[];
46
47