@itfin/components 1.3.83 → 1.3.86
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 +1 -1
- package/src/assets/scss/_variables.scss +3 -3
- package/src/assets/scss/components/_toaster.scss +4 -4
- package/src/assets/scss/components/select/_states.scss +1 -1
- package/src/components/app/message.js +4 -6
- package/src/components/datepicker/DateRangePicker.vue +2 -2
- package/src/components/filter/FacetFilter.vue +138 -0
- package/src/components/filter/FacetItem.vue +162 -0
- package/src/components/filter/FilterItem.vue +423 -0
- package/src/components/filter/index.stories.js +3 -18
- package/src/components/text-field/HoursField.vue +2 -2
package/package.json
CHANGED
|
@@ -26,11 +26,11 @@ $success: #10834e;
|
|
|
26
26
|
$warning: #cda277;
|
|
27
27
|
$danger: #cb4839;
|
|
28
28
|
|
|
29
|
-
$primary: #0B314F;
|
|
30
|
-
$link-color: #0B314F;
|
|
29
|
+
$primary: #0B314F !default;
|
|
30
|
+
$link-color: #0B314F !default;
|
|
31
31
|
$input-btn-focus-width: .125rem;
|
|
32
32
|
|
|
33
|
-
$input-bg: #f3f3f3;
|
|
33
|
+
$input-bg: #f3f3f3 !default;
|
|
34
34
|
$input-border-color: rgba(#000, .08);
|
|
35
35
|
$input-border-radius: 10px;
|
|
36
36
|
$input-font-size: 0.875rem;
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
padding: 15px;
|
|
11
11
|
|
|
12
12
|
.toast-body {
|
|
13
|
-
word-break: break-
|
|
13
|
+
word-break: break-word;
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
&.is-top-left,
|
|
17
17
|
&.is-top-center,
|
|
18
18
|
&.is-top-right {
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
margin-top: -45px;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
|
|
99
|
-
|
|
98
|
+
.itf-toast-message.toast {
|
|
99
|
+
--bs-toast-max-width: 400px;
|
|
100
100
|
z-index: $zindex-toaster;
|
|
101
101
|
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import Vue from 'vue';
|
|
2
1
|
import ToastContainer from './ToastContainer.vue';
|
|
3
2
|
|
|
4
|
-
const MessageConstructor = Vue.extend(ToastContainer);
|
|
5
|
-
|
|
6
3
|
const instances = [];
|
|
7
4
|
let count = 1;
|
|
8
5
|
const containers = {};
|
|
@@ -39,9 +36,10 @@ const Message = function (options) {
|
|
|
39
36
|
Message.close(id, userOnClose);
|
|
40
37
|
};
|
|
41
38
|
const parent = document.getElementById('itf-app');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
|
|
40
|
+
const instance = new ToastContainer({
|
|
41
|
+
parent: $nuxt.$el.__vue__,
|
|
42
|
+
el: document.createElement('itf-toast-container'),
|
|
45
43
|
data: options
|
|
46
44
|
});
|
|
47
45
|
|
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
</slot>
|
|
7
7
|
</div>
|
|
8
8
|
<div class="input-group form-control py-0"
|
|
9
|
-
:class="{ 'focused': focused, 'is-invalid': isInvalid(), 'is-valid': isSuccess() }"
|
|
9
|
+
:class="{ 'disabled': disabled, 'focused': focused, 'is-invalid': isInvalid(), 'is-valid': isSuccess() }"
|
|
10
10
|
ref="group">
|
|
11
11
|
<i-mask-component
|
|
12
12
|
ref="input"
|
|
13
13
|
class="form-control"
|
|
14
|
-
:class="{ 'placeholder-visible': !displayValue || !displayValue[0] }"
|
|
14
|
+
:class="{ 'placeholder-visible': !displayValue || !displayValue[0], 'bg-transparent': disabled }"
|
|
15
15
|
@input="updateValue($event, 0)"
|
|
16
16
|
@focus="onFocus"
|
|
17
17
|
@blur="onBlur($event, 0)"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<itf-form class="facets-panel" :class="{ horizontal }">
|
|
3
|
+
<div v-for="(item, n) of filters" :key="n" class="mb-3">
|
|
4
|
+
<template v-if="item.facet && ((facets[item.facet] && !horizontal) || horizontal)">
|
|
5
|
+
<div><strong>{{ item.label }}</strong></div>
|
|
6
|
+
<div class="facets-container">
|
|
7
|
+
<div v-if="horizontal" class="facets-search">
|
|
8
|
+
<input type="text" :placeholder="$t('search')" :value="query[item.facet]" @input="(e) => setQuery(e, item.facet)">
|
|
9
|
+
</div>
|
|
10
|
+
<div v-if="facets[item.facet]" class="facets-container__scroll">
|
|
11
|
+
<facet-item
|
|
12
|
+
:query="query[item.facet]"
|
|
13
|
+
:facet="facets[item.facet]"
|
|
14
|
+
:item="item"
|
|
15
|
+
:value="filter"
|
|
16
|
+
:show-all="horizontal"
|
|
17
|
+
@input="setFilterValue"
|
|
18
|
+
/>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
</template>
|
|
22
|
+
<filter-item
|
|
23
|
+
v-else-if="!item.facet"
|
|
24
|
+
:type="item.type"
|
|
25
|
+
:filter="item"
|
|
26
|
+
:value="filter"
|
|
27
|
+
:prepend-icon="item.id && item.id.includes('custom_') ? null : 'search'"
|
|
28
|
+
:labels="labels"
|
|
29
|
+
:loading-labels="loadingLabels"
|
|
30
|
+
@input="setFilterValue"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div v-for="(facet, n) of customFields" :key="`cf_${n}`" class="mb-3">
|
|
35
|
+
<div><strong>{{ facet.Name }}</strong></div>
|
|
36
|
+
<div class="facets-container">
|
|
37
|
+
<facet-item
|
|
38
|
+
:facet="facet"
|
|
39
|
+
:item="{ id: facet.Id, title: facet.Name }"
|
|
40
|
+
:value="filter"
|
|
41
|
+
:show-all="horizontal"
|
|
42
|
+
@input="setFilterValue"
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</itf-form>
|
|
47
|
+
</template>
|
|
48
|
+
<style lang="scss" scoped>
|
|
49
|
+
.facets-panel {
|
|
50
|
+
&.horizontal {
|
|
51
|
+
display: grid;
|
|
52
|
+
grid-template-columns: repeat(7, clamp(150px, 13.75%, 15%));
|
|
53
|
+
grid-gap: .5rem;
|
|
54
|
+
|
|
55
|
+
.facets-container {
|
|
56
|
+
border: 1px solid var(--bs-border-color);
|
|
57
|
+
max-height: 100%;
|
|
58
|
+
min-height: 90%;
|
|
59
|
+
|
|
60
|
+
.facets-search {
|
|
61
|
+
border-bottom: 1px solid var(--bs-border-color);
|
|
62
|
+
|
|
63
|
+
input {
|
|
64
|
+
padding: .5rem;
|
|
65
|
+
border: 0;
|
|
66
|
+
width: 100%;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
&::v-deep .facet-bar {
|
|
70
|
+
display: none;
|
|
71
|
+
}
|
|
72
|
+
&__scroll {
|
|
73
|
+
max-height: 200px;
|
|
74
|
+
overflow: auto;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
</style>
|
|
80
|
+
<script>
|
|
81
|
+
import { Vue, Prop, Model, Component } from 'vue-property-decorator';
|
|
82
|
+
import itfButton from '@itfin/components/src/components/button/Button';
|
|
83
|
+
import itfForm from '@itfin/components/src/components/form/Form';
|
|
84
|
+
import FilterItem from './FilterItem.vue';
|
|
85
|
+
import FacetItem from './FacetItem.vue';
|
|
86
|
+
|
|
87
|
+
export default @Component({
|
|
88
|
+
name: 'FacetFilters',
|
|
89
|
+
components: {
|
|
90
|
+
itfForm,
|
|
91
|
+
itfButton,
|
|
92
|
+
FilterItem,
|
|
93
|
+
FacetItem
|
|
94
|
+
}
|
|
95
|
+
})
|
|
96
|
+
class FacetFilters extends Vue {
|
|
97
|
+
@Model('input') value;
|
|
98
|
+
@Prop(String) type;
|
|
99
|
+
@Prop() filter;
|
|
100
|
+
@Prop() labels;
|
|
101
|
+
@Prop() loadingLabels;
|
|
102
|
+
@Prop() facets;
|
|
103
|
+
@Prop(Boolean) horizontal;
|
|
104
|
+
@Prop(Array) filters;
|
|
105
|
+
|
|
106
|
+
query = {};
|
|
107
|
+
|
|
108
|
+
setQuery(e, facet) {
|
|
109
|
+
this.query = {
|
|
110
|
+
...this.query,
|
|
111
|
+
[facet]: e.target.value
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get customFields() {
|
|
116
|
+
if (!this.facets) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
const fields = [];
|
|
120
|
+
for (const key in this.facets) {
|
|
121
|
+
const item = this.facets[key];
|
|
122
|
+
if (item.IsCustomField) {
|
|
123
|
+
fields.push({
|
|
124
|
+
...item,
|
|
125
|
+
Id: key
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return fields;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
setFilterValue (newFilter = {}) {
|
|
133
|
+
const filter = { ...newFilter };
|
|
134
|
+
|
|
135
|
+
this.$emit('update:filter', filter);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<button v-for="(val, n) of mappedValues" :key="n" class="facet-item" :class="{'active': val.isSelected}" @click="onFilterClick(val)">
|
|
4
|
+
<span class="facet-name text-dark">{{ val.Text }}</span>
|
|
5
|
+
<span class="facet-stat text-muted">
|
|
6
|
+
{{ val.Count }}
|
|
7
|
+
<span class="facet-bar"><span :style="{'--bar-width': `${getPercent(val)}%`}" class="facet-bar-progress" /></span>
|
|
8
|
+
</span>
|
|
9
|
+
</button>
|
|
10
|
+
|
|
11
|
+
<itf-button v-if="hasMore" small block @click="toggleMore">
|
|
12
|
+
<span v-if="showMore">{{ $t('common.hideMore', { count: facet.Values.length }) }}</span>
|
|
13
|
+
<span v-else>{{ $t('common.showMore', { count: facet.Values.length }) }}</span>
|
|
14
|
+
</itf-button>
|
|
15
|
+
</div>
|
|
16
|
+
</template>
|
|
17
|
+
<style lang="scss" scoped>
|
|
18
|
+
.facet-item {
|
|
19
|
+
background: transparent;
|
|
20
|
+
cursor: pointer;
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
-webkit-box-align: center;
|
|
23
|
+
align-items: center;
|
|
24
|
+
-webkit-box-pack: justify;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
position: relative;
|
|
27
|
+
box-sizing: border-box;
|
|
28
|
+
height: 1.75rem;
|
|
29
|
+
width: 100%;
|
|
30
|
+
padding-left: 0.25rem;
|
|
31
|
+
padding-right: 0.25rem;
|
|
32
|
+
font-size: 0.875rem;
|
|
33
|
+
line-height: 1.25rem;
|
|
34
|
+
font-weight: 400;
|
|
35
|
+
white-space: normal;
|
|
36
|
+
user-select: none;
|
|
37
|
+
border-radius: 0.25rem;
|
|
38
|
+
border-width: 1px;
|
|
39
|
+
border-style: solid;
|
|
40
|
+
border-color: transparent;
|
|
41
|
+
border-image: initial;
|
|
42
|
+
transition: none 0s ease 0s;
|
|
43
|
+
&.active {
|
|
44
|
+
background-color: rgba(var(--bs-primary-rgb), 25%);
|
|
45
|
+
}
|
|
46
|
+
&:hover, &:focus, &.active {
|
|
47
|
+
border-color: var(--bs-primary);
|
|
48
|
+
}
|
|
49
|
+
.facet-name {
|
|
50
|
+
text-align: left;
|
|
51
|
+
white-space: nowrap;
|
|
52
|
+
overflow: hidden;
|
|
53
|
+
text-overflow: ellipsis;
|
|
54
|
+
}
|
|
55
|
+
.facet-stat {
|
|
56
|
+
display: flex;
|
|
57
|
+
-webkit-box-align: center;
|
|
58
|
+
align-items: center;
|
|
59
|
+
margin-left: 0.25rem;
|
|
60
|
+
}
|
|
61
|
+
.facet-bar {
|
|
62
|
+
display: inline-block;
|
|
63
|
+
margin-left: 0.5rem;
|
|
64
|
+
width: 60px;
|
|
65
|
+
}
|
|
66
|
+
.facet-bar-progress {
|
|
67
|
+
display: block;
|
|
68
|
+
width: var(--bar-width);
|
|
69
|
+
min-width: 5px;
|
|
70
|
+
height: 10px;
|
|
71
|
+
background-color: rgb(197, 205, 223);
|
|
72
|
+
transition: width 0.3s ease 0s;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
</style>
|
|
76
|
+
<script>
|
|
77
|
+
import { Vue, Prop, Model, Component } from 'vue-property-decorator';
|
|
78
|
+
import itfButton from '@itfin/components/src/components/button/Button';
|
|
79
|
+
import { DateTime } from 'luxon';
|
|
80
|
+
|
|
81
|
+
export default @Component({
|
|
82
|
+
name: 'FacetItem',
|
|
83
|
+
components: {
|
|
84
|
+
itfButton
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
class FacetItem extends Vue {
|
|
88
|
+
@Model('input') value;
|
|
89
|
+
@Prop() facet;
|
|
90
|
+
@Prop() item;
|
|
91
|
+
@Prop() query;
|
|
92
|
+
@Prop(Boolean) showAll;
|
|
93
|
+
|
|
94
|
+
showMore = false;
|
|
95
|
+
|
|
96
|
+
toggleMore() {
|
|
97
|
+
this.showMore = !this.showMore;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get hasMore() {
|
|
101
|
+
if (this.showAll) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return this.facet.Values.length > 5;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
get mappedValues() {
|
|
108
|
+
const { id, items = [], multiple } = this.item || {};
|
|
109
|
+
let list = this.facet.Values.map(val => {
|
|
110
|
+
const item = items.find(i => `${i.value}` === `${val.Value}`);
|
|
111
|
+
const isSelected = multiple
|
|
112
|
+
? Array.isArray(this.value[id]) && this.value[id].map(String).includes(`${val.Value}`)
|
|
113
|
+
: `${this.value[id]}` === `${val.Value}`;
|
|
114
|
+
|
|
115
|
+
if (item) {
|
|
116
|
+
// якщо знайдено item, ЗАТРЕ Text в val і замінить його на item.title
|
|
117
|
+
return { ...val, Text: item.title, isSelected };
|
|
118
|
+
}
|
|
119
|
+
return { ...val, isSelected };
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
list = list.map(val => val.Text ? { ...val, Text: this.getFormattedItemTitle(val.Text) } : val);
|
|
123
|
+
|
|
124
|
+
if (this.query) {
|
|
125
|
+
list = list.filter((val) => val.Text.toLowerCase().includes(this.query.toLowerCase()));
|
|
126
|
+
}
|
|
127
|
+
if (!this.showMore && !this.showAll) {
|
|
128
|
+
return list.slice(0, 5);
|
|
129
|
+
}
|
|
130
|
+
return list;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
getFormattedItemTitle(title) {
|
|
134
|
+
return this.isValueDateInStandartFormat(title) ? DateTime.fromFormat(title, 'yyyy-MM-dd', { zone: 'utc' }).toFormat('MM/dd/yyyy') : title;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
isValueDateInStandartFormat(value) {
|
|
138
|
+
return typeof value === 'string' ? DateTime.fromFormat(value, 'yyyy-MM-dd', { zone: 'utc' }).isValid : false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
getPercent(item) {
|
|
142
|
+
return this.facet.Total ? Math.round((item.Count / this.facet.Total) * 100) : 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
onFilterClick(val) {
|
|
146
|
+
const newVal = { ...this.value };
|
|
147
|
+
if (this.item.multiple) {
|
|
148
|
+
newVal[this.item.id] = [...Array.isArray(newVal[this.item.id]) ? newVal[this.item.id] : []].map((s) => s.toString());
|
|
149
|
+
if (newVal[this.item.id].includes(`${val.Value}`)) {
|
|
150
|
+
newVal[this.item.id].splice(newVal[this.item.id].indexOf(`${val.Value}`), 1);
|
|
151
|
+
} else {
|
|
152
|
+
newVal[this.item.id].push(`${val.Value}`);
|
|
153
|
+
}
|
|
154
|
+
} else if (`${newVal[this.item.id]}` === `${val.Value}`) {
|
|
155
|
+
newVal[this.item.id] = null;
|
|
156
|
+
} else {
|
|
157
|
+
newVal[this.item.id] = val.Value;
|
|
158
|
+
}
|
|
159
|
+
this.$emit('input', newVal);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>
|
|
3
|
+
<template v-if="type === 'string'">
|
|
4
|
+
<itf-text-field
|
|
5
|
+
delay-input="300"
|
|
6
|
+
:prepend-icon="prependIcon"
|
|
7
|
+
:clearable="filterClearable"
|
|
8
|
+
:placeholder="filter.label"
|
|
9
|
+
:value="plainValue"
|
|
10
|
+
@input="updateValue(filter.id, $event)"
|
|
11
|
+
/>
|
|
12
|
+
</template>
|
|
13
|
+
<template v-if="type === 'range'">
|
|
14
|
+
<div class="row">
|
|
15
|
+
<div class="col">
|
|
16
|
+
<itf-text-field
|
|
17
|
+
delay-input="300"
|
|
18
|
+
clearable
|
|
19
|
+
:placeholder="`${filter.label} (${$t('from')})`"
|
|
20
|
+
:value="value[filter.fields[0]]"
|
|
21
|
+
@input="updateValue(filter.fields[0], $event)"
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="col">
|
|
25
|
+
<itf-text-field
|
|
26
|
+
delay-input="300"
|
|
27
|
+
clearable
|
|
28
|
+
:placeholder="`(${$t('to')})`"
|
|
29
|
+
:value="value[filter.fields[1]]"
|
|
30
|
+
@input="updateValue(filter.fields[1], $event)"
|
|
31
|
+
/>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
<template v-else-if="type === 'list'">
|
|
36
|
+
<itf-select
|
|
37
|
+
:options="filter.items"
|
|
38
|
+
:clearable="filterClearable"
|
|
39
|
+
:reduce="(item) => item.value"
|
|
40
|
+
:get-option-label="(item) => item.title"
|
|
41
|
+
:selectable="(filter.selectable || (() => true))"
|
|
42
|
+
:value="plainValue"
|
|
43
|
+
:placeholder="filter.label"
|
|
44
|
+
:disabled="filter.disable"
|
|
45
|
+
@input="updateValue(filter.id, $event)"
|
|
46
|
+
>
|
|
47
|
+
<template #option="{ option }">
|
|
48
|
+
<div>
|
|
49
|
+
<span v-if="option.icon">{{ option.icon }}</span>
|
|
50
|
+
{{ option.title }}
|
|
51
|
+
</div>
|
|
52
|
+
</template>
|
|
53
|
+
<template #selected-option="{ option }">
|
|
54
|
+
<div>
|
|
55
|
+
<span v-if="option.icon">{{ option.icon }}</span>
|
|
56
|
+
{{ option.title }}
|
|
57
|
+
</div>
|
|
58
|
+
</template>
|
|
59
|
+
</itf-select>
|
|
60
|
+
</template>
|
|
61
|
+
<template v-else-if="type === 'tree'">
|
|
62
|
+
<itf-select
|
|
63
|
+
multiple
|
|
64
|
+
:clearable="filterClearable"
|
|
65
|
+
:placeholder="filter.label"
|
|
66
|
+
:value="plainValue"
|
|
67
|
+
:options="filter.items"
|
|
68
|
+
:reduce="(item) => item.id"
|
|
69
|
+
:selectable="(item) => !(item.type && item.type === 'group')"
|
|
70
|
+
@input="updateValue(filter.id, $event)"
|
|
71
|
+
>
|
|
72
|
+
<template #selected-option="{ option }">
|
|
73
|
+
<div v-if="option && option.label" class="d-flex align-items-center">
|
|
74
|
+
<div>
|
|
75
|
+
{{ option.label }}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
<template #option="{ option }">
|
|
80
|
+
<div v-if="option" class="d-flex align-items-start">
|
|
81
|
+
<div :style="{ 'padding-left': `${option.type && option.type === 'group' ? 0 : 20}px` }">
|
|
82
|
+
{{ option.label }}
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
</template>
|
|
86
|
+
</itf-select>
|
|
87
|
+
</template>
|
|
88
|
+
<template v-else-if="type === 'project' && hasProjectsAccess">
|
|
89
|
+
<project-selector
|
|
90
|
+
:placeholder="filter.label"
|
|
91
|
+
:rules="[]"
|
|
92
|
+
:clearable="filterClearable"
|
|
93
|
+
:value="{ Id: plainValue }"
|
|
94
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
95
|
+
/>
|
|
96
|
+
</template>
|
|
97
|
+
<template v-else-if="type === 'cashflowLine'">
|
|
98
|
+
<cashflow-line-selector
|
|
99
|
+
class="mb-3"
|
|
100
|
+
:value="{ Id: plainValue }"
|
|
101
|
+
:rules="[]"
|
|
102
|
+
:placeholder="filter.label"
|
|
103
|
+
:clearable="filterClearable"
|
|
104
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
<template v-else-if="(type === 'client' || type === 'counterparty') && isAllowClients">
|
|
108
|
+
<client-selector
|
|
109
|
+
:type="type === 'counterparty' ? 'vendors' : null"
|
|
110
|
+
:placeholder="filter.label"
|
|
111
|
+
:rules="[]"
|
|
112
|
+
:clearable="filterClearable"
|
|
113
|
+
:value="{ Id: plainValue }"
|
|
114
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
115
|
+
/>
|
|
116
|
+
</template>
|
|
117
|
+
<template v-else-if="(type === 'counterparty2') && isAllowClients">
|
|
118
|
+
<counterparty-selector
|
|
119
|
+
:placeholder="filter.label"
|
|
120
|
+
:rules="[]"
|
|
121
|
+
:clearable="filterClearable"
|
|
122
|
+
:value="{ Id: plainValue }"
|
|
123
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
124
|
+
/>
|
|
125
|
+
</template>
|
|
126
|
+
<template v-else-if="type === 'employee' && isCanSeeEmployees">
|
|
127
|
+
<employee-selector
|
|
128
|
+
:placeholder="filter.label"
|
|
129
|
+
:clearable="filterClearable"
|
|
130
|
+
:value="{ Id: plainValue }"
|
|
131
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
132
|
+
/>
|
|
133
|
+
</template>
|
|
134
|
+
<template v-else-if="type === 'businessUnit'">
|
|
135
|
+
<business-unit-selector
|
|
136
|
+
:placeholder="filter.label"
|
|
137
|
+
:clearable="filterClearable"
|
|
138
|
+
:value="{ Id: plainValue }"
|
|
139
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
140
|
+
/>
|
|
141
|
+
</template>
|
|
142
|
+
<template v-else-if="type === 'currency'">
|
|
143
|
+
<currency-selector
|
|
144
|
+
:placeholder="filter.label"
|
|
145
|
+
:clearable="filterClearable"
|
|
146
|
+
:value="{ Id: plainValue }"
|
|
147
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
148
|
+
/>
|
|
149
|
+
</template>
|
|
150
|
+
<template v-else-if="type === 'office'">
|
|
151
|
+
<office-selector
|
|
152
|
+
:placeholder="filter.label"
|
|
153
|
+
:clearable="filterClearable"
|
|
154
|
+
:value="{ Id: plainValue }"
|
|
155
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
156
|
+
/>
|
|
157
|
+
</template>
|
|
158
|
+
<template v-else-if="type === 'account'">
|
|
159
|
+
<account-selector
|
|
160
|
+
:placeholder="filter.label"
|
|
161
|
+
:clearable="filterClearable"
|
|
162
|
+
:value="{ Id: plainValue }"
|
|
163
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
164
|
+
/>
|
|
165
|
+
</template>
|
|
166
|
+
<template v-else-if="type === 'vacancy'">
|
|
167
|
+
<vacancy-selector
|
|
168
|
+
:placeholder="filter.label"
|
|
169
|
+
:clearable="filterClearable"
|
|
170
|
+
:value="{ Id: plainValue }"
|
|
171
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
172
|
+
/>
|
|
173
|
+
</template>
|
|
174
|
+
<template v-else-if="type === 'country'">
|
|
175
|
+
<country-selector
|
|
176
|
+
:placeholder="filter.label"
|
|
177
|
+
:clearable="filterClearable"
|
|
178
|
+
:value="plainValue ? { Name: plainValue } : null"
|
|
179
|
+
@input="updateValue(filter.id, $event && $event.Name)"
|
|
180
|
+
/>
|
|
181
|
+
</template>
|
|
182
|
+
<template v-else-if="type === 'profile' && isProfileAvailable">
|
|
183
|
+
<profile-selector
|
|
184
|
+
:placeholder="filter.label"
|
|
185
|
+
:clearable="filterClearable"
|
|
186
|
+
:value="{ Id: plainValue }"
|
|
187
|
+
@input="updateValue(filter.id, $event && $event.Id)"
|
|
188
|
+
/>
|
|
189
|
+
</template>
|
|
190
|
+
<template v-else-if="type === 'skills'">
|
|
191
|
+
<other-skills-selector
|
|
192
|
+
:taggable="false"
|
|
193
|
+
:placeholder="filter.label"
|
|
194
|
+
:clearable="filterClearable"
|
|
195
|
+
:value="plainValue"
|
|
196
|
+
@input="updateValue(filter.id, $event)"
|
|
197
|
+
/>
|
|
198
|
+
</template>
|
|
199
|
+
<template v-else-if="type === 'label'">
|
|
200
|
+
<itf-select
|
|
201
|
+
:loading="loadingLabels"
|
|
202
|
+
:value="plainValue"
|
|
203
|
+
:placeholder="filter.label"
|
|
204
|
+
:options="prepareLabels"
|
|
205
|
+
multiple
|
|
206
|
+
:reduce="(item) => item.Id"
|
|
207
|
+
:get-option-label="(item) => item.LabelText"
|
|
208
|
+
@input="updateValue(filter.id, $event)"
|
|
209
|
+
>
|
|
210
|
+
<div v-if="filter.conditions" slot="list-header" class="px-2 pb-2">
|
|
211
|
+
<itf-segmented-control
|
|
212
|
+
class="text-uppercase segmented-control"
|
|
213
|
+
:value="value[filter.conditions] || 'and'"
|
|
214
|
+
:items="[{Id: 'and', Name: $t('and')}, {Id: 'or', Name: $t('or')}, {Id: 'not', Name: $t('not')}]"
|
|
215
|
+
@input="updateValue(filter.conditions, $event)"
|
|
216
|
+
/>
|
|
217
|
+
</div>
|
|
218
|
+
</itf-select>
|
|
219
|
+
</template>
|
|
220
|
+
<template v-else-if="type === 'button'">
|
|
221
|
+
<itf-button block secondary :to="{ name: filter.to }">
|
|
222
|
+
{{ filter.label }}
|
|
223
|
+
</itf-button>
|
|
224
|
+
</template>
|
|
225
|
+
<template v-else-if="type === 'switch'">
|
|
226
|
+
<itf-checkbox
|
|
227
|
+
switch
|
|
228
|
+
:label="filter.label"
|
|
229
|
+
:value="plainValue"
|
|
230
|
+
@input="updateValue(filter.id, $event)"
|
|
231
|
+
/>
|
|
232
|
+
</template>
|
|
233
|
+
<template v-else-if="type === 'date'">
|
|
234
|
+
<itf-date-range-picker
|
|
235
|
+
placement="bottom-end"
|
|
236
|
+
:value="dateRangeValue"
|
|
237
|
+
:placeholder="filter.label"
|
|
238
|
+
:clearable="filterClearable"
|
|
239
|
+
:disabled="filter.disable"
|
|
240
|
+
value-format="yyyy-MM-dd"
|
|
241
|
+
@input="updateDateValue($event, filter.fields)"
|
|
242
|
+
/>
|
|
243
|
+
</template>
|
|
244
|
+
<template v-else-if="type === 'team'">
|
|
245
|
+
<team-selector
|
|
246
|
+
:clearable="filterClearable"
|
|
247
|
+
:value="{ Id: plainValue }"
|
|
248
|
+
@input="updateValue(filter.id, ($event && $event.Id) || null)"
|
|
249
|
+
/>
|
|
250
|
+
</template>
|
|
251
|
+
<div v-else-if="type === 'dates'" class="d-flex gap-1">
|
|
252
|
+
<div>
|
|
253
|
+
<itf-date-picker
|
|
254
|
+
placement="bottom-end"
|
|
255
|
+
:value="dateRangeValue[0]"
|
|
256
|
+
:placeholder="`${filter.label} (From)`"
|
|
257
|
+
:clearable="filterClearable"
|
|
258
|
+
value-format="yyyy-MM-dd"
|
|
259
|
+
@input="updateDateValue([$event, dateRangeValue[1]], filter.fields)"
|
|
260
|
+
/>
|
|
261
|
+
</div>
|
|
262
|
+
<div>
|
|
263
|
+
<itf-date-picker
|
|
264
|
+
placement="bottom-end"
|
|
265
|
+
:value="dateRangeValue[1]"
|
|
266
|
+
:placeholder="`${filter.label} (To)`"
|
|
267
|
+
:clearable="filterClearable"
|
|
268
|
+
value-format="yyyy-MM-dd"
|
|
269
|
+
@input="updateDateValue([dateRangeValue[0], $event], filter.fields)"
|
|
270
|
+
/>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<template v-else-if="type === 'date-period'">
|
|
274
|
+
<itf-period-picker
|
|
275
|
+
placement="bottom-end"
|
|
276
|
+
:value="dateRangeValue"
|
|
277
|
+
value-format="yyyy-MM-dd"
|
|
278
|
+
:placeholder="filter.label"
|
|
279
|
+
:clearable="filterClearable"
|
|
280
|
+
@input="updateDateValue($event, filter.fields)"
|
|
281
|
+
/>
|
|
282
|
+
</template>
|
|
283
|
+
</div>
|
|
284
|
+
</template>
|
|
285
|
+
<style lang="scss" scoped>
|
|
286
|
+
.input-group > :not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback) {
|
|
287
|
+
z-index: 3 !important;
|
|
288
|
+
}
|
|
289
|
+
</style>
|
|
290
|
+
<script>
|
|
291
|
+
import { Component, Model, Prop, Vue } from 'vue-property-decorator';
|
|
292
|
+
import itfButton from '@itfin/components/src/components/button/Button';
|
|
293
|
+
import itfTextField from '@itfin/components/src/components/text-field/TextField';
|
|
294
|
+
import itfSelect from '@itfin/components/src/components/select/Select';
|
|
295
|
+
import itfDateRangePicker from '@itfin/components/src/components/datepicker/DateRangePicker';
|
|
296
|
+
import itfDatePicker from '@itfin/components/src/components/datepicker/DatePicker';
|
|
297
|
+
import itfPeriodPicker from '@itfin/components/src/components/datepicker/PeriodPicker';
|
|
298
|
+
import itfCheckbox from '@itfin/components/src/components/checkbox/Checkbox';
|
|
299
|
+
import itfSegmentedControl from '@itfin/components/src/components/segmented-control/SegmentedControl';
|
|
300
|
+
import ProfileSelector from '~/components/Common/Selectors/ProfileSelector';
|
|
301
|
+
import ClientSelector from '~/components/Common/Selectors/ClientSelector';
|
|
302
|
+
import CurrencySelector from '~/components/Common/Selectors/CurrencySelector.vue';
|
|
303
|
+
import BusinessUnitSelector from '~/components/Common/Selectors/BusinessUnitSelector.vue';
|
|
304
|
+
import ProjectSelector from '~/components/Common/Selectors/ProjectSelector';
|
|
305
|
+
import EmployeeSelector from '~/components/Common/Selectors/EmployeeSelector';
|
|
306
|
+
import CounterpartySelector from '~/components/Common/Selectors/CounterpartySelector.vue';
|
|
307
|
+
import OfficeSelector from '~/components/Common/Selectors/OfficeSelector';
|
|
308
|
+
import OtherSkillsSelector from '~/components/Skills/OtherSkillsSelector';
|
|
309
|
+
import AccountSelector from '~/components/Common/Selectors/AccountSelector';
|
|
310
|
+
import VacancySelector from '~/components/Common/Selectors/VacancySelector';
|
|
311
|
+
import CountrySelector from '~/components/Common/Selectors/CountrySelector.vue';
|
|
312
|
+
import TeamSelector from '~/components/Common/Selectors/TeamSelector.vue';
|
|
313
|
+
import CashflowLineSelector from '~/components/Common/Selectors/CashflowLineSelector.vue';
|
|
314
|
+
|
|
315
|
+
import {
|
|
316
|
+
CLIENTS_VIEW_CLIENTS,
|
|
317
|
+
CLIENTS_VIEW_OWN_CLIENTS,
|
|
318
|
+
EMPLOYEES_LIST,
|
|
319
|
+
PROFILES_CAN_CHANGE_PROFILES,
|
|
320
|
+
PROFILES_SEE_PROFILES,
|
|
321
|
+
PROJECTS_LIST
|
|
322
|
+
} from '~/scopes';
|
|
323
|
+
|
|
324
|
+
export default @Component({
|
|
325
|
+
name: 'FilterItem',
|
|
326
|
+
components: {
|
|
327
|
+
BusinessUnitSelector,
|
|
328
|
+
CurrencySelector,
|
|
329
|
+
itfButton,
|
|
330
|
+
itfSelect,
|
|
331
|
+
itfCheckbox,
|
|
332
|
+
itfTextField,
|
|
333
|
+
itfSegmentedControl,
|
|
334
|
+
itfDatePicker,
|
|
335
|
+
itfDateRangePicker,
|
|
336
|
+
itfPeriodPicker,
|
|
337
|
+
ProjectSelector,
|
|
338
|
+
ProfileSelector,
|
|
339
|
+
ClientSelector,
|
|
340
|
+
OfficeSelector,
|
|
341
|
+
EmployeeSelector,
|
|
342
|
+
AccountSelector,
|
|
343
|
+
VacancySelector,
|
|
344
|
+
OtherSkillsSelector,
|
|
345
|
+
CountrySelector,
|
|
346
|
+
TeamSelector,
|
|
347
|
+
CounterpartySelector,
|
|
348
|
+
CashflowLineSelector,
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
class FilterItem extends Vue {
|
|
352
|
+
@Model('input') value;
|
|
353
|
+
@Prop(String) type;
|
|
354
|
+
@Prop() filter;
|
|
355
|
+
@Prop() labels;
|
|
356
|
+
@Prop() loadingLabels;
|
|
357
|
+
@Prop() prependIcon;
|
|
358
|
+
|
|
359
|
+
get isCanSeeEmployees () {
|
|
360
|
+
return this.$scope(EMPLOYEES_LIST);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
get isProfileAvailable () {
|
|
364
|
+
return this.$scope(PROFILES_CAN_CHANGE_PROFILES, PROFILES_SEE_PROFILES);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
get isAllowClients () {
|
|
368
|
+
return this.$scope(CLIENTS_VIEW_CLIENTS, CLIENTS_VIEW_OWN_CLIENTS);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
get hasProjectsAccess() {
|
|
372
|
+
return this.$scope(PROJECTS_LIST);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
get plainValue () {
|
|
376
|
+
return this.value[this.filter.id];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
get dateRangeValue () {
|
|
380
|
+
return this.isCustomFilter
|
|
381
|
+
? [
|
|
382
|
+
this.value[this.filter.id] && this.value[this.filter.id].from,
|
|
383
|
+
this.value[this.filter.id] && this.value[this.filter.id].to
|
|
384
|
+
]
|
|
385
|
+
: [
|
|
386
|
+
this.value[(this.filter.fields || {})[0] || 'from'],
|
|
387
|
+
this.value[(this.filter.fields || {})[1] || 'to']
|
|
388
|
+
];
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
get prepareLabels () {
|
|
392
|
+
return this.labels.map(label => ({ ...label, Color: `#${label.Color}` }));
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
get filterClearable () {
|
|
396
|
+
return typeof this.filter.clearable !== 'undefined' ? this.filter.clearable : true;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
get isCustomFilter () {
|
|
400
|
+
return this.filter.id.toLowerCase().includes('customfield_');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
updateValue (key, value, fields = {}) {
|
|
404
|
+
const filter = { ...this.value };
|
|
405
|
+
filter[key] = value;
|
|
406
|
+
this.$emit('input', filter);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
updateDateValue (value, fields = {}) {
|
|
410
|
+
const filter = { ...this.value };
|
|
411
|
+
|
|
412
|
+
if (this.isCustomFilter) {
|
|
413
|
+
const key = this.filter.id;
|
|
414
|
+
filter[key] = value[0] && value[1] ? { from: value[0], to: value[1] } : null;
|
|
415
|
+
} else {
|
|
416
|
+
filter[fields[0] || 'from'] = value[0];
|
|
417
|
+
filter[fields[1] || 'to'] = value[1];
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this.$emit('input', filter);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
</script>
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { storiesOf } from '@storybook/vue';
|
|
2
|
+
import FiltersList from './FiltersList.vue';
|
|
2
3
|
|
|
3
4
|
storiesOf('Common', module)
|
|
4
5
|
.add('Filter', () => ({
|
|
5
6
|
components: {
|
|
6
|
-
|
|
7
|
-
itfRuleGroup,
|
|
8
|
-
itfFilterBadge,
|
|
9
|
-
itfFilterControl
|
|
7
|
+
FiltersList
|
|
10
8
|
},
|
|
11
9
|
data() {
|
|
12
10
|
return {
|
|
@@ -78,22 +76,9 @@ storiesOf('Common', module)
|
|
|
78
76
|
</pre>
|
|
79
77
|
|
|
80
78
|
|
|
81
|
-
<
|
|
82
|
-
<template #dropdown-item="{ item }">
|
|
83
|
-
{{item.text}}
|
|
84
|
-
</template>
|
|
85
|
-
<template #item="{ item }">
|
|
86
|
-
{{item}}
|
|
87
|
-
</template>
|
|
88
|
-
<template #item="{ item }">
|
|
89
|
-
{{item}}
|
|
90
|
-
</template>
|
|
91
|
-
</itf-filter-control>
|
|
79
|
+
<filters-list />
|
|
92
80
|
|
|
93
81
|
{{filter}}
|
|
94
82
|
|
|
95
|
-
<h2>Example</h2>
|
|
96
|
-
|
|
97
|
-
<itf-rule-group :options="options" first />
|
|
98
83
|
</div>`,
|
|
99
84
|
}));
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
<itf-text-field
|
|
4
4
|
ref="input"
|
|
5
|
-
:value="
|
|
5
|
+
:value="hoursValue"
|
|
6
6
|
:prepend-icon="prependIcon"
|
|
7
7
|
:item-label="itemLabel"
|
|
8
8
|
:placeholder="placeholder"
|
|
@@ -48,7 +48,7 @@ class itfHoursField extends Vue {
|
|
|
48
48
|
this.$emit('input', this.hours ? formatHours(this.hours) : '');
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
get
|
|
51
|
+
get hoursValue() {
|
|
52
52
|
return this.hours ? formatHours(this.hours) : '';
|
|
53
53
|
}
|
|
54
54
|
|