tailjng 0.1.14 → 0.1.15

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.
Files changed (20) hide show
  1. package/cli/file-operations.js +4 -1
  2. package/package.json +1 -1
  3. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.html +10 -2
  4. package/src/lib/components/card/card-crud-complete/complete-crud-card.component.ts +6 -0
  5. package/src/lib/components/input/input-textarea/textarea-input.component.html +2 -2
  6. package/src/lib/components/input/input-textarea/textarea-input.component.ts +10 -3
  7. package/src/lib/components/select/select-dropdown/dropdown-select.component.html +25 -10
  8. package/src/lib/components/select/select-dropdown/dropdown-select.component.ts +72 -8
  9. package/src/lib/components/select/select-dropdown/dropdown-select.types.ts +3 -0
  10. package/src/lib/components/select/select-dropdown/dropdown-select.util.ts +89 -12
  11. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.html +9 -3
  12. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.component.ts +42 -0
  13. package/src/lib/components/select/select-multi-dropdown/multi-dropdown-select.util.ts +2 -0
  14. package/src/lib/components/table/table-crud-complete/complete-crud-table.animations.ts +2 -2
  15. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.html +4 -2
  16. package/src/lib/components/table/table-crud-complete/complete-crud-table.component.ts +3 -4
  17. package/src/lib/components/table/table-crud-complete/complete-crud-table.util.ts +13 -2
  18. package/src/lib/components/toggle-radio/shared/toggle-options.types.ts +9 -9
  19. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.component.ts +26 -3
  20. package/src/lib/components/toggle-radio/toggle-segment/segment-toggle.types.ts +4 -1
