@kodaris/krubble-components 1.0.55 → 1.0.57

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.
@@ -3019,6 +3019,7 @@
3019
3019
  greater_than_equal: { key: 'greater_than_equal', type: 'comparison', dataTypes: ['number', 'date'], label: 'Greater than or equal' },
3020
3020
  between: { key: 'between', type: 'range', dataTypes: ['number', 'date'], label: 'Between' },
3021
3021
  in: { key: 'in', type: 'list', dataTypes: ['string', 'number'], label: 'In' },
3022
+ n_in: { key: 'n_in', type: 'list', dataTypes: ['string', 'number', 'date', 'boolean'], label: 'Not In' },
3022
3023
  empty: { key: 'empty', type: 'nil', dataTypes: ['string', 'number', 'date', 'boolean'], label: 'Empty' },
3023
3024
  n_empty: { key: 'n_empty', type: 'nil', dataTypes: ['string', 'number', 'date', 'boolean'], label: 'Not empty' },
3024
3025
  };
@@ -3351,6 +3352,14 @@
3351
3352
  };
3352
3353
  this.specificity = [getDateSpecificity(rawStart), getDateSpecificity(rawEnd)];
3353
3354
  }
3355
+ else if (kql.startsWith('!(') && kql.endsWith(')')) {
3356
+ this.operator = 'n_in';
3357
+ this.text = kql.substring(2, kql.length - 1).trim();
3358
+ this.value = this.text.split(',')
3359
+ .map(v => v.trim())
3360
+ .map(v => this._parse(v));
3361
+ this.specificity = [getDateSpecificity(this.text)];
3362
+ }
3354
3363
  else if (kql.startsWith('(') && kql.endsWith(')')) {
3355
3364
  this.operator = 'in';
3356
3365
  this.text = kql.substring(1, kql.length - 1).trim();
@@ -3397,7 +3406,7 @@
3397
3406
  this.value = null;
3398
3407
  this.text = '';
3399
3408
  }
3400
- else if (this.operator === 'in') {
3409
+ else if (this.operator === 'in' || this.operator === 'n_in') {
3401
3410
  this.value = [];
3402
3411
  this.text = '';
3403
3412
  }
@@ -3415,7 +3424,7 @@
3415
3424
  this.value = null;
3416
3425
  this.text = '';
3417
3426
  }
