arrmatura 5.0.4 → 5.2.0

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 (81) hide show
  1. package/commons/auth/AuthService.ts +111 -0
  2. package/commons/auth/UserService.ts +28 -78
  3. package/commons/auth/index.ts +4 -3
  4. package/commons/auth/user.xml +18 -20
  5. package/commons/components/buttons.xml +6 -4
  6. package/commons/components/layouts.xml +8 -8
  7. package/commons/components/modal.xml +4 -4
  8. package/commons/components/navigation.xml +21 -7
  9. package/commons/components/searchbar.xml +36 -22
  10. package/commons/components/table.xml +25 -5
  11. package/commons/components/toasts.xml +10 -5
  12. package/commons/forms/FormController.ts +48 -34
  13. package/commons/forms/Item.ts +93 -29
  14. package/commons/forms/ItemCollectionController.ts +113 -0
  15. package/commons/forms/ItemController.ts +50 -21
  16. package/commons/forms/NewItemController.ts +46 -0
  17. package/commons/forms/SmartareaField.xml +3 -3
  18. package/commons/forms/SuggestionField.xml +3 -3
  19. package/commons/forms/fields.xml +28 -15
  20. package/commons/forms/form.xml +3 -3
  21. package/commons/forms/index.ts +4 -4
  22. package/commons/index.ts +2 -2
  23. package/commons/services/ApiClientService.ts +32 -0
  24. package/commons/services/GSheetsService.ts +92 -64
  25. package/commons/services/Invoke.ts +24 -8
  26. package/commons/services/JsonpService.ts +1 -1
  27. package/commons/services/MetadataService.ts +50 -0
  28. package/commons/services/NavigationService.ts +20 -1
  29. package/commons/services/Service.ts +22 -28
  30. package/commons/services/Storage.ts +55 -0
  31. package/commons/services/ToastService.ts +16 -4
  32. package/commons/services/index.ts +3 -4
  33. package/core/Arrmatura.ts +4 -9
  34. package/core/CNode.ts +1 -2
  35. package/core/Component.ts +29 -31
  36. package/core/cnodes/conditionals.ts +3 -2
  37. package/core/expression.ts +1 -1
  38. package/core/register.ts +1 -1
  39. package/core/utils.ts +1 -3
  40. package/core-web/attributes.ts +38 -10
  41. package/core-web/elements.ts +22 -4
  42. package/core-web/index.ts +14 -2
  43. package/core-web/reflow.ts +3 -1
  44. package/core-web/support.ts +27 -0
  45. package/docs/index.js +350 -66
  46. package/lib/{arrayGroupBy.ts → array/arrayGroupBy.ts} +1 -1
  47. package/lib/{arraySortBy.ts → array/arraySortBy.ts} +5 -5
  48. package/lib/{arrayToObject.ts → array/arrayToObject.ts} +0 -0
  49. package/lib/array/arrayish.ts +7 -0
  50. package/lib/array/index.ts +4 -0
  51. package/lib/crypto/index.ts +25 -0
  52. package/lib/{dateFormat.ts → date/index.ts} +61 -3
  53. package/lib/func/curry.ts +11 -0
  54. package/lib/{fx.ts → func/fx.ts} +32 -38
  55. package/lib/{noop.ts → func/identity.ts} +1 -3
  56. package/lib/{nextId.ts → func/nextId.ts} +0 -0
  57. package/lib/func/noop.ts +6 -0
  58. package/lib/geo/index.js +22 -0
  59. package/lib/index.ts +17 -19
  60. package/lib/{obj.ts → object/index.ts} +8 -3
  61. package/lib/{codus.ts → parsing/codus.ts} +1 -1
  62. package/lib/{predicat.ts → parsing/compiler.ts} +0 -0
  63. package/lib/{scalar.ts → parsing/scalarParse.ts} +0 -0
  64. package/lib/{urls.ts → parsing/urls.ts} +0 -0
  65. package/lib/{xml.ts → parsing/xml.ts} +3 -3
  66. package/lib/{l10n.ts → resources/l10n.ts} +2 -2
  67. package/lib/{resources.ru.ts → resources/resources.ru.ts} +0 -0
  68. package/lib/{resources.ts → resources/resources.ts} +0 -0
  69. package/lib/{str.ts → string/index.ts} +2 -20
  70. package/lib/{createListeners.ts → support/createListeners.ts} +0 -0
  71. package/package.json +7 -6
  72. package/commons/forms/ItemCollection.ts +0 -152
  73. package/commons/forms/MetadataService.ts +0 -45
  74. package/commons/pipes/index.ts +0 -21
  75. package/commons/pipes/showCounts.ts +0 -16
  76. package/commons/services/LocalStorage.ts +0 -46
  77. package/lib/consts.ts +0 -3
  78. package/lib/curry.ts +0 -8
  79. package/lib/dateOf.ts +0 -31
  80. package/lib/dateParseISO8601.ts +0 -27
  81. package/lib/guid.ts +0 -7
