@myrmidon/gve-core 6.1.0 → 6.1.1

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, ChangeDetectionStrategy, Component, inject, DestroyRef, signal, computed, effect, viewChild, model, Injectable, untracked, Optional, Inject, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
+ import { input, output, ChangeDetectionStrategy, Component, inject, DestroyRef, signal, computed, effect, viewChild, model, untracked, Injectable, Optional, Inject, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
3
3
  import * as i1 from '@angular/material/core';
4
4
  import { MatRippleModule } from '@angular/material/core';
5
5
  import * as i2$1 from '@myrmidon/ngx-tools';
@@ -16,8 +16,8 @@ import * as i5 from '@angular/material/input';
16
16
  import { MatInputModule } from '@angular/material/input';
17
17
  import * as i6 from '@angular/material/tooltip';
18
18
  import { MatTooltipModule, MatTooltip } from '@angular/material/tooltip';
19
- import { Subject, debounceTime, distinctUntilChanged, startWith, catchError, BehaviorSubject } from 'rxjs';
20
- import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
19
+ import { Subject, debounceTime, distinctUntilChanged, catchError, BehaviorSubject } from 'rxjs';
20
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
21
21
  import * as i2$3 from '@angular/material/snack-bar';
22
22
  import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
23
23
  import * as i2$2 from '@angular/cdk/clipboard';
@@ -669,7 +669,28 @@ class FeatureEditorComponent {
669
669
  * Event emitted when the user cancels the feature editing.
670
670
  */
671
671
  this.featureCancel = output();
672
+ /**
673
+ * The allowed value IDs for the currently selected feature name.
674
+ * Set synchronously in updateForm (on feature load) and in the
675
+ * name.valueChanges subscription (on user interaction).
676
+ */
672
677
  this.nameIds = signal(undefined, ...(ngDevMode ? [{ debugName: "nameIds" }] : []));
678
+ /**
679
+ * Tracks the current feature name. Updated synchronously in updateForm
680
+ * and in the name.valueChanges subscription so that isMultiValued stays
681
+ * in sync regardless of whether the change was programmatic or user-driven.
682
+ */
683
+ this._currentName = signal('', ...(ngDevMode ? [{ debugName: "_currentName" }] : []));
684
+ /**
685
+ * Computed signal that determines if the current feature is multi-valued.
686
+ * Automatically updates when either _currentName or multiValuedFeatureIds
687
+ * changes.
688
+ */
689
+ this.isMultiValued = computed(() => {
690
+ const ids = this.multiValuedFeatureIds();
691
+ const name = this._currentName();
692
+ return !!ids && !!name && ids.includes(name);
693
+ }, ...(ngDevMode ? [{ debugName: "isMultiValued" }] : []));
673
694
  this.name = formBuilder.control('', {
674
695
  validators: [Validators.required, Validators.maxLength(50)],
675
696
  nonNullable: true,
@@ -696,41 +717,29 @@ class FeatureEditorComponent {
696
717
  isGlobal: this.isGlobal,
697
718
  isShortLived: this.isShortLived,
698
719
  });
699
- // Convert name form control valueChanges to a signal for reactive updates
700
- this.nameSignal = toSignal(this.name.valueChanges.pipe(startWith('')), {
701
- initialValue: '',
702
- });
703
- // Computed signal that automatically tracks dependencies and triggers
704
- // change detection when either name or multiValuedFeatureIds changes
705
- this.isMultiValued = computed(() => {
706
- const ids = this.multiValuedFeatureIds();
707
- const name = this.nameSignal();
708
- return !!ids && !!name && ids.includes(name);
709
- }, ...(ngDevMode ? [{ debugName: "isMultiValued" }] : []));
710
- // when feature changes, update the form
720
+ // When the feature input changes, repopulate the form atomically.
721
+ // featValues is read via untracked() so changes to it do not retrigger
722
+ // this effect and accidentally overwrite in-progress user edits.
711
723
  effect(() => {
712
724
  this.updateForm(this.feature());
713
- setTimeout(() => {
714
- this.nameControl()?.nativeElement?.focus();
715
- this.nameIds.set(this.getLabeledIdsFor(this.name.value, this.featValues()));
716
- }, 500);
717
- });
718
- // diagnostic logging for multiValuedFeatureIds
719
- effect(() => {
720
- console.log('[FeatureEditor] multiValuedFeatureIds:', this.multiValuedFeatureIds(), 'isMultiValued:', this.isMultiValued());
725
+ setTimeout(() => this.nameControl()?.nativeElement?.focus(), 0);
721
726
  });
722
727
  }
723
728
  ngOnInit() {
724
- // whenever the name changes, update the value selection
729
+ // Respond to user-initiated name changes only.
730
+ // updateForm uses { emitEvent: false } so programmatic loads never
731
+ // reach this subscription, eliminating the race that caused value.reset()
732
+ // to fire after a 300 ms debounce and wipe the just-loaded value.
725
733
  this._sub = this.name.valueChanges
726
734
  .pipe(debounceTime(300), distinctUntilChanged())
727
735
  .subscribe((name) => {
728
- if (!this._frozen && this.featNames()?.length) {
736
+ this._currentName.set(name);
737
+ this.nameIds.set(this.getLabeledIdsFor(name, this.featValues()));
738
+ // Reset value only when preset names are in use; for free-text names
739
+ // the value is independent and must not be cleared.
740
+ if (this.featNames()?.length) {
729
741
  this.value.reset();
730
- this.nameIds.set(this.getLabeledIdsFor(name, this.featValues()));
731
742
  }
732
- // Note: isMultiValued is now a computed signal that automatically
733
- // updates when nameSignal or multiValuedFeatureIds changes
734
743
  });
735
744
  }
736
745
  ngOnDestroy() {
@@ -740,26 +749,32 @@ class FeatureEditorComponent {
740
749
  if (!id) {
741
750
  return undefined;
742
751
  }
743
- console.log('getLabeledIdsFor', id, featValues);
744
752
  return featValues ? featValues[id] : undefined;
745
753
  }
746
754
  updateForm(feature) {
747
755
  if (!feature) {
756
+ this._currentName.set('');
757
+ this.nameIds.set(undefined);
748
758
  this.form.reset();
749
- // Note: isMultiValued is a computed signal - it will automatically
750
- // update to false when name is reset to empty string
751
759
  }
752
760
  else {
753
- this._frozen = true;
754
- this.name.setValue(feature.name);
755
- this.value.setValue(feature.value);
761
+ // Read featValues outside the reactive context so that changes to
762
+ // featValues alone do not re-trigger the parent effect and overwrite
763
+ // any in-progress user edits.
764
+ const featValues = untracked(() => this.featValues());
765
+ // Update derived signals first so the template can resolve the correct
766
+ // control type (select vs text) before Angular renders.
767
+ this._currentName.set(feature.name);
768
+ this.nameIds.set(this.getLabeledIdsFor(feature.name, featValues));
769
+ // Use emitEvent: false to suppress valueChanges events.
770
+ // This prevents the name.valueChanges subscription (which resets the
771
+ // value control) from firing during a programmatic load.
772
+ this.name.setValue(feature.name, { emitEvent: false });
773
+ this.value.setValue(feature.value, { emitEvent: false });
756
774
  this.setPolicy.setValue(feature.setPolicy || FeatureSetPolicy.multiple);
757
775
  this.isNegated.setValue(feature.isNegated || false);
758
776
  this.isGlobal.setValue(feature.isGlobal || false);
759
777
  this.isShortLived.setValue(feature.isShortLived || false);
760
- // Note: isMultiValued is a computed signal - it will automatically
761
- // update based on the new name value and multiValuedFeatureIds
762
- this._frozen = false;
763
778
  }
764
779
  }
765
780
  addValueToComposite() {
@@ -887,10 +902,6 @@ class FeatureSetEditorComponent {
887
902
  effect(() => {
888
903
  this.applyFeatureFilter(this.features(), this.filter.value);
889
904
  });
890
- // diagnostic logging for multiValuedFeatureIds
891
- effect(() => {
892
- console.log('[FeatureSetEditor] multiValuedFeatureIds:', this.multiValuedFeatureIds());
893
- });
894
905
  }
895
906
  applyFeatureFilter(features, name) {
896
907
  this.filteredFeatures.set(features.filter((feature) => {