Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
[primaryActionBtnLabel]="config.primaryActionBtnLabel"
[primaryActionBtnColor]="config.primaryActionBtnColor"
[primaryActionBtnDisabled]="isPrimaryButtonDisabled$ | async"
contentMaxHeight="calc(100vh - 192px)"
(primaryActionBtnClicked)="onPrimaryActionBtnClicked()"
>
<form [formGroup]="privateSegmentListForm" class="form-standard dense-3">
Expand Down Expand Up @@ -45,26 +46,22 @@
</mat-autocomplete>
</mat-form-field>
} @if (selectedListType && selectedListType !== LIST_TYPES.SEGMENT) {
<app-common-tags-input
formControlName="values"
[inputType]="CommonTagInputType.VALUES"
[label]="config.valuesLabel"
[placeholder]="config.valuesPlaceholder"
[forceValidation]="forceValidation"
[truncationLength]="72"
(downloadRequested)="onDownloadRequested($event)"
></app-common-tags-input>
<mat-form-field appearance="outline">
<mat-label class="ft-14-400">Name</mat-label>
<input matInput formControlName="name" placeholder="e.g., My schools" class="ft-14-400" appTrimInput />
<mat-hint class="form-hint ft-12-400">
{{ config.nameHint | translate }}
</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label class="ft-14-400">Description (optional)</mat-label>
<input matInput formControlName="description" class="ft-14-400" appTrimInput />
</mat-form-field>
<app-common-list-values-input
formControlName="values"
[label]="config.valuesLabel"
[placeholder]="config.valuesPlaceholder"
[loading]="isLoadingMembers$ | async"
[loadingCount]="loadingValuesCount"
(downloadRequested)="onDownloadRequested($event)"
(pendingStateChanged)="onValuesPendingStateChanged($event)"
></app-common-list-values-input>
}
</form>
</app-common-dialog>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, ViewChild } from '@angular/core';
import { CommonModalComponent, CommonTagsInputComponent } from '@shared-component-lib';
import { CommonListValuesInputComponent, CommonModalComponent } from '@shared-component-lib';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { CommonModule } from '@angular/common';
import {
Expand Down Expand Up @@ -51,7 +51,6 @@ import { SEGMENT_TYPE } from '../../../../../../../../../../types/src';
import isEqual from 'lodash.isequal';
import { FeatureFlagsService } from '../../../../../core/feature-flags/feature-flags.service';
import { CommonModalConfig } from '@shared-component-lib/common-modal/common-modal.types';
import { CommonTagInputType } from '../../../../../core/feature-flags/store/feature-flags.model';
import { SharedModule } from '../../../../../shared/shared.module';

@Component({
Expand All @@ -62,7 +61,7 @@ import { SharedModule } from '../../../../../shared/shared.module';
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
CommonTagsInputComponent,
CommonListValuesInputComponent,
CommonModule,
ReactiveFormsModule,
TranslateModule,
Expand All @@ -73,6 +72,7 @@ import { SharedModule } from '../../../../../shared/shared.module';
})
export class UpsertPrivateSegmentListModalComponent {
@ViewChild('typeSelectRef') typeSelectRef: MatSelect;
@ViewChild(CommonListValuesInputComponent) valuesInputComponent?: CommonListValuesInputComponent;
listOptionTypes$: Observable<{ value: string; viewValue: string }[]>;
// Disable the primary button while an add/edit is in flight in any of the three stores this
// modal drives (flag/experiment/segment), to prevent double-submits.
Expand All @@ -86,6 +86,7 @@ export class UpsertPrivateSegmentListModalComponent {
// would send a full-replacement update that drops the unloaded members. Included in
// isPrimaryButtonDisabled$ to block saving during the fetch.
isLoadingMembers$ = new BehaviorSubject<boolean>(false);
valuesPending$ = new BehaviorSubject<boolean>(false);
initialFormValues$ = new BehaviorSubject<PrivateSegmentListFormData>(null);

subscriptions = new Subscription();
Expand All @@ -96,8 +97,6 @@ export class UpsertPrivateSegmentListModalComponent {
isSegmentsListTypeDisabled$: Observable<boolean>;

privateSegmentListForm: FormGroup;
CommonTagInputType = CommonTagInputType;
forceValidation = false;

constructor(
@Inject(MAT_DIALOG_DATA)
Expand Down Expand Up @@ -153,6 +152,17 @@ export class UpsertPrivateSegmentListModalComponent {
return this.privateSegmentListForm?.get(PRIVATE_SEGMENT_LIST_FORM_FIELDS.VALUES);
}

get loadingValuesCount(): number | null {
const sourceList = this.config.params.sourceList;
if (!sourceList?.segment) {
return null;
}

return sourceList.listType?.toLowerCase() === LIST_OPTION_TYPE.INDIVIDUAL.toLowerCase()
? sourceList.segment.individualForSegmentCount ?? sourceList.segment.individualForSegment?.length ?? 0
: sourceList.segment.groupForSegmentCount ?? sourceList.segment.groupForSegment?.length ?? 0;
}

private segmentObjectValidator(): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = control.value;
Expand Down Expand Up @@ -295,10 +305,14 @@ export class UpsertPrivateSegmentListModalComponent {

listenForPrimaryButtonDisabled() {
this.isPrimaryButtonDisabled$ = this.isUpsertLoading$.pipe(
combineLatestWith(this.isInitialFormValueChanged$, this.isLoadingMembers$),
combineLatestWith(this.isInitialFormValueChanged$, this.isLoadingMembers$, this.valuesPending$),
map(
([isLoading, isInitialFormValueChanged, isLoadingMembers]) =>
isLoading || isLoadingMembers || !isInitialFormValueChanged
([isLoading, isInitialFormValueChanged, isLoadingMembers, valuesPending]) =>
isLoading ||
isLoadingMembers ||
valuesPending ||
this.privateSegmentListForm.invalid ||
!isInitialFormValueChanged
)
);
this.subscriptions.add(this.isPrimaryButtonDisabled$.subscribe());
Expand Down Expand Up @@ -329,6 +343,7 @@ export class UpsertPrivateSegmentListModalComponent {
listenToListTypeChanges(): void {
this.subscriptions.add(
this.privateSegmentListForm.get(PRIVATE_SEGMENT_LIST_FORM_FIELDS.LIST_TYPE).valueChanges.subscribe((listType) => {
this.valuesPending$.next(false);
this.resetFormExceptSelectedListType(listType);
this.setValidatorsBasedOnListType(listType);
})
Expand Down Expand Up @@ -377,7 +392,9 @@ export class UpsertPrivateSegmentListModalComponent {
}

onPrimaryActionBtnClicked(): void {
this.forceValidation = true;
if (this.valuesInputComponent && !this.valuesInputComponent.commitPendingChanges()) {
return;
}
if (this.privateSegmentListForm.valid) {
this.sendRequest(this.config.params.action);
} else {
Expand All @@ -386,6 +403,10 @@ export class UpsertPrivateSegmentListModalComponent {
}
}

onValuesPendingStateChanged(valuesPending: boolean): void {
this.valuesPending$.next(valuesPending);
}

sendRequest(action: UPSERT_PRIVATE_SEGMENT_LIST_ACTION): void {
const formData = this.privateSegmentListForm.value;
const listType = formData.listType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<section class="values-section">
@if (!showImportHelper) {
<mat-form-field class="value-input dense-2" appearance="outline" subscriptSizing="dynamic">
<mat-label class="ft-14-400">
{{ label | translate }}
<span class="required-asterisk">*</span>
</mat-label>
<input
matInput
class="ft-14-400"
autocomplete="off"
[formControl]="pendingValueControl"
[placeholder]="placeholder | translate"
[readonly]="loading || isDisabled"
(keydown)="onPendingValueKeydown($event)"
(paste)="onPendingValuePaste($event)"
(blur)="markAsTouched()"
/>
@if (hasPendingValue) {
<button
type="button"
mat-icon-button
class="field-action-button"
[disabled]="loading || isDisabled"
[matTooltip]="'lists.values.add.tooltip.text' | translate"
[attr.aria-label]="'lists.values.add.tooltip.text' | translate"
(click)="commitPendingValues()"
>
<span class="material-symbols-outlined">add_circle</span>
</button>
} @else {
<button
type="button"
mat-icon-button
class="field-action-button"
[disabled]="loading || isDisabled"
[matTooltip]="'lists.values.import.tooltip.text' | translate"
[attr.aria-label]="'lists.values.import.tooltip.text' | translate"
(click)="openImportHelper($event)"
>
<span class="material-symbols-outlined">upload</span>
</button>
}
</mat-form-field>
<p class="ft-12-400 form-hint separator-hint">
{{ 'lists.values.separator-hint.text' | translate }}
</p>
} @else {
<div class="drag-drop-container">
<app-common-import-container
fileType=".csv"
buttonLabel="Choose CSV"
(closeButtonClick)="closeImportHelper($event)"
(filesSelected)="handleFilesSelected($event)"
[importFailed]="importFailed"
></app-common-import-container>
<p class="ft-12-400 form-hint import-hint">
{{ 'feature-flags.upsert-list-modal.import-csv.message.text' | translate }}
<app-common-learn-more-link learnMoreLinkKey="guide.add-lists"></app-common-learn-more-link>
</p>
</div>
} @if (feedbackMessage) {
<p class="feedback-message ft-12-400">{{ feedbackMessage }}</p>
} @if (editErrorMessage) {
<p class="feedback-message ft-12-400">{{ editErrorMessage }}</p>
}

<mat-form-field class="search-input dense-3" subscriptSizing="dynamic">
<input matInput class="ft-14-400" [formControl]="searchControl" [placeholder]="'global.search.text' | translate" />
<mat-icon class="search-icon" matSuffix (click)="refreshSearch()">search</mat-icon>
</mat-form-field>

<div class="values-table-container">
@if (loading) {
<mat-progress-bar class="loading-bar" mode="indeterminate"></mat-progress-bar>
}
<table
mat-table
class="values-table"
[dataSource]="filteredRows"
[ngClass]="{ 'no-data': !filteredRows.length }"
[trackBy]="trackByRowId"
[attr.aria-label]="'lists.values.table-label.text' | translate"
>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef class="value-column ft-14-600">
{{ 'lists.values.value-header.text' | translate }} ({{ displayedValueCount | number }})
</th>
<td mat-cell *matCellDef="let row" class="value-column ft-14-400">
@if (editingRowId === row.id) {
<mat-form-field class="edit-value-input dense-4" appearance="outline" subscriptSizing="dynamic">
<input
matInput
class="ft-14-400"
[formControl]="editValueControl"
(keydown.enter)="saveEdit(row)"
(keydown.escape)="cancelEdit()"
/>
</mat-form-field>
} @else {
<span class="value-text">{{ row.value }}</span>
}
</td>
</ng-container>

<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="actions-column ft-14-600">
<div class="actions-header">
<span>{{ 'lists.values.actions-header.text' | translate }}</span>
<button
type="button"
mat-icon-button
class="table-action-button export-button"
[disabled]="!rows.length || loading || isDisabled"
[matTooltip]="'lists.values.export.tooltip.text' | translate"
[attr.aria-label]="'lists.values.export.tooltip.text' | translate"
(click)="exportValues()"
>
<span class="material-symbols-outlined">download</span>
</button>
</div>
</th>
<td mat-cell *matCellDef="let row" class="actions-column dense-2">
@if (editingRowId === row.id) {
<button
type="button"
mat-icon-button
class="table-action-button"
[matTooltip]="'lists.values.confirm-edit.tooltip.text' | translate"
[attr.aria-label]="'lists.values.confirm-edit.tooltip.text' | translate"
(click)="saveEdit(row)"
>
<mat-icon>check</mat-icon>
</button>
<button
type="button"
mat-icon-button
class="table-action-button"
[matTooltip]="'lists.values.cancel-edit.tooltip.text' | translate"
[attr.aria-label]="'lists.values.cancel-edit.tooltip.text' | translate"
(click)="cancelEdit()"
>
<mat-icon>close</mat-icon>
</button>
} @else {
<button
type="button"
mat-icon-button
class="table-action-button"
[disabled]="loading || isDisabled"
[matTooltip]="'lists.values.edit.tooltip.text' | translate"
[attr.aria-label]="'lists.values.edit.tooltip.text' | translate"
(click)="startEdit(row)"
>
<mat-icon>edit</mat-icon>
</button>
<button
type="button"
mat-icon-button
class="table-action-button"
[disabled]="loading || isDisabled"
[matTooltip]="'lists.values.delete.tooltip.text' | translate"
[attr.aria-label]="'lists.values.delete.tooltip.text' | translate"
(click)="deleteValue(row)"
>
<mat-icon>delete_outline</mat-icon>
</button>
}
</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns; sticky: true"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
<tr *matNoDataRow>
<td class="ft-14-400" [attr.colspan]="displayedColumns.length">
@if (loading) {
{{ 'lists.values.loading.text' | translate }}
} @else {
{{ (rows.length ? 'lists.values.no-search-results.text' : 'lists.values.no-values.text') | translate }}
}
</td>
</tr>
</table>
</div>
</section>
Loading