@@ -6,22 +6,35 @@
6
6
 
7
7
  <component id="TextField">
8
8
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
9
- <input class="form-input" type="text" disabled="{disabled}" placeholder="{placeholder}" value="{value}" change="{onChange}" />
9
+ <input class="form-input" name="{id}" type="text" disabled="{disabled}" placeholder="{placeholder}" value="{value}" change="{onChange}" />
10
+ </FieldItem>
11
+ </component>
12
+
13
+ <component id="PasswordField">
14
+ <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
15
+ <input class="form-input" name="{id}" type="password" disabled="{disabled}" placeholder="{placeholder|or:password}" value="{value}" change="{onChange}" />
16
+ </FieldItem>
17
+ </component>
18
+
19
+ <component id="EmailField">
20
+ <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
21
+ <input class="form-input" type="email" disabled="{disabled}" placeholder="{placeholder|or:password}" value="{value}" change="{onChange}" />
10
22
  </FieldItem>
11
23
  </component>
12
24
 
13
25
  <component id="UrlField">
14
26
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
15
- <div class="input-group has-icon-right">
27
+ <div class="input-group has-icon-right" style="height:2rem;">
16
28
  <ui:fragment ui:if="{editing}">
17
29
  <ui:then>
18
- <input class="form-input border-0" disabled="{disabled}" type="url" blur="-> editing|not" placeholder="{caption}" value="{value}" change="{onChange}" />
30
+ <input class="form-input border-0" disabled="{disabled}" type="url" blur="-> editing|not" placeholder="{placeholder}" value="{value}" change="{onChange}" />
19
31
  <button class="btn btn-primary input-group-btn" click="-> editing|not">
20
32
  <i class="icon icon-check"/>
21
33
  </button>
22
34
  </ui:then>
23
35
  <ui:else>
24
- <a class="form-input text-ellipsis" href="{value}" target="{id}">{value}</a>
36
+ <a class="form-input text-ellipsis" href="{value}" target="{id}" ui:if="value">🔗 {value}</a>
37
+ <a class="form-input text-ellipsis" ui:if="value|not">{placeholder|or:-}</a>
25
38
  <i class="form-icon icon icon-edit c-hand" click="->editing" ui:if="disabled|not"/>
26
39
  </ui:else>
27
40
  </ui:fragment>
@@ -31,20 +44,20 @@
31
44
 
32
45
  <component id="EmailField">
33
46
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
34
- <input class="form-input" disabled="{disabled}" type="email" placeholder="{caption}" value="{value}" change="{onChange}" />
47
+ <input class="form-input" disabled="{disabled}" type="email" placeholder="{placeholder}" value="{value}" change="{onChange}" />
35
48
  </FieldItem>
36
49
  </component>
37
50
 
38
51
  <component id="DateField">