3418
- else if (this.operator === 'in') {
3427
+ else if (this.operator === 'in' || this.operator === 'n_in') {
3419
3428
  this.value = [];
3420
3429
  this.text = '';
3421
3430
  }
@@ -3482,14 +3491,14 @@
3482
3491
  if (this.operator === 'between') {
3483
3492
  return !this.value?.start && !this.value?.end;
3484
3493
  }
3485
- if (this.operator === 'in') {
3494
+ if (this.operator === 'in' || this.operator === 'n_in') {
3486
3495
  return !this.value?.length;
3487
3496
  }
3488
3497
  return this.value === undefined || this.value === null || this.value === '';
3489
3498
  }
3490
- /** Returns true if the value array contains the given value. Only applies to 'in' operator. */
3499
+ /** Returns true if the value array contains the given value. Only applies to 'in' and 'n_in' operators. */
3491
3500
  has(val) {
3492
- if (this.operator !== 'in' || !Array.isArray(this.value)) {
3501
+ if ((this.operator !== 'in' && this.operator !== 'n_in') || !Array.isArray(this.value)) {
3493
3502
  return false;
3494
3503
  }
3495
3504
  // Bucket values from Solr arrive as strings ("true"/"false") but
@@ -3504,9 +3513,9 @@
3504
3513
  }
3505
3514
  return this.value.indexOf(val) >= 0;
3506
3515
  }
3507
- /** Adds or removes a value from the 'in' list and rebuilds text/kql. */
3516
+ /** Adds or removes a value from the 'in' or 'n_in' list and rebuilds text/kql. */
3508
3517
  toggle(val) {
3509
- if (this.operator !== 'in') {
3518
+ if (this.operator !== 'in' && this.operator !== 'n_in') {
3510
3519
  this.setOperator('in');
3511
3520
  }
3512
3521
  // Bucket values from Solr arrive as strings ("true"/"false") —
@@ -3545,8 +3554,8 @@
3545
3554
  if (this.operator === 'n_empty') {
3546
3555
  return true;
3547
3556
  }
3548
- // For 'in' operator, valid as long as there's at least one value (including null for empty buckets)
3549
- if (this.operator === 'in') {
3557
+ // For 'in'/'n_in' operator, valid as long as there's at least one value (including null for empty buckets)
3558
+ if (this.operator === 'in' || this.operator === 'n_in') {
3550
3559
  return Array.isArray(this.value) && this.value.length > 0;
3551
3560
  }
3552
3561
  if (this.type === 'date') {
@@ -3690,6 +3699,18 @@
3690
3699
  data.value = termify(this.value, true);
3691
3700
  }
3692
3701
  break;
3702
+ case 'n_in':
3703
+ if (Array.isArray(this.value) && this.value.length > 0) {
3704
+ data.operation = 'EXPRESSION';
3705
+ data.not = true;
3706
+ data.value = `(${this.value.map((v) => termify(v, true)).join(' OR ')})`;
3707
+ }
3708
+ else {
3709
+ data.operation = 'EXPRESSION';
3710
+ data.not = true;
3711
+ data.value = termify(this.value, true);
3712
+ }
3713
+ break;
3693
3714
  default:
3694
3715
  data.operation = 'EXPRESSION';
3695
3716
  data.value = termify(this.value, true);
@@ -3784,6 +3805,11 @@
3784
3805
  params.operation = 'IN';
3785
3806
  params.values = this.value ?? [];
3786
3807
  break;
3808
+ case 'n_in':
3809
+ params.operation = 'IN';
3810
+ params.not = true;
3811
+ params.values = this.value ?? [];
3812
+ break;
3787
3813
  default:
3788
3814
  throw Error(`${this.operator} operator not supported by db params.`);
3789
3815
  }
@@ -3800,7 +3826,7 @@
3800
3826
  if (this.operator === 'between') {
3801
3827
  this.text = `${this._format(this.value?.start)} - ${this._format(this.value?.end)}`;
3802
3828
  }
3803
- else if (this.operator === 'in') {
3829
+ else if (this.operator === 'in' || this.operator === 'n_in') {
3804
3830
  this.text = this.value.map((v) => this._format(v)).join(',');
3805
3831
  }
3806
3832
  else {
@@ -4014,6 +4040,9 @@
4014
4040
  case 'in':
4015
4041
  this.kql = `(${this.value.map((v) => this._format(v)).join(',')})`;
4016
4042
  break;
4043
+ case 'n_in':
4044
+ this.kql = `!(${this.value.map((v) => this._format(v)).join(',')})`;
4045
+ break;
4017
4046
  default:
4018
4047
  throw Error(`Unknown operator ${this.operator}`);
4019
4048
  }
@@ -4065,6 +4094,7 @@
4065
4094
  this._filterPanelTab = 'filter';
4066
4095
  this._buckets = new Map();
4067
4096
  this._filterPanelPos = { top: 0, left: 0 };
4097
+ this._sorts = [];
4068
4098
  this._resizing = null;
4069
4099
  this._resizeObserver = null;
4070
4100
  this._searchPositionLocked = false;
@@ -4254,7 +4284,7 @@
4254
4284
  const request = {
4255
4285
  page: this._page - 1,
4256
4286
  size: this._pageSize,
4257
- sorts: [],
4287
+ sorts: this._sorts,
4258
4288
  filterFields: [],
4259
4289
  queryFields: [],
4260
4290
  facetFields: []
@@ -4264,7 +4294,7 @@
4264
4294
  continue;
4265
4295
  }
4266
4296
  const filterData = col.filter.toSolrData();
4267
- if (col.facetable && col.filter.operator === 'in') {
4297
+ if (col.facetable && (col.filter.operator === 'in' || col.filter.operator === 'n_in')) {
4268
4298
  filterData.tagged = true;
4269
4299
  }
4270
4300
  request.filterFields.push(filterData);
@@ -4294,7 +4324,7 @@
4294
4324
  const request = {
4295
4325
  page: this._page - 1,
4296
4326
  size: this._pageSize,
4297
- sorts: [],
4327
+ sorts: this._sorts,
4298
4328
  filterFields: [],
4299
4329
  queryFields: [],
4300
4330
  facetFields: []
@@ -4416,7 +4446,7 @@
4416
4446
  }
4417
4447
  }
4418
4448
  // Bucket sync: ensure selected values appear even with 0 results
4419
- if (col.filter && col.filter.operator === 'in' && Array.isArray(col.filter.value)) {
4449
+ if (col.filter && (col.filter.operator === 'in' || col.filter.operator === 'n_in') && Array.isArray(col.filter.value)) {
4420
4450
  for (const selectedVal of col.filter.value) {
4421
4451
  if (!buckets.some(b => b.val === selectedVal)) {
4422
4452
  buckets.push({
@@ -4593,6 +4623,77 @@
4593
4623
  document.addEventListener('mouseup', this._handleResizeEnd);
4594
4624
  }
4595
4625
  // ----------------------------------------------------------------------------
4626
+ // Sorting
4627
+ // ----------------------------------------------------------------------------
4628
+ _handleSortClick(e, column) {
4629
+ if (e.shiftKey) {
4630
+ // Multi-sort: add or cycle existing
4631
+ const existingIndex = this._sorts.findIndex(s => s.sortBy === column.id);
4632
+ if (existingIndex === -1) {
4633
+ this._sorts.push({ sortBy: column.id, sortDirection: 'asc' });
4634
+ }
4635
+ else {
4636
+ const existing = this._sorts[existingIndex];
4637
+ if (existing.sortDirection === 'asc') {
4638
+ existing.sortDirection = 'desc';
4639
+ }
4640
+ else {
4641
+ // on third click, remove sorting for the column
4642
+ this._sorts.splice(existingIndex, 1);
4643
+ }
4644
+ }
4645
+ this.requestUpdate();
4646
+ }
4647
+ else {
4648
+ // Single sort: replace all
4649
+ let existing = null;
4650
+ if (this._sorts.length === 1) {
4651
+ existing = this._sorts.find(s => s.sortBy === column.id);
4652
+ }
4653
+ if (!existing) {
4654
+ this._sorts = [{ sortBy: column.id, sortDirection: 'asc' }];
4655
+ }
4656
+ else if (existing.sortDirection === 'asc') {
4657
+ this._sorts = [{ sortBy: column.id, sortDirection: 'desc' }];
4658
+ }
4659
+ else {
4660
+ this._sorts = [];
4661
+ }
4662
+ }
4663
+ this._page = 1;
4664
+ this._fetch();
4665
+ }
4666
+ _renderSortIndicator(column) {
4667
+ if (!column.sortable) {
4668
+ return A;
4669
+ }
4670
+ const sortIndex = this._sorts.findIndex(s => s.sortBy === column.id);
4671
+ if (sortIndex === -1) {
4672
+ // Ghost arrow: visible only on hover via CSS
4673
+ return b `
4674
+ <span class="header-cell__sort" @click=${(e) => this._handleSortClick(e, column)}>
4675
+ <svg class="header-cell__sort-arrow header-cell__sort-arrow--ghost" viewBox="0 0 24 24" fill="currentColor">
4676
+ <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
4677
+ </svg>
4678
+ </span>
4679
+ `;
4680
+ }
4681
+ let arrowStyle = {};
4682
+ if (this._sorts[sortIndex].sortDirection === 'desc') {
4683
+ arrowStyle = { transform: 'rotate(180deg)' };
4684
+ }
4685
+ return b `
4686
+ <span class="header-cell__sort" @click=${(e) => this._handleSortClick(e, column)}>
4687
+ <svg class="header-cell__sort-arrow" viewBox="0 0 24 24" fill="currentColor" style=${o$1(arrowStyle)}>
4688
+ <path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>
4689
+ </svg>
4690
+ ${this._sorts.length > 1 ? b `
4691
+ <span class="header-cell__sort-priority">${sortIndex + 1}</span>
4692
+ ` : A}
4693
+ </span>
4694
+ `;
4695
+ }
4696
+ // ----------------------------------------------------------------------------
4596
4697
  // Header
4597
4698
  // ----------------------------------------------------------------------------
4598
4699
  _handleAction(action) {
@@ -4786,6 +4887,7 @@
4786
4887
  _getHeaderCellClasses(column, index) {
4787
4888
  return {
4788
4889
  'header-cell': true,
4890
+ 'header-cell--sortable': !!column.sortable,
4789
4891
  'header-cell--align-center': column.align === 'center',
4790
4892
  'header-cell--align-right': column.align === 'right',
4791
4893
  'header-cell--sticky-left': column.sticky === 'left',
@@ -5018,7 +5120,7 @@
5018
5120
  />
5019
5121
  `;
5020
5122
  }
5021
- else if (column.filter.operator === 'in') {
5123
+ else if (column.filter.operator === 'in' || column.filter.operator === 'n_in') {
5022
5124
  valueInput = b `
5023
5125
  <textarea
5024
5126
  class="filter-panel__textarea"
@@ -5273,10 +5375,14 @@
5273
5375
  class=${e$1(this._getHeaderCellClasses(col, i))}
5274
5376
  style=${o$1(this._getCellStyle(col, i))}
5275
5377
  data-column-id=${col.id}
5276
- >${col.label ?? col.id}${col.resizable !== false ? b `<div
5378
+ >
5379
+ <span class="header-cell__label">${col.label ?? col.id}</span>
5380
+ ${this._renderSortIndicator(col)}
5381
+ ${col.resizable !== false ? b `<div
5277
5382
  class="header-cell__resize"
5278
5383
  @mousedown=${(e) => this._handleResizeStart(e, col.id)}
5279
- ></div>` : A}</div>
5384
+ ></div>` : A}
5385
+ </div>
5280
5386
  `)}
5281
5387
  </div>
5282
5388
  ${this._renderFilterRow()}
@@ -5685,9 +5791,7 @@
5685
5791
  .header-cell {
5686
5792
  position: sticky;
5687
5793
  top: 0;
5688
- z-index: 2;
5689
5794
  height: 48px;
5690
- line-height: 48px;
5691
5795
  padding: 0 16px;
5692
5796
  white-space: nowrap;
5693
5797
  box-sizing: border-box;
@@ -5697,8 +5801,8 @@
5697
5801
  border-right: 1px solid #e5e7ebba;
5698
5802
  font-weight: 600;
5699
5803
  color: #374151;
5700
- overflow: hidden;
5701
- text-overflow: ellipsis;
5804
+ display: flex;
5805
+ align-items: center;
5702
5806
  }
5703
5807
 
5704
5808
  .header-cell__resize {
@@ -5708,21 +5812,52 @@
5708
5812
  bottom: 0;
5709
5813
  width: 14px;
5710
5814
  cursor: col-resize;
5815
+ z-index: 10;
5816
+ }
5817
+
5818
+ .header-cell--sortable {
5819
+ user-select: none;
5820
+ }
5821
+
5822
+ .header-cell__label {
5823
+ overflow: hidden;
5824
+ text-overflow: ellipsis;
5825
+ min-width: 0;
5826
+ line-height: 48px;
5827
+ }
5828
+
5829
+ .header-cell__sort {
5830
+ flex-grow: 1;
5711
5831
  display: flex;
5712
5832
  align-items: center;
5713
- justify-content: center;
5714
- z-index: 10;
5833
+ height: 100%;
5834
+ margin-left: 6px;
5835
+ cursor: pointer;
5715
5836
  }
5716
5837
 
5717
- .header-cell__resize::after {
5718
- content: '';
5719
- width: 2px;
5720
- height: 20px;
5721
- background: #c6c6cd;
5838
+ .header-cell__sort-arrow {
5839
+ width: 16px;
5840
+ height: 16px;
5841
+ color: #374151;
5842
+ stroke: currentColor;
5843
+ stroke-width: 1px;
5722
5844
  }
5723
5845
 
5724
- .header-cell:last-child .header-cell__resize::after {
5725
- display: none;
5846
+ .header-cell__sort-arrow--ghost {
5847
+ opacity: 0;
5848
+ color: #374151;
5849
+ transition: opacity 0.15s;
5850
+ }
5851
+
5852
+ .header-cell--sortable:hover .header-cell__sort-arrow--ghost {
5853
+ opacity: 0.4;
5854
+ }
5855
+
5856
+ .header-cell__sort-priority {
5857
+ font-size: 10px;
5858
+ font-weight: 600;
5859
+ color: #374151;
5860
+ line-height: 1;
5726
5861
  }
5727
5862
 
5728
5863
  .cell {
@@ -6188,6 +6323,9 @@
6188
6323
  __decorate$9([
6189
6324
  r$1()
6190
6325
  ], exports.KRTable.prototype, "_buckets", void 0);
6326
+ __decorate$9([
6327
+ r$1()
6328
+ ], exports.KRTable.prototype, "_sorts", void 0);
6191
6329
  __decorate$9([
6192
6330
  n$1({ type: Object })
6193
6331
  ], exports.KRTable.prototype, "def", void 0);