@@ -189,7 +189,10 @@ async function copyComponentFiles(componentName, componentPath, isDependency = f
189
189
 
190
190
  fs.mkdirSync(targetPath, { recursive: true })
191
191
  copyDirectoryRecursive(nodeModulesPath, targetPath, projectRoot)
192
- await copySharedFolderIfNeeded(componentPath, isDependency, { forceSync: true })
192
+ const sharedOk = await copySharedFolderIfNeeded(componentPath, isDependency, { forceSync: true })
193
+ if (!sharedOk) {
194
+ process.exit(1)
195
+ }
193
196
 
194
197
  return true
195
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tailjng",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^19.2.0",
6
6
  "@angular/core": "^19.2.0",
@@ -53,9 +53,17 @@
53
53
  } @else if (displayData.length > 0) {
54
54
 
55
55
  @if (itemTemplate) {
56
- <div class="grid grid-cols-4 max-[1200px]:grid-cols-3 max-[950px]:grid-cols-2 max-[650px]:grid-cols-1 items-start gap-4 my-4">
56
+ <div
57
+ class="grid grid-cols-4 max-[1200px]:grid-cols-3 max-[950px]:grid-cols-2 max-[650px]:grid-cols-1 gap-4 my-4"
58
+ [class.items-stretch]="useStretchCards"
59
+ [class.items-start]="!useStretchCards"
60
+ >
57
61
  @for (item of displayData; track trackById($index, item)) {
58
- <div class="flex flex-col rounded-xl border border-border dark:border-dark-border bg-white dark:bg-foreground shadow-sm hover:shadow-lg hover:border-primary/50 hover:dark:border-primary transition-all duration-300">
62
+ <div
63
+ class="flex w-full flex-col rounded-xl border border-border dark:border-dark-border bg-white dark:bg-foreground shadow-sm hover:shadow-lg hover:border-primary/50 hover:dark:border-primary transition-all duration-300"
64
+ [class.h-full]="useStretchCards"
65
+ [class.self-start]="!useStretchCards"
66
+ >
59
67
  <ng-container
60
68
  *ngTemplateOutlet="
61
69
  itemTemplate;
@@ -629,6 +629,12 @@ export class JCompleteCrudCardComponent implements OnInit {
629
629
 
630
630
  @Input() expandTemplate?: TemplateRef<any>;
631
631
  @Input() onExpandData?: (item: any) => Promise<any>;
632
+ /** false = cards no se estiran a la altura de la fila (útil con expand). Por defecto: !expandTemplate */
633
+ @Input() stretchCards?: boolean;
634
+
635
+ get useStretchCards(): boolean {
636
+ return this.stretchCards ?? !this.expandTemplate;
637
+ }
632
638
 
633
639
  expandedItems: { [key: string]: boolean } = {};
634
640
  expandData: { [key: string]: any } = {};
@@ -6,7 +6,7 @@
6
6
  [value]="value"
7
7
  (input)="onInput($event)"
8
8
  [required]="required"
9
- [disabled]="isDisabled"
9
+ [disabled]="isActuallyDisabled"
10
10
  [readonly]="isReadonly"
11
11
  [rows]="rows"
12
12
  (blur)="onBlur()"
@@ -20,7 +20,7 @@
20
20
  class="absolute right-2 top-2 flex cursor-pointer items-center justify-center p-0 focus:outline-none"
21
21
  [ngClass]="clearButtonClasses"
22
22
  (click)="clearInput()"
23
- [disabled]="isDisabled"
23
+ [disabled]="isActuallyDisabled"
24
24
  aria-label="Clear"
25
25
  >
26
26
  <JIcon
@@ -47,6 +47,7 @@ export class JTextareaInputComponent implements ControlValueAccessor {
47
47
  @Input() name?: string;
48
48
  @Input() placeholder = '';
49
49
 
50
+ /** Consumer-driven disabled (e.g. `[isDisabled]="true"`). */
50
51
  @Input() isDisabled = false;
51
52
  @Input() isReadonly = false;
52
53
  @Input() required = false;
@@ -67,6 +68,7 @@ export class JTextareaInputComponent implements ControlValueAccessor {
67
68
  @Input() ngClasses: Record<string, boolean> = {};
68
69
 
69
70
  innerValue = '';
71
+ private formDisabled = false;
70
72
 
71
73
  private onChange: (value: string) => void = () => {};
72
74
  private onTouched: () => void = () => {};
@@ -109,7 +111,7 @@ export class JTextareaInputComponent implements ControlValueAccessor {
109
111
  this.ngClasses,
110
112
  { 'pr-3': !this.clearButton, 'pr-7': this.clearButton },
111
113
  { 'scroll-element': true },
112
- this.isDisabled ? INPUT_DISABLED_OVERLAY_CLASSES : {},
114
+ this.isActuallyDisabled ? INPUT_DISABLED_OVERLAY_CLASSES : {},
113
115
  );
114
116
  }
115
117
 
@@ -118,7 +120,12 @@ export class JTextareaInputComponent implements ControlValueAccessor {
118
120
  * @returns Hover and disabled utilities for the clear control.
119
121
  */
120
122
  get clearButtonClasses(): Record<string, boolean> {
121
- return inputActionButtonClasses(this.isDisabled);
123
+ return inputActionButtonClasses(this.isActuallyDisabled);
124
+ }
125
+
126
+ /** Final disabled state (input OR reactive-forms). */
127
+ get isActuallyDisabled(): boolean {
128
+ return this.isDisabled || this.formDisabled;
122
129
  }
123
130
 
124
131
  /** Tailwind resize utilities keyed by {@link TextareaResize}. */
@@ -158,7 +165,7 @@ export class JTextareaInputComponent implements ControlValueAccessor {
158
165
  * @param isDisabled Whether the control is disabled.
159
166
  */
160
167
  setDisabledState(isDisabled: boolean): void {
161
- this.isDisabled = isDisabled;
168
+ this.formDisabled = isDisabled;
162
169
  }
163
170
 
164
171
  /**
@@ -12,14 +12,25 @@
12
12
  "
13
13
  [ngClass]="getButtonNgClass()"
14
14
  >
15
- <span
16
- class="truncate text-black dark:text-white"
17
- [ngClass]="{
18
- 'opacity-70 text-muted-foreground dark:text-dark-muted-foreground':
19
- selectedValue === null,
20
- }"
21
- >{{ selectedLabel }}</span
22
- >
15
+ @if (selectedLabelHtml) {
16
+ <span
17
+ class="truncate text-black dark:text-white [&_strong]:font-bold"
18
+ [ngClass]="{
19
+ 'opacity-70 text-muted-foreground dark:text-dark-muted-foreground':
20
+ selectedValue === null,
21
+ }"
22
+ [innerHTML]="selectedLabelHtml"
23
+ ></span>
24
+ } @else {
25
+ <span
26
+ class="truncate text-black dark:text-white"
27
+ [ngClass]="{
28
+ 'opacity-70 text-muted-foreground dark:text-dark-muted-foreground':
29
+ selectedValue === null,
30
+ }"
31
+ >{{ selectedLabel }}</span
32
+ >
33
+ }
23
34
 
24
35
  <div class="flex items-center">
25
36
  @if (showClear && selectedValue !== null) {
@@ -159,9 +170,13 @@
159
170
  }
160
171
 
161
172
  <div
162
- class="flex items-center break-words whitespace-normal overflow-hidden"
173
+ class="flex items-center break-words whitespace-normal overflow-hidden [&_strong]:font-bold"
163
174
  >
164
- {{ option.text }}
175
+ @if (option.html) {
176
+ <span [innerHTML]="option.html"></span>
177
+ } @else {
178
+ {{ option.text }}
179
+ }
165
180
  </div>
166
181
  </div>
167
182
  }
@@ -1,3 +1,4 @@
1
+
1
2
  import { animate, style, transition, trigger } from '@angular/animations';
2
3
  import { CommonModule } from '@angular/common';
3
4
  import {
@@ -47,6 +48,7 @@ import {
47
48
  readDropdownPaginationMeta,
48
49
  rebuildEndpointUrl,
49
50
  resetDropdownPanelStyles,
51
+ serializeDropdownFilters,
50
52
  } from './dropdown-select.util';
51
53
 
52
54
  export type {
@@ -71,8 +73,10 @@ export {
71
73
  readDropdownPaginationMeta,
72
74
  rebuildEndpointUrl,
73
75
  resetDropdownPanelStyles,
76
+ serializeDropdownFilters,
74
77
  resolveOptionLabel,
75
78
  resolveOptionLabelFromTemplate,
79
+ resolveOptionLabelHtmlFromTemplate,
76
80
  getOptionValueFilterKey,
77
81
  canResolveSelectedByFindOne,
78
82
  } from './dropdown-select.util';
@@ -272,6 +276,7 @@ export class JDropdownSelectComponent
272
276
  isColumnSelectorOpen = false;
273
277
  selectedValue: unknown = null;
274
278
  selectedLabel = '';
279
+ selectedLabelHtml: string | null = null;
275
280
  internalOptions: DropdownOption[] = [];
276
281
  filteredOptions: DropdownOption[] = [];
277
282
 
@@ -296,6 +301,7 @@ export class JDropdownSelectComponent
296
301
 
297
302
  private currentPage = 1;
298
303
  private totalPages = 1;
304
+ private defaultFiltersSnapshot = '';
299
305
  private resolvedSelectedOption: DropdownOption | null = null;
300
306
  private isResolvingSelected = false;
301
307
 
@@ -334,6 +340,7 @@ export class JDropdownSelectComponent
334
340
  this.loadData(true);
335
341
  }
336
342
 
343
+ this.defaultFiltersSnapshot = serializeDropdownFilters(this.defaultFilters);
337
344
  this.updateSelectedLabel();
338
345
  }
339
346
 
@@ -346,6 +353,41 @@ export class JDropdownSelectComponent
346
353
  if (changes['options']) {
347
354
  this.processOptions();
348
355
  }
356
+
357
+ if (changes['isDisabled']?.currentValue === true) {
358
+ this.closePanel();
359
+ }
360
+
361
+ if (changes['defaultFilters']) {
362
+ this.syncDefaultFilters();
363
+ }
364
+ }
365
+
366
+ /** Reloads or clears options when dependent `defaultFilters` change. */
367
+ private syncDefaultFilters(): void {
368
+ const snapshot = serializeDropdownFilters(this.defaultFilters);
369
+ if (snapshot === this.defaultFiltersSnapshot) {
370
+ return;
371
+ }
372
+
373
+ this.defaultFiltersSnapshot = snapshot;
374
+ this.searchTerm = '';
375
+ this.options = [];
376
+ this.internalOptions = [];
377
+ this.filteredOptions = [];
378
+
379
+ if (this.isColumnSelectorOpen) {
380
+ this.closePanel();
381
+ }
382
+
383
+ if (this.type === 'searchable' && this.endpoint && !this.isDisabled && this.genericService) {
384
+ if (this.loadOnInit) {
385
+ this.loadData(true);
386
+ }
387
+ }
388
+
389
+ this.updateSelectedLabel();
390
+ this.cdr.markForCheck();
349
391
  }
350
392
 
351
393
  ngOnDestroy(): void {
@@ -368,7 +410,9 @@ export class JDropdownSelectComponent
368
410
  * @param value Selected option value or `null`.
369
411
  */
370
412
  writeValue(value: unknown): void {
371
- if (!optionValuesEqual(this.selectedValue, value)) {
413
+ const valueChanged = !optionValuesEqual(this.selectedValue, value);
414
+
415
+ if (valueChanged) {
372
416
  if (
373
417
  value == null ||
374
418
  !this.resolvedSelectedOption ||
@@ -379,9 +423,17 @@ export class JDropdownSelectComponent
379
423
  }
380
424
 
381
425
  this.selectedValue = value;
426
+
427
+ if (value == null) {
428
+ this.selectedLabel = this.placeholder;
429
+ this.selectedLabelHtml = null;
430
+ this.cdr.markForCheck();
431
+ return;
432
+ }
433
+
382
434
  this.updateSelectedLabel();
383
435
 
384
- if (value != null && this.type === 'searchable' && !this.findSelectedOption()) {
436
+ if (this.type === 'searchable' && !this.findSelectedOption()) {
385
437
  this.resolveSelectedOption();
386
438
  }
387
439
 
@@ -452,6 +504,7 @@ export class JDropdownSelectComponent
452
504
  selectOption(option: DropdownOption): void {
453
505
  this.selectedValue = option.value;
454
506
  this.selectedLabel = option.text;
507
+ this.selectedLabelHtml = option.html ?? null;
455
508
  this.resolvedSelectedOption = option;
456
509
  this.onChange(this.selectedValue);
457
510
  this.selectionChange.emit(option.original ?? option.value);
@@ -465,7 +518,9 @@ export class JDropdownSelectComponent
465
518
  clearSelection(event: Event): void {
466
519
  event.stopPropagation();
467
520
  this.resolvedSelectedOption = null;
468
- this.writeValue(null);
521
+ this.selectedValue = null;
522
+ this.selectedLabel = this.placeholder;
523
+ this.selectedLabelHtml = null;
469
524
  this.onChange(null);
470
525
  this.selectionChange.emit(null);
471
526
  }
@@ -485,12 +540,14 @@ export class JDropdownSelectComponent
485
540
  updateSelectedLabel(): void {
486
541
  if (this.selectedValue === null) {
487
542
  this.selectedLabel = this.placeholder;
543
+ this.selectedLabelHtml = null;
488
544
  return;
489
545
  }
490
546
 
491
547
  const selectedOption = this.findSelectedOption();
492
548
  if (selectedOption) {
493
549
  this.selectedLabel = selectedOption.text;
550
+ this.selectedLabelHtml = selectedOption.html ?? null;
494
551
  this.resolvedSelectedOption = selectedOption;
495
552
  return;
496
553
  }
@@ -500,17 +557,17 @@ export class JDropdownSelectComponent
500
557
  optionValuesEqual(this.resolvedSelectedOption.value, this.selectedValue)
501
558
  ) {
502
559
  this.selectedLabel = this.resolvedSelectedOption.text;
560
+ this.selectedLabelHtml = this.resolvedSelectedOption.html ?? null;
503
561
  this.ensureSelectedOptionPinned();
504
562
  return;
505
563
  }
506
564
 
565
+ this.selectedLabel = this.placeholder;
566
+ this.selectedLabelHtml = null;
567
+
507
568
  if (this.type === 'searchable' && this.resolveSelected) {
508
569
  this.resolveSelectedOption();
509
570
  }
510
-
511
- if (!this.selectedLabel || this.selectedLabel === this.placeholder) {
512
- this.selectedLabel = this.placeholder;
513
- }
514
571
  }
515
572
 
516
573
  /** Keeps a resolved selection visible even if it is not in the current page. */
@@ -536,12 +593,13 @@ export class JDropdownSelectComponent
536
593
 
537
594
  private applyResolvedRecord(record: Record<string, unknown>): void {
538
595
  const [option] = this.mapOptions([record]);
539
- if (!option) {
596
+ if (!option || !optionValuesEqual(option.value, this.selectedValue)) {
540
597
  return;
541
598
  }
542
599
 
543
600
  this.resolvedSelectedOption = option;
544
601
  this.selectedLabel = option.text;
602
+ this.selectedLabelHtml = option.html ?? null;
545
603
  this.ensureSelectedOptionPinned();
546
604
  this.cdr.detectChanges();
547
605
  }
@@ -825,6 +883,12 @@ export class JDropdownSelectComponent
825
883
 
826
884
  if (this.type === 'searchable' && !this.loadOnInit && this.shouldTriggerLoad()) {
827
885
  this.loadData(true);
886
+ } else if (
887
+ this.type === 'searchable' &&
888
+ this.endpoint &&
889
+ this.internalOptions.length === 0
890
+ ) {
891
+ this.loadData(true);
828
892
  }
829
893
 
830
894
  return;
@@ -7,7 +7,10 @@ export type DropdownSortOrder = 'ASC' | 'DESC';
7
7
  /** Normalized option shown in the panel list. */
8
8
  export type DropdownOption<TOriginal = unknown> = {
9
9
  value: unknown;
10
+ /** Plain text label (search, accessibility, fallback). */
10
11
  text: string;
12
+ /** Rich label when `optionLabelTemplate` uses `{b:field}` placeholders. */
13
+ html?: string;
11
14
  original?: TOriginal;
12
15
  };
13
16
 
@@ -72,19 +72,64 @@ export function extractOptionsFromResponse(
72
72
  return Array.isArray(responseData) ? (responseData as unknown[]) : [];
73
73
  }
74
74
 
75
- /** Builds the display label from a template like `El valor es {value} de {label}`. */
75
+ function escapeOptionLabelHtml(value: string): string {
76
+ return value
77
+ .replace(/&/g, '&amp;')
78
+ .replace(/</g, '&lt;')
79
+ .replace(/>/g, '&gt;')
80
+ .replace(/"/g, '&quot;')
81
+ .replace(/'/g, '&#39;');
82
+ }
83
+
84
+ function resolveTemplatePlaceholder(
85
+ option: Record<string, unknown>,
86
+ rawPath: string,
87
+ ): string {
88
+ const path = rawPath.trim().replace(/^b:/, '');
89
+ const value = getNestedValue(option, path);
90
+ if (value == null || value === '') {
91
+ return '';
92
+ }
93
+
94
+ return String(value);
95
+ }
96
+
97
+ /**
98
+ * Builds the display label from a template like `El valor es {value} de {label}`.
99
+ * Use `{b:field}` for bold segments in the panel (rendered as `<strong>`).
100
+ */
76
101
  export function resolveOptionLabelFromTemplate(
77
102
  option: Record<string, unknown>,
78
103
  template: string,
79
104
  ): string {
80
- return template.replace(/\{([^}]+)\}/g, (_, path: string) => {
81
- const value = getNestedValue(option, path.trim());
82
- if (value == null || value === '') {
83
- return '';
84
- }
105
+ return template.replace(/\{(?:b:)?([^}]+)\}/g, (_, path: string) =>
106
+ resolveTemplatePlaceholder(option, path),
107
+ );
108
+ }
85
109
 
86
- return String(value);
110
+ /**
111
+ * HTML label when the template contains `{b:field}` placeholders.
112
+ * Returns `null` when no bold placeholders are used.
113
+ */
114
+ export function resolveOptionLabelHtmlFromTemplate(
115
+ option: Record<string, unknown>,
116
+ template: string,
117
+ ): string | null {
118
+ if (!template.includes('{b:')) {
119
+ return null;
120
+ }
121
+
122
+ let html = template.replace(/\{b:([^}]+)\}/g, (_, path: string) => {
123
+ const value = resolveTemplatePlaceholder(option, `b:${path}`);
124
+ return value ? `<strong>${escapeOptionLabelHtml(value)}</strong>` : '';
87
125
  });
126
+
127
+ html = html.replace(/\{([^}]+)\}/g, (_, path: string) => {
128
+ const value = resolveTemplatePlaceholder(option, path);
129
+ return value ? escapeOptionLabelHtml(value) : '';
130
+ });
131
+
132
+ return html;
88
133
  }
89
134
 
90
135
  /** Builds the display label for one option object. */
@@ -124,11 +169,24 @@ export function mapObjectOptions(
124
169
  labelSeparator: string,
125
170
  optionLabelTemplate?: string,
126
171
  ): DropdownOption[] {
127
- return options.map((option) => ({
128
- value: getNestedValue(option, optionValue),
129
- text: resolveOptionLabel(option, optionLabel, labelSeparator, optionLabelTemplate),
130
- original: option,
131
- }));
172
+ return options.map((option) => {
173
+ const text = resolveOptionLabel(
174
+ option,
175
+ optionLabel,
176
+ labelSeparator,
177
+ optionLabelTemplate,
178
+ );
179
+ const html = optionLabelTemplate?.includes('{b:')
180
+ ? resolveOptionLabelHtmlFromTemplate(option, optionLabelTemplate) ?? undefined
181
+ : undefined;
182
+
183
+ return {
184
+ value: getNestedValue(option, optionValue),
185
+ text,
186
+ html,
187
+ original: option,
188
+ };
189
+ });
132
190
  }
133
191
 
134
192
  /** Local text filter for static dropdown mode. */
@@ -144,6 +202,25 @@ export function filterOptionsBySearch(
144
202
  return options.filter((option) => option.text.toLowerCase().includes(normalized));
145
203
  }
146
204
 
205
+ /** Stable JSON key for default filter change detection. */
206
+ export function serializeDropdownFilters(filters: Record<string, unknown>): string {
207
+ const normalized = Object.keys(filters)
208
+ .sort()
209
+ .reduce<Record<string, unknown>>((acc, key) => {
210
+ const value = filters[key];
211
+
212
+ if (Array.isArray(value)) {
213
+ acc[key] = [...value].map(String).sort();
214
+ return acc;
215
+ }
216
+
217
+ acc[key] = value;
218
+ return acc;
219
+ }, {});
220
+
221
+ return JSON.stringify(normalized);
222
+ }
223
+
147
224
  /** Compares option values loosely (`1` equals `"1"`). */
148
225
  export function optionValuesEqual(a: unknown, b: unknown): boolean {
149
226
  if (a === b) {
@@ -180,9 +180,15 @@
180
180
  [ariaHidden]="true"
181
181
  />
182
182
  }
183
- <label class="text-black dark:text-white text-sm font-medium w-full">{{
184
- option.text
185
- }}</label>
183
+ <label
184
+ class="text-black dark:text-white text-sm font-medium w-full [&_strong]:font-bold"
185
+ >
186
+ @if (option.html) {
187
+ <span [innerHTML]="option.html"></span>
188
+ } @else {
189
+ {{ option.text }}
190
+ }
191
+ </label>
186
192
  </div>
187
193
  }
188
194
  @if (filteredProcessedOptions.length === 0 && !isLoading) {
@@ -48,6 +48,7 @@ import {
48
48
  readDropdownPaginationMeta,
49
49
  rebuildEndpointUrl,
50
50
  resetDropdownPanelStyles,
51
+ serializeDropdownFilters,
51
52
  } from './multi-dropdown-select.util';
52
53
 
53
54
  export type {
@@ -74,6 +75,9 @@ export {
74
75
  rebuildEndpointUrl,
75
76
  resetDropdownPanelStyles,
76
77
  resolveOptionLabel,
78
+ resolveOptionLabelFromTemplate,
79
+ resolveOptionLabelHtmlFromTemplate,
80
+ serializeDropdownFilters,
77
81
  } from './multi-dropdown-select.util';
78
82
 
79
83
  /**
@@ -278,6 +282,7 @@ export class JMultiDropdownSelectComponent
278
282
 
279
283
  private currentPage = 1;
280
284
  private totalPages = 1;
285
+ private defaultFiltersSnapshot = '';
281
286
  private resolvedSelectedOptions = new Map<string, MultiDropdownOption>();
282
287
  private isResolvingSelected = false;
283
288
 
@@ -321,6 +326,8 @@ export class JMultiDropdownSelectComponent
321
326
  if (this.type === 'searchable' && this.loadOnInit) {
322
327
  this.loadData(true);
323
328
  }
329
+
330
+ this.defaultFiltersSnapshot = serializeDropdownFilters(this.defaultFilters);
324
331
  }
325
332
 
326
333
  ngAfterViewInit(): void {
@@ -332,6 +339,41 @@ export class JMultiDropdownSelectComponent
332
339
  if (changes['options']) {
333
340
  this.processOptions();
334
341
  }
342
+
343
+ if (changes['isDisabled']?.currentValue === true) {
344
+ this.closePanel();
345
+ }
346
+
347
+ if (changes['defaultFilters']) {
348
+ this.syncDefaultFilters();
349
+ }
350
+ }
351
+
352
+ /** Reloads or clears options when dependent `defaultFilters` change. */
353
+ private syncDefaultFilters(): void {
354
+ const snapshot = serializeDropdownFilters(this.defaultFilters);
355
+ if (snapshot === this.defaultFiltersSnapshot) {
356
+ return;
357
+ }
358
+
359
+ this.defaultFiltersSnapshot = snapshot;
360
+ this.searchTerm = '';
361
+ this.options = [];
362
+ this.processedOptions = [];
363
+ this.filteredProcessedOptions = [];
364
+
365
+ if (this.isDropdownOpen) {
366
+ this.closePanel();
367
+ }
368
+
369
+ if (this.type === 'searchable' && this.endpoint && !this.isDisabled && this.genericService) {
370
+ if (this.loadOnInit) {
371
+ this.loadData(true);
372
+ }
373
+ }
374
+
375
+ this.updateDisplayLabel();
376
+ this.cdr.markForCheck();
335
377
  }
336
378
 
337
379
  ngOnDestroy(): void {
@@ -15,6 +15,8 @@ export {
15
15
  resetDropdownPanelStyles,
16
16
  resolveOptionLabel,
17
17
  resolveOptionLabelFromTemplate,
18
+ resolveOptionLabelHtmlFromTemplate,
19
+ serializeDropdownFilters,
18
20
  } from '../select-dropdown/dropdown-select.util';
19
21
 
20
22
  /** Coerces numeric strings to numbers so checkbox matching stays stable. */
@@ -9,8 +9,8 @@ export const crudTableSlideToggleAnimation = trigger('slideToggle', [
9
9
 
10
10
  /** Group header expand/collapse animation. */
11
11
  export const crudTableGroupSlideToggleAnimation = trigger('groupSlideToggle', [
12
- state('collapsed', style({ height: '0', opacity: 0, overflow: 'hidden' })),
13
- state('expanded', style({ height: '*', opacity: 1, overflow: 'visible' })),
12
+ state('collapsed', style({ height: '0', opacity: 0 })),
13
+ state('expanded', style({ height: '*', opacity: 1 })),
14
14
  transition('collapsed => expanded', [animate('380ms cubic-bezier(0.4, 0, 0.2, 1)')]),
15
15
  transition('expanded => collapsed', [animate('300ms cubic-bezier(0.4, 0, 0.6, 1)')]),
16
16
  ]);
@@ -130,7 +130,7 @@
130
130
  size="16"
131
131
  iconClass="text-white animate-spin" [ariaHidden]="true" />
132
132
  } @else if (
133
- getSortKey(sortColumn) !== column.key ||
133
+ getSortKey(sortColumn) !== getSortKey(column.key) ||
134
134
  sortDirection === "none"
135
135
  ) {
136
136
  <JIcon [icon]="Icons.ChevronsUpDown"
@@ -656,7 +656,9 @@
656
656
  >
657
657
  <div
658
658
  [@groupSlideToggle]="getGroupExpansionState(item)"
659
- class="j-group-expand-panel overflow-hidden"
659
+ class="j-group-expand-panel"
660
+ [class.overflow-hidden]="getGroupExpansionState(item) === 'collapsed'"
661
+ [class.overflow-visible]="getGroupExpansionState(item) === 'expanded'"
660
662
  >
661
663
  <table
662
664
  class="j-group-inner-table table-fixed w-full min-w-full border-collapse bg-white dark:bg-foreground"
@@ -437,7 +437,7 @@ export class JCompleteCrudTableComponent implements OnInit {
437
437
  this.totalItems = this.data.length;
438
438
  }
439
439
 
440
- if (response.meta?.sort) {
440
+ if (response.meta?.sort?.by && typeof response.meta.sort.by === 'string') {
441
441
  this.sortColumn = response.meta.sort.by;
442
442
  this.sortDirection = response.meta.sort.order.toLowerCase() as SortDirection;
443
443
  }
@@ -783,9 +783,8 @@ export class JCompleteCrudTableComponent implements OnInit {
783
783
  return resolveOptionIcon(icon, data);
784
784
  }
785
785
 
786
- getDisabled(option: OptionsTable, data: any): boolean {
787
- return resolveOptionDisabled(option, data);
788
- }
786
+ getDisabled = (option: OptionsTable, data: any): boolean =>
787
+ resolveOptionDisabled(option, data);
789
788
 
790
789
  getIsVisible(option: OptionsTable, data: any): boolean {
791
790
  return resolveOptionVisible(option, data);
@@ -220,6 +220,18 @@ export function cacheExpandedRowKeys(
220
220
 
221
221
  // ── Row styling ──────────────────────────────────────────────────────────────
222
222
 
223
+ function normalizeRowColorConditionKey(value: unknown): string {
224
+ if (value === null || value === undefined) {
225
+ return 'null';
226
+ }
227
+
228
+ if (typeof value === 'boolean') {
229
+ return String(value);
230
+ }
231
+
232
+ return String(value).toUpperCase();
233
+ }
234
+
223
235
  export function buildRowNgClass(
224
236
  row: Record<string, unknown>,
225
237
  columns: TableColumn<any>[],
@@ -241,8 +253,7 @@ export function buildRowNgClass(
241
253
  value = (value as Record<string, unknown> | undefined)?.[key];
242
254
  }
243
255
 
244
- const normalizedValue =
245
- value === null || value === undefined ? 'null' : String(value).toUpperCase();
256
+ const normalizedValue = normalizeRowColorConditionKey(value);
246
257
  const semanticClass = columnWithColor.rowColorCondition?.[normalizedValue] as
247
258
  | CrudTableRowColorSemantic
248
259
  | undefined;
@@ -1,9 +1,9 @@
1
- export type ToggleOption<T = unknown> = {
2
- value: T;
3
- label: string;
4
- original?: unknown;
5
- };
6
-
7
- export type ToggleSortOrder = 'ASC' | 'DESC';
8
-
9
- export type ToggleRadioLayout = 'vertical' | 'horizontal';
1
+ export type ToggleOption<T = unknown> = {
2
+ value: T;
3
+ label: string;
4
+ original?: unknown;
5
+ };
6
+
7
+ export type ToggleSortOrder = 'ASC' | 'DESC';
8
+
9
+ export type ToggleRadioLayout = 'vertical' | 'horizontal';
@@ -20,8 +20,9 @@ import {
20
20
  normalizeToggleOptions,
21
21
  } from '../shared/toggle-options.util';
22
22
  import { ToggleOption, ToggleSortOrder } from '../shared/toggle-options.types';
23
+ import { ToggleSegmentType } from './segment-toggle.types';
23
24
 
24
- export type { ToggleOption, ToggleSortOrder } from './segment-toggle.types';
25
+ export type { ToggleOption, ToggleSortOrder, ToggleSegmentType } from './segment-toggle.types';
25
26
 
26
27
  /**
27
28
  * Segmented single-select control (pill/tab style).
@@ -56,6 +57,8 @@ export type { ToggleOption, ToggleSortOrder } from './segment-toggle.types';
56
57
  export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueAccessor {
57
58
  readonly Icons = Icons;
58
59
 
60
+ /** `static` = local `options` · `searchable` = loads from `endpoint` on init. */
61
+ @Input() type: ToggleSegmentType = 'static';
59
62
  @Input() endpoint = '';
60
63
  @Input() options: unknown[] = [];
61
64
  @Input() optionLabel: string | string[] = 'label';
@@ -126,7 +129,7 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
126
129
  }
127
130
 
128
131
  ngOnInit(): void {
129
- if (this.endpoint && this.loadOnInit) {
132
+ if (this.shouldLoadFromApi()) {
130
133
  this.loadOptionsFromApi();
131
134
  return;
132
135
  }
@@ -135,6 +138,21 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
135
138
  }
136
139
 
137
140
  ngOnChanges(changes: SimpleChanges): void {
141
+ const loadTriggers = [
142
+ 'endpoint',
143
+ 'type',
144
+ 'loadOnInit',
145
+ 'defaultFilters',
146
+ 'dataPath',
147
+ 'responseKey',
148
+ 'sort',
149
+ ];
150
+
151
+ if (loadTriggers.some((key) => changes[key]) && this.shouldLoadFromApi()) {
152
+ this.loadOptionsFromApi();
153
+ return;
154
+ }
155
+
138
156
  const optionInputs = [
139
157
  'options',
140
158
  'optionLabel',
@@ -144,11 +162,16 @@ export class JToggleSegmentComponent implements OnInit, OnChanges, ControlValueA
144
162
  'optionLabelFn',
145
163
  ];
146
164
 
147
- if (optionInputs.some((key) => changes[key]) && !this.endpoint) {
165
+ if (optionInputs.some((key) => changes[key]) && !this.shouldLoadFromApi()) {
148
166
  this.processOptions();
149
167
  }
150
168
  }
151
169
 
170
+ /** Whether options should be fetched from `endpoint`. */
171
+ private shouldLoadFromApi(): boolean {
172
+ return !!this.endpoint && (this.loadOnInit || this.type === 'searchable');
173
+ }
174
+
152
175
  setDisabledState(isDisabled: boolean): void {
153
176
  this.isComponentDisabled = isDisabled;
154
177
  }
@@ -1 +1,4 @@
1
- export type { ToggleOption, ToggleSortOrder } from '../shared/toggle-options.types';
1
+ export type { ToggleOption, ToggleSortOrder } from '../shared/toggle-options.types';
2
+
3
+ /** `static` = local options · `searchable` = API endpoint (loads on init). */
4
+ export type ToggleSegmentType = 'static' | 'searchable';