@itfin/components 2.0.29 → 2.0.31

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.
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M10.0003 15.8337C13.222 15.8337 15.8337 13.222 15.8337 10.0003C15.8337 6.77866 13.222 4.16699 10.0003 4.16699C6.77866 4.16699 4.16699 6.77866 4.16699 10.0003C4.16699 13.222 6.77866 15.8337 10.0003 15.8337ZM7.91697 10.0003C7.91697 8.84973 8.84971 7.91699 10.0003 7.91699C11.1509 7.91699 12.0836 8.84973 12.0836 10.0003C12.0836 11.1509 11.1509 12.0837 10.0003 12.0837C8.84971 12.0837 7.91697 11.1509 7.91697 10.0003ZM10.0003 6.25032C7.92924 6.25032 6.25031 7.92925 6.25031 10.0003C6.25031 12.0714 7.92924 13.7503 10.0003 13.7503C12.0714 13.7503 13.7503 12.0714 13.7503 10.0003C13.7503 7.92925 12.0714 6.25032 10.0003 6.25032Z" fill="currentColor"/>
3
+ </svg>
@@ -1,9 +1,6 @@
1
1
  <template>
2
2
  <div tabindex="-1" :class="{'b-panel': !nocard, 'b-panel__collapsed': collapsed, 'b-panel__active': !collapsed}">
3
- <div v-if="alert" class="b-panel-alert d-flex align-items-center justify-content-center gap-2" :class="[`b-panel-alert--${alertType}`]">
4
- <itf-icon name="bell" :size="24" new class="b-panel-alert__icon" />
5
- <p class="mb-0">{{ alert }}</p>
6
- </div>
3
+ <slot name="before-header"></slot>
7
4
  <div v-if="collapsed && !nocard" class="b-panel__expand" @click.stop.prevent="expandPanel">
8
5
  <itf-button v-if="closeable" icon small class="b-panel__expand_button" @click="closePanel">
9
6
  <itf-icon name="cross" />
@@ -26,9 +23,14 @@
26
23
  </div>
27
24
  <div class="d-flex gap-1">
28
25
  <slot name="buttons"></slot>
29
- <itf-button v-if="expandable" icon default class="b-panel__expand_button d-none d-md-block" @click="fullsizePanel">
30
- <itf-icon new name="expand" />
31
- </itf-button>
26
+ <template v-if="expandable">
27
+ <itf-button v-if="!isFullSize" icon default class="b-panel__expand_button d-none d-md-block" @click="fullsizePanel">
28
+ <itf-icon new name="expand" />
29
+ </itf-button>
30
+ <itf-button v-else icon default class="b-panel__expand_button d-none d-md-block" @click="collapsePanel">
31
+ <itf-icon new name="collapse" />
32
+ </itf-button>
33
+ </template>
32
34
  <itf-button v-if="closeable" icon default class="b-panel__expand_button" @click="closePanel">
33
35
  <itf-icon new name="close" />
34
36
  </itf-button>
@@ -161,29 +163,6 @@
161
163
  top: 0;
162
164
  z-index: 10;
163
165
  }
164
- .b-panel-alert {
165
- border-radius: 6px 6px 0 0;
166
- padding: 2px 16px;
167
- font-size: 12px;
168
- line-height: 16px;
169
- text-align: center;
170
- color: var(--b-panel-white);
171
- background-color: var(--b-panel-info);
172
-
173
- &__icon {
174
- animation: bellRing 5s ease-in-out infinite;
175
- }
176
-
177
- &--success {
178
- background-color: var(--b-panel-success);
179
- }
180
- &--warning {
181
- background-color: var(--b-panel-warning);
182
- }
183
- &--danger {
184
- background-color: var(--b-panel-danger);
185
- }
186
- }
187
166
  </style>
188
167
  <script>
189
168
  import { Vue, Prop, Component } from 'vue-property-decorator';
