@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itfin/components",
3
- "version": "1.3.42",
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>
@@ -132,6 +132,11 @@ class itfTable2 extends Vue {
132
132
  }
133
133
 
134
134
  mounted() {
135
+ this.onSchemaUpdate();
136
+ }
137
+
138
+ @Watch('schema')
139
+ onSchemaUpdate() {
135
140
  this.state = this.getTableState();
136
141
  this.selectedIds = this.state.selectedIds;
137
142
  }
@@ -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="2" autogrow v-else-if="column.type === 'textarea'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
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
- <span>{{getCalculateMethodValue(column.calculate, column)}}</span>
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 "lodash/round";
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
- &nbsp;And&nbsp;
17
- </button>
18
- <button class="btn btn-xs btn-purple-outline btn-radius"
19
- :class=" !isAnd ? 'btn-purple-outline-focus' : '' " @click.prevent="clickOr">
20
- &nbsp;Or&nbsp;
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>