arrmatura 4.2.2 → 5.0.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.
- package/README.md +30 -25
- package/commons/auth/UserService.ts +107 -0
- package/commons/auth/index.ts +4 -0
- package/commons/auth/user.xml +66 -0
- package/commons/components/buttons.xml +23 -0
- package/commons/components/index.ts +13 -0
- package/commons/components/inputs.xml +33 -0
- package/commons/components/layouts.xml +77 -0
- package/commons/components/media.xml +21 -0
- package/commons/components/modal.xml +37 -0
- package/commons/components/navigation.xml +59 -0
- package/commons/components/popups.xml +12 -0
- package/commons/components/searchbar.xml +40 -0
- package/commons/components/table.xml +120 -0
- package/commons/components/toasts.xml +13 -0
- package/commons/components/viewport.xml +48 -0
- package/commons/forms/FieldItem.xml +20 -0
- package/commons/forms/FormController.ts +73 -0
- package/commons/forms/Item.ts +68 -0
- package/commons/forms/ItemCollection.ts +152 -0
- package/commons/forms/ItemController.ts +62 -0
- package/commons/forms/MetadataService.ts +45 -0
- package/commons/forms/MultiEnumField.xml +44 -0
- package/commons/forms/MultivalueController.ts +49 -0
- package/commons/forms/SmartareaField.xml +51 -0
- package/commons/forms/SuggestionController.ts +80 -0
- package/commons/forms/SuggestionField.xml +28 -0
- package/commons/forms/fields.xml +138 -0
- package/commons/forms/form.xml +37 -0
- package/commons/forms/index.ts +29 -0
- package/commons/index.ts +6 -0
- package/commons/pipes/index.ts +21 -0
- package/commons/pipes/showCounts.ts +16 -0
- package/commons/services/ErrorHandlingService.ts +11 -0
- package/commons/services/FirebaseService.ts +185 -0
- package/commons/services/GSheetsService.ts +92 -0
- package/commons/services/HeartbeatService.ts +35 -0
- package/commons/services/Invoke.ts +13 -0
- package/commons/services/JsonpService.ts +18 -0
- package/commons/services/Loader.ts +17 -0
- package/commons/services/LocalStorage.ts +46 -0
- package/commons/services/NavigationService.ts +55 -0
- package/commons/services/Service.ts +68 -0
- package/commons/services/ServiceWorker.ts +111 -0
- package/commons/services/ToastService.ts +49 -0
- package/commons/services/index.ts +14 -0
- package/core/Arrmatura.ts +79 -0
- package/core/CNode.ts +112 -0
- package/core/Component.ts +431 -0
- package/core/Platform.ts +3 -0
- package/core/cnodes/composition.ts +100 -0
- package/core/cnodes/conditionals.ts +51 -0
- package/core/cnodes/elementary.ts +45 -0
- package/core/cnodes/iterations.ts +94 -0
- package/core/cnodes/routing.ts +25 -0
- package/core/compileNode.ts +39 -0
- package/core/expression.ts +73 -0
- package/core/index.ts +2 -0
- package/core/register.ts +43 -0
- package/core/utils.ts +25 -0
- package/core-web/WebPlatform.ts +25 -0
- package/core-web/attributes.ts +197 -0
- package/core-web/elements.ts +64 -0
- package/core-web/index.ts +19 -0
- package/core-web/reflow.ts +30 -0
- package/core-web/type.ts +19 -0
- package/docs/index.js +191 -4077
- package/lib/arrayGroupBy.ts +19 -0
- package/lib/arraySortBy.ts +49 -0
- package/lib/arrayToObject.ts +18 -0
- package/lib/codus.ts +25 -0
- package/lib/consts.ts +3 -0
- package/lib/createListeners.ts +12 -0
- package/lib/curry.ts +8 -0
- package/lib/dateFormat.ts +69 -0
- package/lib/dateOf.ts +31 -0
- package/lib/dateParseISO8601.ts +27 -0
- package/lib/fx.ts +62 -0
- package/lib/guid.ts +7 -0
- package/lib/index.ts +19 -0
- package/lib/l10n.ts +40 -0
- package/lib/nextId.ts +9 -0
- package/lib/noop.ts +8 -0
- package/lib/obj.ts +62 -0
- package/lib/predicat.ts +26 -0
- package/lib/resources.ru.ts +20 -0
- package/lib/resources.ts +20 -0
- package/lib/scalar.ts +25 -0
- package/lib/str.ts +76 -0
- package/lib/urls.ts +91 -0
- package/lib/xml.ts +117 -0
- package/package.json +17 -8
- package/docs/index.js.map +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { arrayToObject } from '../../lib';
|
|
2
|
+
import { Service } from '../services/Service';
|
|
3
|
+
|
|
4
|
+
export class MultivalueController extends Service {
|
|
5
|
+
value: string = '';
|
|
6
|
+
options: any;
|
|
7
|
+
optionsHash?: Data;
|
|
8
|
+
|
|
9
|
+
get items() {
|
|
10
|
+
return this.bits.map((id) => ({ id, name: this.optionsHash?.[id]?.name || id }));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get bits() {
|
|
14
|
+
return this.value ? this.value.split(',') : [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
setOptions(opts: Obj[]) {
|
|
18
|
+
this.optionsHash = arrayToObject(opts);
|
|
19
|
+
this.options = opts;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get availableOptions() {
|
|
23
|
+
const result = this.options?.filter((e) => !this.value?.includes(e.id));
|
|
24
|
+
return result?.length ? result : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
change(data?: Hash) {
|
|
28
|
+
throw new Error('change not defined.' + data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
onToggleOpened(_: never, { opened = false }) {
|
|
32
|
+
return { opened: !opened };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
onReset() {
|
|
36
|
+
this.change({ value: '' });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
onInsert({ value = '' }) {
|
|
40
|
+
this.change({ value: [...this.bits, value].join(',') });
|
|
41
|
+
return { opened: false };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onDelete({ value = '' }) {
|
|
45
|
+
this.change({
|
|
46
|
+
value: this.bits.filter((e) => e !== value).join(','),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<component id="Smartarea">
|
|
2
|
+
<div class="form-autocomplete" ui:if="options">
|
|
3
|
+
|
|
4
|
+
<ui:fragment ui:if="disabled">
|
|
5
|
+
<ui:then>
|
|
6
|
+
<div class="form-input" style="height: auto;min-height:1.5rem;">{value}</div>
|
|
7
|
+
</ui:then>
|
|
8
|
+
<ui:else>
|
|
9
|
+
<div class="has-icon-right" click="{open|bindArgs:true}">
|
|
10
|
+
<div class="form-input text-{value|then::gray}" style="height: auto;min-height:1.5rem;">{value|or:@placeholder}</div>
|
|
11
|
+
<i class="form-icon icon icon-edit c-hand"></i>
|
|
12
|
+
</div>
|
|
13
|
+
</ui:else>
|
|
14
|
+
</ui:fragment>
|
|
15
|
+
|
|
16
|
+
<Modal open="{opened}" mode="sm" ui:if="{opened}" close="{open|bindArgs:false}">
|
|
17
|
+
<Modal:header>
|
|
18
|
+
<h6>{caption}</h6>
|
|
19
|
+
</Modal:header>
|
|
20
|
+
|
|
21
|
+
<div class="form-autocomplete">
|
|
22
|
+
<div class="has-icon-right">
|
|
23
|
+
<textarea class="form-input" style="padding-right: 3rem;height:6rem;" keypress="{change}" value="{value}" />
|
|
24
|
+
<i class="form-icon icon icon-{value|then:cross:arrow-down} c-hand" click="{reset}" ui:if="value"></i>
|
|
25
|
+
</div>
|
|
26
|
+
<div style="overflow-y: auto;height:15rem;margin:0.5rem 0;">
|
|
27
|
+
<ul class="menu2" style="min-width: 180px;list-style: none;">
|
|
28
|
+
<li class="menu-item" ui:for="item of options" data-value="{item.id}" click="{append}">
|
|
29
|
+
<a>
|
|
30
|
+
<div class="tile tile-centered">
|
|
31
|
+
<div class="tile-content">🏷️ {item.name|or:@item.id}</div>
|
|
32
|
+
</div>
|
|
33
|
+
</a>
|
|
34
|
+
</li>
|
|
35
|
+
</ul>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<Modal:footer>
|
|
40
|
+
<Btn class="float-right" mode="primary" title="OK" action="{open|bindArgs:false}"/>
|
|
41
|
+
</Modal:footer>
|
|
42
|
+
</Modal>
|
|
43
|
+
</div>
|
|
44
|
+
</component>
|
|
45
|
+
|
|
46
|
+
<component id="SmartareaField">
|
|
47
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
48
|
+
<SuggestionController ui:ref="ctrl" options="{options}" value="{value}" change="{onChange}"/>
|
|
49
|
+
<Smartarea caption="{caption}" disabled="{disabled}" options="<-ctrl.smartOptions" value="{value}" disabled="{disabled}" opened="{opened}" open="->opened" placeholder="{placeholder}" change="->ctrl.change" append="->ctrl.append" reset="->ctrl.cutLast"/>
|
|
50
|
+
</FieldItem>
|
|
51
|
+
</component>
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Service } from '../services/Service';
|
|
2
|
+
|
|
3
|
+
export class SuggestionController extends Service {
|
|
4
|
+
value: string = '';
|
|
5
|
+
options: any;
|
|
6
|
+
|
|
7
|
+
get bits() {
|
|
8
|
+
return this.value
|
|
9
|
+
? this.value
|
|
10
|
+
.split(',')
|
|
11
|
+
.map((s) => s.trim())
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
: [];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get lastBitIncluded() {
|
|
17
|
+
let bits = this.bits;
|
|
18
|
+
if (!bits.length) return false;
|
|
19
|
+
const lastBit = bits[bits.length - 1] || '';
|
|
20
|
+
return this.options.some((e) => e.id === lastBit);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get smartOptions() {
|
|
24
|
+
let options = this.options;
|
|
25
|
+
let bits = this.bits;
|
|
26
|
+
if (!bits.length) return options;
|
|
27
|
+
const lastBit = bits[bits.length - 1] || '';
|
|
28
|
+
if (lastBit.length > 1 && !this.lastBitIncluded) {
|
|
29
|
+
const keyword = lastBit.toLowerCase();
|
|
30
|
+
options = options.filter((e) => (e.name || e.id).toLowerCase().includes(keyword));
|
|
31
|
+
// if (options2.length) {
|
|
32
|
+
// options = options2;
|
|
33
|
+
// }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
options = options.filter((e) => !bits.includes(e.id));
|
|
37
|
+
// if (options2.length === 1 && value === options2[0].id) return null;
|
|
38
|
+
return options.slice(0, 15);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get actualOptions() {
|
|
42
|
+
const options = this.options;
|
|
43
|
+
const value = String(this.value || '');
|
|
44
|
+
const keyword = value?.toLowerCase() || '';
|
|
45
|
+
if (keyword.length < 2) return options.slice(0, 15);
|
|
46
|
+
const options2 = options.filter((e) => (e.name || e.id).toLowerCase().startsWith(keyword));
|
|
47
|
+
if (options2.length === 1 && value === options2[0].id) return null;
|
|
48
|
+
return options2.slice(0, 15);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
change(data?: Hash) {
|
|
52
|
+
throw new Error('change not defined.' + data);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onOpen(opened = false) {
|
|
56
|
+
return { opened };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
onReset() {
|
|
60
|
+
this.change({ value: '' });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onAppend({ value = '' }) {
|
|
64
|
+
const bits = this.bits;
|
|
65
|
+
if (!this.lastBitIncluded) {
|
|
66
|
+
bits.pop();
|
|
67
|
+
}
|
|
68
|
+
this.change({ value: [...bits, value].join(', ') });
|
|
69
|
+
}
|
|
70
|
+
onCutLast() {
|
|
71
|
+
const bits = this.bits;
|
|
72
|
+
bits.pop();
|
|
73
|
+
this.change({ value: [...bits].join(', ') });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
onChange({ value = '' }) {
|
|
77
|
+
this.change({ value });
|
|
78
|
+
return { opened: false };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<component id="SuggestionInput">
|
|
2
|
+
<div class="form-autocomplete">
|
|
3
|
+
<div class="has-icon-right">
|
|
4
|
+
<input class="form-input" type="text" disabled="{disabled}" placeholder="{placeholder}" focus="{open|bindArgs:true}" blur2="{open|bindArgs:false|nextTick:200}" keypress="{change}" value="{value}" />
|
|
5
|
+
<i class="form-icon icon icon-{value|then:cross:arrow-down} c-hand" style="margin-right:{options.length|then:1.5:0.25}rem;" click="{reset}" ui:if="disabled|not|and:@value"></i>
|
|
6
|
+
<ui:fragment ui:if="options|and:@options.length">
|
|
7
|
+
<i class="form-icon icon icon-arrow-up c-hand" click="{open|bindArgs:false}" ui:if="opened"></i>
|
|
8
|
+
<i class="form-icon icon icon-arrow-down c-hand" click="{open|bindArgs:true}" ui:if="opened|not"></i>
|
|
9
|
+
</ui:fragment>
|
|
10
|
+
</div>
|
|
11
|
+
<ul class="menu" style="position2: relative; min-width: 180px;max-height:7rem;overflow-y:auto;" ui:if="{options|and:@options.length|and:@opened}">
|
|
12
|
+
<li class="menu-item" ui:for="item of options" data-value="{item.id}" click="{change}">
|
|
13
|
+
<a>
|
|
14
|
+
<div class="tile tile-centered">
|
|
15
|
+
<div class="tile-content">{item.name|or:@item.id}</div>
|
|
16
|
+
</div>
|
|
17
|
+
</a>
|
|
18
|
+
</li>
|
|
19
|
+
</ul>
|
|
20
|
+
</div>
|
|
21
|
+
</component>
|
|
22
|
+
|
|
23
|
+
<component id="SuggestionField">
|
|
24
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
25
|
+
<SuggestionController ui:ref="ctrl" options="{options}" value="{value}" change="{onChange}"/>
|
|
26
|
+
<SuggestionInput options="<-ctrl.actualOptions" value="{value}" opened="<- ctrl.opened" open="-> ctrl.open" disabled="{disabled}" change="->ctrl.change" append="->ctrl.change" reset="->ctrl.reset"/>
|
|
27
|
+
</FieldItem>
|
|
28
|
+
</component>
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<component id="Field">
|
|
2
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
3
|
+
<input class="form-input" type="text" disabled="{disabled}" placeholder="{placeholder}" value="{value}" change="{onChange}" />
|
|
4
|
+
</FieldItem>
|
|
5
|
+
</component>
|
|
6
|
+
|
|
7
|
+
<component id="TextField">
|
|
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}" />
|
|
10
|
+
</FieldItem>
|
|
11
|
+
</component>
|
|
12
|
+
|
|
13
|
+
<component id="UrlField">
|
|
14
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
15
|
+
<div class="input-group has-icon-right">
|
|
16
|
+
<ui:fragment ui:if="{editing}">
|
|
17
|
+
<ui:then>
|
|
18
|
+
<input class="form-input border-0" disabled="{disabled}" type="url" blur="-> editing|not" placeholder="{caption}" value="{value}" change="{onChange}" />
|
|
19
|
+
<button class="btn btn-primary input-group-btn" click="-> editing|not">
|
|
20
|
+
<i class="icon icon-check"/>
|
|
21
|
+
</button>
|
|
22
|
+
</ui:then>
|
|
23
|
+
<ui:else>
|
|
24
|
+
<a class="form-input text-ellipsis" href="{value}" target="{id}">{value}</a>
|
|
25
|
+
<i class="form-icon icon icon-edit c-hand" click="->editing" ui:if="disabled|not"/>
|
|
26
|
+
</ui:else>
|
|
27
|
+
</ui:fragment>
|
|
28
|
+
</div>
|
|
29
|
+
</FieldItem>
|
|
30
|
+
</component>
|
|
31
|
+
|
|
32
|
+
<component id="EmailField">
|
|
33
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
34
|
+
<input class="form-input" disabled="{disabled}" type="email" placeholder="{caption}" value="{value}" change="{onChange}" />
|
|
35
|
+
</FieldItem>
|
|
36
|
+
</component>
|
|
37
|
+
|
|
38
|
+
<component id="DateField">
|
|
39
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
40
|
+
<input class="form-input" disabled="{disabled}" type="date" placeholder="{caption}" value="{value}" change="{onChange}" />
|
|
41
|
+
</FieldItem>
|
|
42
|
+
</component>
|
|
43
|
+
|
|
44
|
+
<component id="DateAndWorkingHoursField">
|
|
45
|
+
<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}" />
|
|
48
|
+
<Select class="form-select ml-2" disabled="{disabled}" change="{onChange|assignFirstArgKeyValue:key:@typeSpec}" emptyCaption="-" value="{data|dot:@typeSpec}" options=":enums.workingHours" />
|
|
49
|
+
</div>
|
|
50
|
+
</FieldItem>
|
|
51
|
+
</component>
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
<component id="DateAndTimeField">
|
|
55
|
+
<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}" />
|
|
58
|
+
<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
|
+
</div>
|
|
60
|
+
</FieldItem>
|
|
61
|
+
</component>
|
|
62
|
+
|
|
63
|
+
<component id="DatePeriodField">
|
|
64
|
+
<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}" />
|
|
67
|
+
<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
|
+
</div>
|
|
69
|
+
</FieldItem>
|
|
70
|
+
</component>
|
|
71
|
+
|
|
72
|
+
<component id="DateTimeField">
|
|
73
|
+
<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}" />
|
|
75
|
+
</FieldItem>
|
|
76
|
+
</component>
|
|
77
|
+
|
|
78
|
+
<component id="TimeField">
|
|
79
|
+
<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}" />
|
|
81
|
+
</FieldItem>
|
|
82
|
+
</component>
|
|
83
|
+
|
|
84
|
+
<component id="NumberField">
|
|
85
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
86
|
+
<input class="form-input" disabled="{disabled}" type="number" placeholder="{placeholder}" value="{value}" change="{onChange}" />
|
|
87
|
+
</FieldItem>
|
|
88
|
+
</component>
|
|
89
|
+
|
|
90
|
+
<component id="TextareaField">
|
|
91
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
92
|
+
<textarea class="form-input" style="min-height: 15vw" disabled="{disabled}" placeholder="{placeholder}" rows="3" change="{onChange}" value="{value}"></textarea>
|
|
93
|
+
</FieldItem>
|
|
94
|
+
</component>
|
|
95
|
+
|
|
96
|
+
<component id="BooleanField">
|
|
97
|
+
<div class="form-group">
|
|
98
|
+
<div class="col-sm-12 flexoid">
|
|
99
|
+
<label class="form-switch">
|
|
100
|
+
<span>{caption}</span>
|
|
101
|
+
<input type="checkbox" toggle="{onChange}" checked="{value|truthy}" />
|
|
102
|
+
<i class="form-icon"></i>
|
|
103
|
+
</label>
|
|
104
|
+
<div class="border-1 centroid text-secondary s-circle m-1 px-2" style="height:1.3rem; width:1.3rem;" data-value="{showHint|not}" click="->showHint|dot:value" ui:if="{hint}">
|
|
105
|
+
<span>?</span>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<div class="col-12" ui:if="{showHint}">
|
|
109
|
+
<p class="form-input-hint mb-1">{hint}</p>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</component>
|
|
113
|
+
|
|
114
|
+
<component id="EnumField">
|
|
115
|
+
<FieldItem caption="{caption}" class={class} error="{error}" required="{required}" hint="{hint}">
|
|
116
|
+
<Select class="form-select" change="{onChange}" value="{value|or:@defaultValue}" options="{options}" emptyCaption="{emptyCaption}" disabled="{disabled}" />
|
|
117
|
+
</FieldItem>
|
|
118
|
+
</component>
|
|
119
|
+
|
|
120
|
+
<component id="SelectField">
|
|
121
|
+
<FieldItem caption="{caption}" class="{class}" error="{error}" required="{required}" hint="{hint}">
|
|
122
|
+
<Select class="form-select" change="{onChange}" value="{value|or:@defaultValue}" options="{options}" emptyCaption="{emptyCaption}" disabled="{disabled}" />
|
|
123
|
+
</FieldItem>
|
|
124
|
+
</component>
|
|
125
|
+
|
|
126
|
+
<component id="ChoiceField">
|
|
127
|
+
<FieldItem caption="{caption}" class="{class}" error="{error}" required="{required}" hint="{hint}">
|
|
128
|
+
<div class="btn-group btn-group-block">
|
|
129
|
+
<Btn title="{option.name}" class="option-{value}{option.id|equals:@value|then::-other}" mode="{option.id|equals:@value|then:primary}" data="{option.id|pack:value}" action="{onChange}" ui:for="option of options"/>
|
|
130
|
+
</div>
|
|
131
|
+
</FieldItem>
|
|
132
|
+
</component>
|
|
133
|
+
|
|
134
|
+
<component id="RadioField">
|
|
135
|
+
<FieldItem caption="{caption}" error="{error}" required="{required}" hint="{hint}">
|
|
136
|
+
<RadioGroup change="{onChange}" value="{value}" data="{data}" options=":enums.{typeSpec}" disabled="{disabled}" />
|
|
137
|
+
</FieldItem>
|
|
138
|
+
</component>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<component id="Forma">
|
|
2
|
+
<div autocomplete="off" class="form-horizontal px-2">
|
|
3
|
+
<FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" />
|
|
4
|
+
<ui:tag tag="{field.type|capitalize}Field" ui:props="{field}" ui:if="field.visible" ui:for="field of <-formController.fieldProps" />
|
|
5
|
+
</div>
|
|
6
|
+
</component>
|
|
7
|
+
|
|
8
|
+
<component id="GroupForma">
|
|
9
|
+
<div autocomplete="off" class="form-horizontal px-2">
|
|
10
|
+
<FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" />
|
|
11
|
+
<FieldGroup ui:props="{group}" ui:for="group of <-formController.fieldsGroups" />
|
|
12
|
+
</div>
|
|
13
|
+
</component>
|
|
14
|
+
|
|
15
|
+
<component id="TabbedForma">
|
|
16
|
+
<div autocomplete="off" class="form-horizontal px-2">
|
|
17
|
+
<FormController ui:ref="formController" data="{data}" meta="{meta}" change="{change}" />
|
|
18
|
+
<TabbedFormaContent ui:props="<-formController.tabsInfo" />
|
|
19
|
+
</div>
|
|
20
|
+
</component>
|
|
21
|
+
|
|
22
|
+
<component id="TabbedFormaContent">
|
|
23
|
+
<Tabs items="{tabs}" change="->tabId|dot:id" value="{tabId|or:@initialTab}" ui:if="enabled" />
|
|
24
|
+
<div ui:if="tabId|or:@initialTab|equals:@tab.id" ui:for="tab of tabs">
|
|
25
|
+
<FieldGroup ui:props="{group}" ui:for="group of tab.items" />
|
|
26
|
+
</div>
|
|
27
|
+
</component>
|
|
28
|
+
|
|
29
|
+
<component id="FieldGroup">
|
|
30
|
+
<h6 class="mt-2 pt-2 px-2 text-gray">{id}</h6>
|
|
31
|
+
<div class="panel">
|
|
32
|
+
<div class="p-2">
|
|
33
|
+
<ui:tag tag="{field.type|capitalize}Field" ui:props="{field}" ui:if="field.visible" ui:for="field of items" />
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</component>
|
|
37
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { FormController } from './FormController';
|
|
2
|
+
import { ItemCollection } from './ItemCollection';
|
|
3
|
+
import { ItemController } from './ItemController';
|
|
4
|
+
import { MetadataService } from './MetadataService';
|
|
5
|
+
import { MultivalueController } from './MultivalueController';
|
|
6
|
+
import { SuggestionController } from './SuggestionController';
|
|
7
|
+
|
|
8
|
+
import form from './form.xml';
|
|
9
|
+
import fields from './fields.xml';
|
|
10
|
+
|
|
11
|
+
import SuggestionField from './SuggestionField.xml';
|
|
12
|
+
import SmartareaField from './SmartareaField.xml';
|
|
13
|
+
import FieldItem from './FieldItem.xml';
|
|
14
|
+
import MultiEnumField from './MultiEnumField.xml';
|
|
15
|
+
|
|
16
|
+
export default [
|
|
17
|
+
form,
|
|
18
|
+
fields,
|
|
19
|
+
SuggestionField,
|
|
20
|
+
SmartareaField,
|
|
21
|
+
FieldItem,
|
|
22
|
+
MultiEnumField,
|
|
23
|
+
FormController,
|
|
24
|
+
ItemCollection,
|
|
25
|
+
ItemController,
|
|
26
|
+
MetadataService,
|
|
27
|
+
MultivalueController,
|
|
28
|
+
SuggestionController,
|
|
29
|
+
];
|
package/commons/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as libs from '../../lib';
|
|
2
|
+
import { dateFormat, dateOf, urlParse } from '../../lib';
|
|
3
|
+
import { showCounts } from './showCounts';
|
|
4
|
+
|
|
5
|
+
// useful pipes
|
|
6
|
+
export const pipes: Hash<Pipe> = {
|
|
7
|
+
...libs,
|
|
8
|
+
|
|
9
|
+
queryParam: (_, key: string) => urlParse(window.location.search).params[key],
|
|
10
|
+
queryParams: (_) => urlParse(window.location.search).params,
|
|
11
|
+
date: (x) => dateFormat(dateOf(x)),
|
|
12
|
+
rest: (x, pos = 1) => (x ? x.slice(pos) : []),
|
|
13
|
+
findByKey: (x, key, val) => x?.find?.((e: Hash) => e[key] == val),
|
|
14
|
+
filterByKey: (x, key, val) => x?.filter?.((e: Hash) => !e[key] || e[key] == val),
|
|
15
|
+
resolve: (x, id, key) => x?.find?.((e: Hash) => e.id == id)?.[key],
|
|
16
|
+
limit: (x, size = 50) => (x ? x.slice(0, size) : []),
|
|
17
|
+
ifAbove: (x, limit = 0) => (+x > +limit ? x : ''),
|
|
18
|
+
showCounts,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default pipes;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const showCounts = (counts: Obj, nicNema = '⭕', neZnojdzeno = '⭕') => {
|
|
2
|
+
if (!counts) {
|
|
3
|
+
return '…';
|
|
4
|
+
}
|
|
5
|
+
const { total, actual } = counts;
|
|
6
|
+
if (!total) {
|
|
7
|
+
return nicNema;
|
|
8
|
+
}
|
|
9
|
+
if (!actual) {
|
|
10
|
+
return neZnojdzeno;
|
|
11
|
+
}
|
|
12
|
+
if (actual === total) {
|
|
13
|
+
return total;
|
|
14
|
+
}
|
|
15
|
+
return actual + ' / ' + total;
|
|
16
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Service } from './Service';
|
|
2
|
+
|
|
3
|
+
export class ErrorHandlingService extends Service {
|
|
4
|
+
handleError({ message = '', code, source = {} }) {
|
|
5
|
+
this.show({ message: source + ': ' + message, code, mode: 'error' });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
show({ message = '', code = '', source }) {
|
|
9
|
+
console.error(source + ': ERROR: ', code, message);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { Service } from './Service';
|
|
2
|
+
|
|
3
|
+
const unpackDocs = (s) =>
|
|
4
|
+
s.docs.reduce((r, e) => {
|
|
5
|
+
const d = e.data();
|
|
6
|
+
d.id = e.id;
|
|
7
|
+
r.push(d);
|
|
8
|
+
return r;
|
|
9
|
+
}, []);
|
|
10
|
+
|
|
11
|
+
const firebase = window.firebase;
|
|
12
|
+
|
|
13
|
+
export class FirebaseService {
|
|
14
|
+
firestore: any;
|
|
15
|
+
auth: any;
|
|
16
|
+
providers: { Google: any };
|
|
17
|
+
|
|
18
|
+
constructor({ ...config }) {
|
|
19
|
+
firebase.initializeApp(config);
|
|
20
|
+
this.firestore = firebase.firestore();
|
|
21
|
+
this.firestore.enablePersistence({ synchronizeTabs: true }).catch(function (err) {
|
|
22
|
+
if (err.code === 'failed-precondition') {
|
|
23
|
+
// Multiple tabs open, persistence can only be enabled
|
|
24
|
+
// in one tab at a time.
|
|
25
|
+
// ...
|
|
26
|
+
} else if (err.code === 'unimplemented') {
|
|
27
|
+
// The current browser does not support all of the
|
|
28
|
+
// features required to enable persistence
|
|
29
|
+
// ...
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
this.auth = firebase.auth();
|
|
33
|
+
this.auth.languageCode = 'by';
|
|
34
|
+
this.providers = {
|
|
35
|
+
Google: firebase.auth.GoogleAuthProvider,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
done() {
|
|
40
|
+
firebase.app().delete();
|
|
41
|
+
}
|
|
42
|
+
// auth
|
|
43
|
+
signInAnonymously() {
|
|
44
|
+
this.auth.signInAnonymously().catch(function (error) {
|
|
45
|
+
var errorMessage = error.message;
|
|
46
|
+
console.log('signInAnonymously err=' + errorMessage);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
listenUser(cb) {
|
|
50
|
+
this.auth.onAuthStateChanged(cb);
|
|
51
|
+
}
|
|
52
|
+
getCurrentUser() {
|
|
53
|
+
return this.auth.currentUser;
|
|
54
|
+
}
|
|
55
|
+
linkProvider() {
|
|
56
|
+
const provider = new this.providers.Google();
|
|
57
|
+
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
|
|
58
|
+
return this.auth
|
|
59
|
+
.signInWithPopup(provider)
|
|
60
|
+
.then((result) => {
|
|
61
|
+
// This gives you a Google Access Token. You can use it to access the Google API.
|
|
62
|
+
// var token = result.credential.accessToken
|
|
63
|
+
// The signed-in user info.
|
|
64
|
+
var googleUser = result.user;
|
|
65
|
+
var credential = this.auth.GoogleAuthProvider.credential(googleUser.getAuthResponse().id_token);
|
|
66
|
+
this.getCurrentUser()
|
|
67
|
+
.linkAndRetrieveDataWithCredential(credential)
|
|
68
|
+
.then(
|
|
69
|
+
function (usercred) {
|
|
70
|
+
var user = usercred.user;
|
|
71
|
+
console.log('Anonymous account successfully upgraded', user);
|
|
72
|
+
},
|
|
73
|
+
function (error) {
|
|
74
|
+
console.log('Error upgrading anonymous account', error);
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
// ...
|
|
78
|
+
})
|
|
79
|
+
.catch(function (error) {
|
|
80
|
+
// Handle Errors here.
|
|
81
|
+
// var errorCode = error.code
|
|
82
|
+
var errorMessage = error.message;
|
|
83
|
+
// The email of the user's account used.
|
|
84
|
+
// var email = error.email
|
|
85
|
+
// The firebase.auth.AuthCredential type that was used.
|
|
86
|
+
// var credential = error.credential
|
|
87
|
+
console.log('signInAnonymously err=' + errorMessage);
|
|
88
|
+
// ...
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
logout() {
|
|
92
|
+
return this.signInAnonymously();
|
|
93
|
+
}
|
|
94
|
+
getCollection(coll, since) {
|
|
95
|
+
return ((c) => (since ? c.where('modified_at', '>', since) : c))(this.firestore.collection(coll))
|
|
96
|
+
.get()
|
|
97
|
+
.then(unpackDocs);
|
|
98
|
+
}
|
|
99
|
+
// db
|
|
100
|
+
listenCollection(coll, ts = 0, cb) {
|
|
101
|
+
return ((c) => (ts ? c.where('modified_at', '>', ts) : c))(this.firestore.collection(coll)).onSnapshot(function (
|
|
102
|
+
querySnapshot
|
|
103
|
+
) {
|
|
104
|
+
var r = [];
|
|
105
|
+
querySnapshot.forEach(function (e) {
|
|
106
|
+
const d = e.data();
|
|
107
|
+
d.id = e.id;
|
|
108
|
+
r.push(d);
|
|
109
|
+
});
|
|
110
|
+
cb(null, { [coll]: r });
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
nextId(coll) {
|
|
114
|
+
return this.firestore.collection(coll).doc().id;
|
|
115
|
+
}
|
|
116
|
+
update(delta) {
|
|
117
|
+
const now = Date.now().valueOf();
|
|
118
|
+
// Get a new write batch
|
|
119
|
+
var batch = this.firestore.batch();
|
|
120
|
+
Object.keys(delta).forEach((coll) => {
|
|
121
|
+
const c = this.firestore.collection(coll);
|
|
122
|
+
delta[coll].forEach((d) => {
|
|
123
|
+
d.modified_at = now;
|
|
124
|
+
if (!d.created_at) {
|
|
125
|
+
d.created_at = now;
|
|
126
|
+
}
|
|
127
|
+
var ref = c.doc('' + d.id);
|
|
128
|
+
batch.set(ref, d, { merge: true });
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
return batch.commit();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export class FirebaseUserService extends Service {
|
|
136
|
+
get fb() {
|
|
137
|
+
return this.$.app.firebase.impl;
|
|
138
|
+
}
|
|
139
|
+
init() {
|
|
140
|
+
this.fb.listenUser((user) => {
|
|
141
|
+
if (user) {
|
|
142
|
+
} else {
|
|
143
|
+
this.fb.signInAnonymously();
|
|
144
|
+
// User is signed out.
|
|
145
|
+
// ...
|
|
146
|
+
}
|
|
147
|
+
this.emit('signed');
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
getInfo() {
|
|
151
|
+
const user = this.fb.getCurrentUser();
|
|
152
|
+
if (user !== null) {
|
|
153
|
+
user.providerData.forEach(function (profile) {
|
|
154
|
+
// console.log('Sign-in provider: ' + profile.providerId)
|
|
155
|
+
// console.log(' Provider-specific UID: ' + profile.uid)
|
|
156
|
+
// console.log(' Name: ' + profile.displayName)
|
|
157
|
+
// console.log(' Email: ' + profile.email)
|
|
158
|
+
// console.log(' Photo URL: ' + profile.photoURL)
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return !user
|
|
162
|
+
? { isLoading: true }
|
|
163
|
+
: {
|
|
164
|
+
...user,
|
|
165
|
+
isLoading: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
onLogin() {
|
|
169
|
+
return this.fb.linkProvider();
|
|
170
|
+
}
|
|
171
|
+
onLogout() {
|
|
172
|
+
return this.fb.logout();
|
|
173
|
+
}
|
|
174
|
+
onSigned() {
|
|
175
|
+
return {
|
|
176
|
+
_: NaN,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const hot = typeof module === 'undefined' ? null : module.hot;
|
|
182
|
+
if (hot) {
|
|
183
|
+
// hot.addStatusHandler(function (d) {})
|
|
184
|
+
hot.accept();
|
|
185
|
+
}
|