@@ -211,11 +190,10 @@ class Panel extends Vue {
211
190
  @Prop() panel;
212
191
  @Prop(Boolean) collapsed;
213
192
  @Prop(Boolean) closeable;
193
+ @Prop(Boolean) isFullSize;
214
194
  @Prop(Boolean) expandable;
215
195
  @Prop(Boolean) animate;
216
196
  @Prop(Boolean) nocard;
217
- @Prop(String) alert;
218
- @Prop({ type: String, default: 'info' }) alertType; // info | success | warning | danger
219
197
 
220
198
  openPanel(...args) {
221
199
  this.$emit('open', args);
@@ -225,6 +203,10 @@ class Panel extends Vue {
225
203
  this.$emit('expand');
226
204
  }
227
205
 
206
+ collapsePanel() {
207
+ this.$emit('collapse');
208
+ }
209
+
228
210
  fullsizePanel() {
229
211
  this.$emit('fullsize');
230
212
  }
@@ -5,6 +5,7 @@
5
5
  import { Vue, Component, Inject, Prop } from 'vue-property-decorator';
6
6
  import { IPanel } from './PanelList.vue';
7
7
  import {stackToHash} from "@itfin/components/src/components/panels/helpers";
8
+ import {getRootPanelList} from "@itfin/components/src/components/panels";
8
9
 
9
10
  @Component({
10
11
  components: {
@@ -15,7 +16,6 @@ import {stackToHash} from "@itfin/components/src/components/panels/helpers";
15
16
  }
16
17
  })
17
18
  export default class PanelLink extends Vue {
18
- @Inject({ default: null }) panelList;
19
19
  @Inject({ default: null }) currentPanel;
20
20
 
21
21
  @Prop(Boolean) global: boolean;
@@ -35,7 +35,7 @@ export default class PanelLink extends Vue {
35
35
  }
36
36
 
37
37
  get activeList() {
38
- return this.list ?? this.panelList;
38
+ return this.list ?? getRootPanelList();
39
39
  }
40
40
 
41
41
  get isActive() {
@@ -62,6 +62,8 @@ export default class PanelLink extends Vue {
62
62
  }
63
63
 
64
64
  onClick(e) {
65
+ e.preventDefault();
66
+ e.stopPropagation();
65
67
  this.$emit('open', {
66
68
  panel: this.panel,
67
69
  payload: this.payload || {},
@@ -70,8 +72,6 @@ export default class PanelLink extends Vue {
70
72
  if (!this.activeList) {
71
73
  return;
72
74
  }
73
- e.preventDefault();
74
- e.stopPropagation();
75
75
  this.activeList.openPanel(this.panel, this.payload || {}, this.append ? undefined : this.currentPanel?.index + 1);
76
76
  }
77
77
  }
@@ -17,17 +17,20 @@
17
17
  :icon="panel.icon"
18
18
  :payload="panel.payload"
19
19
  :expandable="panelsStack.length > 1"
20
+ :isFullSize="isFullSize"
20
21
  :collapsed="panel.isCollapsed"
21
22
  :closeable="panel.isCloseable"
22
23
  :animate="panel.isAnimate"
23
- :alert="panel.alert"
24
- :alertType="panel.alertType"
25
24
  @open="openPanel($event[0], $event[1], n + 1)"
26
25
  @expand="expandPanel(panel)"
27
26
  @fullsize="fullsizePanel(panel)"
27
+ @collapse="collapsePanel(panel)"
28
28
  @close="closePanel(panel)"
29
29
  @open-menu="$emit('open-menu', panel.type, panel.payload)"
30
30
  >
31
+ <template #before-header>
32
+ <slot name="before-header" :panel="panel" :index="n" :payload="panel.payload"></slot>
33
+ </template>
31
34
  <slot
32
35
  :name="panel.type"
33
36
  :panel="panel"
@@ -151,7 +154,6 @@ $double-an-time: $an-time * 2;
151
154
  //transition: opacity $an-time linear;
152
155
  }
153
156
  }
154
-
155
157
  //.slide-enter-active > div {
156
158
  // opacity: 0;
157
159
  //}
@@ -162,16 +164,16 @@ $double-an-time: $an-time * 2;
162
164
  </style>
163
165
  <script lang="ts">
164
166
  import { Vue, Component, Prop } from 'vue-property-decorator';
167
+ import itfIcon from '../icon/Icon.vue';
165
168
  import Panel from './Panel.vue';
166
169
  import {hashToStack, stackToHash} from "@itfin/components/src/components/panels/helpers";
170
+ import {setRootPanelList} from "@itfin/components/src/components/panels";
167
171
 
168
172
  interface VisualOptions {
169
173
  title: string;
170
174
  icon?: string;
171
175
  }
172
176
 
173
- export type PanelAlertType = 'info' | 'success' | 'warning' | 'danger';
174
-
175
177
  export interface IPanel {
176
178
  id: number;
177
179
  title: string;
@@ -182,8 +184,6 @@ export interface IPanel {
182
184
  isCollapsed: boolean;
183
185
  isCloseable: boolean;
184
186
  isAnimate: boolean;
185
- alert?: string;
186
- alertType?: PanelAlertType;
187
187
  open: (type: string, visOptions: VisualOptions, payload: any) => void;
188
188
  close: () => void;
189
189
  expand: () => void;
@@ -202,6 +202,7 @@ export interface IPanel {
202
202
 
203
203
  @Component({
204
204
  components: {
205
+ itfIcon,
205
206
  Panel
206
207
  },
207
208
  directives: {
@@ -216,12 +217,14 @@ export default class PanelList extends Vue {
216
217
  @Prop() firstPanel: IPanel;
217
218
  @Prop() panels: Record<string, Component>;
218
219
  @Prop({ default: () => {} }) searchPanel: (type: string) => boolean;
220
+ @Prop({ type: String, default: 'path' }) routeType: string;
219
221
 
220
222
  panelsStack:IPanel[] = [];
221
223
 
222
224
  nextId:number = 0;
223
225
 
224
226
  created() {
227
+ setRootPanelList(this);
225
228
  if (this.firstPanel) {
226
229
  this.internalOpenPanel(this.firstPanel.type, this.firstPanel.payload);
227
230
  }
@@ -288,15 +291,10 @@ export default class PanelList extends Vue {
288
291
  if (typeof panel.caption !== 'function') {
289
292
  throw new Error('Panel component must have a "caption" function');
290
293
  }
291
- if (this.panels[type].alert && typeof this.panels[type].alert !== 'function') {
292
- throw new Error('Panel component "alert" field must have function type');
293
- }
294
294
  const newPanel:any = {
295
295
  id: this.nextId++,
296
296
  nocard: panel.nocard,
297
297
  title: panel.caption(this.$t.bind(this), payload),
298
- alert: this.panels[type].alert ? this.panels[type].alert(this.$t.bind(this), payload) : undefined,
299
- alertType: this.panels[type].alertType,
300
298
  icon: panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
301
299
  components: {
302
300
  default: panel.default ?? undefined,
@@ -324,6 +322,10 @@ export default class PanelList extends Vue {
324
322
  isAnimation = newStack.length === openIndex;
325
323
  newStack = newStack.slice(0, openIndex);
326
324
  }
325
+ if (newStack.length > 0 && !newStack.find(p => !p.isCollapsed)) {
326
+ // якщо немає відкритих панелей, то перша панель має бути розгорнута
327
+ newStack[0].isCollapsed = false;
328
+ }
327
329
  this.panelsStack = newStack;
328
330
  return new Promise(res => {
329
331
  this.$nextTick(() => { // щоб панелі змінювались при редагуванні
@@ -363,8 +365,6 @@ export default class PanelList extends Vue {
363
365
  newPanel.setPayload = (value: any) => {
364
366
  newPanel.payload = value;
365
367
  newPanel.title = panel.caption(this.$t.bind(this), value);
366
- newPanel.alert = panel.alert ? panel.alert(this.$t.bind(this), payload) : undefined;
367
- newPanel.alertType = panel.alertType;
368
368
  newPanel.icon = panel.icon ? panel.icon(this.$t.bind(this), payload) : null,
369
369
  this.setPanelHash()
370
370
  }
@@ -415,12 +415,40 @@ export default class PanelList extends Vue {
415
415
  fullsizePanel(panel: IPanel) {
416
416
  const newStack = [...this.panelsStack];
417
417
  for (const p of newStack) {
418
+ p.isLastOpened = !p.isCollapsed && p !== panel;
418
419
  p.isCollapsed = p !== panel;
419
420
  }
420
421
  this.panelsStack = newStack;
421
422
  this.setPanelHash();
422
423
  }
423
424
 
425
+ get isFullSize() {
426
+ return this.panelsStack.filter(p => !p.isCollapsed).length === 1;
427
+ }
428
+
429
+ expandPanel(panel: IPanel) {
430
+ const newStack = [...this.panelsStack];
431
+ const index = newStack.findIndex(p => p.id === panel.id);
432
+ newStack[index].isCollapsed = false;
433
+ this.panelsStack = newStack;
434
+ this.ensureOnlyTwoOpenPanels(panel.id);
435
+ this.setPanelHash();
436
+ }
437
+
438
+ collapsePanel(panel: IPanel) {
439
+ const newStack = [...this.panelsStack];
440
+ const currenctIndex = newStack.findIndex(p => p.id === panel.id);
441
+ const lastOpenedIndex = newStack.findIndex(p => p.isLastOpened);
442
+ if (lastOpenedIndex !== -1) { // якщо зебрежена остання відкрита панель
443
+ newStack[lastOpenedIndex].isCollapsed = false
444
+ } else if (newStack[currenctIndex-1]) { // якщо після оновлення сторінки відсутнє значення "остання відкрита", то відкриваємо ту, що зліва
445
+ newStack[currenctIndex-1].isCollapsed = false;
446
+ }
447
+ this.panelsStack = newStack;
448
+ this.ensureOnlyTwoOpenPanels(panel.id);
449
+ this.setPanelHash();
450
+ }
451
+
424
452
  getPanels(type) {
425
453
  return this.panelsStack.filter(panel => panel.type === type);
426
454
  }
@@ -430,14 +458,18 @@ export default class PanelList extends Vue {
430
458
  }
431
459
 
432
460
  setPanelHash() {
433
- const hash = stackToHash(this.panelsStack).replace(/^#/, '');
434
- this.$router.push({ hash });
461
+ const hash = stackToHash(this.panelsStack, this.routeType === 'path').replace(/^#/, '');
462
+ if (this.routeType === 'path') {
463
+ this.$router.push({ path: `/${hash}` });
464
+ } else {
465
+ this.$router.push({ hash });
466
+ }
435
467
  }
436
468
 
437
469
  async parsePanelHash() {
438
- const {hash} = location;
470
+ const hash = this.routeType === 'path' ? location.pathname : location.hash;
439
471
  if (hash) {
440
- const panels = hashToStack(hash);
472
+ const panels = hashToStack(hash, this.routeType === 'path');
441
473
  const newStack = [];
442
474
  this.panelsStack = [];
443
475
  for (const panelIndex in panels) {
@@ -1,3 +1,6 @@
1
+ import JSON5 from 'json5'
2
+ import {isPathType} from "@itfin/components/src/components/panels";
3
+
1
4
  export interface IPanel {
2
5
  type: string;
3
6
  payload?: any;
@@ -6,25 +9,38 @@ export interface IPanel {
6
9
 
7
10
  export function stackToHash(stack: IPanel[]) {
8
11
  const hash = stack.map(panel => {
9
- return `${panel.type}${panel.isCollapsed ? '' : '!'}=${JSON.stringify(panel.payload || {})}`;
10
- }).join('&');
11
- return `#${hash}`;
12
+ let json = JSON5.stringify(panel.payload || {});
13
+ json = json.substring(1, json.length - 1); // Remove the outer {}
14
+ return `${panel.type}${panel.isCollapsed ? '!' : ''}${json ? '=' : ''}${json}`;
15
+ }).join(isPathType() ? '/' : '&');
16
+ return isPathType() ? `/${hash}` : `#${hash}`;
12
17
  }
13
18
 
14
19
 
15
20
  export function hashToStack(hash: string|undefined): IPanel[] {
16
21
  let stack:IPanel[] = [];
17
22
  if (hash) {
18
- const str = hash.replace(/^#/, '');
23
+ const str = hash.replace(isPathType() ? /^\// : /^#/, '');
19
24
 
20
- stack = str.split('&').map(item => {
25
+ stack = str.split(isPathType() ? '/' : '&').map(item => {
26
+ if (!item.includes('=')) {
27
+ return { type: item.replace('!', ''), isCollapsed: item.includes('!'), payload: {} };
28
+ }
21
29
  const [type, payload] = item.split('=');
22
- const isCollapsed = !type.includes('!');
30
+ const isCollapsed = type.includes('!');
23
31
  let payloadObj:any = {};
24
32
  try {
25
- payloadObj = JSON.parse(decodeURIComponent(payload));
33
+ let json = decodeURIComponent(payload);
34
+ if (!json.startsWith('{')) {
35
+ json = `{${json}`; // Ensure it starts with a '{' to be valid JSON
36
+ }
37
+ if (!json.endsWith('}')) {
38
+ json += '}'; // Ensure it ends with a '}' to be valid JSON
39
+ }
40
+ payloadObj = JSON5.parse(json);
26
41
  } catch (e) {
27
42
  // ignore
43
+ console.warn(`Error parsing payload for type ${type}:`, payload, e);
28
44
  }
29
45
  return {
30
46
  type: type.replace('!', ''),
@@ -0,0 +1,24 @@
1
+ const PanelsSettings = {
2
+ pathType: 'path', // 'hash' | 'path'
3
+ rootPanelList: null, // This will be set to the root panel list when the app is initialized
4
+ };
5
+
6
+ export function getPanelsSettings() {
7
+ return PanelsSettings;
8
+ }
9
+
10
+ export function getRootPanelList() {
11
+ return PanelsSettings.rootPanelList;
12
+ }
13
+
14
+ export function isPathType() {
15
+ return PanelsSettings.pathType === 'path';
16
+ }
17
+
18
+ export function setPanelsPathType(settings: any) {
19
+ PanelsSettings.pathType = settings.pathType ?? 'path';
20
+ }
21
+
22
+ export function setRootPanelList(rootPanelList: any) {
23
+ PanelsSettings.rootPanelList = rootPanelList;
24
+ }
@@ -4,6 +4,7 @@
4
4
  <itf-filter-panel
5
5
  :search-placeholder="searchPlaceholder"
6
6
  search
7
+ ref="filters"
7
8
  :mini="panel.isMultiple()"
8
9
  class="py-2 px-3"
9
10
  :endpoint="filtersEndpoint"
@@ -161,14 +162,18 @@ class itfView extends Vue {
161
162
  }
162
163
  }
163
164
 
164
- async loadData() {
165
+ async loadData(reloadFilters = false) {
165
166
  if (!this.endpoint) {
166
167
  return;
167
168
  }
169
+ if (reloadFilters) {
170
+ this.loadFilters();
171
+ }
168
172
  this.$emit('load', this.filter);
169
173
  this.loadingData = true;
170
174
  await this.$try(async () => {
171
175
  const res = await this.$axios.$get(this.endpoint, {
176
+ preventRaceCondition: true,
172
177
  params: {
173
178
  ...this.filter,
174
179
  page: this.page,
@@ -240,5 +245,11 @@ class itfView extends Vue {
240
245
  this.setPanelPayload({ ...filter, page: 1 });
241
246
  this.loadData();
242
247
  }
248
+
249
+ loadFilters() {
250
+ if (this.filtersEndpoint) {
251
+ this.$refs.filters?.loadData();
252
+ }
253
+ }
243
254
  }
244
255
  </script>