39
52
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
40
- <input class="form-input" disabled="{disabled}" type="date" placeholder="{caption}" value="{value}" change="{onChange}" />
53
+ <input class="form-input" disabled="{disabled}" type="date" placeholder="{placeholder}" value="{value}" change="{onChange}" />
41
54
  </FieldItem>
42
55
  </component>
43
56
 
44
57
  <component id="DateAndWorkingHoursField">
45
58
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
46
- <div class="flexoid">
47
- <input class="form-input" disabled="{disabled}" type="date" placeholder="{caption}" value="{value}" change="{onChange}" />
59
+ <div class="d-flex">
60
+ <input class="form-input" disabled="{disabled}" type="date" placeholder="{placeholder}" value="{value}" change="{onChange}" />
48
61
  <Select class="form-select ml-2" disabled="{disabled}" change="{onChange|assignFirstArgKeyValue:key:@typeSpec}" emptyCaption="-" value="{data|dot:@typeSpec}" options=":enums.workingHours" />
49
62
  </div>
50
63
  </FieldItem>
@@ -53,8 +66,8 @@
53
66
 
54
67
  <component id="DateAndTimeField">
55
68
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
56
- <div class="flexoid">
57
- <input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{caption}" value="{value}" change="{onChange}" />
69
+ <div class="d-flex">
70
+ <input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{placeholder}" value="{value}" change="{onChange}" />
58
71
  <input class="form-input flex-1 ml-2" disabled="{value|not|or:@disabled}" type="text" value="{data|dot:@typeSpec}" change="{onChange|assignFirstArgKeyValue:key:@typeSpec}" />
59
72
  </div>
60
73
  </FieldItem>
@@ -62,8 +75,8 @@
62
75
 
63
76
  <component id="DatePeriodField">
64
77
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
65
- <div class="flexoid">
66
- <input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{caption}" value="{value}" change="{onChange}" />
78
+ <div class="d-flex">
79
+ <input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{placeholder}" value="{value}" change="{onChange}" />
67
80
  <input class="form-input flex-1 ml-2" style="min-width: 6rem;" disabled="{value|not|or:@disabled}" type="date" value="{data|dot:@typeSpec}" change="{onChange|assignFirstArgKeyValue:key:@typeSpec}" />
68
81
  </div>
69
82
  </FieldItem>
@@ -71,13 +84,13 @@
71
84
 
72
85
  <component id="DateTimeField">
73
86
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
74
- <input class="form-input" disabled="{disabled}" type="datetime-local" placeholder="{caption}" value="{value|dateFormat:yyyy-mm-ddTt}" change="{onChange}" />
87
+ <input class="form-input" disabled="{disabled}" type="datetime-local" placeholder="{placeholder}" value="{value|dateFormat:yyyy-mm-ddTt}" change="{onChange}" />
75
88
  </FieldItem>
76
89
  </component>
77
90
 
78
91
  <component id="TimeField">
79
92
  <FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
80
- <input class="form-input" disabled="{disabled}" type="text" placeholder="{caption}" value="{value|dateFormat:t}" change="{onChange}" />
93
+ <input class="form-input" disabled="{disabled}" type="text" placeholder="{placeholder}" value="{value|dateFormat:t}" change="{onChange}" />
81
94
  </FieldItem>
82
95
  </component>
83
96
 
@@ -95,7 +108,7 @@
95
108
 
96
109
  <component id="BooleanField">
97
110
  <div class="form-group">
98
- <div class="col-sm-12 flexoid">
111
+ <div class="col-sm-12 d-flex">
99
112
  <label class="form-switch">
100
113
  <span>{caption}</span>
101
114
  <input type="checkbox" toggle="{onChange}" checked="{value|truthy}" />
@@ -7,14 +7,14 @@
7
7
 
8
8
  <component id="GroupForma">
9
9
  <div autocomplete="off" class="form-horizontal px-2">
