@itfin/components 1.4.35 → 1.4.36

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.4.35",
3
+ "version": "1.4.36",
4
4
  "author": "Vitalii Savchuk <esvit666@gmail.com>",
5
5
  "scripts": {
6
6
  "serve": "vue-cli-service serve",
@@ -74,12 +74,9 @@ class itfApp extends Vue {
74
74
  try {
75
75
  await func();
76
76
  } catch (err) {
77
- if (err.code !== 'ERR_CANCELED') {
78
- console.error(err);
79
- this.showError(err.message);
80
- if (errFunc) {
81
- errFunc(err);
82
- }
77
+ this.showError(err.message);
78
+ if (errFunc) {
79
+ errFunc(err);
83
80
  }
84
81
  }
85
82
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  <div class="itf-checkbox form-check" :class="{ 'form-switch': this.switch, 'itf-checkbox__large': large, 'itf-checkbox__medium': medium }">
4
4
  <input class="form-check-input" ref="input" :id="id" type="checkbox" name="checkbox" v-model="isChecked" :disabled="isDisabled" />
5
- <label :for="id" class="form-check-label w-100" :class="{ 'disabled': isDisabled }">
5
+ <label :for="id" class="form-check-label">
6
6
  <slot name="label">
7
7
  {{label}}
8
8
  <slot name="icon"></slot>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-radio-box form-check card" :class="{ 'disabled': disabled, 'itf-radio__large': large, 'itf-radio__medium': medium, 'active': isChecked, 'right': right, 'left': !right }">
2
+ <div class="itf-radio-box form-check card" :class="{ 'itf-radio__large': large, 'itf-radio__medium': medium, 'active': isChecked, 'right': right, 'left': !right }">
3
3
  <input class="form-check-input" :id="id" type="radio" :name="radioName" v-model="isChecked" :value="true" :disabled="disabled" />
4
4
  <label :for="id" slot="label" class="form-check-label card-body">
5
5
 
@@ -18,17 +18,6 @@
18
18
  position: relative;
19
19
  cursor: pointer;
20
20
 
21
- &:not(.disabled) {
22
- cursor: pointer;
23
-
24
- .form-check-label {
25
- cursor: pointer;
26
- }
27
- }
28
-
29
- .form-check-label {
30
- cursor: not-allowed;
31
- }
32
21
  &.left {
33
22
  padding: 0 0 0 2.5rem;
34
23
 
@@ -49,9 +38,13 @@
49
38
  &.active {
50
39
  background-color: rgba(var(--bs-primary-rgb),.1) !important;
51
40
  }
52
- &:hover:not(.disabled) {
41
+ &:hover {
53
42
  background-color: rgba(0,0,0,.05);
54
43
  }
44
+
45
+ .form-check-label {
46
+ cursor: pointer;
47
+ }
55
48
  }
56
49
  </style>
57
50
  <script>
@@ -1,5 +1,5 @@
1
1
  <template>
2
- <div class="itf-filter-panel d-flex flex-column gap-3 align-items-start">
2
+ <div class="itf-filter-panel d-flex flex-column align-items-start" :class="{'gap-3': !filtersOnly}">
3
3
  <div v-if="!filtersOnly" class="d-flex gap-2 justify-content-between w-100">
4
4
  <slot name="search">
5
5
  <div>
@@ -35,8 +35,8 @@
35
35
  class="itf-filter-panel__badge"
36
36
  :ref="'item-' + n"
37
37
  v-model="filter[facet.name]"
38
- :is-default="filter[facet.name].isDefault"
39
- :text="filter[facet.name].label"
38
+ :is-default="filter[facet.name] && filter[facet.name].isDefault"
39
+ :text="filter[facet.name] && filter[facet.name].label"
40
40
  :type="facet.type"
41
41
  :icon="facet.icon"
42
42
  :options="facet.options"
@@ -52,7 +52,7 @@
52
52
  </div>
53
53
  <slot name="after-filters"></slot>
54
54
  </div>
55
- <div v-if="loading">
55
+ <div v-if="loading && !visibleFilters.length">
56
56
  <span class="itf-spinner"></span>
57
57
  {{$t('loading')}}
58
58
  </div>
@@ -177,6 +177,8 @@ class FilterPanel extends Vue {
177
177
  (entries) => {
178
178
  entries.forEach(entry => {
179
179
  const index = parseInt(entry.target.dataset.index);
180
+ const filter = this.filters[index];
181
+ const value = this.filter[filter.name];
180
182
  if (entry.isIntersecting) {
181
183
  this.visibleItems.add(index); // Додаємо, якщо елемент у полі зору
182
184
  } else {
@@ -206,7 +208,7 @@ class FilterPanel extends Vue {
206
208
 
207
209
  get visibleFilters() {
208
210
  if (this.mini) {
209
- return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden).slice(0, 2);
211
+ return sortBy(this.filters, (f) => this.filter[f.name].isDefault).filter(f => !f.options?.hidden);
210
212
  }
211
213
  return this.filters.filter(f => !f.options?.hidden);
212
214
  }
@@ -233,15 +235,7 @@ class FilterPanel extends Vue {
233
235
 
234
236
  this.filters = this.staticFilters ?? [];
235
237
  if (this.endpoint) {
236
- this.loading = true;
237
- await this.$try(async () => {
238
- const payload = this.panel ? this.panel.getPayload() : {};
239
- const {filters, tableSchema} = await this.$axios.$get(this.endpoint, { params: payload });
240
- this.filters = filters;
241
- this.$emit('set-table-schema', tableSchema);
242
- this.loadFiltersValue();
243
- });
244
- this.loading = false;
238
+ this.loadData();
245
239
  } else {
246
240
  this.loadFiltersValue();
247
241
  }
@@ -250,6 +244,23 @@ class FilterPanel extends Vue {
250
244
  }
251
245
  }
252
246
 
247
+ async loadData() {
248
+ this.loading = true;
249
+ await this.$try(async () => {
250
+ const payload = this.panel ? this.panel.getPayload() : {};
251
+ const {filters, tableSchema} = await this.$axios.$get(this.endpoint, {
252
+ preventRaceCondition: true,
253
+ params: payload
254
+ });
255
+ this.filters = filters;
256
+ this.loading = false;
257
+ this.$emit('set-table-schema', tableSchema);
258
+ this.loadFiltersValue();
259
+ }, () => {
260
+ this.loading = false;
261
+ });
262
+ }
263
+
253
264
  toggleFilters() {
254
265
  this.showFilters = !this.showFilters;
255
266
  if (this.stateName) {
@@ -269,7 +280,7 @@ class FilterPanel extends Vue {
269
280
  filterValue.to = payload.to;
270
281
  } else {
271
282
  filter[item.name] = payload[item.name] ? this.formatValue(item, { value: payload[item.name] }) : { isDefault: true, ...item.options.defaultValue };
272
- filterValue[item.name] = payload[item.name] ?? filter[item.name].value;
283
+ filterValue[item.name] = payload[item.name];
273
284
  }
274
285
  }
275
286
  }
@@ -282,6 +293,7 @@ class FilterPanel extends Vue {
282
293
  this.filter = filter;
283
294
  this.filterValue = filterValue;
284
295
  this.$emit('input', this.filterValue);
296
+ this.$emit('loaded', this.filterValue);
285
297
  this.initObserver();
286
298
  }
287
299
  }
@@ -318,6 +330,7 @@ class FilterPanel extends Vue {
318
330
  this.panel.setPayload({ ...payload, ...this.filterValue });
319
331
  }
320
332
  this.$emit('input', this.filterValue);
333
+ this.$emit('change', this.filterValue);
321
334
  }
322
335
 
323
336
  get daysList() {
@@ -363,7 +376,11 @@ class FilterPanel extends Vue {
363
376
  }
364
377
  } else if (facet.type === 'date') {
365
378
  const date = DateTime.fromISO(value.value);
366
- value.label = (date.isValid ? value.value : DateTime.fromISO(facet.options.defaultValue.value ?? DateTime.now())).toFormat('dd MMM yyyy');
379
+ value.label = (date.isValid ? date : DateTime.fromISO(facet.options.defaultValue.value ?? DateTime.now().toISO())).toFormat('dd MMM yyyy');
380
+ value.isDefault = facet.options.defaultValue ? value.value === facet.options.defaultValue.value : false;
381
+ } else if (facet.type === 'month') {
382
+ const date = DateTime.fromISO(value.value);
383
+ value.label = capitalizeFirstLetter((date.isValid ? date : DateTime.fromISO(facet.options.defaultValue.value)).toFormat('LLLL yyyy'));
367
384
  value.isDefault = facet.options.defaultValue ? value.value === facet.options.defaultValue.value : false;
368
385
  } else if (facet.type === 'facets-list') {
369
386
  const firstItem = facet.options.items.find(item => item.value === (Array.isArray(value.value) ? value.value[0] : value.value));
@@ -402,6 +419,10 @@ class FilterPanel extends Vue {
402
419
  }
403
420
  value.hidden = facet.options?.hidden ?? false;
404
421
  return value;
422
+
423
+ function capitalizeFirstLetter(string) {
424
+ return string.charAt(0).toUpperCase() + string.slice(1);
425
+ }
405
426
  }
406
427
  }
407
428
  </script>
@@ -1,19 +1,19 @@
1
1
  <template>
2
- <div v-loading="loading" class="px-3 pt-2 h-100 d-flex flex-column">
2
+ <div v-loading="loading" class="px-3 pt-2 h-100">
3
3
  <itf-form
4
4
  ref="editForm"
5
- class="d-flex flex-column justify-content-between flex-grow-1"
5
+ class="d-flex flex-column justify-content-between h-100"
6
6
  @keydown.native.shift.enter.stop.prevent="onSaveClick"
7
7
  @keydown.native.esc.stop.prevent="$emit('cancel')"
8
8
  >
9
9
  <slot></slot>
10
10
  <div class="py-3 justify-content-end d-flex align-items-center sticky-container">
11
11
  <div v-if="!hideFooter">
12
- <itf-button v-tooltip.delay="'Hot key: Esc'" secondary squircle :loading="loading" :disabled="loading" @click="$emit('cancel')">
13
- <span>{{ cancelBtnText }}</span>
12
+ <itf-button v-tooltip.delay="'Hot key: Esc'" secondary :loading="loading" :disabled="loading" @click="$emit('cancel')">
13
+ <span>{{ $t('components.modal.cancel') }}</span>
14
14
  </itf-button>
15
- <itf-button v-tooltip.delay="'Hot key: Shift + Enter'" primary squircle :loading="loading" :disabled="loading" @click="onSaveClick">
16
- <span>{{ saveBtnText }}</span>
15
+ <itf-button v-tooltip.delay="'Hot key: Shift + Enter'" primary :loading="loading" :disabled="loading" @click="onSaveClick">
16
+ <span>{{ $t('components.modal.save') }}</span>
17
17
  </itf-button>
18
18
  </div>
19
19
  </div>
@@ -51,8 +51,6 @@ import itfButton from '../button/Button.vue';
51
51
  export default class PanelItemEdit extends Vue {
52
52
  @Prop(Boolean) loading;
53
53
  @Prop(Boolean) hideFooter;
54
- @Prop({ type: String, default: function() { return this.$t('components.modal.save') } }) saveBtnText;
55
- @Prop({ type: String, default: function() { return this.$t('components.modal.cancel') } }) cancelBtnText;
56
54
 
57
55
  onSaveClick() {
58
56
  if (this.$refs.editForm && !this.$refs.editForm.doValidation()) {
@@ -17,20 +17,14 @@
17
17
  :icon="panel.icon"
18
18
  :payload="panel.payload"
19
19
  :expandable="panelsStack.length > 1"
20
- :isFullSize="isFullSize"
21
20
  :collapsed="panel.isCollapsed"
22
21
  :closeable="panel.isCloseable"
23
22
  :animate="panel.isAnimate"
24
23
  @open="openPanel($event[0], $event[1], n + 1)"
25
24
  @expand="expandPanel(panel)"
26
25
  @fullsize="fullsizePanel(panel)"
27
- @collapse="collapsePanel(panel)"
28
26
  @close="closePanel(panel)"
29
- @open-menu="$emit('open-menu', panel.type, panel.payload)"
30
27
  >
31
- <template #before-header>
32
- <slot name="before-header" :panel="panel" :index="n" :payload="panel.payload"></slot>
33
- </template>
34
28
  <slot
35
29
  :name="panel.type"
36
30
  :panel="panel"
@@ -40,9 +34,9 @@
40
34
  :close="() => closePanel(panel)"
41
35
  :expand="() => expandPanel(panel)"
42
36
  :fullsize="() => fullsizePanel(panel)">
43
- <component v-if="panel.components.default" :is="panel.components.default" :panel="panel" :payload="panel.payload" />
37
+ <component :is="panels[panel.type].default || panels[panel.type]" :panel="panel" :payload="panel.payload" />
44
38
  </slot>
45
- <template v-if="$scopedSlots[`${panel.type}.title`] || panel.components.title" #title>
39
+ <template v-if="$scopedSlots[`${panel.type}.title`] || panels[panel.type].title" #title>
46
40
  <slot
47
41
  :name="`${panel.type}.title`"
48
42
  :panel="panel"
@@ -52,10 +46,10 @@
52
46
  :close="() => closePanel(panel)"
53
47
  :expand="() => expandPanel(panel)"
54
48
  :fullsize="() => fullsizePanel(panel)">
55
- <component v-if="panel.components.title" :is="panel.components.title" :panel="panel" :payload="panel.payload" />
49
+ <component v-if="panels[panel.type].title" :is="panels[panel.type].title" :panel="panel" :payload="panel.payload" />
56
50
  </slot>
57
51
  </template>
58
- <template v-if="$scopedSlots[`${panel.type}.buttons`] || panel.components.buttons" #buttons>
52
+ <template v-if="$scopedSlots[`${panel.type}.buttons`] || panels[panel.type].buttons" #buttons>
59
53
  <slot
60
54
  :name="`${panel.type}.buttons`"
61
55
  :panel="panel"
@@ -65,10 +59,10 @@
65
59
  :close="() => closePanel(panel)"
66
60
  :expand="() => expandPanel(panel)"
67
61
  :fullsize="() => fullsizePanel(panel)">
68
- <component v-if="panel.components.buttons" :is="panel.components.buttons" :panel="panel" :payload="panel.payload" />
62
+ <component v-if="panels[panel.type].buttons" :is="panels[panel.type].buttons" :panel="panel" :payload="panel.payload" />
69
63
  </slot>
70
64
  </template>
71
- <template v-if="$scopedSlots[`${panel.type}.header`] || panel.components.header" #header>
65
+ <template v-if="$scopedSlots[`${panel.type}.header`] || panels[panel.type].header" #header>
72
66
  <slot
73
67
  :name="`${panel.type}.header`"
74
68
  :panel="panel"
@@ -78,7 +72,7 @@
78
72
  :close="() => closePanel(panel)"
79
73
  :expand="() => expandPanel(panel)"
80
74
  :fullsize="() => fullsizePanel(panel)">
81
- <component v-if="panel.components.header" :is="panel.components.header" :panel="panel" :payload="panel.payload" />
75
+ <component v-if="panels[panel.type].header" :is="panels[panel.type].header" :panel="panel" :payload="panel.payload" />
82
76
  </slot>
83
77
  </template>
84
78
  </panel>
@@ -154,6 +148,7 @@ $double-an-time: $an-time * 2;
154
148
  //transition: opacity $an-time linear;
155
149
  }
156
150
  }
151
+
157
152
  //.slide-enter-active > div {
158
153
  // opacity: 0;
159
154
  //}
@@ -164,10 +159,8 @@ $double-an-time: $an-time * 2;
164
159
  </style>
165
160
  <script lang="ts">
166
161
  import { Vue, Component, Prop } from 'vue-property-decorator';
167
- import itfIcon from '../icon/Icon.vue';
168
- import Panel from './Panel.vue';
169
- import {hashToStack, stackToHash} from "@itfin/components/src/components/panels/helpers";
170
- import {emitGlobalEvent, setRootPanelList} from "@itfin/components/src/components/panels";
162
+ import Panel from './Panel';
163
+ import {hashToStack, stackToHash} from "./helpers";
171
164
 
172
165
  interface VisualOptions {
173
166
  title: string;
@@ -202,7 +195,6 @@ export interface IPanel {
202
195
 
203
196
  @Component({
204
197
  components: {
205
- itfIcon,
206
198
  Panel
207
199
  },
208
200
  directives: {
@@ -216,15 +208,12 @@ export interface IPanel {
216
208
  export default class PanelList extends Vue {
217
209
  @Prop() firstPanel: IPanel;
218
210
  @Prop() panels: Record<string, Component>;
219
- @Prop({ default: () => {} }) searchPanel: (type: string) => boolean;
220
- @Prop({ type: String, default: 'path' }) routeType: string;
221
211
 
222
212
  panelsStack:IPanel[] = [];
223
213
 
224
214
  nextId:number = 0;
225
215
 
226
216
  created() {
227
- setRootPanelList(this);
228
217
  if (this.firstPanel) {
229
218
  this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
230
219
  }
@@ -278,30 +267,18 @@ export default class PanelList extends Vue {
278
267
  this.panelsStack = newStack;
279
268
  }
280
269
 
281
- async internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
282
- let panel = this.panels[type];
283
- if (!panel) {
284
- panel = await this.searchPanel(type, this.panels);
285
- if (!panel) {
286
- console.error(`Panel type "${type}" not found`);
287
- return;
288
- }
289
- panel.type = type;
270
+ internalOpenPanel(type: string, payload: any = {}, openIndex?: number, noEvents = false) {
271
+ if (!this.panels[type]) {
272
+ return;
290
273
  }
291
- if (typeof panel.caption !== 'function') {
274
+ if (typeof this.panels[type].caption !== 'function') {
292
275
  throw new Error('Panel component must have a "caption" function');
293
276
  }
294
277
  const newPanel:any = {
295
278
  id: this.nextId++,
296
- nocard: panel.nocard,
297
- title: panel.caption(this.$t.bind(this), payload),
298
- icon: panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
299
- components: {
300
- default: panel.default ?? undefined,
301
- buttons: panel.buttons ?? undefined,
302
- header: panel.header ?? undefined,
303
- title: panel.title ?? undefined,
304
- },
279
+ nocard: this.panels[type].nocard,
280
+ title: this.panels[type].caption(this.$t.bind(this), payload),
281
+ icon: this.panels[type].icon ? this.panels[type].icon(this.$t.bind(this), payload) : null,
305
282
  type,
306
283
  payload,
307
284
  isCollapsed: false,
@@ -312,7 +289,7 @@ export default class PanelList extends Vue {
312
289
  newPanel.isCloseable = false;
313
290
  }
314
291
  let newStack = [...this.panelsStack];
315
- if (panel.permanentExpanded && newStack.length) {
292
+ if (this.panels[type].permanentExpanded && newStack.length) {
316
293
  for (const panel of newStack) {
317
294
  panel.isCollapsed = true;
318
295
  }
@@ -322,32 +299,25 @@ export default class PanelList extends Vue {
322
299
  isAnimation = newStack.length === openIndex;
323
300
  newStack = newStack.slice(0, openIndex);
324
301
  }
325
- if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed)) {
326
- // якщо немає відкритих панелей, то перша панель має бути розгорнута
327
- newStack[0].isCollapsed = false;
328
- }
329
302
  this.panelsStack = newStack;
330
303
  return new Promise(res => {
331
304
  this.$nextTick(() => { // щоб панелі змінювались при редагуванні
332
305
  const n = newStack.length;
333
306
  newPanel.isAnimate = isAnimation;
334
- newPanel.permanentExpanded = !!panel.permanentExpanded;
307
+ newPanel.permanentExpanded = !!this.panels[type].permanentExpanded;
335
308
  newPanel.emit = (event, ...args) => this.emitEvent(event, ...args);
336
- newPanel.open = (type, payload, index?:number) => this.openPanel(type, payload, index ?? n + 1);
309
+ newPanel.open = (type, payload) => this.openPanel(type, payload, n + 1);
337
310
  newPanel.close = () => this.closePanel(newPanel);
338
311
  newPanel.expand = () => this.expandPanel(newPanel);
339
312
  newPanel.getTitle = () => newPanel.title;
340
313
  newPanel.getIcon = () => newPanel.icon;
341
- newPanel.setTitle = (title: string) => { newPanel.title = title; this.updateTitle() };
314
+ newPanel.setTitle = (title: string) => { newPanel.title = title; };
342
315
  newPanel.setIcon = (icon: string) => { newPanel.icon = icon; };
343
- newPanel.on = (eventName: string|string[], func: (event: string, ...args: any[]) => any) => {
344
- const eventNames = Array.isArray(eventName) ? eventName : [eventName];
345
- for (const evName of eventNames) {
346
- if (!newPanel.__events[evName]) {
347
- newPanel.__events[evName] = [];
348
- }
349
- newPanel.__events[evName].push(func);
316
+ newPanel.on = (eventName, func: (event: string, ...args: any[]) => any) => {
317
+ if (!newPanel.__events[eventName]) {
318
+ newPanel.__events[eventName] = [];
350
319
  }
320
+ newPanel.__events[eventName].push(func);
351
321
  };
352
322
  newPanel.off = (eventName, func: (event: string, ...args: any[]) => any) => {
353
323
  if (newPanel.__events[eventName]) {
@@ -367,7 +337,9 @@ export default class PanelList extends Vue {
367
337
  newPanel.getPayload = () => newPanel.payload;
368
338
  newPanel.setPayload = (value: any) => {
369
339
  newPanel.payload = value;
370
- this.setPanelHash();
340
+ newPanel.title = this.panels[type].caption(this.$t.bind(this), value);
341
+ newPanel.icon = this.panels[type].icon ? this.panels[type].icon(this.$t.bind(this), payload) : null,
342
+ this.setPanelHash()
371
343
  }
372
344
  newStack.push(newPanel);
373
345
  this.panelsStack = newStack;
@@ -381,19 +353,12 @@ export default class PanelList extends Vue {
381
353
  });
382
354
  }
383
355
 
384
- updateTitle() {
385
- const titles = this.panelsStack.map(p => p.getTitle()).filter(Boolean).reverse();
386
- this.$root.$options.head.titleChunk = titles.join(' / ');
387
- this.$meta().refresh();
388
- }
389
-
390
356
  async openPanel(type: string, payload: any, openIndex?: number) {
391
357
  await this.internalOpenPanel(type, payload, openIndex);
392
- this.setPanelHash();
358
+ this.setPanelHash()
393
359
  }
394
360
 
395
361
  emitEvent(event: string, ...args: any[]) {
396
- emitGlobalEvent(event, ...args);
397
362
  for (const panel of this.panelsStack) {
398
363
  if (panel.__events[event]) {
399
364
  for (const func of panel.__events[event]) {
@@ -423,40 +388,12 @@ export default class PanelList extends Vue {
423
388
  fullsizePanel(panel: IPanel) {
424
389
  const newStack = [...this.panelsStack];
425
390
  for (const p of newStack) {
426
- p.isLastOpened = !p.isCollapsed && p !== panel;
427
391
  p.isCollapsed = p !== panel;
428
392
  }
429
393
  this.panelsStack = newStack;
430
394
  this.setPanelHash();
431
395
  }
432
396
 
433
- get isFullSize() {
434
- return this.panelsStack.filter(p => !p.isCollapsed).length === 1;
435
- }
436
-
437
- expandPanel(panel: IPanel) {
438
- const newStack = [...this.panelsStack];
439
- const index = newStack.findIndex(p => p.id === panel.id);
440
- newStack[index].isCollapsed = false;
441
- this.panelsStack = newStack;
442
- this.ensureOnlyTwoOpenPanels(panel.id);
443
- this.setPanelHash();
444
- }
445
-
446
- collapsePanel(panel: IPanel) {
447
- const newStack = [...this.panelsStack];
448
- const currenctIndex = newStack.findIndex(p => p.id === panel.id);
449
- const lastOpenedIndex = newStack.findIndex(p => p.isLastOpened);
450
- if (lastOpenedIndex !== -1) { // якщо зебрежена остання відкрита панель
451
- newStack[lastOpenedIndex].isCollapsed = false
452
- } else if (newStack[currenctIndex-1]) { // якщо після оновлення сторінки відсутнє значення "остання відкрита", то відкриваємо ту, що зліва
453
- newStack[currenctIndex-1].isCollapsed = false;
454
- }
455
- this.panelsStack = newStack;
456
- this.ensureOnlyTwoOpenPanels(panel.id);
457
- this.setPanelHash();
458
- }
459
-
460
397
  getPanels(type) {
461
398
  return this.panelsStack.filter(panel => panel.type === type);
462
399
  }
@@ -466,19 +403,14 @@ export default class PanelList extends Vue {
466
403
  }
467
404
 
468
405
  setPanelHash() {
469
- const hash = stackToHash(this.panelsStack, this.routeType === 'path').replace(/^#/, '');
470
- if (this.routeType === 'path') {
471
- this.$router.push({ path: `/${hash}` });
472
- } else {
473
- this.$router.push({ hash });
474
- }
475
- this.updateTitle();
406
+ const hash = stackToHash(this.panelsStack).replace(/^#/, '');
407
+ this.$router.push({ hash });
476
408
  }
477
409
 
478
410
  async parsePanelHash() {
479
- const hash = this.routeType === 'path' ? location.pathname : location.hash;
411
+ const {hash} = location;
480
412
  if (hash) {
481
- const panels = hashToStack(hash, this.routeType === 'path');
413
+ const panels = hashToStack(hash);
482
414
  const newStack = [];
483
415
  this.panelsStack = [];
484
416
  for (const panelIndex in panels) {
@@ -499,7 +431,6 @@ export default class PanelList extends Vue {
499
431
  }
500
432
  this.panelsStack = newStack;
501
433
  this.emitEvent('panels.changed', this.panelsStack);
502
- this.updateTitle();
503
434
  }
504
435
  }
505
436
 
@@ -1,51 +1,33 @@
1
- import JSON5 from 'json5'
2
- import {isPathType} from "./index";
3
-
4
1
  export interface IPanel {
5
2
  type: string;
6
3
  payload?: any;
7
4
  isCollapsed?: boolean;
8
5
  }
9
6
 
10
- const COLLAPSE_SYMBOL = '~'
11
- const PARAMS_SYMBOL = ';'
12
-
13
7
  export function stackToHash(stack: IPanel[]) {
14
8
  const hash = stack.map(panel => {
15
- let json = JSON5.stringify(panel.payload || {});
16
- json = json.substring(1, json.length - 1); // Remove the outer {}
17
- return `${panel.type}${panel.isCollapsed ? COLLAPSE_SYMBOL : ''}${json ? PARAMS_SYMBOL : ''}${json}`;
18
- }).join(isPathType() ? '/' : '&');
19
- return isPathType() ? `/${hash}` : `#${hash}`;
9
+ return `${panel.type}${panel.isCollapsed ? '' : '!'}=${JSON.stringify(panel.payload || {})}`;
10
+ }).join('&');
11
+ return `#${hash}`;
20
12
  }
21
13
 
22
14
 
23
15
  export function hashToStack(hash: string|undefined): IPanel[] {
24
16
  let stack:IPanel[] = [];
25
- const str = hash.replace(isPathType() ? /^\// : /^#/, '');
26
- if (str) {
27
- stack = str.split(isPathType() ? '/' : '&').map(item => {
28
- if (!item.includes(PARAMS_SYMBOL)) {
29
- return { type: item.replace(COLLAPSE_SYMBOL, ''), isCollapsed: item.includes(COLLAPSE_SYMBOL), payload: {} };
30
- }
31
- const [type, payload] = item.split(PARAMS_SYMBOL);
32
- const isCollapsed = type.includes(COLLAPSE_SYMBOL);
17
+ if (hash) {
18
+ const str = hash.replace(/^#/, '');
19
+
20
+ stack = str.split('&').map(item => {
21
+ const [type, payload] = item.split('=');
22
+ const isCollapsed = !type.includes('!');
33
23
  let payloadObj:any = {};
34
24
  try {
35
- let json = decodeURIComponent(payload);
36
- if (!json.startsWith('{')) {
37
- json = `{${json}`; // Ensure it starts with a '{' to be valid JSON
38
- }
39
- if (!json.endsWith('}')) {
40
- json += '}'; // Ensure it ends with a '}' to be valid JSON
41
- }
42
- payloadObj = JSON5.parse(json);
25
+ payloadObj = JSON.parse(decodeURIComponent(payload));
43
26
  } catch (e) {
44
27
  // ignore
45
- console.warn(`Error parsing payload for type ${type}:`, payload, e);
46
28
  }
47
29
  return {
48
- type: type.replace(COLLAPSE_SYMBOL, ''),
30
+ type: type.replace('!', ''),
49
31
  isCollapsed,
50
32
  payload: payloadObj
51
33
  };
@@ -7,11 +7,9 @@
7
7
  'permanent-checkboxes': selectedIds.length
8
8
  }" :style="{ '--indicator-area-width': `${indicatorType === 'none' ? 1 : indicatorWidth}px`, '--shadow-area-width': `${shadowWidth}px` }">
9
9
  <itf-notice-popout :visible="showGroupOperations" class="rounded-3 bg-black text-white">
10
- <div class="d-flex gap-2 ps-2 align-items-center small itf-table2_mass-operations">
11
- <slot name="group-operations-count">
12
- <div>{{$tc('components.table.selectedItems', selectedIds.length, { n: selectedIds.length })}}</div>
13
- <div class="opacity-50">•</div>
14
- </slot>
10
+ <div class="d-flex gap-2 ps-3 align-items-center small itf-table2_mass-operations">
11
+ <div>{{$tc('components.table.selectedItems', selectedIds.length, { n: selectedIds.length })}}</div>
12
+ <div class="opacity-50">•</div>
15
13
  <a href="" class="me-3 opacity-50 text-white text-decoration-none" @click.stop.prevent="selectedIds = []">{{$t('components.table.cancelSelected')}}</a>
16
14
  <div>
17
15
  <slot name="group-operations"></slot>
@@ -19,61 +17,64 @@
19
17
  </div>
20
18
  </itf-notice-popout>
21
19
  <div class="scrollable scrollable-x">
22
- <itf-checkbox-group v-model="selectedIds">
23
- <template v-for="(group, index) in groups">
24
- <div class="table-view-body">
25
- <itf-table-group
26
- :key="index"
27
- @update="$emit('update', { ...$event, group, groupIndex: index })"
28
- @row-click="$emit('row-click', $event)"
29
- :id-property="idProperty"
30
- :columns="columns"
31
- @update:columns="onColumnsUpdate"
32
- :rows="group.rows"
33
- :title="group.name"
34
- :selected-ids.sync="selectedIds"
35
- :add-new-rows="addNewRows"
36
- :shadow-width="shadowWidth"
37
- :column-sorting="columnSorting"
38
- :column-resizing="columnResizing"
39
- :show-grouping="showGrouping"
40
- :show-summary="showSummary"
41
- :show-add-column="showAddColumn"
42
- :show-actions="showActions"
43
- :show-header="!noHeader"
44
- :schema="schema"
45
- :editable="editable"
46
- :no-column-menu="noColumnMenu"
47
- :no-select-all="noSelectAll"
48
- :currencies="currencies"
49
- :currency="currency"
50
- :subrows-property="subrowsProperty"
51
- :divider-property="dividerProperty"
52
- :indicator-type="indicatorType"
53
- :expanded-all="expandedAll"
54
- :indicatorWidth="indicatorWidth"
55
- :striped="striped"
56
- :expanded-ids="expandedIds"
57
- :css-property="cssProperty"
58
- :sticky-header="stickyHeader"
59
- :editable-property="editableProperty"
60
- :sorting.sync="_sorting"
61
- :active="active"
62
- @update:expanded-ids="$emit('update:expanded-ids', $event)"
63
- @new="$emit('new', $event)"
64
- @filter="$emit('filter', $event)"
65
- @add-column="$emit('add-column', $event)"
66
- >
67
- <template v-for="(_, name) in $slots" #[name]="slotData">
68
- <slot :name="name" v-bind="slotData || {}" />
69
- </template>
70
- <template v-for="(_, name) in $scopedSlots" #[name]="slotData">
71
- <slot :name="name" v-bind="slotData || {}" />
72
- </template>
73
- </itf-table-group>
74
- </div>
75
- </template>
76
- </itf-checkbox-group>
20
+ <itf-checkbox-group v-model="selectedIds">
21
+ <template v-for="(group, index) in groups">
22
+ <div class="table-view-body">
23
+ <itf-table-group
24
+ :key="index"
25
+ @update="$emit('update', { ...$event, group, groupIndex: index })"
26
+ @row-click="$emit('row-click', $event)"
27
+ :id-property="idProperty"
28
+ :columns="columns"
29
+ @update:columns="onColumnsUpdate"
30
+ :rows="group.rows"
31
+ :title="group.name"
32
+ :selected-ids.sync="selectedIds"
33
+ :add-new-rows="addNewRows"
34
+ :shadow-width="shadowWidth"
35
+ :column-sorting="columnSorting"
36
+ :column-resizing="columnResizing"
37
+ :show-grouping="showGrouping"
38
+ :show-summary="showSummary"
39
+ :show-add-column="showAddColumn"
40
+ :show-actions="showActions"
41
+ :show-header="!noHeader"
42
+ :schema="schema"
43
+ :editable="editable"
44
+ :no-column-menu="noColumnMenu"
45
+ :no-select-all="noSelectAll"
46
+ :currencies="currencies"
47
+ :currency="currency"
48
+ :subrows-property="subrowsProperty"
49
+ :async-subrows-property="asyncSubrowsProperty"
50
+ :divider-property="dividerProperty"
51
+ :indicator-type="indicatorType"
52
+ :expanded-all="expandedAll"
53
+ :indicatorWidth="indicatorWidth"
54
+ :striped="striped"
55
+ :expanded-ids="expandedIds"
56
+ :css-property="cssProperty"
57
+ :sticky-header="stickyHeader"
58
+ :editable-property="editableProperty"
59
+ :sorting.sync="_sorting"
60
+ :sort-as-string="sortAsString"
61
+ :active="active"
62
+ @loadChildren="$emit('loadChildren', $event)"
63
+ @update:expanded-ids="$emit('update:expanded-ids', $event)"
64
+ @new="$emit('new', $event)"
65
+ @filter="$emit('filter', $event)"
66
+ @add-column="$emit('add-column', $event)"
67
+ >
68
+ <template v-for="(_, name) in $slots" #[name]="slotData">
69
+ <slot :name="name" v-bind="slotData || {}" />
70
+ </template>
71
+ <template v-for="(_, name) in $scopedSlots" #[name]="slotData">
72
+ <slot :name="name" v-bind="slotData || {}" />
73
+ </template>
74
+ </itf-table-group>
75
+ </div>
76
+ </template>
77
+ </itf-checkbox-group>
77
78
  </div>
78
79
  </div>
79
80
 
@@ -104,11 +105,13 @@ export default @Component({
104
105
  })
105
106
  class itfTable2 extends Vue {
106
107
  // @Prop({ required: true, type: Array }) columns;
108
+ @Prop(Boolean) sortAsString;
107
109
  @Prop({ required: true, type: Array }) rows;
108
110
  @Prop({ type: String, default: null }) groupBy;
109
111
  @Prop({ type: String, default: null }) idProperty;
110
112
  @Prop({ type: String, default: null }) cssProperty;
111
113
  @Prop({ type: String, default: null }) subrowsProperty;
114
+ @Prop({ type: String, default: null }) asyncSubrowsProperty;
112
115
  @Prop({ type: String, default: null }) dividerProperty;
113
116
  @Prop({ type: String, default: null }) editableProperty;
114
117
  @Prop({ default: null }) active;
@@ -244,7 +247,8 @@ class itfTable2 extends Vue {
244
247
  @Watch('selectedIds')
245
248
  onSelectedIdsUpdate(selectedIds) {
246
249
  this.state.selectedIds = selectedIds;
247
- this.saveTableState();
250
+ // метод saveTableState не зберігає selectedIds в localStorage, не впевнений що він тут треба
251
+ // this.saveTableState();
248
252
  }
249
253
 
250
254
  onColumnsUpdate(columns) {
@@ -7,6 +7,7 @@
7
7
  :columns="columns"
8
8
  :id-property="idProperty"
9
9
  :subrows-property="subrowsProperty"
10
+ :async-subrows-property="asyncSubrowsProperty"
10
11
  :divider-property="dividerProperty"
11
12
  :show-add-column="showAddColumn"
12
13
  :show-actions="showActions"
@@ -144,6 +145,7 @@ class itfTableBody extends Vue {
144
145
  @Prop() rows;
145
146
  @Prop() idProperty;
146
147
  @Prop() subrowsProperty;
148
+ @Prop() asyncSubrowsProperty;
147
149
  @Prop() dividerProperty;
148
150
  @Prop() active;
149
151
  @Prop(Boolean) showAddColumn;
@@ -164,6 +166,10 @@ class itfTableBody extends Vue {
164
166
  this.$emit('update:expanded-ids', this.expandedIds.includes(item[this.idProperty])
165
167
  ? this.expandedIds.filter((id) => id !== item[this.idProperty])
166
168
  : [...this.expandedIds, item[this.idProperty]]);
169
+
170
+ if (this.asyncSubrowsProperty && item[this.asyncSubrowsProperty] && item[this.asyncSubrowsProperty]) {
171
+ this.$emit('loadChildren', item);
172
+ }
167
173
  }
168
174
  }
169
175
  </script>
@@ -17,7 +17,7 @@
17
17
  <div class="shadow-area"></div>
18
18
  <div class="header-wrapper" :class="{'header-additional-column': showAddColumn}" @click.prevent="toggleGroup">
19
19
  <a class="header-content position-sticky d-flex align-items-center">
20
- <itf-button squircle icon small secondary class="collapse-arrow">
20
+ <itf-button icon small secondary class="collapse-arrow">
21
21
  <itf-icon :name="isShowTable ? 'chevron_down' : 'chevron_right'"/>
22
22
  </itf-button>
23
23
  <span class="d-flex align-items-center line-overflow group-header-value text-primary"
@@ -39,6 +39,7 @@
39
39
  :show-add-column="showAddColumn"
40
40
  :show-actions="showActions"
41
41
  :id-property="idProperty"
42
+ :sort-as-string="sortAsString"
42
43
  :rows="rows"
43
44
  :schema="schema"
44
45
  :editable="editable"
@@ -61,11 +62,13 @@
61
62
  @row-click="$emit('row-click', $event)"
62
63
  :id-property="idProperty"
63
64
  :subrows-property="subrowsProperty"
65
+ :async-subrows-property="asyncSubrowsProperty"
64
66
  :divider-property="dividerProperty"
65
67
  :rows="rows"
66
68
  :editable="editable"
67
69
  :currency="currency"
68
70
  :currencies="currencies"
71
+ :sort-as-string="sortAsString"
69
72
  :columns="visibleColumns"
70
73
  :no-select-all="noSelectAll"
71
74
  :selected-ids="selectedIds"
@@ -78,6 +81,7 @@
78
81
  :css-property="cssProperty"
79
82
  :editable-property="editableProperty"
80
83
  :active="active"
84
+ @loadChildren="$emit('loadChildren', $event)"
81
85
  @update:expanded-ids="$emit('update:expanded-ids', $event)"
82
86
  >
83
87
  <template v-for="(_, name) in $slots" #[name]="slotData">
@@ -91,11 +95,11 @@
91
95
 
92
96
  <!-- Лінія додати нову -->
93
97
  <div v-if="isShowTable && addNewRows"
94
- class="table-row-template d-flex align-items-stretch">
98
+ class="table-row-template table-row-template__new-row d-flex align-items-stretch">
95
99
  <div class="shadow-area"></div>
96
100
  <a href="" @click.prevent="$emit('new', title)" data-test="table-add-new-item"
97
101
  class="d-flex align-items-center flex-grow-1 table-add-new-item text-decoration-none">
98
- <span class="d-sticky d-flex align-items-center py-1">
102
+ <span class="d-sticky d-flex align-items-center py-1 px-2 small">
99
103
  <itf-icon name="plus"/>
100
104
  <span>{{ newLabel }}</span>
101
105
  </span>
@@ -264,7 +268,11 @@
264
268
  min-height: var(--table-small-row-size);
265
269
  }
266
270
 
271
+ .table-row-template.table-row-template__new-row {
272
+ min-height: 2rem;
273
+ }
267
274
  .table-add-new-item {
275
+ background-color: var(--itf-table-header-bg);
268
276
  border-right:var(--itf-table-border-base-width) solid var(--itf-table-border-base-color);
269
277
  border-left:var(--itf-table-border-base-width) solid var(--itf-table-border-base-color);
270
278
  border-bottom: var(--itf-table-border-base-width) solid var(--itf-table-border-base-color);
@@ -273,7 +281,7 @@
273
281
  border-bottom-right-radius: var(--itf-table-table-border-radius);
274
282
 
275
283
  & > span {
276
- left: var(--shadow-area-width);
284
+ left: calc(var(--shadow-area-width) + 4px);
277
285
  position: sticky;
278
286
  padding-left: var(--shadow-area-width);
279
287
  //border-left: var(--itf-table-border-base-width) solid var(--itf-table-border-base-color);
@@ -359,6 +367,7 @@ class itfTableGroup extends Vue {
359
367
  @Prop() title;
360
368
  @Prop() idProperty;
361
369
  @Prop() subrowsProperty;
370
+ @Prop() asyncSubrowsProperty;
362
371
  @Prop() dividerProperty;
363
372
  @Prop() currency;
364
373
  @Prop() currencies;
@@ -378,6 +387,7 @@ class itfTableGroup extends Vue {
378
387
  @Prop(Boolean) expandedAll;
379
388
  @Prop(Boolean) striped;
380
389
  @Prop(Boolean) stickyHeader;
390
+ @Prop(Boolean) sortAsString;
381
391
  @Prop() indicatorWidth;
382
392
  @Prop() shadowWidth;
383
393
  @Prop() cssProperty;
@@ -4,8 +4,8 @@
4
4
  <div ref="container" class="table-row-template">
5
5
  <div accept-group="items" class="table-view-body-space" v-dropzone="{ payload: 0 }"></div>
6
6
  <div class="shadow-area"></div>
7
- <div class="table-view-header-value reserved sticky">
8
- <itf-checkbox v-if="indicatorType === 'checkbox' && visibleHeader && !noSelectAll" ungrouped value="all" v-model="selectAll" ref="selectAll" />
7
+ <div v-if="indicatorType !== 'none'" class="table-view-header-value reserved sticky">
8
+ <itf-checkbox v-if="indicatorType !== 'none' && visibleHeader && !noSelectAll" ungrouped value="all" v-model="selectAll" ref="selectAll" />
9
9
  </div>
10
10
 
11
11
  <template v-for="(column, n) in visibleAttributes">
@@ -29,14 +29,16 @@
29
29
  <div v-if="visibleHeader" group="tablecolumns"
30
30
  class="table-header"
31
31
  @drop="reorderColumns"
32
- v-draggable="{ handle: true, payload: { index: n, item: column }, mirror: {yAxis:false} }">
32
+ v-draggable="{ dragHandleClass: null, handle: true, payload: { index: n, item: column }, mirror: {yAxis:false} }">
33
33
  <itf-dropdown text append-to-body shadow ref="dropdown" class="w-100" :disabled="noColumnMenu">
34
34
  <template #button>
35
35
  <div class="itf-table2__header-title d-flex w-100 align-items-center" :title="getTitle(column.title)">
36
36
  <itf-icon class="itf-table2__header-icon" new v-if="column.icon" :name="column.icon"></itf-icon>
37
37
  <div class="flex-grow-1 w-100 itf-table2__title-container d-flex align-items-center" :class="{'justify-content-end': column.align === 'end'}">
38
- <div class="itf-table2__title text-truncate">{{getTitle(column.title)}}</div>
39
- <div v-if="column.prefix" class="itf-table2__subtitle text-truncate" v-text="column.prefix" />
38
+ <div class="itf-table2__title text-truncate">
39
+ {{getTitle(column.title)}}
40
+ <div v-if="column.prefix" class="itf-table2__subtitle text-truncate" :class="{'text-end': column.align === 'end'}" v-text="column.prefix" />
41
+ </div>
40
42
  </div>
41
43
  </div>
42
44
  <itf-icon v-if="sortColumnParams[column.property]" :name="sortColumnParams[column.property] === 'asc' ? 'sort-asc' : 'sort-desc'" new :size="20" class="ms-1" />
@@ -194,6 +196,7 @@ class itfTableHeader extends Vue {
194
196
  @Prop(Boolean) noColumnMenu;
195
197
  @Prop(Boolean) noSelectAll;
196
198
  @Prop(Boolean) editable;
199
+ @Prop(Boolean) sortAsString;
197
200
  @Prop() idProperty;
198
201
  @Prop() indicatorType;
199
202
 
@@ -411,8 +414,10 @@ class itfTableHeader extends Vue {
411
414
  }
412
415
 
413
416
  sortBy(column, order) {
414
- let sort = order === 'desc' ? `-${column.property}` : column.property;
415
- console.info(sort);
417
+ let sort = { [column.property]: order };
418
+ if (this.sortAsString) {
419
+ sort = order === 'desc' ? `-${column.property}` : column.property;
420
+ }
416
421
  this.$emit('update:sorting', sort);
417
422
  }
418
423
  }
@@ -2,7 +2,10 @@
2
2
  <div>
3
3
  <div @click.prevent.stop="toggle" class="d-flex align-items-center flex-nowrap" :class="{'active-toggle': visible}">
4
4
  <div class="item-toggle text-muted">
5
- <template v-if="visible && expanded">
5
+ <template v-if="visible && loading">
6
+ <div class="itf-spinner"></div>
7
+ </template>
8
+ <template v-else-if="visible && expanded">
6
9
  <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
7
10
  width="16" height="16" viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
8
11
  <path d="M184.7,413.1l2.1-1.8l156.5-136c5.3-4.6,8.6-11.5,8.6-19.2c0-7.7-3.4-14.6-8.6-19.2L187.1,101l-2.6-2.3
@@ -23,6 +26,10 @@
23
26
  </div>
24
27
  </template>
25
28
  <style lang="scss" scoped>
29
+ .itf-spinner {
30
+ width: 1rem;
31
+ height: 1rem;
32
+ }
26
33
  .active-toggle {
27
34
  cursor: pointer;
28
35
  }
@@ -43,6 +50,7 @@ export default @Component({
43
50
  class itfTableRowToggle extends Vue {
44
51
  @Prop(Boolean) expanded;
45
52
  @Prop(Boolean) visible;
53
+ @Prop(Boolean) loading;
46
54
 
47
55
  toggle() {
48
56
  this.$emit('toggle');
@@ -25,6 +25,22 @@
25
25
  <span v-if="indicatorType === 'order'">{{ (n + 1) }}</span>
26
26
  <span v-else-if="indicatorType === 'property'">{{ item[idProperty] }}</span>
27
27
  <span v-else-if="indicatorType === 'checkbox'"><itf-checkbox :value="item[idProperty]" /></span>
28
+ <a href="" @click.prevent.stop="$emit('toggle', item)" v-else-if="indicatorType === 'toggle'">
29
+ <template v-if="subrowsProperty && item[subrowsProperty] && item[subrowsProperty].length">
30
+ <template v-if="item[subrowsProperty] && item[subrowsProperty].length && !isExpanded(item)">
31
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-plus-square" viewBox="0 0 16 16">
32
+ <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
33
+ <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>
34
+ </svg>
35
+ </template>
36
+ <template v-else>
37
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dash-square" viewBox="0 0 16 16">
38
+ <path d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z"/>
39
+ <path d="M4 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 4 8"/>
40
+ </svg>
41
+ </template>
42
+ </template>
43
+ </a>
28
44
  </div>
29
45
  </div>
30
46
  <div accept-group="items" class="table-item-inner" @click="$emit('row-click', item)">
@@ -33,37 +49,37 @@
33
49
  v-if="column.visible !== false"
34
50
  :data-column="k"
35
51
  :style="`width: ${column.width}px; max-width: ${column.width}px; left: ${column.left}px;`"
36
- :class="{'justify-content-end': column.align === 'end', 'sticky': column.pinned, 'last-sticky-column': k === lastPinnedIndex, 'editable': column.editable && editable}"
52
+ :class="{'sticky': column.pinned, 'last-sticky-column': k === lastPinnedIndex, 'editable': column.editable && editable}"
37
53
  class="table-view-item-value d-flex h-100">
38
- <div class="table-view-item-value-content" :class="{'px-2': !(column.editable && editable)}">
39
- <slot
40
- :name="`column.${column.property}`"
41
- :toggle="() => $emit('toggle', item)"
42
- :hasSubitems="!!(subrowsProperty && item[subrowsProperty] && item[subrowsProperty].length)"
43
- :isExpanded="!!(item[subrowsProperty] && item[subrowsProperty].length && !isExpanded(item))"
44
- :level="level" :editable="column.editable && editable" :item="item" :column="column" :updateFields="(val) => updateValues(item, val, n, column)" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)">
45
- <template v-if="column.editable && editable && (!editableProperty || item[editableProperty])">
46
- <slot :name="`edit.${column.type}`" :level="level" :toggle="() => $emit('toggle', item)" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)" :item="item" :column="column">
47
- <itf-text-field class="w-100 h-100" v-if="column.type === 'text'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
48
- <itf-text-field class="w-100 h-100" v-if="column.type === 'number'" type="number" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
49
- <itf-hours-field
50
- class="w-100 h-100"
51
- placeholder="00h 00m"
52
- v-else-if="column.type === 'time'"
53
- :hours="getValue(item, column)"
54
- @update:hours="updateValue(item, $event, n, column)"
55
- />
56
- <itf-textarea class="w-100 h-100" :rows="1" autogrow v-else-if="column.type === 'textarea'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
57
- <itf-money-field class="w-100 h-100" currency-disabled :currency="currency" :currencies="currencies" v-else-if="column.type === 'money'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
58
- <itf-select class="w-100 h-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>
59
- </slot>
60
- </template>
61
- <div v-else class="h-100 align-items-center d-flex w-100">
62
- <slot :name="`format.${column.type}`" :toggle="() => $emit('toggle', item)" :level="level" :value="getValue(item, column)" :update="(value) => updateValue(item, value, n, column)" :item="item" :column="column">
63
- {{getValue(item, column)}}
64
- </slot>
65
- </div>
66
- </slot>
54
+ <div class="table-view-item-value-content align-items-center" :class="{'justify-content-end': column.align === 'end', 'px-2': !(column.editable && editable)}">
55
+ <slot
56
+ :name="`column.${column.property}`"
57
+ :toggle="() => $emit('toggle', item)"
58
+ :hasSubitems="hasSubitems(item)"
59
+ :isExpanded="!!(hasSubitems(item) && !isExpanded(item))"
60
+ :level="level" :editable="column.editable && editable" :item="item" :column="column" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)">
61
+ <template v-if="column.editable && editable && (!editableProperty || item[editableProperty])">
62
+ <slot :name="`edit.${column.type}`" :level="level" :toggle="() => $emit('toggle', item)" :update="(val) => updateValue(item, val, n, column)" :value="getValue(item, column)" :item="item" :column="column">
63
+ <itf-text-field class="w-100 h-100" v-if="column.type === 'text'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
64
+ <itf-text-field class="w-100 h-100" v-if="column.type === 'number'" type="number" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
65
+ <itf-hours-field
66
+ class="w-100 h-100"
67
+ placeholder="00h 00m"
68
+ v-else-if="column.type === 'time'"
69
+ :hours="getValue(item, column)"
70
+ @update:hours="updateValue(item, $event, n, column)"
71
+ />
72
+ <itf-textarea class="w-100 h-100" :rows="1" autogrow v-else-if="column.type === 'textarea'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
73
+ <itf-money-field class="w-100 h-100" currency-disabled :currency="currency" :currencies="currencies" v-else-if="column.type === 'money'" :value="getValue(item, column)" @input="updateValue(item, $event, n, column)" />
74
+ <itf-select class="w-100 h-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>
75
+ </slot>
76
+ </template>
77
+ <span v-else class="text-truncate">
78
+ <slot :name="`format.${column.type}`" :toggle="() => $emit('toggle', item)" :level="level" :value="getValue(item, column)" :update="(value) => updateValue(item, value, n, column)" :item="item" :column="column">
79
+ {{getValue(item, column)}}
80
+ </slot>
81
+ </span>
82
+ </slot>
67
83
  </div>
68
84
  </div>
69
85
  </template>
@@ -86,6 +102,7 @@
86
102
  :columns="columns"
87
103
  :id-property="idProperty"
88
104
  :subrows-property="subrowsProperty"
105
+ :async-subrows-property="asyncSubrowsProperty"
89
106
  :show-add-column="showAddColumn"
90
107
  :show-actions="showActions"
91
108
  :no-select-all="noSelectAll"
@@ -145,6 +162,7 @@ class itfTableRows extends Vue {
145
162
  @Prop() rows;
146
163
  @Prop() idProperty;
147
164
  @Prop() subrowsProperty;
165
+ @Prop() asyncSubrowsProperty;
148
166
  @Prop() dividerProperty;
149
167
  @Prop() active;
150
168
  @Prop(Boolean) showAddColumn;
@@ -174,6 +192,12 @@ class itfTableRows extends Vue {
174
192
  return this.columns.findIndex((column) => column.lastPinned);
175
193
  }
176
194
 
195
+ hasSubitems(item) {
196
+ const hasFactItems = this.subrowsProperty && item[this.subrowsProperty] && item[this.subrowsProperty].length;
197
+ const hasPlanItems = this.asyncSubrowsProperty && item[this.asyncSubrowsProperty] && item[this.asyncSubrowsProperty];
198
+ return !!(hasFactItems || hasPlanItems);
199
+ }
200
+
177
201
  updateValue(item, value, index, column) {
178
202
  const newItem = { ...item };
179
203
  if (newItem[column.property] !== value) {
@@ -188,20 +212,8 @@ class itfTableRows extends Vue {
188
212
  }
189
213
  }
190
214
 
191
- updateValues(item, values, index, column) {
192
- const newItem = { ...item };
193
- Object.assign(newItem, values);
194
- this.$emit('update', {
195
- index,
196
- item,
197
- inputValue: values,
198
- value: newItem,
199
- column
200
- });
201
- }
202
-
203
215
  isActive(id) {
204
- if (!this.idProperty) {
216
+ if (!this.idProperty || !this.active) {
205
217
  return false;
206
218
  }
207
219
  if (Array.isArray(this.active)) {
@@ -7,10 +7,10 @@
7
7
  --itf-table-alt-bg: #F9FAFB;
8
8
  --itf-table-alt-selected-bg: #eff1f3;
9
9
  --itf-table-header-bg: #f5f7f8;
10
- --itf-table-header-color: #7A818EA5;
10
+ --itf-table-header-color: #575b63;
11
11
  --itf-table-mirror-bg: #F2F4F7;
12
12
  --itf-table-border-color: transparent; //var(--itf-table-header-bg);
13
- --itf-table-header-border-color: #7A818E33;
13
+ --itf-table-header-border-color: #8E97A533;
14
14
  --itf-table-border-base-color: var(--itf-table-header-bg); // кольори границь таблиці без внутрішніх рядків
15
15
  --itf-table-border-base-width: 2px;
16
16
  --itf-table-hover-header-bg: #dfe5ef;
@@ -23,6 +23,8 @@
23
23
  --itf-table-summary-text: var(--bs-tertiary-color);
24
24
  --itf-table-table-border-radius: 1rem;
25
25
  --itf-table-header-height: 2.25rem;
26
+ --itf-table-divider-bg: #F7F8FA;
27
+ --itf-table-divider-border: rgba(238, 238, 238, 1);
26
28
 
27
29
  --group-title-height: 40px;
28
30
  --table-row-height: none;
@@ -43,6 +45,9 @@ body[data-theme="dark"] {
43
45
  --itf-table-selected-bg: #011534;
44
46
  --itf-table-active-bg: #022e72;
45
47
  --itf-table-summary-text: #82909d80;
48
+ --itf-table-border-base-color: var(--itf-table-header-bg);
49
+ --itf-table-divider-bg: #0f0f0f;
50
+ --itf-table-divider-border: rgb(100, 100, 100, .1);
46
51
  }
47
52
  .itf-table2 {
48
53
  font-size: var(--itf-table-content-font-size, var(--itf-table-font-size));
@@ -60,7 +65,7 @@ body[data-theme="dark"] {
60
65
  height: 100%;
61
66
  }
62
67
  .scroller {
63
- margin-bottom: 12px;
68
+ //margin-bottom: .5rem;
64
69
  }
65
70
  .scrollable-x {
66
71
  overflow-x: scroll;
@@ -97,34 +102,18 @@ body[data-theme="dark"] {
97
102
  position: sticky;
98
103
  top: 0;
99
104
  bottom: 0;
100
- right: -.5rem;
105
+ right: -5px;
101
106
  z-index: 8;
107
+ padding-right: 5px;
108
+ padding-left: 5px;
102
109
  display: flex;
103
110
  align-items: center;
104
-
105
- @media (max-width: 768px) {
106
- position: relative;
107
- opacity: 1;
108
- }
109
111
  }
110
112
  .on-hover {
111
113
  opacity: 0;
112
- width: 0;
113
- padding: 4px .5rem;
114
- overflow: hidden;
115
114
  pointer-events: none;
116
- position: absolute;
117
- right: 0;
118
- background: linear-gradient(90deg, transparent 0, var(--itf-table2-row-bg) 10px);
119
-
120
- @media (max-width: 768px) {
121
- width: max-content;
122
- opacity: 1;
123
- position: relative;
124
- }
125
115
  }
126
116
  .table-row-template:hover .on-hover {
127
- width: max-content;
128
117
  opacity: 1;
129
118
  pointer-events: all;
130
119
  }
@@ -451,11 +440,11 @@ body[data-theme="dark"] {
451
440
  }
452
441
 
453
442
  &__row-divider {
454
- background-color: #F7F8FA;
443
+ background-color: var(--itf-table-divider-bg);
455
444
  height: 5px;
456
445
  padding: 0;
457
- border-top: 1px solid rgba(238, 238, 238, 1);
458
- border-bottom: 1px solid rgba(238, 238, 238, 1);
446
+ border-top: 1px solid var(--itf-table-divider-border);
447
+ border-bottom: 1px solid var(--itf-table-divider-border);
459
448
  }
460
449
  //&:hover, &.permanent-editable-border {
461
450
  // .table-view-item-value.editable {
@@ -468,6 +457,7 @@ body[data-theme="dark"] {
468
457
  position: relative;
469
458
  z-index: 2;
470
459
  width: 100%;
460
+ display: flex;
471
461
  height: 100%;
472
462
  background: var(--itf-table2-row-bg)
473
463
  }
@@ -10,7 +10,8 @@
10
10
  :endpoint="filtersEndpoint"
11
11
  :panel="panel"
12
12
  v-model="filter"
13
- @input="onFilterSet"
13
+ @loaded="onFilterSet($event, true)"
14
+ @change="onFilterSet($event, false)"
14
15
  @set-table-schema="setTableSchema"
15
16
  >
16
17
  <template #after-filter-btn>
@@ -213,7 +214,7 @@ class itfView extends Vue {
213
214
 
214
215
  getDownloadLinks() {
215
216
  const state = this.$refs.table ? this.$refs.table.getTableState() : null;
216
- const filter = this.filter;
217
+ const filter = { ...this.filter };
217
218
  const sorting = this.sorting;
218
219
  const filterableColumnsNames = (state?.columns ?? []).filter(column => column.visible).map(column => column.property);
219
220
 
@@ -297,7 +298,7 @@ class itfView extends Vue {
297
298
  this.$emit('load', this.filter);
298
299
  this.loadingData = true;
299
300
  await this.$try(async () => {
300
- let filter = this.filter;
301
+ let filter = { ...this.filter };
301
302
  if (this.oldFormat) {
302
303
  filter = Object.keys(filter).reduce((acc, key) => {
303
304
  acc[`filter[${key}]`] = filter[key];
@@ -370,7 +371,9 @@ class itfView extends Vue {
370
371
  sources.forEach(source => {
371
372
  if (source && typeof source === 'object') {
372
373
  Object.entries(source).forEach(([key, value]) => {
373
- if (value !== undefined) {
374
+ if (key === 'page' && value === null) {
375
+ delete target[key];
376
+ } else if (value !== undefined) {
374
377
  target[key] = value;
375
378
  }
376
379
  });
@@ -380,10 +383,14 @@ class itfView extends Vue {
380
383
  }
381
384
  }
382
385
 
383
- onFilterSet(filter) {
384
- this.page = 1;
385
- this.selectedIds = [];
386
- this.setPanelPayload({ ...filter, page: 1 });
386
+ onFilterSet(filter, keepPage = false) {
387
+ if (keepPage) {
388
+ // при завантаженні сторінки не потрібно скидувати сторінку
389
+ this.setPanelPayload({ ...filter });
390
+ } else {
391
+ this.page = 1;
392
+ this.setPanelPayload({ ...filter, page: null });
393
+ }
387
394
  this.loadData();
388
395
  }
389
396