@itfin/components 1.3.42 → 1.3.44
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/package.json +3 -2
- package/src/components/filter/ConditionGroup.vue +196 -0
- package/src/components/filter/FilterInput.vue +64 -0
- package/src/components/panels/Panel.vue +153 -0
- package/src/components/panels/PanelList.vue +197 -0
- package/src/components/table/Table2.vue +5 -0
- package/src/components/table/TableBody.vue +21 -6
- package/src/components/table/TableGroup.vue +5 -2
- package/src/helpers/formatters.js +5 -2
- package/src/helpers/formatters.spec.js +17 -0
- package/src/components/filter/FilterBadge.vue +0 -78
- package/src/components/filter/FilterControl.vue +0 -105
- package/src/components/filter/Rule.vue +0 -129
- package/src/components/filter/RuleGroup.vue +0 -205
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itfin/components",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.44",
|
|
4
4
|
"author": "Vitalii Savchuk <esvit666@gmail.com>",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"serve": "vue-cli-service serve",
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
"@popperjs/core": "^2.11.8",
|
|
39
39
|
"@shopify/draggable": "^1.0.0-beta.8",
|
|
40
40
|
"@vue/composition-api": "^1.7.1",
|
|
41
|
+
"@vue/vue2-jest": "^27.0.0",
|
|
41
42
|
"@vue/web-component-wrapper": "^1.3.0",
|
|
42
43
|
"air-datepicker": "^3.3.5",
|
|
43
44
|
"bootstrap": "^5.3.x",
|
|
@@ -56,7 +57,6 @@
|
|
|
56
57
|
"vue-virtual-scroller": "^1.1.2"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
|
-
"@vue/cli-service": "^5.0.1",
|
|
60
60
|
"@babel/eslint-parser": "^7.19.1",
|
|
61
61
|
"@babel/plugin-proposal-numeric-separator": "^7.18.6",
|
|
62
62
|
"@babel/plugin-syntax-numeric-separator": "^7.10.4",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@vue/cli-plugin-babel": "^5.0.8",
|
|
68
68
|
"@vue/cli-plugin-eslint": "^5.0.8",
|
|
69
69
|
"@vue/cli-plugin-unit-jest": "^5.0.8",
|
|
70
|
+
"@vue/cli-service": "^5.0.1",
|
|
70
71
|
"@vue/eslint-config-airbnb": "^7.0.0",
|
|
71
72
|
"@vue/test-utils": "^1.1.1",
|
|
72
73
|
"babel-eslint": "^10.1.0",
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="b-condition-group-input w-100 pt-1 pb-2 px-2">
|
|
4
|
+
<div class="d-flex align-items-start">
|
|
5
|
+
<div v-if="value.conditions.length > 1" class="condition-group-type">
|
|
6
|
+
<itf-select
|
|
7
|
+
:value="value.link"
|
|
8
|
+
:reduce="(item) => item.value"
|
|
9
|
+
:options="[{ label: 'AND', value: 'and' }, { label: 'OR', value: 'or' }]"
|
|
10
|
+
@input="onLinkUpdate"
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
<div class="flex-1 pt-1">
|
|
14
|
+
<div v-for="(filter, n) in value.conditions" :key="n" :class="{'tree-node': value.conditions.length > 1}">
|
|
15
|
+
<div class="d-flex align-items-start">
|
|
16
|
+
<div class="flex-grow-1 py-2 me-2">
|
|
17
|
+
<itf-condition-group
|
|
18
|
+
v-if="filter.link"
|
|
19
|
+
:level="level + 1"
|
|
20
|
+
class="rounded"
|
|
21
|
+
:value="filter"
|
|
22
|
+
@input="onFilterUpdate(n, $event)"
|
|
23
|
+
:class="{'bg-group1': level === 0, 'bg-group2': level === 1}"
|
|
24
|
+
/>
|
|
25
|
+
<filter-input
|
|
26
|
+
v-else
|
|
27
|
+
:value="filter"
|
|
28
|
+
@input="onFilterUpdate(n, $event)"
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="pt-2 mt-1">
|
|
32
|
+
<itf-button small icon @click="removeField(n)">
|
|
33
|
+
<itf-icon name="trash" />
|
|
34
|
+
</itf-button>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<itf-button small secondary @click="addCondition">
|
|
42
|
+
<itf-icon name="plus" />
|
|
43
|
+
Add condition
|
|
44
|
+
</itf-button>
|
|
45
|
+
<itf-button v-if="level < maxDepth" small secondary @click="addConditionGroup">
|
|
46
|
+
<itf-icon name="plus" />
|
|
47
|
+
Add group
|
|
48
|
+
</itf-button>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
</template>
|
|
52
|
+
<style>
|
|
53
|
+
.b-condition-group-input {
|
|
54
|
+
--b-filterGroupBorder: rgba(0,0,0,.05);
|
|
55
|
+
--b-filterGroup1Bg: #f9fafb;
|
|
56
|
+
--b-filterGroup2Bg: #f1f2f4;
|
|
57
|
+
|
|
58
|
+
[data-theme="dark"] & {
|
|
59
|
+
--b-filterGroup1Bg: #303436;
|
|
60
|
+
--b-filterGroup2Bg: #3a3e41;
|
|
61
|
+
--b-filterGroupBorder: hsla(0,0%,100%,.03);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
.bg-group1 {
|
|
65
|
+
background: var(--b-filterGroup1Bg);
|
|
66
|
+
border: 1px solid var(--b-filterGroupBorder);
|
|
67
|
+
}
|
|
68
|
+
.bg-group2 {
|
|
69
|
+
background: var(--b-filterGroup2Bg);
|
|
70
|
+
border: 1px solid var(--b-filterGroupBorder);
|
|
71
|
+
}
|
|
72
|
+
.condition-group-type {
|
|
73
|
+
width: 85px;
|
|
74
|
+
z-index: 10;
|
|
75
|
+
margin-right: 1rem;
|
|
76
|
+
margin-top: .5rem;
|
|
77
|
+
margin-bottom: .5rem;
|
|
78
|
+
position: relative;
|
|
79
|
+
}
|
|
80
|
+
.flex-1 {
|
|
81
|
+
flex: 1 1 0%;
|
|
82
|
+
}
|
|
83
|
+
.tree-node {
|
|
84
|
+
--h-line-position: 1.5rem;
|
|
85
|
+
--line-color: #D2D5DF;
|
|
86
|
+
--line-width: 1px;
|
|
87
|
+
--line-radius: 10px;
|
|
88
|
+
--line-connection-position: -3.5rem;
|
|
89
|
+
--node-space: .5rem;
|
|
90
|
+
margin-bottom: var(--node-space);
|
|
91
|
+
position: relative;
|
|
92
|
+
|
|
93
|
+
&:first-child:before {
|
|
94
|
+
border-left: solid var(--line-width) var(--line-color);
|
|
95
|
+
top: var(--h-line-position);
|
|
96
|
+
height: calc(100% - var(--h-line-position) + var(--node-space));
|
|
97
|
+
width: var(--line-radius);
|
|
98
|
+
}
|
|
99
|
+
&:before {
|
|
100
|
+
display: inline-block;
|
|
101
|
+
border-left: solid var(--line-width) var(--line-color);
|
|
102
|
+
content: "";
|
|
103
|
+
height: calc(100% + var(--node-space));
|
|
104
|
+
position: absolute;
|
|
105
|
+
left: var(--line-connection-position);
|
|
106
|
+
}
|
|
107
|
+
&:first-child:after {
|
|
108
|
+
height: var(--line-radius);
|
|
109
|
+
}
|
|
110
|
+
&:after {
|
|
111
|
+
display: inline-block;
|
|
112
|
+
border-top: solid var(--line-width) var(--line-color);
|
|
113
|
+
content: "";
|
|
114
|
+
width: calc(var(--line-connection-position)* -1);
|
|
115
|
+
position: absolute;
|
|
116
|
+
top: var(--h-line-position);
|
|
117
|
+
left: var(--line-connection-position);
|
|
118
|
+
}
|
|
119
|
+
&:last-child:before {
|
|
120
|
+
border-color: var(--line-color);
|
|
121
|
+
top: 0%;
|
|
122
|
+
height: var(--h-line-position);
|
|
123
|
+
width: var(--line-radius);
|
|
124
|
+
border-bottom-left-radius: var(--line-radius);
|
|
125
|
+
}
|
|
126
|
+
&:last-child:after {
|
|
127
|
+
border-top: none;
|
|
128
|
+
border-bottom: solid var(--line-width) var(--line-color);
|
|
129
|
+
border-left: solid var(--line-width) var(--line-color);
|
|
130
|
+
height: var(--line-radius);
|
|
131
|
+
border-bottom-left-radius: var(--line-radius);
|
|
132
|
+
top: calc(var(--h-line-position) - var(--line-radius));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
136
|
+
<script>
|
|
137
|
+
import { Component, Prop, Model, Vue, Watch } from 'vue-property-decorator';
|
|
138
|
+
import itfButton from '@itfin/components/src/components/button/Button.vue';
|
|
139
|
+
import itfTextField from '@itfin/components/src/components/text-field/TextField.vue';
|
|
140
|
+
import itfDropdown from '@itfin/components/src/components/dropdown/Dropdown.vue';
|
|
141
|
+
import itfSelect from '@itfin/components/src/components/select/Select.vue';
|
|
142
|
+
import itfIcon from '@itfin/components/src/components/icon/Icon.vue';
|
|
143
|
+
import FilterInput from '~/components/Panels/FilterInput.vue';
|
|
144
|
+
|
|
145
|
+
export default @Component({
|
|
146
|
+
name: 'itfConditionGroup',
|
|
147
|
+
components: {
|
|
148
|
+
itfTextField,
|
|
149
|
+
itfSelect,
|
|
150
|
+
itfDropdown,
|
|
151
|
+
itfButton,
|
|
152
|
+
itfIcon,
|
|
153
|
+
FilterInput
|
|
154
|
+
}
|
|
155
|
+
})
|
|
156
|
+
class ConditionGroup extends Vue {
|
|
157
|
+
@Model('input', { default: () => ({ link: 'and', conditions: [] }) }) value;
|
|
158
|
+
@Prop({ default: 0 }) level;
|
|
159
|
+
@Prop({ default: 1 }) maxDepth;
|
|
160
|
+
|
|
161
|
+
onLinkUpdate(link) {
|
|
162
|
+
this.$emit('input', { ...this.value, link });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
onFilterUpdate(index, filter) {
|
|
166
|
+
this.applyValue((conditions) => {
|
|
167
|
+
conditions[index] = filter;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
applyValue(func) {
|
|
172
|
+
const newValue = { ...this.value, conditions: [...this.value.conditions] };
|
|
173
|
+
func(newValue.conditions);
|
|
174
|
+
this.$emit('input', newValue);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
addCondition() {
|
|
178
|
+
this.applyValue((conditions) => {
|
|
179
|
+
conditions.push({ operator: 'is', values: [] });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
addConditionGroup() {
|
|
184
|
+
this.applyValue((conditions) => {
|
|
185
|
+
conditions.push({ link: 'and', conditions: [] });
|
|
186
|
+
});
|
|
187
|
+
// this.filters.push({ type: 'group' });
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
removeField(index) {
|
|
191
|
+
this.applyValue((conditions) => {
|
|
192
|
+
conditions.splice(index, 1);
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
</script>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
|
|
3
|
+
<div class="d-flex">
|
|
4
|
+
<div class="w-25">
|
|
5
|
+
<itf-select :options="fields" />
|
|
6
|
+
</div>
|
|
7
|
+
<div class="w-25 px-1">
|
|
8
|
+
<itf-select
|
|
9
|
+
:reduce="(item) => item.id"
|
|
10
|
+
:get-option-label="(item) => item.title"
|
|
11
|
+
:options="conditions"
|
|
12
|
+
>
|
|
13
|
+
<template #selected-option="{ option }">
|
|
14
|
+
<span class="sign-box"> {{option.title}}</span>
|
|
15
|
+
</template>
|
|
16
|
+
<template #option="{ option }">
|
|
17
|
+
{{option.title}}
|
|
18
|
+
</template>
|
|
19
|
+
</itf-select>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="w-50">
|
|
22
|
+
<itf-text-field />
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
</template>
|
|
27
|
+
<style>
|
|
28
|
+
</style>
|
|
29
|
+
<script>
|
|
30
|
+
import { Component, Prop, Model, Vue, Watch } from 'vue-property-decorator';
|
|
31
|
+
import itfButton from '@itfin/components/src/components/button/Button.vue';
|
|
32
|
+
import itfTextField from '@itfin/components/src/components/text-field/TextField.vue';
|
|
33
|
+
import itfSelect from '@itfin/components/src/components/select/Select.vue';
|
|
34
|
+
import itfDropdown from '@itfin/components/src/components/dropdown/Dropdown.vue';
|
|
35
|
+
import itfIcon from '@itfin/components/src/components/icon/Icon.vue';
|
|
36
|
+
|
|
37
|
+
const conditions = [
|
|
38
|
+
{ id: 'eq', title: 'equal', sign: '=' },
|
|
39
|
+
{ id: 'notEq', title: 'not equal', sign: '≠' },
|
|
40
|
+
{ id: 'contains', title: 'contains', sign: '∋' },
|
|
41
|
+
{ id: 'noContains', title: 'not contains', sign: '∌' },
|
|
42
|
+
{ id: 'startsWith', title: 'starts with', sign: 'A…' },
|
|
43
|
+
{ id: 'endsWith', title: 'ends with', sign: '…Z' },
|
|
44
|
+
// { id: 'exists', title: 'exists', sign: '∃' }
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export default @Component({
|
|
48
|
+
name: 'itfFilterInput',
|
|
49
|
+
components: {
|
|
50
|
+
itfTextField,
|
|
51
|
+
itfSelect,
|
|
52
|
+
itfDropdown,
|
|
53
|
+
itfButton,
|
|
54
|
+
itfIcon
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
class FilterInput extends Vue {
|
|
58
|
+
@Prop() fields;
|
|
59
|
+
|
|
60
|
+
get conditions() {
|
|
61
|
+
return conditions;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
</script>
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div tabindex="-1" class="b-panel" :class="{'b-panel__collapsed': collapsed, 'b-panel__active': !collapsed}">
|
|
3
|
+
<div v-if="collapsed" class="b-panel__expand">
|
|
4
|
+
<itf-button v-if="closeable" icon small class="b-panel__expand_button" @click="closePanel">
|
|
5
|
+
<itf-icon name="cross" />
|
|
6
|
+
</itf-button>
|
|
7
|
+
<a href="" class="b-panel__expand_text_container" @click.stop.prevent="expandPanel">
|
|
8
|
+
<itf-icon v-if="icon" :name="icon" class="mt-2" />
|
|
9
|
+
|
|
10
|
+
<div class="b-panel__expand_text" v-text="title"></div>
|
|
11
|
+
</a>
|
|
12
|
+
</div>
|
|
13
|
+
<div v-show="!collapsed" class="b-panel-header">
|
|
14
|
+
<div>
|
|
15
|
+
<slot name="title">
|
|
16
|
+
<div class="b-panel__title" v-text="title"></div>
|
|
17
|
+
</slot>
|
|
18
|
+
</div>
|
|
19
|
+
<div>
|
|
20
|
+
<slot name="buttons"></slot>
|
|
21
|
+
<itf-button v-if="expandable" icon small class="b-panel__expand_button" @click="fullsizePanel">
|
|
22
|
+
<itf-icon name="expand" />
|
|
23
|
+
</itf-button>
|
|
24
|
+
<itf-button v-if="closeable" icon small class="b-panel__expand_button" @click="closePanel">
|
|
25
|
+
<itf-icon name="cross" />
|
|
26
|
+
</itf-button>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div v-show="!collapsed" class="b-panel-body">
|
|
30
|
+
<slot></slot>
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
<style lang="scss">
|
|
35
|
+
.b-panel {
|
|
36
|
+
--b-panel-bg: var(--bs-body-bg);
|
|
37
|
+
--b-panel-color: var(--bs-body-color);
|
|
38
|
+
--b-panel-box-shadow: 0px 2px 6px 0px rgba(21,23,25,.05);
|
|
39
|
+
|
|
40
|
+
margin-left: 8px;
|
|
41
|
+
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
border-radius: 6px;
|
|
45
|
+
margin-bottom: 8px;
|
|
46
|
+
margin-right: 8px;
|
|
47
|
+
margin-top: 8px;
|
|
48
|
+
z-index: 0;
|
|
49
|
+
background: var(--b-panel-bg);
|
|
50
|
+
box-shadow: var(--b-panel-box-shadow);
|
|
51
|
+
|
|
52
|
+
&__collapsed {
|
|
53
|
+
flex-basis: 38px;
|
|
54
|
+
flex-grow: 0;
|
|
55
|
+
flex-shrink: 0;
|
|
56
|
+
min-width: 38px;
|
|
57
|
+
overflow: hidden;
|
|
58
|
+
position: relative;
|
|
59
|
+
}
|
|
60
|
+
&__active {
|
|
61
|
+
overflow-y: hidden;
|
|
62
|
+
|
|
63
|
+
flex-basis: 0;
|
|
64
|
+
flex-grow: 3;
|
|
65
|
+
flex-shrink: 0;
|
|
66
|
+
min-width: 490px;
|
|
67
|
+
}
|
|
68
|
+
&__expand {
|
|
69
|
+
align-items: center;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
height: 100%;
|
|
73
|
+
position: relative;
|
|
74
|
+
padding-top: 2px;
|
|
75
|
+
}
|
|
76
|
+
&__expand_text_container {
|
|
77
|
+
text-decoration: none;
|
|
78
|
+
align-items: center;
|
|
79
|
+
justify-content: start;
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-basis: 0;
|
|
82
|
+
flex-direction: column;
|
|
83
|
+
flex-grow: 1;
|
|
84
|
+
flex-shrink: 0;
|
|
85
|
+
min-height: 0;
|
|
86
|
+
}
|
|
87
|
+
&__expand_text {
|
|
88
|
+
min-height: 0;
|
|
89
|
+
padding-bottom: 12px;
|
|
90
|
+
padding-top: 12px;
|
|
91
|
+
writing-mode: vertical-lr;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
.b-panel-body {
|
|
95
|
+
overflow: auto;
|
|
96
|
+
position: relative;
|
|
97
|
+
z-index: 0;
|
|
98
|
+
align-items: stretch;
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: column;
|
|
101
|
+
height: 100%;
|
|
102
|
+
}
|
|
103
|
+
.b-panel-header {
|
|
104
|
+
align-items: center;
|
|
105
|
+
background: var(--b-panel-bg);
|
|
106
|
+
color: var(--b-panel-color);
|
|
107
|
+
display: flex;
|
|
108
|
+
gap: 8px;
|
|
109
|
+
justify-content: space-between;
|
|
110
|
+
min-height: 45px;
|
|
111
|
+
min-width: 0;
|
|
112
|
+
padding: 8px;
|
|
113
|
+
position: sticky;
|
|
114
|
+
top: 0;
|
|
115
|
+
z-index: 10;
|
|
116
|
+
}
|
|
117
|
+
</style>
|
|
118
|
+
<script>
|
|
119
|
+
import { Vue, Prop, Component } from 'vue-property-decorator';
|
|
120
|
+
import itfIcon from '@itfin/components/src/components/icon/Icon';
|
|
121
|
+
import itfButton from '@itfin/components/src/components/button/Button.vue';
|
|
122
|
+
|
|
123
|
+
export default @Component({
|
|
124
|
+
components: {
|
|
125
|
+
itfIcon,
|
|
126
|
+
itfButton
|
|
127
|
+
},
|
|
128
|
+
directives: {
|
|
129
|
+
},
|
|
130
|
+
filters: {
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
class Panel extends Vue {
|
|
134
|
+
@Prop() title;
|
|
135
|
+
@Prop() icon;
|
|
136
|
+
@Prop() payload;
|
|
137
|
+
@Prop(Boolean) collapsed;
|
|
138
|
+
@Prop(Boolean) closeable;
|
|
139
|
+
@Prop(Boolean) expandable;
|
|
140
|
+
|
|
141
|
+
expandPanel() {
|
|
142
|
+
this.$emit('expand');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fullsizePanel() {
|
|
146
|
+
this.$emit('fullsize');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
closePanel() {
|
|
150
|
+
this.$emit('close');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
</script>
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="b-panel-list-container">
|
|
3
|
+
<div class="b-panel-list">
|
|
4
|
+
<template v-for="(panel, n) of panelsStack">
|
|
5
|
+
<panel
|
|
6
|
+
:key="n"
|
|
7
|
+
:title="panel.title"
|
|
8
|
+
:icon="panel.icon"
|
|
9
|
+
:payload="panel.payload"
|
|
10
|
+
:expandable="panelsStack.length > 1"
|
|
11
|
+
:collapsed="panel.isCollapsed"
|
|
12
|
+
:closeable="panel.isCloseable"
|
|
13
|
+
@expand="expandPanel(panel)"
|
|
14
|
+
@fullsize="fullsizePanel(panel)"
|
|
15
|
+
@close="closePanel(panel)"
|
|
16
|
+
>
|
|
17
|
+
<slot
|
|
18
|
+
:name="panel.type"
|
|
19
|
+
:panel="panel"
|
|
20
|
+
:payload="panel.payload"
|
|
21
|
+
:multiple="isOpenMultiple"
|
|
22
|
+
:open="(title, icon, type, payload) => openPanel(title, icon, type, payload, n + 1)"
|
|
23
|
+
:close="() => closePanel(panel)"
|
|
24
|
+
:expand="() => expandPanel(panel)"
|
|
25
|
+
:fullsize="() => fullsizePanel(panel)">
|
|
26
|
+
</slot>
|
|
27
|
+
<template v-if="$scopedSlots[`${panel.type}.title`]" #title>
|
|
28
|
+
<slot
|
|
29
|
+
:name="`${panel.type}.title`"
|
|
30
|
+
:panel="panel"
|
|
31
|
+
:payload="panel.payload"
|
|
32
|
+
:multiple="isOpenMultiple"
|
|
33
|
+
:open="(title, icon, type, payload) => openPanel(title, icon, type, payload, n + 1)"
|
|
34
|
+
:close="() => closePanel(panel)"
|
|
35
|
+
:expand="() => expandPanel(panel)"
|
|
36
|
+
:fullsize="() => fullsizePanel(panel)">
|
|
37
|
+
</slot>
|
|
38
|
+
</template>
|
|
39
|
+
<template v-if="$scopedSlots[`${panel.type}.buttons`]" #buttons>
|
|
40
|
+
<slot
|
|
41
|
+
:name="`${panel.type}.buttons`"
|
|
42
|
+
:panel="panel"
|
|
43
|
+
:payload="panel.payload"
|
|
44
|
+
:multiple="isOpenMultiple"
|
|
45
|
+
:open="(title, icon, type, payload) => openPanel(title, icon, type, payload, n + 1)"
|
|
46
|
+
:close="() => closePanel(panel)"
|
|
47
|
+
:expand="() => expandPanel(panel)"
|
|
48
|
+
:fullsize="() => fullsizePanel(panel)">
|
|
49
|
+
</slot>
|
|
50
|
+
</template>
|
|
51
|
+
</panel>
|
|
52
|
+
</template>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</template>
|
|
56
|
+
<style>
|
|
57
|
+
.b-panel-list-container {
|
|
58
|
+
width: 100%;
|
|
59
|
+
height: 100%;
|
|
60
|
+
position: relative;
|
|
61
|
+
flex-grow: 1;
|
|
62
|
+
}
|
|
63
|
+
.b-panel-list {
|
|
64
|
+
align-items: stretch;
|
|
65
|
+
//background: var(--bs-body-bg);
|
|
66
|
+
bottom: 0;
|
|
67
|
+
display: flex;
|
|
68
|
+
height: 100%;
|
|
69
|
+
left: 0;
|
|
70
|
+
overflow-x: auto;
|
|
71
|
+
position: absolute;
|
|
72
|
+
right: 0;
|
|
73
|
+
top: 0;
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
<script lang="ts">
|
|
77
|
+
import { Vue, Component, Prop } from 'vue-property-decorator';
|
|
78
|
+
import Panel from '~/components/Panels/Panel.vue';
|
|
79
|
+
|
|
80
|
+
interface IPanel {
|
|
81
|
+
id: number;
|
|
82
|
+
title: string;
|
|
83
|
+
icon: string;
|
|
84
|
+
type: string;
|
|
85
|
+
payload: any;
|
|
86
|
+
isCollapsed: boolean;
|
|
87
|
+
isCloseable: boolean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
@Component({
|
|
91
|
+
components: {
|
|
92
|
+
Panel
|
|
93
|
+
},
|
|
94
|
+
directives: {
|
|
95
|
+
},
|
|
96
|
+
filters: {
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
export default class PanelList extends Vue {
|
|
100
|
+
@Prop() firstPanel: IPanel;
|
|
101
|
+
|
|
102
|
+
panelsStack:IPanel[] = [];
|
|
103
|
+
|
|
104
|
+
nextId:number = 0;
|
|
105
|
+
|
|
106
|
+
created() {
|
|
107
|
+
if (this.firstPanel) {
|
|
108
|
+
this.openPanel(this.firstPanel.title, this.firstPanel.icon, this.firstPanel.type, this.firstPanel.payload);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
get isOpenMultiple() {
|
|
113
|
+
return this.panelsStack.filter(p => !p.isCollapsed).length > 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
expandPanel(panel: IPanel) {
|
|
117
|
+
const newStack = [...this.panelsStack];
|
|
118
|
+
const index = newStack.findIndex(p => p.id === panel.id);
|
|
119
|
+
newStack[index].isCollapsed = false;
|
|
120
|
+
this.panelsStack = newStack;
|
|
121
|
+
this.ensureOnlyTwoOpenPanels(panel.id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ensureOnlyTwoOpenPanels(keepOpenId: number) {
|
|
125
|
+
const newStack = [...this.panelsStack];
|
|
126
|
+
const openPanels = newStack.filter((panel) => !panel.isCollapsed);
|
|
127
|
+
if (openPanels.length > 1) {
|
|
128
|
+
const keepOpenIds = [keepOpenId];
|
|
129
|
+
const [panel1, panel2] = openPanels.filter((panel) => panel.id !== keepOpenId);
|
|
130
|
+
const indexKeep = newStack.findIndex(p => p.id === keepOpenId);
|
|
131
|
+
const index1 = newStack.findIndex(p => p === panel1);
|
|
132
|
+
const index2 = panel2 && newStack.findIndex(p => p === panel2);
|
|
133
|
+
if (index1 - 1 === indexKeep || index1 + 1 === indexKeep) {
|
|
134
|
+
keepOpenIds.push(panel1.id);
|
|
135
|
+
}
|
|
136
|
+
if (panel2 && (index2 - 1 === indexKeep || index2 + 1 === indexKeep)) {
|
|
137
|
+
keepOpenIds.push(panel2.id);
|
|
138
|
+
}
|
|
139
|
+
if (keepOpenIds.length === 1) {
|
|
140
|
+
if (newStack.length - 1 === indexKeep) {
|
|
141
|
+
keepOpenIds.push(newStack[indexKeep - 1].id);
|
|
142
|
+
} else {
|
|
143
|
+
keepOpenIds.push(newStack[indexKeep + 1].id);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const panel of newStack) {
|
|
147
|
+
panel.isCollapsed = !keepOpenIds.includes(panel.id);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
this.panelsStack = newStack;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
openPanel(title: string, icon: string, type: string, payload: any, openIndex?: number) {
|
|
154
|
+
const newPanel:IPanel = {
|
|
155
|
+
id: this.nextId++,
|
|
156
|
+
title,
|
|
157
|
+
icon,
|
|
158
|
+
type,
|
|
159
|
+
payload,
|
|
160
|
+
isCollapsed: false,
|
|
161
|
+
isCloseable: true
|
|
162
|
+
};
|
|
163
|
+
if (!this.panelsStack.length) {
|
|
164
|
+
newPanel.isCloseable = false;
|
|
165
|
+
}
|
|
166
|
+
let newStack = [...this.panelsStack];
|
|
167
|
+
if (openIndex) {
|
|
168
|
+
newStack = newStack.slice(0, openIndex);
|
|
169
|
+
}
|
|
170
|
+
newStack.push(newPanel);
|
|
171
|
+
this.panelsStack = newStack;
|
|
172
|
+
this.ensureOnlyTwoOpenPanels(newPanel.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
closePanel(panel: IPanel) {
|
|
176
|
+
this.panelsStack = this.panelsStack.filter(p => p !== panel);
|
|
177
|
+
let openPanel = this.panelsStack.find(p => !p.isCollapsed);
|
|
178
|
+
if (!openPanel) {
|
|
179
|
+
openPanel = this.panelsStack[this.panelsStack.length - 1];
|
|
180
|
+
openPanel.isCollapsed = false;
|
|
181
|
+
}
|
|
182
|
+
const openPanelIndex = this.panelsStack.findIndex(p => p === openPanel);
|
|
183
|
+
if (openPanelIndex > 0) {
|
|
184
|
+
this.panelsStack[openPanelIndex - 1].isCollapsed = false;
|
|
185
|
+
}
|
|
186
|
+
this.ensureOnlyTwoOpenPanels(openPanel.id);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
fullsizePanel(panel: IPanel) {
|
|
190
|
+
const newStack = [...this.panelsStack];
|
|
191
|
+
for (const p of newStack) {
|
|
192
|
+
p.isCollapsed = p !== panel;
|
|
193
|
+
}
|
|
194
|
+
this.panelsStack = newStack;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
</script>
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
<itf-checkbox :value="item[idProperty]" />
|
|
31
31
|
</div>
|
|
32
32
|
</div>
|
|
33
|
-
<div accept-group="items" class="table-item-inner">
|
|
33
|
+
<div accept-group="items" class="table-item-inner" @click="$emit('row-click', item)">
|
|
34
34
|
<template v-for="(column, k) in visibleAttributes">
|
|
35
35
|
<div
|
|
36
36
|
v-if="column.visible !== false"
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
:style="`width: ${column.width}px; max-width: ${column.width}px; left: ${column.left}px;`"
|
|
39
39
|
:class="{'sticky': column.pinned, 'last-sticky-column': k === lastPinnedIndex, 'flex-grow-1': column.grow, 'px-2': !(column.editable && editable), 'editable': column.editable && editable}"
|
|
40
40
|
class="table-view-item-value d-flex h-100">
|
|
41
|
-
<slot :name="`column.${column.property}`" :item="item" :column="column">
|
|
41
|
+
<slot :name="`column.${column.property}`" :editable="column.editable && editable" :item="item" :column="column" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)">
|
|
42
42
|
<template v-if="column.editable && editable">
|
|
43
43
|
<slot :name="`edit.${column.type}`" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)" :item="item" :column="column">
|
|
44
44
|
<itf-text-field class="w-100" v-if="column.type === 'text'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
|
|
@@ -49,11 +49,12 @@
|
|
|
49
49
|
:hours="getValue(item, column)"
|
|
50
50
|
@update:hours="updateValue(item, $event, n, column)"
|
|
51
51
|
/>
|
|
52
|
-
<itf-textarea class="w-100" :rows="
|
|
52
|
+
<itf-textarea class="w-100" :rows="1" autogrow v-else-if="column.type === 'textarea'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
|
|
53
53
|
<itf-money-field class="w-100" currency-disabled :currency="currency" :currencies="currencies" v-else-if="column.type === 'money'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
|
|
54
|
+
<itf-select class="w-100" v-else-if="column.type === 'select'" :reduce="(item) => item.value" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" :options="column.options"></itf-select>
|
|
54
55
|
</slot>
|
|
55
56
|
</template>
|
|
56
|
-
<div v-else>
|
|
57
|
+
<div v-else class="h-100 align-items-center d-flex w-100">
|
|
57
58
|
<slot :name="`format.${column.type}`" :value="getValue(item, column)" :update="(value) => updateValue(item, value, n, column)" :item="item" :column="column">
|
|
58
59
|
{{getValue(item, column)}}
|
|
59
60
|
</slot>
|
|
@@ -76,15 +77,21 @@
|
|
|
76
77
|
<div class="shadow-area"></div>
|
|
77
78
|
<div class="indicator sticky"></div>
|
|
78
79
|
<div class="table-item-inner">
|
|
79
|
-
<div class="table-view-item-value w-100 align-items-center p-3">
|
|
80
|
+
<div class="table-view-item-value w-100 align-items-center p-3 no-results">
|
|
80
81
|
{{$t('components.table.noResults')}}
|
|
81
82
|
</div>
|
|
83
|
+
<div class="boundary right"></div>
|
|
82
84
|
</div>
|
|
83
85
|
</div>
|
|
84
86
|
</div>
|
|
85
87
|
</div>
|
|
86
88
|
</template>
|
|
87
89
|
<style lang="scss">
|
|
90
|
+
.no-results {
|
|
91
|
+
text-align: center;
|
|
92
|
+
margin-left: -22px;
|
|
93
|
+
justify-content: center;
|
|
94
|
+
}
|
|
88
95
|
.table-row-template {
|
|
89
96
|
display: flex;
|
|
90
97
|
height: 100%;
|
|
@@ -97,6 +104,12 @@
|
|
|
97
104
|
flex-direction: row;
|
|
98
105
|
}
|
|
99
106
|
|
|
107
|
+
.scroller > .table-view-item:last-child {
|
|
108
|
+
.table-item-inner, .indicator {
|
|
109
|
+
border-bottom: 1px solid var(--itf-table-border-color);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
100
113
|
.table-item-inner .extra {
|
|
101
114
|
flex-grow: 1;
|
|
102
115
|
border-color: var(--bs-border-color);
|
|
@@ -203,6 +216,7 @@ import itfTextField from '../text-field/TextField.vue';
|
|
|
203
216
|
import itfMoneyField from '../text-field/MoneyField.vue';
|
|
204
217
|
import itfTextarea from '../text-field/Textarea.vue';
|
|
205
218
|
import itfHoursField from '../text-field/HoursField.vue';
|
|
219
|
+
import itfSelect from '../select/Select.vue';
|
|
206
220
|
// import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
|
207
221
|
|
|
208
222
|
export default @Component({
|
|
@@ -213,6 +227,7 @@ export default @Component({
|
|
|
213
227
|
itfHoursField,
|
|
214
228
|
itfTextarea,
|
|
215
229
|
itfTextField,
|
|
230
|
+
itfSelect,
|
|
216
231
|
// RecycleScroller
|
|
217
232
|
}
|
|
218
233
|
})
|
|
@@ -242,7 +257,7 @@ class itfTableBody extends Vue {
|
|
|
242
257
|
updateValue(item, value, index, column) {
|
|
243
258
|
const newItem = { ...item };
|
|
244
259
|
set(newItem, column.property, value);
|
|
245
|
-
this.$emit('update', { index, item, value: newItem });
|
|
260
|
+
this.$emit('update', { index, item, value: newItem, column });
|
|
246
261
|
}
|
|
247
262
|
}
|
|
248
263
|
</script>
|
|
@@ -105,7 +105,9 @@
|
|
|
105
105
|
</span>
|
|
106
106
|
<span v-else>
|
|
107
107
|
<span class="summary-text text-uppercase pe-2">{{getCalculateMethodTitle(column.calculate)}}</span>
|
|
108
|
-
<
|
|
108
|
+
<slot :name="`summary.${column.property}`" :value="getCalculateMethodValue(column.calculate, column)" :raw="!column.calculate.includes('count') && !column.calculate.includes('percent')">
|
|
109
|
+
<span>{{getCalculateMethodValue(column.calculate, column)}}</span>
|
|
110
|
+
</slot>
|
|
109
111
|
</span>
|
|
110
112
|
</span>
|
|
111
113
|
</template>
|
|
@@ -290,7 +292,7 @@
|
|
|
290
292
|
import get from 'lodash/get';
|
|
291
293
|
import sortBy from 'lodash/sortBy';
|
|
292
294
|
import uniq from "lodash/uniq";
|
|
293
|
-
import round from "
|
|
295
|
+
import { round } from "../../helpers/formatters";
|
|
294
296
|
import {Vue, Component, Prop, Watch} from 'vue-property-decorator';
|
|
295
297
|
import itfDropdown from '../dropdown/Dropdown.vue';
|
|
296
298
|
import itfButton from '../button/Button.vue';
|
|
@@ -350,6 +352,7 @@ class itfTableGroup extends Vue {
|
|
|
350
352
|
item.lastPinned = index === pinned.length - 1;
|
|
351
353
|
return item
|
|
352
354
|
});
|
|
355
|
+
console.info(list);
|
|
353
356
|
return list;
|
|
354
357
|
}
|
|
355
358
|
|
|
@@ -14,11 +14,11 @@ export function parseHours (str, { hoursInDay } = { hoursInDay: 8 }) {
|
|
|
14
14
|
return isNegative * Math.round(parseFloat(source.replace(',', '.')) * MINUTES_IN_HOUR * SECONDS_IN_MINUTE);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
const match = source.match(/^((\d+)d)?\s*((\d+)h)?\s*((\d+)m)?$/);
|
|
17
|
+
const match = source.match(/^((\d+)d)?\s*((\d+)h)?\s*((\d+)m)?\s*((\d+)s)?$/);
|
|
18
18
|
if (!match) {
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
-
const [,, days,, hours,, minutes] = match;
|
|
21
|
+
const [,, days,, hours,, minutes,,secs] = match;
|
|
22
22
|
|
|
23
23
|
let seconds = 0;
|
|
24
24
|
if (days) {
|
|
@@ -30,6 +30,9 @@ export function parseHours (str, { hoursInDay } = { hoursInDay: 8 }) {
|
|
|
30
30
|
if (minutes) {
|
|
31
31
|
seconds += Number(minutes) * SECONDS_IN_MINUTE;
|
|
32
32
|
}
|
|
33
|
+
if (secs) {
|
|
34
|
+
seconds += Number(secs);
|
|
35
|
+
}
|
|
33
36
|
return isNegative * seconds;
|
|
34
37
|
}
|
|
35
38
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseHours
|
|
3
|
+
} from './formatters';
|
|
4
|
+
|
|
5
|
+
describe('Formatters', () => {
|
|
6
|
+
const $t = (str) => str;
|
|
7
|
+
|
|
8
|
+
test('parseHours', () => {
|
|
9
|
+
expect(parseHours('10s')).toEqual(10);
|
|
10
|
+
expect(parseHours('12h')).toEqual(12 * 60 * 60);
|
|
11
|
+
expect(parseHours('10m')).toEqual(10 * 60);
|
|
12
|
+
expect(parseHours('12h 10m')).toEqual(12 * 60 * 60 + 10 * 60);
|
|
13
|
+
expect(parseHours('12h 10s')).toEqual(12 * 60 * 60 + 10);
|
|
14
|
+
expect(parseHours('12h 10m 10s')).toEqual(12 * 60 * 60 + 10 * 60 + 10);
|
|
15
|
+
expect(parseHours('1d 12h 10m 10s')).toEqual(8 * 60 * 60 + 12 * 60 * 60 + 10 * 60 + 10);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<span class="b-filter-badge border rounded d-inline-flex ps-3 pe-1 gap-1 align-items-center">
|
|
5
|
-
Status
|
|
6
|
-
|
|
7
|
-
<itf-select
|
|
8
|
-
class="mx-2"
|
|
9
|
-
v-model="operator"
|
|
10
|
-
simple
|
|
11
|
-
:reduce="(item) => item.id"
|
|
12
|
-
:get-option-label="(item) => item.title"
|
|
13
|
-
:options="operators"
|
|
14
|
-
>
|
|
15
|
-
<template #selected-option="{ option }">
|
|
16
|
-
<span class="sign-box"> {{option.sign || option.title}}</span>
|
|
17
|
-
</template>
|
|
18
|
-
<template #option="{ option }">
|
|
19
|
-
<span class="sign-box" v-if="option.sign"> {{option.sign}}</span>
|
|
20
|
-
{{option.title}}
|
|
21
|
-
</template>
|
|
22
|
-
<template #open-indicator><div></div></template>
|
|
23
|
-
</itf-select>
|
|
24
|
-
|
|
25
|
-
<div>
|
|
26
|
-
<itf-select
|
|
27
|
-
simple
|
|
28
|
-
multiple
|
|
29
|
-
:get-option-label="(item) => item.title"
|
|
30
|
-
:options="[{ title:'text' }, { title:'text2' }]"
|
|
31
|
-
>
|
|
32
|
-
</itf-select>
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
<itf-button icon>
|
|
36
|
-
<itf-icon name="close" />
|
|
37
|
-
</itf-button>
|
|
38
|
-
</span>
|
|
39
|
-
|
|
40
|
-
</template>
|
|
41
|
-
<style>
|
|
42
|
-
.b-filter-badge {
|
|
43
|
-
padding: 1px;
|
|
44
|
-
|
|
45
|
-
.sign-box {
|
|
46
|
-
height: 24px;
|
|
47
|
-
text-align: center;
|
|
48
|
-
line-height: 22px;
|
|
49
|
-
width: 24px;
|
|
50
|
-
border-radius: 0.25rem;
|
|
51
|
-
background-color: var(--bs-tertiary-bg);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
</style>
|
|
55
|
-
<script>
|
|
56
|
-
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
|
57
|
-
import {getOperatorsByType} from "./constants";
|
|
58
|
-
import itfButton from '../button/Button.vue';
|
|
59
|
-
import itfSelect from '../select/Select.vue';
|
|
60
|
-
import itfIcon from '../icon/Icon.vue';
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
export default @Component({
|
|
64
|
-
name: 'itfFilterBadge',
|
|
65
|
-
components: {
|
|
66
|
-
itfButton,
|
|
67
|
-
itfSelect,
|
|
68
|
-
itfIcon
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
class itfFilterBadge extends Vue {
|
|
72
|
-
@Prop({ type: String, required: true }) type;
|
|
73
|
-
|
|
74
|
-
get operators() {
|
|
75
|
-
return getOperatorsByType(this.type);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
</script>
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
<div class="d-flex align-items-center">
|
|
5
|
-
<itf-text-field
|
|
6
|
-
style="width: 250px;"
|
|
7
|
-
prepend-icon="search"
|
|
8
|
-
placeholder="Search"
|
|
9
|
-
@change="onQueryChanged"
|
|
10
|
-
/>
|
|
11
|
-
|
|
12
|
-
<itf-dropdown class="ms-3" :button-options="{ secondary: true }">
|
|
13
|
-
<template #button>
|
|
14
|
-
<itf-icon name="flip_view" class="me-1" />
|
|
15
|
-
Group
|
|
16
|
-
</template>
|
|
17
|
-
|
|
18
|
-
<div v-for="(filter, n) of filters" :key="n" class="dropdown-item">
|
|
19
|
-
<slot name="dropdown-item" :item="filter">{{filter.text}}</slot>
|
|
20
|
-
</div>
|
|
21
|
-
</itf-dropdown>
|
|
22
|
-
|
|
23
|
-
<template v-if="filters && filters.length">
|
|
24
|
-
<itf-dropdown class="mx-3" :button-options="{ secondary: true }">
|
|
25
|
-
<template #button>
|
|
26
|
-
<itf-icon name="filter" class="me-1" />
|
|
27
|
-
Filter
|
|
28
|
-
</template>
|
|
29
|
-
|
|
30
|
-
<div v-for="(filter, n) of filters" :key="n" class="dropdown-item">
|
|
31
|
-
<slot name="dropdown-item" :item="filter">{{filter.text}}</slot>
|
|
32
|
-
</div>
|
|
33
|
-
</itf-dropdown>
|
|
34
|
-
|
|
35
|
-
<div class="flex-grow-1">
|
|
36
|
-
<itf-filter-badge type="string" />
|
|
37
|
-
</div>
|
|
38
|
-
</template>
|
|
39
|
-
</div>
|
|
40
|
-
|
|
41
|
-
</template>
|
|
42
|
-
<style>
|
|
43
|
-
</style>
|
|
44
|
-
<script>
|
|
45
|
-
import { Component, Prop, Model, Vue, Watch } from 'vue-property-decorator';
|
|
46
|
-
import itfButton from '../button/Button.vue';
|
|
47
|
-
import itfTextField from '../text-field/TextField.vue';
|
|
48
|
-
import itfDropdown from '../dropdown/Dropdown.vue';
|
|
49
|
-
import itfIcon from '../icon/Icon.vue';
|
|
50
|
-
import itfFilterBadge from './FilterBadge.vue';
|
|
51
|
-
|
|
52
|
-
export default @Component({
|
|
53
|
-
name: 'itfFilterControl',
|
|
54
|
-
components: {
|
|
55
|
-
itfFilterBadge,
|
|
56
|
-
itfTextField,
|
|
57
|
-
itfDropdown,
|
|
58
|
-
itfButton,
|
|
59
|
-
itfIcon
|
|
60
|
-
}
|
|
61
|
-
})
|
|
62
|
-
class itfFilterControl extends Vue {
|
|
63
|
-
@Model('input') value;
|
|
64
|
-
@Prop({ type: String, default: 'query' }) queryField;
|
|
65
|
-
@Prop({ type: Array, default: () => ([]) }) filters;
|
|
66
|
-
@Prop(Boolean) urlSync;
|
|
67
|
-
|
|
68
|
-
mounted() {
|
|
69
|
-
if (this.urlSync) {
|
|
70
|
-
if (window.itfinUrlWatcher) {
|
|
71
|
-
throw new Error('urlSync component already exists');
|
|
72
|
-
}
|
|
73
|
-
window.itfinUrlWatcher = this;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@Watch('$route')
|
|
78
|
-
onRouteChange() {
|
|
79
|
-
const newValue = { ...(this.value || {}) };
|
|
80
|
-
const query = this.$route.query;
|
|
81
|
-
if (query) {
|
|
82
|
-
Object.keys(query).forEach((key) => {
|
|
83
|
-
newValue[key] = query[key];
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
this.$emit('input', newValue);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
beforeDestroy() {
|
|
90
|
-
if (this.urlSync) {
|
|
91
|
-
window.itfinUrlWatcher = null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
onQueryChanged(value) {
|
|
96
|
-
const newValue = { ...(this.value || {}) };
|
|
97
|
-
newValue[this.queryField] = value;
|
|
98
|
-
if (this.urlSync) {
|
|
99
|
-
this.$router.push({ ...this.$route, query: newValue });
|
|
100
|
-
} else {
|
|
101
|
-
this.$emit('input', newValue);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
</script>
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div class="form-group and-or-rule d-flex">
|
|
3
|
-
<div class="col-3">
|
|
4
|
-
<select class="form-control input-sm" v-model="key">
|
|
5
|
-
<option v-for="option in options.keys" :value="option.id">
|
|
6
|
-
{{option.name}}
|
|
7
|
-
</option>
|
|
8
|
-
</select>
|
|
9
|
-
</div>
|
|
10
|
-
|
|
11
|
-
<div style="width: 150px" class="px-2">
|
|
12
|
-
<select class="form-control input-sm input-filter" v-model="operator">
|
|
13
|
-
<option v-for="option in operators" :value="option.id">
|
|
14
|
-
{{option.title}}
|
|
15
|
-
</option>
|
|
16
|
-
</select>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
<div class="col-3">
|
|
20
|
-
<input type="text" class="form-control input-sm" v-model="value" placeholder="">
|
|
21
|
-
</div>
|
|
22
|
-
<div class="col-3">
|
|
23
|
-
<itf-button small @click.prevent="deleteSelf()">
|
|
24
|
-
<itf-icon name="close" />
|
|
25
|
-
</itf-button>
|
|
26
|
-
</div>
|
|
27
|
-
</div>
|
|
28
|
-
</template>
|
|
29
|
-
<style>
|
|
30
|
-
.input-filter {
|
|
31
|
-
background: var(--bs-primary-bg-subtle);
|
|
32
|
-
}
|
|
33
|
-
.and-or-rule {
|
|
34
|
-
position: relative;
|
|
35
|
-
margin-left: 15px !important;
|
|
36
|
-
padding-left: 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
.and-or-rule:before,
|
|
40
|
-
.and-or-rule:after {
|
|
41
|
-
content: '';
|
|
42
|
-
position: absolute;
|
|
43
|
-
left: -1px;
|
|
44
|
-
width: 16px;
|
|
45
|
-
height: calc(50% + 15px);
|
|
46
|
-
border-color: #c0c5e2;
|
|
47
|
-
border-style: solid;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
.and-or-rule:before {
|
|
51
|
-
top: -15px;
|
|
52
|
-
border-width: 0 0 2px 2px;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
.and-or-rule:after {
|
|
56
|
-
top: 50%;
|
|
57
|
-
border-width: 0 0 0 2px;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
.and-or-rule:last-child:after {
|
|
61
|
-
border: none;
|
|
62
|
-
}
|
|
63
|
-
</style>
|
|
64
|
-
<script>
|
|
65
|
-
import { Component, Prop, Watch, Vue } from "vue-property-decorator";
|
|
66
|
-
import itfButton from '../button/Button.vue';
|
|
67
|
-
import itfIcon from '../icon/Icon.vue';
|
|
68
|
-
|
|
69
|
-
const operations = [
|
|
70
|
-
{
|
|
71
|
-
type: 'string',
|
|
72
|
-
operators: [
|
|
73
|
-
{ id: 'eq', title: 'equal' },
|
|
74
|
-
{ id: 'notEq', title: 'not equal' },
|
|
75
|
-
{ id: 'contains', title: 'contains' },
|
|
76
|
-
{ id: 'noContains', title: 'not contains' },
|
|
77
|
-
{ id: 'startsWith', title: 'starts with' },
|
|
78
|
-
{ id: 'endsWith', title: 'ends with' }
|
|
79
|
-
]
|
|
80
|
-
}
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
export default @Component({
|
|
84
|
-
name: 'itfRule',
|
|
85
|
-
components: { itfButton, itfIcon }
|
|
86
|
-
})
|
|
87
|
-
class itfRule extends Vue {
|
|
88
|
-
@Prop() options;
|
|
89
|
-
@Prop() type;
|
|
90
|
-
|
|
91
|
-
key = -99;
|
|
92
|
-
|
|
93
|
-
operator = -99;
|
|
94
|
-
|
|
95
|
-
value = '';
|
|
96
|
-
|
|
97
|
-
get operators() {
|
|
98
|
-
return operations.find(op => op.type === this.type)?.operators || [];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
@Watch('options.keys.options')
|
|
102
|
-
onOptionUpdated() {
|
|
103
|
-
this.key = -99;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
@Watch('options.conditions.options')
|
|
107
|
-
onOption2Updated() {
|
|
108
|
-
this.condition = -99;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
deleteSelf () {
|
|
112
|
-
this.$emit('delete-rule');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
queryFormStatus () {
|
|
116
|
-
return {
|
|
117
|
-
'key': this.key,
|
|
118
|
-
'operator': this.operator,
|
|
119
|
-
'value': this.value
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
fillRuleStatus (data) {
|
|
124
|
-
this.key = data.key;
|
|
125
|
-
this.operator = data.operator;
|
|
126
|
-
this.value = data.value;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
</script>
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<div>
|
|
3
|
-
<itf-dropdown>
|
|
4
|
-
<template #button>
|
|
5
|
-
<itf-icon name="filter" />
|
|
6
|
-
Filter
|
|
7
|
-
</template>
|
|
8
|
-
|
|
9
|
-
<div class="dropdown-item" @click="addGroup">Add group</div>
|
|
10
|
-
<div class="dropdown-item" @click="addRule">Add condition</div>
|
|
11
|
-
</itf-dropdown>
|
|
12
|
-
<div class="form-group and-or-top col-12">
|
|
13
|
-
<div class="col-5" style="padding: 0">
|
|
14
|
-
<button class="btn btn-xs btn-purple-outline btn-radius"
|
|
15
|
-
:class=" isAnd ? 'btn-purple-outline-focus' : '' " @click.prevent="clickAnd">
|
|
16
|
-
And
|
|
17
|
-
</button>
|
|
18
|
-
<button class="btn btn-xs btn-purple-outline btn-radius"
|
|
19
|
-
:class=" !isAnd ? 'btn-purple-outline-focus' : '' " @click.prevent="clickOr">
|
|
20
|
-
Or
|
|
21
|
-
</button>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div class="col-7 btn-and-or">
|
|
25
|
-
<itf-button v-if="!first" class="btn btn-xs btn-purple pull-right" @click.prevent="deleteSelf()">
|
|
26
|
-
<i class="fa fa-fw fa-close"></i>
|
|
27
|
-
</itf-button>
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
|
|
31
|
-
<rule
|
|
32
|
-
v-for="(rule, index) in rules"
|
|
33
|
-
ref="rules"
|
|
34
|
-
:options="options"
|
|
35
|
-
:key="rule"
|
|
36
|
-
type="string"
|
|
37
|
-
@delete-rule="deleteRule(index)">
|
|
38
|
-
</rule>
|
|
39
|
-
|
|
40
|
-
<itf-rule-group
|
|
41
|
-
class="and-or-offset col-11"
|
|
42
|
-
v-for="(group, index) in groups" ref="groups"
|
|
43
|
-
:options="options" :key="group" @delete-group="deleteGroup(index)">
|
|
44
|
-
</itf-rule-group>
|
|
45
|
-
|
|
46
|
-
</div>
|
|
47
|
-
</template>
|
|
48
|
-
<style>
|
|
49
|
-
.and-or-template {
|
|
50
|
-
position: relative;
|
|
51
|
-
margin-bottom: 20px;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
.and-or-template:before,
|
|
55
|
-
.and-or-template:after {
|
|
56
|
-
content: '';
|
|
57
|
-
position: absolute;
|
|
58
|
-
left: -17px;
|
|
59
|
-
width: 16px;
|
|
60
|
-
height: calc(50% + 18px);
|
|
61
|
-
border-color: #c0c5e2;
|
|
62
|
-
border-style: solid;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.and-or-template:before {
|
|
66
|
-
top: -18px;
|
|
67
|
-
border-width: 0 0 2px 2px;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
.and-or-template:after {
|
|
71
|
-
top: 50%;
|
|
72
|
-
border-width: 0 0 0 2px;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
.and-or-first:before,
|
|
76
|
-
.and-or-first:after,
|
|
77
|
-
.and-or-template:last-child:after {
|
|
78
|
-
border: none;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
.and-or-top,
|
|
82
|
-
.btn-and-or {
|
|
83
|
-
padding: 0;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.btn-and-or button {
|
|
87
|
-
margin-left: 4px;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
.and-or-offset {
|
|
91
|
-
margin-left: 30px;
|
|
92
|
-
}
|
|
93
|
-
</style>
|
|
94
|
-
<script>
|
|
95
|
-
import { Component, Prop, Watch, Vue } from 'vue-property-decorator';
|
|
96
|
-
import itfButton from '../button/Button.vue';
|
|
97
|
-
import itfIcon from '../icon/Icon.vue';
|
|
98
|
-
import itfDropdown from '../dropdown/Dropdown.vue';
|
|
99
|
-
import Rule from './Rule'
|
|
100
|
-
|
|
101
|
-
export default @Component({
|
|
102
|
-
name: 'itfRuleGroup',
|
|
103
|
-
components: {
|
|
104
|
-
itfIcon,
|
|
105
|
-
itfButton,
|
|
106
|
-
itfDropdown,
|
|
107
|
-
Rule
|
|
108
|
-
}
|
|
109
|
-
})
|
|
110
|
-
class itfRule extends Vue {
|
|
111
|
-
@Prop({ type: Object, required: true }) options;
|
|
112
|
-
@Prop({ type: Boolean, default: false }) first;
|
|
113
|
-
|
|
114
|
-
isAnd = true;
|
|
115
|
-
groups = [];
|
|
116
|
-
rules = [];
|
|
117
|
-
|
|
118
|
-
created () {
|
|
119
|
-
this.addRule();
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
clickAnd () {
|
|
123
|
-
this.isAnd = true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
clickOr () {
|
|
127
|
-
this.isAnd = false;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
addRule () {
|
|
131
|
-
var id = this.generateId();
|
|
132
|
-
this.rules.push(id);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
addGroup () {
|
|
136
|
-
var id = this.generateId();
|
|
137
|
-
this.groups.push(id);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
deleteSelf () {
|
|
141
|
-
this.$emit('delete-group');
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
deleteRule (index) {
|
|
145
|
-
this.rules.splice(index, 1);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
deleteGroup (index) {
|
|
149
|
-
this.groups.splice(index, 1);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
queryFormStatus () {
|
|
153
|
-
var query = {};
|
|
154
|
-
var rules = this.$refs.rules || {};
|
|
155
|
-
var groups = this.$refs.groups || {};
|
|
156
|
-
var i, j;
|
|
157
|
-
|
|
158
|
-
query['condition'] = this.isAnd ? 'AND' : 'OR';
|
|
159
|
-
query['rules'] = [];
|
|
160
|
-
for(i = 0; i < rules.length; i++){
|
|
161
|
-
query.rules.push(rules[i].queryFormStatus ());
|
|
162
|
-
}
|
|
163
|
-
for(j = 0; j < groups.length; j++){
|
|
164
|
-
query.rules[query.rules.length] = groups[j].queryFormStatus();
|
|
165
|
-
}
|
|
166
|
-
return query;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
fillFormStatus (data) {
|
|
170
|
-
var i, len;
|
|
171
|
-
var group = this;
|
|
172
|
-
group.rules = [];
|
|
173
|
-
group.groups = [];
|
|
174
|
-
if(data){
|
|
175
|
-
group.isAnd = /and/i.test(data.condition);
|
|
176
|
-
len = data.rules.length;
|
|
177
|
-
for(i = 0; i < len; i++){
|
|
178
|
-
if(data.rules[i].condition){
|
|
179
|
-
group.groups.push(group.generateId());
|
|
180
|
-
(function (i, index) {
|
|
181
|
-
group.$nextTick(function () {
|
|
182
|
-
group.$refs.groups[index].fillFormStatus(data.rules[i]);
|
|
183
|
-
});
|
|
184
|
-
})(i, group.groups.length - 1);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
group.rules.push(group.generateId());
|
|
188
|
-
(function (i, index) {
|
|
189
|
-
group.$nextTick(function () {
|
|
190
|
-
group.$refs.rules[index].fillRuleStatus(data.rules[i]);
|
|
191
|
-
});
|
|
192
|
-
})(i, group.rules.length - 1);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
generateId () {
|
|
199
|
-
return 'xxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
200
|
-
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
|
|
201
|
-
return v.toString(16);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
</script>
|