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,120 @@
|
|
|
1
|
+
<component id="LoadingIndicator">
|
|
2
|
+
<div class="loading {large|then:loading-lg} {class}" />
|
|
3
|
+
</component>
|
|
4
|
+
|
|
5
|
+
<component id="EmptyData">
|
|
6
|
+
<div class="empty w100 {class}">
|
|
7
|
+
<div class="empty-icon">
|
|
8
|
+
<Icon type="{icon|or:people}" />
|
|
9
|
+
</div>
|
|
10
|
+
<p class="empty-title h5">{title}</p>
|
|
11
|
+
<p ui:if="{subtitle}" class="empty-subtitle">{subtitle}</p>
|
|
12
|
+
<div class="empty-action">
|
|
13
|
+
<ui:slot />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
</component>
|
|
17
|
+
|
|
18
|
+
<component id="LoadableItems">
|
|
19
|
+
<LoadingIndicator class="empty" ui:if="data|not">
|
|
20
|
+
<ui:else>
|
|
21
|
+
<h5 class="error text-error" ui:if="data.error">{data.error.message|or:@data.error}</h5>
|
|
22
|
+
<EmptyData title="{emptyCaption|or:Empty}" ui:if="data.length|not">
|
|
23
|
+
<ui:else>
|
|
24
|
+
<div style="{style}" ui:for="item of data">
|
|
25
|
+
<ui:tag tag="{itemType}" ui:props="{item}" item="{item}" context="{context}" action="{action}" />
|
|
26
|
+
</div>
|
|
27
|
+
</ui:else>
|
|
28
|
+
</EmptyData>
|
|
29
|
+
</ui:else>
|
|
30
|
+
</LoadingIndicator>
|
|
31
|
+
</component>
|
|
32
|
+
|
|
33
|
+
<component id="LoadableEntity">
|
|
34
|
+
<LoadingIndicator class="empty" ui:if="data|not">
|
|
35
|
+
<ui:else>
|
|
36
|
+
<h5 class="error text-error" ui:if="data.error">
|
|
37
|
+
<span>{data.error.message|or:@data.error}</span>
|
|
38
|
+
<ui:else>
|
|
39
|
+
<EmptyData title="{emptyCaption|or:Empty}" ui:if="data.id|not">
|
|
40
|
+
<ui:else>
|
|
41
|
+
<ui:tag tag="{type}" data="{data}" context="{context}" action="{action}" />
|
|
42
|
+
</ui:else>
|
|
43
|
+
</EmptyData>
|
|
44
|
+
</ui:else>
|
|
45
|
+
</h5>
|
|
46
|
+
</ui:else>
|
|
47
|
+
</LoadingIndicator>
|
|
48
|
+
</component>
|
|
49
|
+
|
|
50
|
+
<component id="Cell">
|
|
51
|
+
<span class="" style="padding-left: 0px">{value}</span>
|
|
52
|
+
</component>
|
|
53
|
+
|
|
54
|
+
<component id="EnumCell">
|
|
55
|
+
<span class="" style="padding-left: 0px">:map.{typeSpec}.{value}</span>
|
|
56
|
+
</component>
|
|
57
|
+
|
|
58
|
+
<component id="DateCell">
|
|
59
|
+
<span class="" style="padding-left: 0px">{value|date}</span>
|
|
60
|
+
</component>
|
|
61
|
+
|
|
62
|
+
<component id="Table">
|
|
63
|
+
<table style="position: relative">
|
|
64
|
+
<thead class="table-thead">
|
|
65
|
+
<tr>
|
|
66
|
+
<th class="col-index" style="position: sticky; top: -1"></th>
|
|
67
|
+
<th class="col-index" style="position: sticky; top: -1" ui:for="item of columns">
|
|
68
|
+
<span>{item.name}</span>
|
|
69
|
+
<div class="table-column-sorter" ui:if="item.sortable">
|
|
70
|
+
<span class="table-column-sorter-up off" title="↑" data-id="-{item.id}" click="{doSort}">
|
|
71
|
+
<i class="anticon anticon-caret-up"></i
|
|
72
|
+
> </span>
|
|
73
|
+
<span class="table-column-sorter-down off" title="↓" data-id="{item.id}" click="{doSort}">
|
|
74
|
+
<i class="anticon anticon-caret-down"></i
|
|
75
|
+
> </span>
|
|
76
|
+
</div>
|
|
77
|
+
</th>
|
|
78
|
+
</tr>
|
|
79
|
+
</thead>
|
|
80
|
+
<TableRow ui:for="item of data" data="{item}" selected="{item.selected}" columns="{columns}" kind="{kind}" />
|
|
81
|
+
</table>
|
|
82
|
+
</component>
|
|
83
|
+
|
|
84
|
+
<component id="TableRow">
|
|
85
|
+
<tbody class="table-tbody">
|
|
86
|
+
<tr class="table-row table-row-level-0" style="position: sticky">
|
|
87
|
+
<td class="col-index" data-id="{data.id}" click="{doItem}">
|
|
88
|
+
<div ui:if="{expandable}" role="button" class="table-row-expand-icon table-row-expanded" aria-label="Collapse row" data-selected="false" click="->">
|
|
89
|
+
<ui:else>
|
|
90
|
+
<div role="button" class="table-row-expand-icon table-row-collapsed" aria-label="Expand row" data-selected="true" click="->"></div>
|
|
91
|
+
</ui:else>
|
|
92
|
+
</div>
|
|
93
|
+
</td>
|
|
94
|
+
<td class="col-index" ui:for="col of columns">
|
|
95
|
+
<ui:tag tag="{col.type|capitalize}Cell" ui:props="{col}" value="{col.id|propObjectValueByKey}" data="{data}" />
|
|
96
|
+
</td>
|
|
97
|
+
</tr>
|
|
98
|
+
<tr ui:if="{expanded}" class="table-expanded-row table-expanded-row-level-1">
|
|
99
|
+
<td class=""></td>
|
|
100
|
+
<td class="" colspan="100">
|
|
101
|
+
<ui:tag tag="{kind|capitalize}RowInsider" data="{data}" />
|
|
102
|
+
</td>
|
|
103
|
+
</tr>
|
|
104
|
+
</tbody>
|
|
105
|
+
</component>
|
|
106
|
+
|
|
107
|
+
<component id="Chips">
|
|
108
|
+
<div class="py-2" style="z-index: 2;{style}">
|
|
109
|
+
<div class="ui mini labels">
|
|
110
|
+
<span class="chip c-hand {tag.selected|then:bg-success:bg-secondary} {tag.count|then:badge}" data-badge="{tag.count}" style="padding:1rem" data-id="{tag.id}" click="{click}" ui:for="tag of data">
|
|
111
|
+
<span>
|
|
112
|
+
<span>{tag.name}</span>
|
|
113
|
+
</span>
|
|
114
|
+
</span>
|
|
115
|
+
<span ui:if="data|findByKey:selected:true|truthy" class="label py-1 px-2 s-circle c-hand" click="{click|assignFirstArgKeyValue:id:}">
|
|
116
|
+
<Icon type="cross"/>
|
|
117
|
+
</span>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
</component>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<component id="ToastList">
|
|
2
|
+
<div style="position:absolute;top:1.5rem;right:1rem;left:1rem;z-index:3;{style}">
|
|
3
|
+
<Toast ui:props={toast} ui:for="toast of <-toasters.items"/>
|
|
4
|
+
</div>
|
|
5
|
+
</component>
|
|
6
|
+
|
|
7
|
+
<component id="Toast">
|
|
8
|
+
<div class="toast mt-1 toast-{mode|or:@mode|or:primary}">
|
|
9
|
+
<button class="btn btn-clear float-right" click="{close}"></button>
|
|
10
|
+
<p>{message}</p>
|
|
11
|
+
</div>
|
|
12
|
+
</component>
|
|
13
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<component id="ViewPort">
|
|
2
|
+
<ui:fragment ui:if="slot(aside)">
|
|
3
|
+
<Sidebar caption="{caption}" size="{sidebarWidth}">
|
|
4
|
+
<Sidebar:aside>
|
|
5
|
+
<ui:slot id="aside" />
|
|
6
|
+
</Sidebar:aside>
|
|
7
|
+
<main>
|
|
8
|
+
<ui:slot />
|
|
9
|
+
</main>
|
|
10
|
+
</Sidebar>
|
|
11
|
+
<ui:else>
|
|
12
|
+
<main>
|
|
13
|
+
<ui:slot />
|
|
14
|
+
</main>
|
|
15
|
+
</ui:else>
|
|
16
|
+
</ui:fragment>
|
|
17
|
+
</component>
|
|
18
|
+
|
|
19
|
+
<component id="Sidebar">
|
|
20
|
+
<div class="off-canvas off-canvas-sidebar-show">
|
|
21
|
+
<a class="off-canvas-toggle btn btn-primary btn-action show-lg" href="#sidebar">
|
|
22
|
+
<Icon type="menu" />
|
|
23
|
+
</a>
|
|
24
|
+
<div id="sidebar" class="off-canvas-sidebar">
|
|
25
|
+
<Aside caption="{caption}">
|
|
26
|
+
<ui:slot id="aside" />
|
|
27
|
+
</Aside>
|
|
28
|
+
</div>
|
|
29
|
+
<a class="off-canvas-overlay" href="#"></a>
|
|
30
|
+
<div class="off-canvas-content">
|
|
31
|
+
<ui:slot />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</component>
|
|
35
|
+
|
|
36
|
+
<component id="Aside">
|
|
37
|
+
<aside style="display:flex; flex-direction: column; height: 100vh; width:{size|or:300}px">
|
|
38
|
+
<div class="text-center" ui:if="{caption}">
|
|
39
|
+
<a href="#/main">
|
|
40
|
+
<h5 class="p-2">{caption}</h5>
|
|
41
|
+
</a>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="m-2" style="flex: 1; overflow-y: auto">
|
|
44
|
+
<ui:slot />
|
|
45
|
+
</div>
|
|
46
|
+
</aside>
|
|
47
|
+
</component>
|
|
48
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<component id="FieldItem">
|
|
2
|
+
<div class="form-group {error|then:has-error} {class}">
|
|
3
|
+
<div class="col-4 col-sm-12">
|
|
4
|
+
<label class="form-label d-flex" for="input-example-1" ui:if="{caption}">
|
|
5
|
+
<div class="text-ellipsis" title="{caption}">{caption}</div>
|
|
6
|
+
<div ui:if="{required}" class="text-error text-tiny ml-1">✱</div>
|
|
7
|
+
<div class="border-1 text-secondary s-circle centroid mx-1 px-2" style="height:1.3rem;width:1.3rem;" data-value="{showHint|not}" click="->showHint|dot:value" ui:if="{hint}">
|
|
8
|
+
<span>?</span>
|
|
9
|
+
</div>
|
|
10
|
+
</label>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="col-8 col-sm-12">
|
|
13
|
+
<ui:slot />
|
|
14
|
+
</div>
|
|
15
|
+
<div class="col-12">
|
|
16
|
+
<p class="form-input-hint mb-1" ui:if="{showHint}">{hint}</p>
|
|
17
|
+
<p class="form-input-hint mb-1" ui:if="{error}">{error}</p>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
</component>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { arrayGroupBy } from 'arrayGroupBy';
|
|
2
|
+
import { getPredicat } from 'predicat';
|
|
3
|
+
import { strFormat } from '../../lib';
|
|
4
|
+
import { Service } from '../services/Service';
|
|
5
|
+
|
|
6
|
+
export class FormController extends Service {
|
|
7
|
+
data: { [k: string]: unknown } = {};
|
|
8
|
+
meta: any;
|
|
9
|
+
error: any;
|
|
10
|
+
change: any;
|
|
11
|
+
|
|
12
|
+
setData(d: Data) {
|
|
13
|
+
const data = { ...d };
|
|
14
|
+
this.data = data;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get fieldsGroups(): Obj[] {
|
|
18
|
+
return Object.values(arrayGroupBy(this.fieldProps, 'group'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get tabsInfo() {
|
|
22
|
+
const tabs = Object.values(arrayGroupBy(this.fieldProps, ({ tab = 'tab' }) => tab)).map(({ id, items }) => ({
|
|
23
|
+
id,
|
|
24
|
+
name: id,
|
|
25
|
+
items: Object.values(arrayGroupBy(items, 'group')),
|
|
26
|
+
}));
|
|
27
|
+
return { enabled: tabs.length > 1, initialTab: tabs?.[0]?.id, tabs };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resolveEnum(typeSpec: string = '', data: Hash) {
|
|
31
|
+
if (typeSpec[0] === '[') {
|
|
32
|
+
return typeSpec
|
|
33
|
+
.slice(1, typeSpec.length - 1)
|
|
34
|
+
.split(',')
|
|
35
|
+
.map((s) => s.trim().split('='))
|
|
36
|
+
.map(([id, name = id]) => ({ id, name }));
|
|
37
|
+
}
|
|
38
|
+
const [key, subid, subKey] = strFormat(typeSpec, data).split('.');
|
|
39
|
+
if (subKey) {
|
|
40
|
+
return (this.res('enums.' + key) || []).find(({ id }) => id === subid)?.[subKey] || [];
|
|
41
|
+
}
|
|
42
|
+
return this.res('enums.' + key) || [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get fieldProps() {
|
|
46
|
+
return this.meta?.map((field: { [k: string]: string }) => {
|
|
47
|
+
const { id, visible, disabled = false, caption, name, type, typeSpec } = field;
|
|
48
|
+
const form = this;
|
|
49
|
+
const data = this.data;
|
|
50
|
+
const value = data[id];
|
|
51
|
+
return {
|
|
52
|
+
...field,
|
|
53
|
+
error: (form.error && form.error.fields && form.error.fields[id]) || null,
|
|
54
|
+
get disabled(): boolean {
|
|
55
|
+
return getPredicat(disabled)(data);
|
|
56
|
+
},
|
|
57
|
+
get visible(): boolean {
|
|
58
|
+
return getPredicat(visible || 'true')(data);
|
|
59
|
+
},
|
|
60
|
+
get options(): Data[] | Data | null {
|
|
61
|
+
return form.resolveEnum(typeSpec, data);
|
|
62
|
+
},
|
|
63
|
+
value: value === undefined ? null : value,
|
|
64
|
+
caption: caption || name,
|
|
65
|
+
data,
|
|
66
|
+
onChange: ({ value, key }: { value: unknown; key?: string }) => {
|
|
67
|
+
this.data = { ...this.data, [key || id]: value };
|
|
68
|
+
this.change?.(this.data);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { arraySortBy } from 'arraySortBy';
|
|
2
|
+
|
|
3
|
+
export const analyzeDataByTags = (data: Item[], selection: Hash<boolean>, initials = '') => {
|
|
4
|
+
const sel = Object.entries(selection)
|
|
5
|
+
.filter(([_, val]) => val)
|
|
6
|
+
.map(([key]) => key);
|
|
7
|
+
|
|
8
|
+
if (sel.length || !initials) {
|
|
9
|
+
const actualData = data.filter((e) => sel.reduce((r, s) => r && e.$tags.has(s), true));
|
|
10
|
+
const tagsHash = actualData.reduce((r, e) => {
|
|
11
|
+
e.$tags.forEach((id: string) => {
|
|
12
|
+
const tag = r[id] || (r[id] = { id, name: id, count: 0, selected: selection[id] });
|
|
13
|
+
if (!tag.selected) {
|
|
14
|
+
tag.count++;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return r;
|
|
18
|
+
}, {});
|
|
19
|
+
return { actualData, tags: Object.values(tagsHash) };
|
|
20
|
+
} else {
|
|
21
|
+
const tags = initials.split(',').map((id) => ({ id, name: id, count: 0, selected: false }));
|
|
22
|
+
data.forEach((e: Item) => {
|
|
23
|
+
tags.forEach((tag) => {
|
|
24
|
+
if (e.$tags.has(tag.id)) {
|
|
25
|
+
tag.count++;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
return { actualData: data, tags };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const getSuggestions = (data: Item[], kluq: string) => {
|
|
34
|
+
if (!kluq) return null;
|
|
35
|
+
|
|
36
|
+
const words = {};
|
|
37
|
+
data.forEach((e) =>
|
|
38
|
+
e.$searchData.replace(/\w{3,}/g, (w) => {
|
|
39
|
+
words[w] = 1;
|
|
40
|
+
return '';
|
|
41
|
+
})
|
|
42
|
+
);
|
|
43
|
+
return arraySortBy(
|
|
44
|
+
Object.keys(words)
|
|
45
|
+
.filter((e) => e.includes(kluq))
|
|
46
|
+
.map((s) => ({ name: s, id: s })),
|
|
47
|
+
'name'
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export class Item {
|
|
52
|
+
constructor(opts = {}, opts2 = {}) {
|
|
53
|
+
Object.assign(this, opts, opts2);
|
|
54
|
+
}
|
|
55
|
+
get ts() {
|
|
56
|
+
return this.$ts || (this.$ts = this.modified_at || this.created_at || -1);
|
|
57
|
+
}
|
|
58
|
+
set ts(_) {}
|
|
59
|
+
get $tags() {
|
|
60
|
+
return this.$$tags || (this.$$tags = new Set((this.status + ',' + this.type || '').split(',').filter(Boolean)));
|
|
61
|
+
}
|
|
62
|
+
get title() {
|
|
63
|
+
return this.$title || (this.$title = this.name || this.subject || this.topic || this.id);
|
|
64
|
+
}
|
|
65
|
+
get $searchData() {
|
|
66
|
+
return this.$$searchData || (this.$$searchData = (this.subject + ' ' + this.notes).toLowerCase());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { Service } from '../services/Service';
|
|
2
|
+
import { analyzeDataByTags, getSuggestions, Item } from './Item';
|
|
3
|
+
import { arraySortBy } from 'arraySortBy';
|
|
4
|
+
|
|
5
|
+
export class ItemCollection extends Service {
|
|
6
|
+
selection: Hash = {};
|
|
7
|
+
shownLimit = 50;
|
|
8
|
+
sortBy = '-ts';
|
|
9
|
+
kluqPoshuku = '';
|
|
10
|
+
data?: Item[];
|
|
11
|
+
inputKluqPoshuku: any;
|
|
12
|
+
newEntry: any;
|
|
13
|
+
entry: any;
|
|
14
|
+
initials = '';
|
|
15
|
+
|
|
16
|
+
setData(data: Hash[]) {
|
|
17
|
+
this.data = !data ? [] : data.filter((e) => e.status !== 'deleted').map((e) => new Item(e));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getInfo() {
|
|
21
|
+
if (!this.data) {
|
|
22
|
+
return {};
|
|
23
|
+
}
|
|
24
|
+
const searchedData = !this.kluqPoshuku
|
|
25
|
+
? this.data
|
|
26
|
+
: this.data.filter((e) => e.$searchData.includes(this.kluqPoshuku));
|
|
27
|
+
const { actualData, tags } = analyzeDataByTags(searchedData, this.selection, this.initials);
|
|
28
|
+
const sortedData = arraySortBy(actualData, this.sortBy);
|
|
29
|
+
return {
|
|
30
|
+
tags: tags.sort((a, b) => (a.selected === b.selected ? (a.id > b.id ? 1 : -1) : !a.selected ? 1 : -1)),
|
|
31
|
+
data: sortedData.slice(0, this.shownLimit),
|
|
32
|
+
openEntry: ({ id }) =>
|
|
33
|
+
this.emit(
|
|
34
|
+
'this.openEntry',
|
|
35
|
+
this.data?.find((e) => e.id === id)
|
|
36
|
+
),
|
|
37
|
+
counts: {
|
|
38
|
+
total: this.data.length,
|
|
39
|
+
actual: actualData.length,
|
|
40
|
+
hasMore: this.shownLimit < actualData.length,
|
|
41
|
+
},
|
|
42
|
+
search: {
|
|
43
|
+
kluq: this.kluqPoshuku,
|
|
44
|
+
value: this.inputKluqPoshuku,
|
|
45
|
+
suggestions:
|
|
46
|
+
this.kluqPoshuku === this.inputKluqPoshuku ? null : getSuggestions(this.data, this.inputKluqPoshuku),
|
|
47
|
+
input: (data) => this.emit('this.inputKluq', data),
|
|
48
|
+
enter: (data) => this.emit('this.znajdzPoKluqu', data),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
onTag({ id }, { selection }: this) {
|
|
54
|
+
return {
|
|
55
|
+
selection: !id
|
|
56
|
+
? {}
|
|
57
|
+
: {
|
|
58
|
+
...selection,
|
|
59
|
+
[id]: !selection[id],
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onZnajdzPoKluqu({ value }) {
|
|
65
|
+
return {
|
|
66
|
+
inputKluqPoshuku: value || '',
|
|
67
|
+
kluqPoshuku: value || '',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
onInputKluq({ value }) {
|
|
71
|
+
return {
|
|
72
|
+
inputKluqPoshuku: value || '',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
onSortBy({ value }) {
|
|
76
|
+
return {
|
|
77
|
+
sortBy: value || '-ts',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
onShowMore({ size = 50 }, { shownLimit = 50 }) {
|
|
81
|
+
return {
|
|
82
|
+
shownLimit: shownLimit + size,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* New Entry
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
onOpenAddNew(data) {
|
|
90
|
+
return {
|
|
91
|
+
newEntry: {
|
|
92
|
+
...data,
|
|
93
|
+
...this.newEntry,
|
|
94
|
+
cancel: () => this.emit('this.cancelAddNew'),
|
|
95
|
+
submit: (data) => this.emit('this.saveNew', data),
|
|
96
|
+
open: true,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onCancelAddNew() {
|
|
102
|
+
return {
|
|
103
|
+
newEntry: { ...this.newEntry, open: false },
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async onSaveNew(data) {
|
|
108
|
+
delete data.id;
|
|
109
|
+
await this.upsert(data);
|
|
110
|
+
return {
|
|
111
|
+
newEntry: { ...this.newEntry, data, open: false },
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
upsert(data: any) {
|
|
115
|
+
throw new Error('Method not implemented.');
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Entry
|
|
119
|
+
*/
|
|
120
|
+
|
|
121
|
+
onOpenEntry(data) {
|
|
122
|
+
return {
|
|
123
|
+
entry: {
|
|
124
|
+
id: data.id,
|
|
125
|
+
data: Object.entries(data).reduce((r, [k, v]) => {
|
|
126
|
+
if (k[0] !== '$') {
|
|
127
|
+
r[k] = v;
|
|
128
|
+
}
|
|
129
|
+
return r;
|
|
130
|
+
}, {}),
|
|
131
|
+
...this.entry,
|
|
132
|
+
cancel: () => this.emit('this.cancelEntry'),
|
|
133
|
+
submit: (data) => this.emit('this.saveEntry', data),
|
|
134
|
+
delete: () => this.emit('this.deleteEntry', { id: data.id }),
|
|
135
|
+
open: true,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onCancelEntry() {
|
|
141
|
+
return { entry: { open: false } };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async onSaveEntry(data) {
|
|
145
|
+
await this.upsert(data);
|
|
146
|
+
return { entry: { open: false } };
|
|
147
|
+
}
|
|
148
|
+
async onDeleteEntry(data) {
|
|
149
|
+
await this.upsert({ ...data, status: 'deleted' });
|
|
150
|
+
return { entry: { open: false } };
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Service } from '../services/Service';
|
|
2
|
+
import { Item } from './Item';
|
|
3
|
+
import { getPredicat } from 'predicat';
|
|
4
|
+
|
|
5
|
+
export class ItemController extends Service {
|
|
6
|
+
all?: Hash<Hash>;
|
|
7
|
+
delta?: Hash;
|
|
8
|
+
itemId: string = '';
|
|
9
|
+
meta: any;
|
|
10
|
+
context: any;
|
|
11
|
+
|
|
12
|
+
get initialData() {
|
|
13
|
+
const initial = this.all?.[this.itemId];
|
|
14
|
+
if (!initial?.id) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const { $row, ...d } = initial;
|
|
18
|
+
return d;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get actualData() {
|
|
22
|
+
const initial = this.initialData;
|
|
23
|
+
if (!initial) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
const data = { ...this.context, ...initial, ...this.delta };
|
|
27
|
+
this.meta?.map((field: { [k: string]: string }) => {
|
|
28
|
+
const { id, visible } = field;
|
|
29
|
+
const isVisible = getPredicat(visible || 'true')(data);
|
|
30
|
+
if (!isVisible && data[id]) {
|
|
31
|
+
data[id] = '';
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get item() {
|
|
38
|
+
return new Item(this.actualData || {});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
upsert(data?: Hash) {
|
|
42
|
+
throw new Error('upsert not defined.' + data);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onReset = () => ({ delta: {} });
|
|
46
|
+
|
|
47
|
+
onChange = (delta: Hash) => ({ delta: { ...this.delta, ...delta } });
|
|
48
|
+
|
|
49
|
+
onSave() {
|
|
50
|
+
const data = this.actualData;
|
|
51
|
+
if (data) {
|
|
52
|
+
this.upsert(data);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
onDelete() {
|
|
57
|
+
const id = this.initialData?.id;
|
|
58
|
+
if (id) {
|
|
59
|
+
this.upsert({ id, status: 'deleted' });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { JsonpService } from '../services/JsonpService';
|
|
2
|
+
import { LocalStorage } from '../services/LocalStorage';
|
|
3
|
+
import { Service } from '../services/Service';
|
|
4
|
+
|
|
5
|
+
export class MetadataService extends Service {
|
|
6
|
+
local = new LocalStorage('local');
|
|
7
|
+
url: string = '';
|
|
8
|
+
remote = new JsonpService();
|
|
9
|
+
version: any;
|
|
10
|
+
resources = {};
|
|
11
|
+
meta = { fields: [] };
|
|
12
|
+
kind: string = '';
|
|
13
|
+
|
|
14
|
+
init() {
|
|
15
|
+
return this.syncAll();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
syncAll() {
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
const skey = '$meta_' + (this.kind || encodeURIComponent(this.url));
|
|
21
|
+
const { version, ...meta } = this.local.get(skey) || {};
|
|
22
|
+
if (this.version && version >= this.version) {
|
|
23
|
+
Object.assign(this.resources, meta, { strings: { ...this.resources.strings, ...meta.strings } });
|
|
24
|
+
return { ready: true, meta };
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
ready: false,
|
|
28
|
+
'*': fetch(this.url, { redirect: 'follow' })
|
|
29
|
+
.then((res) => res.json())
|
|
30
|
+
.then(
|
|
31
|
+
(meta) => {
|
|
32
|
+
this.log('metadata ', now, meta);
|
|
33
|
+
// this.toast('metadata loaded');
|
|
34
|
+
Object.assign(this.resources, meta, { strings: { ...this.resources.strings, ...meta.strings } });
|
|
35
|
+
this.local.set(skey, meta);
|
|
36
|
+
return { ready: true, meta };
|
|
37
|
+
},
|
|
38
|
+
(err) => {
|
|
39
|
+
this.toast('error: metadata ' + JSON.stringify(err?.message || err || 'unknown'));
|
|
40
|
+
return { ready: true, meta: {} };
|
|
41
|
+
}
|
|
42
|
+
),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<component id="MultiEnumAppender">
|
|
2
|
+
<div class="form-autocomplete" ui:if="options">
|
|
3
|
+
<div class="has-icon-right" click="{toggle}">
|
|
4
|
+
<div class="form-input text-gray">{placeholder}</div>
|
|
5
|
+
<i class="form-icon icon icon-{opened|then:arrow-up:plus} c-hand"></i>
|
|
6
|
+
</div>
|
|
7
|
+
<Modal open="{opened}" mode="sm" close="{toggle}">
|
|
8
|
+
<Modal:header>
|
|
9
|
+
<h6>{caption}</h6>
|
|
10
|
+
</Modal:header>
|
|
11
|
+
<div style="overflow-y: auto;height:15rem;margin:0;">
|
|
12
|
+
<ul class="menu2" style="min-width: 180px;list-style: none;">
|
|
13
|
+
<li class="menu-item" ui:for="option of options" click="-> ctrl.insert|assignKeyValue:value:@option.id">
|
|
14
|
+
<a>
|
|
15
|
+
<div class="tile tile-centered">
|
|
16
|
+
<div class="tile-content">🔸 {option.name|or:@option.id}</div>
|
|
17
|
+
</div>
|
|
18
|
+
</a>
|
|
19
|
+
</li>
|
|
20
|
+
</ul>
|
|
21
|
+
</div>
|
|
22
|
+
<!-- <Modal:footer>
|
|
23
|
+
<Btn class="float-right" mode="primary" title="OK" action="{toggle}"/>
|
|
24
|
+
</Modal:footer> -->
|
|
25
|
+
</Modal>
|
|
26
|
+
</div>
|
|
27
|
+
</component>
|
|
28
|
+
|
|
29
|
+
<component id="MultiEnumField">
|
|
30
|
+
<FieldItem caption="{caption}" class={class} error="{error}" required="{required}" hint="{hint}">
|
|
31
|
+
|
|
32
|
+
<MultivalueController ui:ref="ctrl" value="{value}" options="{options}" change="{onChange}"/>
|
|
33
|
+
|
|
34
|
+
<div class="mb-1 has-icon-right" ui:for="item of <-ctrl.items">
|
|
35
|
+
<div class="form-input" style="height: auto; min-height: 1.5rem;">
|
|
36
|
+
<div class="">{item.name}</div>
|
|
37
|
+
<i class="form-icon icon icon-cross p-2 c-hand" click="-> ctrl.delete|assignKeyValue:value:@item.id" ui:if="disabled|not"></i>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<MultiEnumAppender caption="➕ {caption}" placeholder="{placeholder}" options="<-ctrl.availableOptions" opened="<-ctrl.opened" toggle="->ctrl.toggleOpened" ui:if="disabled|not"/>
|
|
42
|
+
|
|
43
|
+
</FieldItem>
|
|
44
|
+
</component>
|