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.
- package/commons/auth/AuthService.ts +111 -0
- package/commons/auth/UserService.ts +28 -78
- package/commons/auth/index.ts +4 -3
- package/commons/auth/user.xml +18 -20
- package/commons/components/buttons.xml +6 -4
- package/commons/components/layouts.xml +8 -8
- package/commons/components/modal.xml +4 -4
- package/commons/components/navigation.xml +21 -7
- package/commons/components/searchbar.xml +36 -22
- package/commons/components/table.xml +25 -5
- package/commons/components/toasts.xml +10 -5
- package/commons/forms/FormController.ts +48 -34
- package/commons/forms/Item.ts +93 -29
- package/commons/forms/ItemCollectionController.ts +113 -0
- package/commons/forms/ItemController.ts +50 -21
- package/commons/forms/NewItemController.ts +46 -0
- package/commons/forms/SmartareaField.xml +3 -3
- package/commons/forms/SuggestionField.xml +3 -3
- package/commons/forms/fields.xml +28 -15
- package/commons/forms/form.xml +3 -3
- package/commons/forms/index.ts +4 -4
- package/commons/index.ts +2 -2
- package/commons/services/ApiClientService.ts +32 -0
- package/commons/services/GSheetsService.ts +92 -64
- package/commons/services/Invoke.ts +24 -8
- package/commons/services/JsonpService.ts +1 -1
- package/commons/services/MetadataService.ts +50 -0
- package/commons/services/NavigationService.ts +20 -1
- package/commons/services/Service.ts +22 -28
- package/commons/services/Storage.ts +55 -0
- package/commons/services/ToastService.ts +16 -4
- package/commons/services/index.ts +3 -4
- package/core/Arrmatura.ts +4 -9
- package/core/CNode.ts +1 -2
- package/core/Component.ts +29 -31
- package/core/cnodes/conditionals.ts +3 -2
- package/core/expression.ts +1 -1
- package/core/register.ts +1 -1
- package/core/utils.ts +1 -3
- package/core-web/attributes.ts +38 -10
- package/core-web/elements.ts +22 -4
- package/core-web/index.ts +14 -2
- package/core-web/reflow.ts +3 -1
- package/core-web/support.ts +27 -0
- package/docs/index.js +350 -66
- package/lib/{arrayGroupBy.ts → array/arrayGroupBy.ts} +1 -1
- package/lib/{arraySortBy.ts → array/arraySortBy.ts} +5 -5
- package/lib/{arrayToObject.ts → array/arrayToObject.ts} +0 -0
- package/lib/array/arrayish.ts +7 -0
- package/lib/array/index.ts +4 -0
- package/lib/crypto/index.ts +25 -0
- package/lib/{dateFormat.ts → date/index.ts} +61 -3
- package/lib/func/curry.ts +11 -0
- package/lib/{fx.ts → func/fx.ts} +32 -38
- package/lib/{noop.ts → func/identity.ts} +1 -3
- package/lib/{nextId.ts → func/nextId.ts} +0 -0
- package/lib/func/noop.ts +6 -0
- package/lib/geo/index.js +22 -0
- package/lib/index.ts +17 -19
- package/lib/{obj.ts → object/index.ts} +8 -3
- package/lib/{codus.ts → parsing/codus.ts} +1 -1
- package/lib/{predicat.ts → parsing/compiler.ts} +0 -0
- package/lib/{scalar.ts → parsing/scalarParse.ts} +0 -0
- package/lib/{urls.ts → parsing/urls.ts} +0 -0
- package/lib/{xml.ts → parsing/xml.ts} +3 -3
- package/lib/{l10n.ts → resources/l10n.ts} +2 -2
- package/lib/{resources.ru.ts → resources/resources.ru.ts} +0 -0
- package/lib/{resources.ts → resources/resources.ts} +0 -0
- package/lib/{str.ts → string/index.ts} +2 -20
- package/lib/{createListeners.ts → support/createListeners.ts} +0 -0
- package/package.json +7 -6
- package/commons/forms/ItemCollection.ts +0 -152
- package/commons/forms/MetadataService.ts +0 -45
- package/commons/pipes/index.ts +0 -21
- package/commons/pipes/showCounts.ts +0 -16
- package/commons/services/LocalStorage.ts +0 -46
- package/lib/consts.ts +0 -3
- package/lib/curry.ts +0 -8
- package/lib/dateOf.ts +0 -31
- package/lib/dateParseISO8601.ts +0 -27
- package/lib/guid.ts +0 -7
package/commons/forms/fields.xml
CHANGED
|
@@ -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="{
|
|
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}"
|
|
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="{
|
|
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="{
|
|
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="
|
|
47
|
-
<input class="form-input" disabled="{disabled}" type="date" placeholder="{
|
|
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="
|
|
57
|
-
<input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{
|
|
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="
|
|
66
|
-
<input class="form-input flex-1" disabled="{disabled}" type="date" placeholder="{
|
|
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="{
|
|
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="{
|
|
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
|
|
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}" />
|
package/commons/forms/form.xml
CHANGED
|
@@ -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" />
|
package/commons/forms/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FormController } from './FormController';
|
|
2
|
-
import {
|
|
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
|
-
|
|
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 '
|
|
2
|
-
import { urlAppendParams } from 'urls';
|
|
1
|
+
import { arrayToObject } from '../../lib';
|
|
3
2
|
import { JsonpService } from './JsonpService';
|
|
4
|
-
import {
|
|
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
|
-
|
|
8
|
+
token: string = '';
|
|
9
|
+
kind: string = 'items';
|
|
10
10
|
remote = new JsonpService();
|
|
11
|
-
persistence: 'local' | 'transient' = 'transient';
|
|
12
|
-
local?:
|
|
11
|
+
persistence: 'local' | 'session' | 'transient' = 'transient';
|
|
12
|
+
local?: Storage;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
this.local
|
|
16
|
-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
return items;
|
|
26
|
+
set since(s) {
|
|
27
|
+
this.local?.set(this.kind + '_since', s || 0);
|
|
46
28
|
}
|
|
47
29
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return
|
|
30
|
+
init() {
|
|
31
|
+
this.local = new Storage(this.persistence);
|
|
32
|
+
return this.sync();
|
|
51
33
|
}
|
|
52
34
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
35
|
+
submit(_data: any): Promise<any> {
|
|
36
|
+
throw new Error('Method not implemented.');
|
|
37
|
+
}
|
|
56
38
|
|
|
57
|
-
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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((
|
|
79
|
-
|
|
80
|
-
return { busy: false };
|
|
78
|
+
.catch((error) => {
|
|
79
|
+
return { busy: false, error };
|
|
81
80
|
}),
|
|
82
81
|
};
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
|
|
86
|
-
return
|
|
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
|
-
|
|
90
|
-
return this.
|
|
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.
|
|
4
|
-
|
|
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
|
}
|
|
@@ -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 '
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
17
|
+
return this.#$.getByRef(ref);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
up(d: Delta) {
|
|
21
|
+
return this.#$.up(d);
|
|
16
22
|
}
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
return this
|
|
24
|
+
defer(fn: ComponentProcedure) {
|
|
25
|
+
return this.#$.defer(fn);
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
|
|
23
|
-
return this
|
|
28
|
+
emit(key: string, data: Delta) {
|
|
29
|
+
return this.#$?.emit(key, data);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
return this
|
|
32
|
+
createEmitter(key: string) {
|
|
33
|
+
return (data: Delta) => this.emit(key, data);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
res(
|
|
31
|
-
return this
|
|
36
|
+
res(key: string) {
|
|
37
|
+
return this.#$.res(key);
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
log(...args) {
|
|
35
|
-
|
|
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
|
|
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
|
+
}
|