10
- <FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" />
10
+ <FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" groups="{groups}" />
11
11
  <FieldGroup ui:props="{group}" ui:for="group of <-formController.fieldsGroups" />
12
12
  </div>
13
13
  </component>
14
14
 
15
15
  <component id="TabbedForma">
16
16
  <div autocomplete="off" class="form-horizontal px-2">
17
- <FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" />
17
+ <FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" tabs="{tabs}" groups="{groups}" />
18
18
  <TabbedFormaContent ui:props="<-formController.tabsInfo" />
19
19
  </div>
20
20
  </component>
@@ -27,7 +27,7 @@
27
27
  </component>
28
28
 
29
29
  <component id="FieldGroup">
30
- <h6 class="mt-2 pt-2 px-2 text-gray">{id}</h6>
30
+ <h6 class="mt-2 pt-2 px-2 text-gray">{name|or:id}</h6>
31
31
  <div class="panel">
32
32
  <div class="p-2">
33
33
  <ui:tag tag="{field.type|capitalize}Field" ui:props="{field}" ui:if="field.visible" ui:for="field of items" />
@@ -1,7 +1,7 @@
1
1
  import { FormController } from './FormController';
2
- import { ItemCollection } from './ItemCollection';
2
+ import { ItemCollectionController } from './ItemCollectionController';
3
+ import { NewItemController } from './NewItemController';
3
4
  import { ItemController } from './ItemController';
4
- import { MetadataService } from './MetadataService';
5
5
  import { MultivalueController } from './MultivalueController';
6
6
  import { SuggestionController } from './SuggestionController';
7
7
 
@@ -21,9 +21,9 @@ export default [
21
21
  FieldItem,
22
22
  MultiEnumField,
23
23
  FormController,
24
- ItemCollection,
24
+ ItemCollectionController,
25
+ NewItemController,
25
26
  ItemController,
26
- MetadataService,
27
27
  MultivalueController,
28
28
  SuggestionController,
29
29
  ];
package/commons/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import components from './components';
2
2
  import * as services from './services';
3
3
  import forms from './forms';
4
- import auth from './auth';
4
+ import * as auth from './auth';
5
5
 
6
- export default [...components, ...forms, ...auth, ...Object.values(services)];
6
+ export default [...components, ...forms, ...Object.values(auth), ...Object.values(services)];
@@ -0,0 +1,32 @@
1
+ import { urlAppendParams } from '../../lib';
2
+ import { Service } from './Service';
3
+
4
+ export class ApiClientService extends Service {
5
+ baseUrl: string = '';
6
+ token: string = '';
7
+ onTokenExpired?: Procedure;
8
+
9
+ post(path: string = '', data: any, options: object) {
10
+ return fetch(urlAppendParams(this.baseUrl + path), {
11
+ method: 'POST',
12
+ mode: 'cors',
13
+ redirect: 'follow',
14
+ ...options,
15
+ body: JSON.stringify({ token: this.token, data }),
16
+ })
17
+ .then((res) => res.json())
18
+ .then(({ error, ...rest }) => {
19
+ if (error) {
20
+ if (error.code === '401') {
21
+ this.onTokenExpired?.();
22
+ }
23
+ throw error;
24
+ }
25
+ return rest;
26
+ });
27
+ }
28
+
29
+ onPost({ path, body, options }: { path: string; body: any; options: object }) {
30
+ return this.post(path, body, options);
31
+ }
32
+ }
@@ -1,92 +1,120 @@
1
- import { arrayToObject } from 'arrayToObject';
2
- import { urlAppendParams } from 'urls';
1
+ import { arrayToObject } from '../../lib';
3
2
  import { JsonpService } from './JsonpService';
4
- import { LocalStorage } from './LocalStorage';
3
+ import { Storage } from './Storage';
5
4
  import { Service } from './Service';
6
5
 
7
6
  export class GSheetsService extends Service {
8
7
  url: string = '';
9
- kind: string = 'sheet';
8
+ token: string = '';
9
+ kind: string = 'items';
10
10
  remote = new JsonpService();
11
- persistence: 'local' | 'transient' = 'transient';
12
- local?: LocalStorage;
11
+ persistence: 'local' | 'session' | 'transient' = 'transient';
12
+ local?: Storage;
13
13
 
14
- init() {
15
- this.local = new LocalStorage(this.persistence);
16
- return this.syncAll();
14
+ get items() {
15
+ return this.local?.get(this.kind) || null;
16
+ }
17
+
18
+ set items(items) {
19
+ this.local?.set(this.kind, !items ? null : items.filter((e) => !e.deleted && e.status !== 'deleted'));
17
20
  }
18
21
 
19
- syncAll() {
20
- const now = Date.now();
21
- const { since, items: items0 = {} } = this.local?.get(this.kind) || { since: 0, items: {} };
22
- return !this.url
23
- ? null
24
- : {
25
- isLoading: true,
26
- '*': fetch(urlAppendParams(this.url, { since }), { redirect: 'follow' })
27
- .then((res) => res.json())
28
- .then(({ items: items1, error }) => {
29
- if (error) throw error;
30
-
31
- this.log('sync' + this.kind, now, items1);
32
- const items = { ...items0, ...arrayToObject(items1) };
33
- this.local?.set(this.kind, { since: now, items });
34
- return { items, isLoading: false };
35
- })
36
- .catch((err) => {
37
- this.toast({ mode: 'error', message: 'Ошибка: ' + JSON.stringify(err?.message || err || 'unknown') });
38
- return { isLoading: false };
39
- }),
40
- };
22
+ get since() {
23
+ return +this.local?.get(this.kind + '_since') || 0;
41
24
  }
42
25
 
43
- getAll() {
44
- const { items } = this.local?.get(this.kind) || {};
45
- return items;
26
+ set since(s) {
27
+ this.local?.set(this.kind + '_since', s || 0);
46
28
  }
47
29
 
48
- getItems() {
49
- let items = this.getAll();
50
- return Object.values(items || {});
30
+ init() {
31
+ this.local = new Storage(this.persistence);
32
+ return this.sync();
51
33
  }
52
34
 
53
- async update(delta: Delta) {
54
- const now = Date.now();
55
- const { since, items: items0 = [] } = this.local?.get(this.kind) || { since: 0, items: {} };
35
+ submit(_data: any): Promise<any> {
36
+ throw new Error('Method not implemented.');
37
+ }
56
38
 
57
- this.log('update', delta);
39
+ sync() {
40
+ const promise = this.submit({ path: `?action=items&kind=${this.kind}`, body: { since: String(this.since || 0) } })
41
+ .then(({ items, reset, timestamp }) => {
42
+ this.log('sync ' + this.kind, timestamp, items?.length);
43
+ if (items?.length || !this.since) {
44
+ if (!reset) {
45
+ const items0 = this.items || [];
46
+ items = Object.values({ ...arrayToObject(items0), ...arrayToObject(items) });
47
+ }
48
+ return { items, isLoading: false, since: timestamp };
49
+ }
50
+ return { isLoading: false };
51
+ })
52
+ .catch((error) => {
53
+ const message = error?.message || error || 'unknown';
54
+ this.toast({ id: message, mode: 'error', message: 'Ошибка: ' + message });
55
+ return { isLoading: false, error: message };
56
+ });
58
57
 
58
+ return {
59
+ isLoading: true,
60
+ error: null,
61
+ '*items': promise,
62
+ };
63
+ }
64
+
65
+ submitData(data: Delta) {
59
66
  return {
60
67
  busy: true,
61
- '*': fetch(this.url, {
62
- method: 'POST',
63
- mode: 'cors',
64
- redirect: 'follow',
65
- body: JSON.stringify({ action: 'upsert', delta }),
66
- })
67
- .then((res) => res.json())
68
- .then(({ error, ...item }) => {
69
- if (error) throw error;
70
- // items: items1 }
71
- this.log('update sync' + this.kind, now, item.id);
72
- const items = { ...items0, [item.id]: item };
73
- this.toast('Сохранено успешно');
74
-
75
- this.local?.set(this.kind, { since, items });
76
- return { busy: false }; //items,
68
+ error: null,
69
+ '*upsert': this.submit({ path: `?action=upsert&kind=${this.kind}`, body: data })
70
+ .then(({ item }) => {
71
+ this.log('update sync' + this.kind, item.id);
72
+ return {
73
+ busy: false,
74
+ lastUpdatedItemId: item.id,
75
+ items: [item, ...this.items?.filter((e: Hash) => e.id != item.id)],
76
+ };
77
77
  })
78
- .catch((err) => {
79
- this.toast({ mode: 'error', message: 'Ошибка: update ' + JSON.stringify(err?.message || err || 'unknown') });
80
- return { busy: false };
78
+ .catch((error) => {
79
+ return { busy: false, error };
81
80
  }),
82
81
  };
83
82
  }
84
83
 
85
- async onUpsert(data) {
86
- return this.update(data);
84
+ syncItem(data: Delta) {
85
+ return {
86
+ isItemLoading: true,
87
+ error: null,
88
+ '*item': this.submit({ path: `?action=item&kind=${this.kind}`, body: data })
89
+ .then(({ item }) => {
90
+ this.log('sync item' + this.kind, item.id, item);
91
+ return {
92
+ isItemLoading: false,
93
+ lastUpdatedItemId: item.id,
94
+ items: [item, ...this.items?.filter((e: Hash) => e.id != item.id)],
95
+ };
96
+ })
97
+ .catch((error) => {
98
+ return { isItemLoading: false, error: error };
99
+ }),
100
+ };
101
+ }
102
+
103
+ // event handlers:
104
+
105
+ onReload() {
106
+ return this.sync();
107
+ }
108
+
109
+ onUpsert(data: Delta) {
110
+ return this.submitData(data);
111
+ }
112
+
113
+ onItem(data: Delta) {
114
+ return this.syncItem(data);
87
115
  }
88
116
 
89
- async onDelete({ id }) {
90
- return this.update({ [id]: { status: 'deleted' } });
117
+ onDelete({ id }: Delta) {
118
+ return this.submitData({ [String(id)]: { status: 'deleted' } });
91
119
  }
92
120
  }
@@ -1,13 +1,29 @@
1
1
  export class Invoke {
2
+ action?: (d?: Delta) => void;
3
+ data?: Delta;
4
+ delay = 0;
5
+ hot = false;
6
+ trigger?: any;
7
+
8
+ setTrigger(val: any) {
9
+ this.trigger = val;
10
+ if (val) {
11
+ this.reload();
12
+ }
13
+ }
14
+
2
15
  init() {
3
- if (this.delay) {
4
- setTimeout(() => {
5
- if (!this.isDone) {
6
- this.action(this.data);
7
- }
8
- }, this.delay * 1000);
9
- } else {
10
- this.action(this.data);
16
+ if (this.hot) {
17
+ this.reload();
11
18
  }
12
19
  }
20
+
21
+ reload() {
22
+ setTimeout(() => {
23
+ if (!this.isDone) {
24
+ console.log(this.data);
25
+ this.action?.(this.data);
26
+ }
27
+ }, (this.delay || 0) * 1000);
28
+ }
13
29
  }
@@ -1,4 +1,4 @@
1
- import { urlAppendParams } from 'urls';
1
+ import { urlAppendParams } from '../../lib';
2
2
 
3
3
  let count = 0;
4
4
 
@@ -0,0 +1,50 @@
1
+ import { Storage } from './Storage';
2
+ import { Service } from './Service';
3
+
4
+ const mergeMeta = (meta: Hash<any>, target: Hash<any>) =>
5
+ Object.entries(meta).reduce((res, [key, val]) => {
6
+ res[key] = typeof res[key] === 'object' && !Array.isArray(res[key]) ? { ...res[key], ...val } : val;
7
+ return res;
8
+ }, target);
9
+
10
+ const load = (url: string, version: string) =>
11
+ fetch(url, {
12
+ method: 'POST',
13
+ mode: 'cors',
14
+ redirect: 'follow',
15
+ body: JSON.stringify({ data: { version } }),
16
+ }).then((res) => res.json());
17
+
18
+ export class MetadataService extends Service {
19
+ url: string = '';
20
+ version: string = '';
21
+ ttl = 24 * 3600000;
22
+
23
+ init() {
24
+ const local = new Storage('local', this.url.replace(/\//g, '_'));
25
+ const meta0 = local.get('$meta') || {};
26
+ if (+(meta0?.timestamp || 0) + this.ttl > Date.now()) {
27
+ mergeMeta(meta0, this.res(''));
28
+ return { ready: true, meta: meta0 };
29
+ }
30
+ return {
31
+ ready: false,
32
+ meta: meta0,
33
+ error: null,
34
+ '*': load(this.url, meta0?.version).then(
35
+ (meta2) => {
36
+ const isUpdated = meta0?.version !== meta2?.version;
37
+ const meta = !isUpdated ? { ...meta0 } : meta2;
38
+ meta.timestamp = Date.now();
39
+ mergeMeta(meta, this.res(''));
40
+ local.set('$meta', meta);
41
+ return { ready: true, isUpdated, meta };
42
+ },
43
+ (error) => {
44
+ const message = error?.message || error || 'unknown';
45
+ return { ready: true, error: message };
46
+ }
47
+ ),
48
+ };
49
+ }
50
+ }
@@ -1,4 +1,4 @@
1
- import { urlParse, urlStringify } from 'urls';
1
+ import { urlParse, urlStringify } from '../../lib';
2
2
  import { Service } from './Service';
3
3
 
4
4
  export class NavigationService extends Service {
@@ -27,6 +27,10 @@ export class NavigationService extends Service {
27
27
  window.removeEventListener('hashchange', this.hashchange);
28
28
  }
29
29
 
30
+ setTitle(d: string) {
31
+ window.document.title = d;
32
+ }
33
+
30
34
  setHash(d) {
31
35
  const { host, path = ['*'], params } = urlParse('/' + d);
32
36
  const state = {
@@ -46,6 +50,21 @@ export class NavigationService extends Service {
46
50
  };
47
51
  }
48
52
 
53
+ onReload() {
54
+ window.location.reload();
55
+ }
56
+
57
+ onCopyToClipboard({ value }) {
58
+ navigator.clipboard.writeText(value).then(
59
+ () => {
60
+ this.toast('Copied to clipboard');
61
+ },
62
+ (err) => {
63
+ this.toast('Could not copy to clipboard: ' + err.message);
64
+ }
65
+ );
66
+ }
67
+
49
68
  onHash({ value }) {
50
69
  this.setHash('/' + value);
51
70
  }
@@ -2,38 +2,43 @@ import { Component } from '../../core/Component';
2
2
  import { Toast } from './ToastService';
3
3
 
4
4
  export class Service {
5
- $: Component;
6
- constructor(state, $: Component) {
5
+ #$: Component;
6
+ state: any;
7
+
8
+ constructor(state: unknown, $: Component) {
7
9
  this.state = state;
8
- this.$ = $;
10
+ this.#$ = $;
9
11
  }
10
12
 
11
13
  /**
12
14
  * Framework
13
15
  */
14
16
  lookupService(ref: string) {
15
- return this.$.getByRef(ref);
17
+ return this.#$.getByRef(ref);
18
+ }
19
+
20
+ up(d: Delta) {
21
+ return this.#$.up(d);
16
22
  }
17
23
 
18
- up(...args) {
19
- return this.$.up(...args);
24
+ defer(fn: ComponentProcedure) {
25
+ return this.#$.defer(fn);
20
26
  }
21
27
 
22
- defer(...args) {
23
- return this.$.defer(...args);
28
+ emit(key: string, data: Delta) {
29
+ return this.#$?.emit(key, data);
24
30
  }
25
31
 
26
- emit(...args) {
27
- return this.$.emit(...args);
32
+ createEmitter(key: string) {
33
+ return (data: Delta) => this.emit(key, data);
28
34
  }
29
35
 
30
- res(...args) {
31
- return this.$.res(...args);
36
+ res(key: string) {
37
+ return this.#$.res(key);
32
38
  }
33
39
 
34
- log(...args) {
35
- console.log(this.$.ref + ': ', ...args);
36
- return args[0];
40
+ log(val: any, ...args: unknown[]) {
41
+ return this.#$.log(val, ...args);
37
42
  }
38
43
 
39
44
  /**
@@ -44,9 +49,9 @@ export class Service {
44
49
  // may overriden from props
45
50
  const handler = this.lookupService('errorHandler');
46
51
  if (handler) {
47
- handler.handleError({ message, code, source: this.$ });
52
+ handler.handleError({ message, code, source: this.#$ });
48
53
  } else {
49
- console.error(this.$.ref + ': ERROR: ', message, code);
54
+ console.error(this.#$.ref + ': ERROR: ', message, code);
50
55
  }
51
56
  }
52
57
 
@@ -54,15 +59,4 @@ export class Service {
54
59
  // may overriden from props
55
60
  this.emit('toasters.send', typeof message === 'string' ? { message } : message);
56
61
  }
57
-
58
- safe(p, def) {
59
- return p.catch((error) => {
60
- this.handleError(error);
61
- return def ? def(error) : { error };
62
- });
63
- }
64
-
65
- onClearError() {
66
- return { error: null };
67
- }
68
62
  }
@@ -0,0 +1,55 @@
1
+ const parse = (s: string | null) => (s ? JSON.parse(s) : undefined);
2
+
3
+ class MemoryStore {
4
+ #map = new Map<string, string>();
5
+
6
+ clear() {
7
+ return this.#map.clear();
8
+ }
9
+ getItem(key: string) {
10
+ return this.#map.get(key) || null;
11
+ }
12
+
13
+ setItem(key: string, value: string): void {
14
+ this.#map.set(key, value);
15
+ }
16
+ removeItem(key: string): void {
17
+ this.#map.delete(key);
18
+ }
19
+ }
20
+
21
+ export class Storage {
22
+ cache: Hash = {};
23
+ storage: Omit<MemoryStore, 'prototype'>;
24
+
25
+ constructor(mode?: 'local' | 'transient' | 'session', readonly ns = '') {
26
+ this.storage =
27
+ mode === 'local' ? window.localStorage : mode === 'session' ? window.sessionStorage : new MemoryStore();
28
+ }
29
+
30
+ clear() {
31
+ this.cache = {};
32
+ return this.storage.clear();
33
+ }
34
+
35
+ get(key: string) {
36
+ return this.cache[key] || (this.cache[key] = parse(this.storage.getItem(key + this.ns)));
37
+ }
38
+
39
+ set(key: string, val: unknown) {
40
+ this.cache[key] = val;
41
+ if (val) {
42
+ this.storage.setItem(key + this.ns, JSON.stringify(val));
43
+ } else {
44
+ this.storage.removeItem(key + this.ns);
45
+ }
46
+ }
47
+
48
+ assign(delta: Delta) {
49
+ Object.entries(delta).forEach(([key, val = null]) => this.set(key, val));
50
+ }
51
+
52
+ transform(key: string, fn: Pipe) {
53
+ return this.set(key, fn(this.get(key)));
54
+ }
55
+ }