@pl4yzonellc/valar-ui 1.0.1 → 1.0.3

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.
package/README.md CHANGED
@@ -22,6 +22,28 @@ After building your library with `ng build valar-ui`, go to the dist folder
22
22
 
23
23
  Run `ng test valar-ui` to execute the unit tests via [Karma](https://karma-runner.github.io).
24
24
 
25
+ ## Feature Flags Tenant Flow
26
+
27
+ `FeatureFlagsModule.forRoot(...)` configures API host/version and root tenant header only.
28
+
29
+ ```ts
30
+ FeatureFlagsModule.forRoot({
31
+ apiBaseUrl: 'https://api.example.com',
32
+ apiVersion: 'v1',
33
+ rootTenantId: environment.tenantId,
34
+ });
35
+ ```
36
+
37
+ Runtime tenant selection is provided by the consuming app via component Input/Output:
38
+
39
+ ```html
40
+ <valar-feature-flags-manager [(tenantId)]="selectedTenantId"></valar-feature-flags-manager>
41
+ ```
42
+
43
+ - `tenantId` input is used for API paths like `/v1/feature-flags/tenant_<tenantId>`.
44
+ - `tenantId` is also included in PUT payload as `tenant_<tenantId>`.
45
+ - `rootTenantId` from `forRoot` is sent as the `x-tenant-id` header.
46
+
25
47
  ## Further help
26
48
 
27
49
  To get more help on the Angular CLI use `ng help` or go check out
@@ -6,7 +6,7 @@ import { CommonModule } from '@angular/common';
6
6
  import * as i2 from '@angular/forms';
7
7
  import { FormsModule } from '@angular/forms';
8
8
  import * as i1 from '@angular/common/http';
9
- import { provideHttpClient } from '@angular/common/http';
9
+ import { HttpHeaders, provideHttpClient } from '@angular/common/http';
10
10
  import { map, Subject, takeUntil, finalize, of, delay, throwError } from 'rxjs';
11
11
 
12
12
  /** Injection token for the library config */
@@ -26,33 +26,54 @@ class FeatureFlagsApiService {
26
26
  this.http = http;
27
27
  const version = config.apiVersion ?? 'v1';
28
28
  this.baseUrl = `${config.apiBaseUrl}/${version}/feature-flags`;
29
+ this.rootTenantId = config.rootTenantId;
30
+ this.tenantHeaderName = config.tenantHeaderName ?? 'x-tenant-id';
29
31
  }
30
- /** GET /v1/feature-flags/:tenantId */
32
+ /** GET /v1/feature-flags/tenant_<tenantId> */
31
33
  getByTenantId(tenantId) {
32
- return this.http.get(`${this.baseUrl}/${encodeURIComponent(tenantId)}`).pipe(map(doc => doc ?? null));
34
+ return this.http.get(this.tenantScopedBaseUrl(tenantId), this.requestOptions())
35
+ .pipe(map(doc => doc ?? null));
33
36
  }
34
37
  /** PUT /v1/feature-flags (merge-upsert) */
35
- upsert(payload) {
36
- return this.http.put(this.baseUrl, payload);
38
+ upsert(tenantId, payload) {
39
+ const requestPayload = {
40
+ ...payload,
41
+ tenantId: this.toTenantToken(tenantId),
42
+ };
43
+ return this.http.put(this.baseUrl, requestPayload, this.requestOptions());
37
44
  }
38
- /** DELETE /v1/feature-flags/:tenantId/:group/:key */
45
+ /** DELETE /v1/feature-flags/tenant_<tenantId>/:group/:key */
39
46
  deleteFlag(tenantId, group, key) {
40
47
  const url = [
41
- this.baseUrl,
42
- encodeURIComponent(tenantId),
48
+ this.tenantScopedBaseUrl(tenantId),
43
49
  encodeURIComponent(group),
44
50
  encodeURIComponent(key),
45
51
  ].join('/');
46
- return this.http.delete(url);
52
+ return this.http.delete(url, this.requestOptions());
47
53
  }
48
- /** DELETE /v1/feature-flags/:tenantId/:group */
54
+ /** DELETE /v1/feature-flags/tenant_<tenantId>/:group */
49
55
  deleteGroup(tenantId, group) {
50
56
  const url = [
51
- this.baseUrl,
52
- encodeURIComponent(tenantId),
57
+ this.tenantScopedBaseUrl(tenantId),
53
58
  encodeURIComponent(group),
54
59
  ].join('/');
55
- return this.http.delete(url);
60
+ return this.http.delete(url, this.requestOptions());
61
+ }
62
+ tenantScopedBaseUrl(tenantId) {
63
+ return `${this.baseUrl}/${encodeURIComponent(this.toTenantToken(tenantId))}`;
64
+ }
65
+ toTenantToken(tenantId) {
66
+ const normalized = tenantId.trim();
67
+ if (normalized.startsWith('tenant_'))
68
+ return normalized;
69
+ return `tenant_${normalized}`;
70
+ }
71
+ requestOptions() {
72
+ return {
73
+ headers: new HttpHeaders({
74
+ [this.tenantHeaderName]: this.rootTenantId,
75
+ }),
76
+ };
56
77
  }
57
78
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsApiService, deps: [{ token: i1.HttpClient }, { token: FEATURE_FLAGS_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); }
58
79
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsApiService }); }
@@ -68,6 +89,7 @@ class FlagGroupCardComponent {
68
89
  constructor() {
69
90
  this.groupName = '';
70
91
  this.flags = [];
92
+ this.isNewGroup = false;
71
93
  this.dirtyKeys = new Set();
72
94
  this.isAddingFlag = false;
73
95
  this.newFlagKey = '';
@@ -178,15 +200,17 @@ class FlagGroupCardComponent {
178
200
  }
179
201
  }
180
202
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FlagGroupCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
181
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.5", type: FlagGroupCardComponent, isStandalone: false, selector: "valar-flag-group-card", inputs: { groupName: "groupName", flags: "flags", dirtyKeys: "dirtyKeys", isAddingFlag: "isAddingFlag", newFlagKey: "newFlagKey", newFlagValue: "newFlagValue", newFlagType: "newFlagType" }, outputs: { flagChanged: "flagChanged", revertFlag: "revertFlag", addFlagRequest: "addFlagRequest", cancelAddFlag: "cancelAddFlag", confirmAddFlag: "confirmAddFlag", newFlagKeyChange: "newFlagKeyChange", newFlagValueChange: "newFlagValueChange", newFlagTypeChange: "newFlagTypeChange", deleteFlagRequest: "deleteFlagRequest", deleteGroupRequest: "deleteGroupRequest" }, ngImport: i0, template: "<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.card-header{background-color:#f8f9fa}table code{font-size:.85em;color:#6f42c1}.badge{font-size:.75em}.row-dirty{background-color:#fff8e1!important;border-left:3px solid #ffc107}.row-dirty:hover{background-color:#fff3cd!important}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
203
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.5", type: FlagGroupCardComponent, isStandalone: false, selector: "valar-flag-group-card", inputs: { groupName: "groupName", flags: "flags", isNewGroup: "isNewGroup", dirtyKeys: "dirtyKeys", isAddingFlag: "isAddingFlag", newFlagKey: "newFlagKey", newFlagValue: "newFlagValue", newFlagType: "newFlagType" }, outputs: { flagChanged: "flagChanged", revertFlag: "revertFlag", addFlagRequest: "addFlagRequest", cancelAddFlag: "cancelAddFlag", confirmAddFlag: "confirmAddFlag", newFlagKeyChange: "newFlagKeyChange", newFlagValueChange: "newFlagValueChange", newFlagTypeChange: "newFlagTypeChange", deleteFlagRequest: "deleteFlagRequest", deleteGroupRequest: "deleteGroupRequest" }, ngImport: i0, template: "<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span *ngIf=\"isNewGroup\" class=\"badge bg-success ms-2\">NEW</span>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.card-header{background-color:#f8f9fa}table code{font-size:.85em;color:#6f42c1}.badge{font-size:.75em}.row-dirty{background-color:#fff8e1!important;border-left:3px solid #ffc107}.row-dirty:hover{background-color:#fff3cd!important}\n"], dependencies: [{ kind: "directive", type: i1$1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }] }); }
182
204
  }
183
205
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FlagGroupCardComponent, decorators: [{
184
206
  type: Component,
185
- args: [{ selector: 'valar-flag-group-card', standalone: false, template: "<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.card-header{background-color:#f8f9fa}table code{font-size:.85em;color:#6f42c1}.badge{font-size:.75em}.row-dirty{background-color:#fff8e1!important;border-left:3px solid #ffc107}.row-dirty:hover{background-color:#fff3cd!important}\n"] }]
207
+ args: [{ selector: 'valar-flag-group-card', standalone: false, template: "<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span *ngIf=\"isNewGroup\" class=\"badge bg-success ms-2\">NEW</span>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n", styles: [":host{display:block}.card-header{background-color:#f8f9fa}table code{font-size:.85em;color:#6f42c1}.badge{font-size:.75em}.row-dirty{background-color:#fff8e1!important;border-left:3px solid #ffc107}.row-dirty:hover{background-color:#fff3cd!important}\n"] }]
186
208
  }], propDecorators: { groupName: [{
187
209
  type: Input
188
210
  }], flags: [{
189
211
  type: Input
212
+ }], isNewGroup: [{
213
+ type: Input
190
214
  }], dirtyKeys: [{
191
215
  type: Input
192
216
  }], isAddingFlag: [{
@@ -258,6 +282,7 @@ class FeatureFlagsManagerComponent {
258
282
  constructor(api) {
259
283
  this.api = api;
260
284
  this.tenantId = '';
285
+ this.tenantIdChange = new EventEmitter();
261
286
  this.groups = [];
262
287
  this.loading = false;
263
288
  this.error = null;
@@ -278,8 +303,23 @@ class FeatureFlagsManagerComponent {
278
303
  */
279
304
  this.pendingChanges = new Map();
280
305
  this.showSummaryModal = false;
306
+ this.newlyCreatedGroups = new Set();
281
307
  this.destroy$ = new Subject();
282
308
  }
309
+ ngOnChanges(changes) {
310
+ if (!changes['tenantId'])
311
+ return;
312
+ const id = this.tenantId.trim();
313
+ if (!id) {
314
+ this.groups = [];
315
+ this.loaded = false;
316
+ this.pendingChanges.clear();
317
+ this.clearMessages();
318
+ return;
319
+ }
320
+ this.tenantIdChange.emit(id);
321
+ this.loadFlags();
322
+ }
283
323
  ngOnDestroy() {
284
324
  this.destroy$.next();
285
325
  this.destroy$.complete();
@@ -308,7 +348,7 @@ class FeatureFlagsManagerComponent {
308
348
  loadFlags() {
309
349
  const id = this.tenantId.trim();
310
350
  if (!id) {
311
- this.error = 'Please enter a Tenant ID.';
351
+ this.error = 'Tenant ID is required.';
312
352
  return;
313
353
  }
314
354
  this.clearMessages();
@@ -319,6 +359,7 @@ class FeatureFlagsManagerComponent {
319
359
  .subscribe({
320
360
  next: (doc) => {
321
361
  this.parseDocument(doc);
362
+ this.reconcileNewGroupBadges();
322
363
  this.loaded = true;
323
364
  if (!doc || this.groups.length === 0) {
324
365
  this.successMsg = 'No flags found for this tenant yet. Start adding some!';
@@ -371,21 +412,22 @@ class FeatureFlagsManagerComponent {
371
412
  this.showSummaryModal = false;
372
413
  this.clearMessages();
373
414
  this.loading = true;
374
- // Build the upsert payload: { tenantId, group1: { k1: v1 }, group2: { k2: v2 } }
375
- const payload = { tenantId: this.tenantId };
415
+ // Build the upsert payload: { group1: { k1: v1 }, group2: { k2: v2 } }
416
+ const payload = {};
376
417
  for (const change of this.pendingChanges.values()) {
377
418
  if (!payload[change.group]) {
378
419
  payload[change.group] = {};
379
420
  }
380
421
  payload[change.group][change.key] = change.newValue;
381
422
  }
382
- this.api.upsert(payload)
423
+ this.api.upsert(this.tenantId, payload)
383
424
  .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))
384
425
  .subscribe({
385
426
  next: (doc) => {
386
427
  const count = this.pendingChanges.size;
387
428
  this.pendingChanges.clear();
388
429
  this.parseDocument(doc);
430
+ this.reconcileNewGroupBadges();
389
431
  this.successMsg = `${count} flag${count !== 1 ? 's' : ''} updated successfully!`;
390
432
  },
391
433
  error: (err) => {
@@ -398,9 +440,31 @@ class FeatureFlagsManagerComponent {
398
440
  const name = this.newGroupName.trim();
399
441
  if (!name)
400
442
  return;
401
- this.showAddGroup = false;
402
- this.addingFlagToGroup = name;
403
- this.newGroupName = '';
443
+ this.clearMessages();
444
+ if (this.groups.some(g => g.name === name)) {
445
+ this.showAddGroup = false;
446
+ this.addingFlagToGroup = name;
447
+ this.newGroupName = '';
448
+ return;
449
+ }
450
+ this.loading = true;
451
+ this.api.upsert(this.tenantId, { [name]: {} })
452
+ .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))
453
+ .subscribe({
454
+ next: (doc) => {
455
+ this.parseDocument(doc);
456
+ this.ensureGroupVisible(name);
457
+ this.newlyCreatedGroups.add(name);
458
+ this.loaded = true;
459
+ this.successMsg = `Group "${name}" created.`;
460
+ this.showAddGroup = false;
461
+ this.addingFlagToGroup = name;
462
+ this.newGroupName = '';
463
+ },
464
+ error: (err) => {
465
+ this.error = err.error?.message ?? 'Failed to create group.';
466
+ },
467
+ });
404
468
  }
405
469
  /** Stage a new flag as a pending change */
406
470
  confirmAddFlag(groupName) {
@@ -437,6 +501,7 @@ class FeatureFlagsManagerComponent {
437
501
  .subscribe({
438
502
  next: (doc) => {
439
503
  this.parseDocument(doc);
504
+ this.reconcileNewGroupBadges();
440
505
  this.successMsg = `Flag "${groupName}.${flagKey}" deleted.`;
441
506
  },
442
507
  error: (err) => this.error = err.error?.message ?? 'Failed to delete flag.',
@@ -456,6 +521,8 @@ class FeatureFlagsManagerComponent {
456
521
  .subscribe({
457
522
  next: (doc) => {
458
523
  this.parseDocument(doc);
524
+ this.newlyCreatedGroups.delete(groupName);
525
+ this.reconcileNewGroupBadges();
459
526
  this.successMsg = `Group "${groupName}" deleted.`;
460
527
  },
461
528
  error: (err) => this.error = err.error?.message ?? 'Failed to delete group.',
@@ -465,6 +532,12 @@ class FeatureFlagsManagerComponent {
465
532
  trackGroup(_index, group) {
466
533
  return group.name;
467
534
  }
535
+ isNewGroup(group) {
536
+ if (!this.newlyCreatedGroups.has(group.name))
537
+ return false;
538
+ const hasStagedFirstFlag = this.dirtyKeysForGroup(group.name).size > 0;
539
+ return group.flags.length === 0 && !hasStagedFirstFlag;
540
+ }
468
541
  parseDocument(doc) {
469
542
  this.groups = [];
470
543
  if (!doc)
@@ -508,13 +581,34 @@ class FeatureFlagsManagerComponent {
508
581
  valuesEqual(a, b) {
509
582
  return a === b;
510
583
  }
584
+ ensureGroupVisible(groupName) {
585
+ if (this.groups.some(g => g.name === groupName))
586
+ return;
587
+ this.groups.push({ name: groupName, flags: [] });
588
+ this.groups.sort((a, b) => a.name.localeCompare(b.name));
589
+ }
590
+ reconcileNewGroupBadges() {
591
+ const existingGroupNames = new Set(this.groups.map(g => g.name));
592
+ for (const groupName of Array.from(this.newlyCreatedGroups)) {
593
+ const group = this.groups.find(g => g.name === groupName);
594
+ const hasGroup = existingGroupNames.has(groupName);
595
+ const hasFlags = !!group && group.flags.length > 0;
596
+ if (!hasGroup || hasFlags) {
597
+ this.newlyCreatedGroups.delete(groupName);
598
+ }
599
+ }
600
+ }
511
601
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsManagerComponent, deps: [{ token: FeatureFlagsApiService }], target: i0.ɵɵFactoryTarget.Component }); }
512
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.5", type: FeatureFlagsManagerComponent, isStandalone: false, selector: "valar-feature-flags-manager", ngImport: i0, template: "<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n\n <!-- Tenant ID Input -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <label for=\"tenantIdInput\" class=\"form-label fw-semibold\">Tenant ID</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\" id=\"tenant-addon\">\n <i class=\"bi bi-building\" aria-hidden=\"true\"></i>\n </span>\n <input\n id=\"tenantIdInput\"\n type=\"text\"\n class=\"form-control\"\n placeholder=\"e.g. tenant_acme\"\n [(ngModel)]=\"tenantId\"\n (keyup.enter)=\"loadFlags()\"\n [attr.aria-describedby]=\"'tenant-addon'\"\n aria-label=\"Tenant ID\" />\n <button\n class=\"btn btn-primary\"\n [disabled]=\"loading || !tenantId.trim()\"\n (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading\u2026' : 'Load Flags' }}\n </button>\n </div>\n <div class=\"form-text\">Enter a tenant ID and press Enter or click Load Flags.</div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"!newGroupName.trim()\">\n Next\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n", styles: [":host{display:block}.bi{vertical-align:-.125em}.action-bar{position:fixed;bottom:0;left:0;right:0;z-index:1030;background:linear-gradient(135deg,#343a40,#495057);padding:.75rem 1rem;box-shadow:0 -4px 12px #00000040;animation:slideUp .25s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FlagGroupCardComponent, selector: "valar-flag-group-card", inputs: ["groupName", "flags", "dirtyKeys", "isAddingFlag", "newFlagKey", "newFlagValue", "newFlagType"], outputs: ["flagChanged", "revertFlag", "addFlagRequest", "cancelAddFlag", "confirmAddFlag", "newFlagKeyChange", "newFlagValueChange", "newFlagTypeChange", "deleteFlagRequest", "deleteGroupRequest"] }, { kind: "component", type: ChangesSummaryModalComponent, selector: "valar-changes-summary-modal", inputs: ["changes", "tenantId"], outputs: ["confirm", "dismiss"] }] }); }
602
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.5", type: FeatureFlagsManagerComponent, isStandalone: false, selector: "valar-feature-flags-manager", inputs: { tenantId: "tenantId" }, outputs: { tenantIdChange: "tenantIdChange" }, usesOnChanges: true, ngImport: i0, template: "<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n <!-- Tenant Context -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <label class=\"form-label fw-semibold mb-1\">Tenant ID</label>\n <div><code>{{ tenantId }}</code></div>\n </div>\n <button class=\"btn btn-primary\" [disabled]=\"loading\" (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading...' : 'Reload Flags' }}\n </button>\n </div>\n <div class=\"form-text\">\n Tenant is provided by parent component input, while <code>x-tenant-id</code> comes from module config.\n </div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"loading || !newGroupName.trim()\">\n {{ loading ? 'Creating...' : 'Create Group' }}\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [isNewGroup]=\"isNewGroup(group)\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n\n", styles: [":host{display:block}.bi{vertical-align:-.125em}.action-bar{position:fixed;bottom:0;left:0;right:0;z-index:1030;background:linear-gradient(135deg,#343a40,#495057);padding:.75rem 1rem;box-shadow:0 -4px 12px #00000040;animation:slideUp .25s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}\n"], dependencies: [{ kind: "directive", type: i1$1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: FlagGroupCardComponent, selector: "valar-flag-group-card", inputs: ["groupName", "flags", "isNewGroup", "dirtyKeys", "isAddingFlag", "newFlagKey", "newFlagValue", "newFlagType"], outputs: ["flagChanged", "revertFlag", "addFlagRequest", "cancelAddFlag", "confirmAddFlag", "newFlagKeyChange", "newFlagValueChange", "newFlagTypeChange", "deleteFlagRequest", "deleteGroupRequest"] }, { kind: "component", type: ChangesSummaryModalComponent, selector: "valar-changes-summary-modal", inputs: ["changes", "tenantId"], outputs: ["confirm", "dismiss"] }] }); }
513
603
  }
514
604
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsManagerComponent, decorators: [{
515
605
  type: Component,
516
- args: [{ selector: 'valar-feature-flags-manager', standalone: false, template: "<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n\n <!-- Tenant ID Input -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <label for=\"tenantIdInput\" class=\"form-label fw-semibold\">Tenant ID</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\" id=\"tenant-addon\">\n <i class=\"bi bi-building\" aria-hidden=\"true\"></i>\n </span>\n <input\n id=\"tenantIdInput\"\n type=\"text\"\n class=\"form-control\"\n placeholder=\"e.g. tenant_acme\"\n [(ngModel)]=\"tenantId\"\n (keyup.enter)=\"loadFlags()\"\n [attr.aria-describedby]=\"'tenant-addon'\"\n aria-label=\"Tenant ID\" />\n <button\n class=\"btn btn-primary\"\n [disabled]=\"loading || !tenantId.trim()\"\n (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading\u2026' : 'Load Flags' }}\n </button>\n </div>\n <div class=\"form-text\">Enter a tenant ID and press Enter or click Load Flags.</div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"!newGroupName.trim()\">\n Next\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n", styles: [":host{display:block}.bi{vertical-align:-.125em}.action-bar{position:fixed;bottom:0;left:0;right:0;z-index:1030;background:linear-gradient(135deg,#343a40,#495057);padding:.75rem 1rem;box-shadow:0 -4px 12px #00000040;animation:slideUp .25s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}\n"] }]
517
- }], ctorParameters: () => [{ type: FeatureFlagsApiService }] });
606
+ args: [{ selector: 'valar-feature-flags-manager', standalone: false, template: "<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n <!-- Tenant Context -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <label class=\"form-label fw-semibold mb-1\">Tenant ID</label>\n <div><code>{{ tenantId }}</code></div>\n </div>\n <button class=\"btn btn-primary\" [disabled]=\"loading\" (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading...' : 'Reload Flags' }}\n </button>\n </div>\n <div class=\"form-text\">\n Tenant is provided by parent component input, while <code>x-tenant-id</code> comes from module config.\n </div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"loading || !newGroupName.trim()\">\n {{ loading ? 'Creating...' : 'Create Group' }}\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [isNewGroup]=\"isNewGroup(group)\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n\n", styles: [":host{display:block}.bi{vertical-align:-.125em}.action-bar{position:fixed;bottom:0;left:0;right:0;z-index:1030;background:linear-gradient(135deg,#343a40,#495057);padding:.75rem 1rem;box-shadow:0 -4px 12px #00000040;animation:slideUp .25s ease-out}@keyframes slideUp{0%{transform:translateY(100%);opacity:0}to{transform:translateY(0);opacity:1}}\n"] }]
607
+ }], ctorParameters: () => [{ type: FeatureFlagsApiService }], propDecorators: { tenantId: [{
608
+ type: Input
609
+ }], tenantIdChange: [{
610
+ type: Output
611
+ }] } });
518
612
 
519
613
  /**
520
614
  * In-memory mock of FeatureFlagsApiService.
@@ -530,21 +624,18 @@ class MockFeatureFlagsApiService {
530
624
  }
531
625
  // ────────────────────────── Public API (same shape as real service) ──
532
626
  getByTenantId(tenantId) {
533
- const doc = this.store.get(tenantId) ?? null;
627
+ const doc = this.store.get(this.toTenantToken(tenantId)) ?? null;
534
628
  return of(this.clone(doc)).pipe(delay(250));
535
629
  }
536
- upsert(payload) {
537
- const { tenantId, ...groups } = payload;
538
- if (!tenantId?.trim()) {
539
- return throwError(() => ({ error: { message: 'tenantId is required' } }));
540
- }
541
- let doc = this.store.get(tenantId);
630
+ upsert(tenantId, payload) {
631
+ const tenantToken = this.toTenantToken(tenantId);
632
+ let doc = this.store.get(tenantToken);
542
633
  if (!doc) {
543
- doc = this.newDoc(tenantId);
544
- this.store.set(tenantId, doc);
634
+ doc = this.newDoc(tenantToken);
635
+ this.store.set(tenantToken, doc);
545
636
  }
546
637
  // Merge groups via dot-path (mirrors the backend's $set behavior)
547
- for (const [groupName, groupFlags] of Object.entries(groups)) {
638
+ for (const [groupName, groupFlags] of Object.entries(payload)) {
548
639
  if (RESERVED_KEYS.has(groupName))
549
640
  continue;
550
641
  const existing = doc[groupName] ?? {};
@@ -554,10 +645,11 @@ class MockFeatureFlagsApiService {
554
645
  return of(this.clone(doc)).pipe(delay(200));
555
646
  }
556
647
  deleteFlag(tenantId, group, key) {
557
- const doc = this.store.get(tenantId);
648
+ const tenantToken = this.toTenantToken(tenantId);
649
+ const doc = this.store.get(tenantToken);
558
650
  if (!doc) {
559
651
  return throwError(() => ({
560
- error: { message: `No feature flags found for tenantId='${tenantId}'` },
652
+ error: { message: `No feature flags found for tenantId='${tenantToken}'` },
561
653
  }));
562
654
  }
563
655
  const g = doc[group];
@@ -568,10 +660,11 @@ class MockFeatureFlagsApiService {
568
660
  return of(this.clone(doc)).pipe(delay(200));
569
661
  }
570
662
  deleteGroup(tenantId, group) {
571
- const doc = this.store.get(tenantId);
663
+ const tenantToken = this.toTenantToken(tenantId);
664
+ const doc = this.store.get(tenantToken);
572
665
  if (!doc) {
573
666
  return throwError(() => ({
574
- error: { message: `No feature flags found for tenantId='${tenantId}'` },
667
+ error: { message: `No feature flags found for tenantId='${tenantToken}'` },
575
668
  }));
576
669
  }
577
670
  delete doc[group];
@@ -658,6 +751,12 @@ class MockFeatureFlagsApiService {
658
751
  const now = new Date().toISOString();
659
752
  return { tenantId, createdAt: now, updatedAt: now };
660
753
  }
754
+ toTenantToken(tenantId) {
755
+ const normalized = tenantId.trim();
756
+ if (normalized.startsWith('tenant_'))
757
+ return normalized;
758
+ return `tenant_${normalized}`;
759
+ }
661
760
  /** Deep-clone to avoid leaking mutable references */
662
761
  clone(obj) {
663
762
  return JSON.parse(JSON.stringify(obj));
@@ -678,7 +777,12 @@ class FeatureFlagsModule {
678
777
  * FeatureFlagsModule.forRoot({
679
778
  * apiBaseUrl: 'http://localhost:3000',
680
779
  * apiVersion: 'v1',
780
+ * rootTenantId: 'root001', // sent in x-tenant-id header
681
781
  * })
782
+ *
783
+ * <valar-feature-flags-manager
784
+ * [(tenantId)]="selectedTenantId">
785
+ * </valar-feature-flags-manager>
682
786
  */
683
787
  static forRoot(config) {
684
788
  return {
@@ -1 +1 @@
1
- {"version":3,"file":"pl4yzonellc-valar-ui.mjs","sources":["../../../projects/valar-ui/src/lib/feature-flags/models/feature-flags.model.ts","../../../projects/valar-ui/src/lib/feature-flags/services/feature-flags-api.service.ts","../../../projects/valar-ui/src/lib/feature-flags/components/flag-group-card/flag-group-card.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/flag-group-card/flag-group-card.component.html","../../../projects/valar-ui/src/lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component.html","../../../projects/valar-ui/src/lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component.html","../../../projects/valar-ui/src/lib/feature-flags/services/mock-feature-flags-api.service.ts","../../../projects/valar-ui/src/lib/feature-flags/feature-flags.module.ts","../../../projects/valar-ui/src/public-api.ts","../../../projects/valar-ui/src/pl4yzonellc-valar-ui.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\n/**\n * Primitive types allowed as feature flag values.\n * Mirrors the backend's FlagPrimitive type.\n */\nexport type FlagPrimitive = boolean | string | number | null;\n\n/**\n * A group of feature flags: key → primitive value.\n */\nexport type FlagGroup = Record<string, FlagPrimitive>;\n\n/**\n * The full feature flags document for a tenant.\n * Dynamic group names sit at the top level alongside metadata.\n */\nexport interface FeatureFlagsDocument {\n _id?: string;\n tenantId: string;\n createdAt?: string;\n updatedAt?: string;\n [groupName: string]: FlagGroup | string | undefined;\n}\n\n/**\n * Shape of the upsert request body sent to the backend.\n * tenantId + any number of group objects.\n */\nexport interface UpsertFlagsPayload {\n tenantId: string;\n [groupName: string]: FlagGroup | string;\n}\n\n/**\n * Configuration for the FeatureFlags library module.\n * The consuming app provides this via forRoot().\n */\nexport interface FeatureFlagsConfig {\n /** Base URL of the Valar API, e.g. 'http://localhost:3000' */\n apiBaseUrl: string;\n /** API version prefix, defaults to 'v1' */\n apiVersion?: string;\n}\n\n/** Injection token for the library config */\nexport const FEATURE_FLAGS_CONFIG = new InjectionToken<FeatureFlagsConfig>(\n 'FEATURE_FLAGS_CONFIG'\n);\n\n/** Reserved keys that are NOT flag groups */\nexport const RESERVED_KEYS = new Set([\n '_id', 'tenantId', 'createdAt', 'updatedAt', '__v', 'id',\n]);\n\n/** A single staged change for the batch-edit workflow */\nexport interface PendingChange {\n group: string;\n key: string;\n oldValue: FlagPrimitive;\n newValue: FlagPrimitive;\n isNew?: boolean; // true if this is a brand-new flag\n}\n","import { HttpClient } from '@angular/common/http';\nimport { Inject, Injectable } from '@angular/core';\nimport { Observable, map } from 'rxjs';\nimport {\n FEATURE_FLAGS_CONFIG,\n FeatureFlagsConfig,\n FeatureFlagsDocument,\n UpsertFlagsPayload,\n} from '../models';\n\n/**\n * HTTP service that talks to the Valar feature-flags backend.\n * Base URL is injected via FEATURE_FLAGS_CONFIG so the consuming\n * app controls where requests go.\n */\n@Injectable()\nexport class FeatureFlagsApiService {\n private readonly baseUrl: string;\n\n constructor(\n private readonly http: HttpClient,\n @Inject(FEATURE_FLAGS_CONFIG) config: FeatureFlagsConfig,\n ) {\n const version = config.apiVersion ?? 'v1';\n this.baseUrl = `${config.apiBaseUrl}/${version}/feature-flags`;\n }\n\n /** GET /v1/feature-flags/:tenantId */\n getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null> {\n return this.http.get<FeatureFlagsDocument | null>(\n `${this.baseUrl}/${encodeURIComponent(tenantId)}`\n ).pipe(map(doc => doc ?? null));\n }\n\n /** PUT /v1/feature-flags (merge-upsert) */\n upsert(payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument> {\n return this.http.put<FeatureFlagsDocument>(this.baseUrl, payload);\n }\n\n /** DELETE /v1/feature-flags/:tenantId/:group/:key */\n deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument> {\n const url = [\n this.baseUrl,\n encodeURIComponent(tenantId),\n encodeURIComponent(group),\n encodeURIComponent(key),\n ].join('/');\n return this.http.delete<FeatureFlagsDocument>(url);\n }\n\n /** DELETE /v1/feature-flags/:tenantId/:group */\n deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument> {\n const url = [\n this.baseUrl,\n encodeURIComponent(tenantId),\n encodeURIComponent(group),\n ].join('/');\n return this.http.delete<FeatureFlagsDocument>(url);\n }\n}\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { FlagPrimitive } from '../../models';\n\nexport interface FlagEntry {\n key: string;\n value: FlagPrimitive;\n}\n\nexport interface FlagChangedEvent {\n key: string;\n oldValue: FlagPrimitive;\n newValue: FlagPrimitive;\n}\n\n@Component({\n selector: 'valar-flag-group-card',\n templateUrl: './flag-group-card.component.html',\n styleUrls: ['./flag-group-card.component.css'],\n standalone: false,\n})\nexport class FlagGroupCardComponent {\n @Input() groupName = '';\n @Input() flags: FlagEntry[] = [];\n @Input() dirtyKeys: Set<string> = new Set();\n @Input() isAddingFlag = false;\n @Input() newFlagKey = '';\n @Input() newFlagValue = '';\n @Input() newFlagType: 'string' | 'boolean' | 'number' = 'string';\n\n @Output() flagChanged = new EventEmitter<FlagChangedEvent>();\n @Output() revertFlag = new EventEmitter<string>();\n @Output() addFlagRequest = new EventEmitter<void>();\n @Output() cancelAddFlag = new EventEmitter<void>();\n @Output() confirmAddFlag = new EventEmitter<void>();\n @Output() newFlagKeyChange = new EventEmitter<string>();\n @Output() newFlagValueChange = new EventEmitter<string>();\n @Output() newFlagTypeChange = new EventEmitter<'string' | 'boolean' | 'number'>();\n @Output() deleteFlagRequest = new EventEmitter<string>();\n @Output() deleteGroupRequest = new EventEmitter<void>();\n\n collapsed = false;\n confirmDeleteGroup = false;\n confirmDeleteKey: string | null = null;\n\n /** Which flag key is currently in inline-edit mode */\n editingKey: string | null = null;\n editValue = '';\n editType: 'string' | 'boolean' | 'number' = 'string';\n\n isDirty(key: string): boolean {\n return this.dirtyKeys.has(key);\n }\n\n /** Enter inline-edit mode for a flag */\n startEdit(flag: FlagEntry): void {\n this.editingKey = flag.key;\n this.editType = this.detectType(flag.value);\n this.editValue = flag.value === null ? '' : String(flag.value);\n }\n\n /** Commit the inline edit — emits change to parent, stays in view mode */\n commitEdit(flag: FlagEntry): void {\n const newValue = this.coerceValue(this.editValue, this.editType);\n this.flagChanged.emit({ key: flag.key, oldValue: flag.value, newValue });\n this.editingKey = null;\n }\n\n /** Cancel inline edit without staging anything */\n cancelEdit(): void {\n this.editingKey = null;\n }\n\n displayValue(value: FlagPrimitive): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return String(value);\n }\n\n typeBadgeClass(value: FlagPrimitive): string {\n if (typeof value === 'boolean') return 'bg-info text-dark';\n if (typeof value === 'number') return 'bg-warning text-dark';\n if (value === null) return 'bg-secondary';\n return 'bg-primary';\n }\n\n typeLabel(value: FlagPrimitive): string {\n if (value === null) return 'null';\n return typeof value;\n }\n\n isBooleanFlag(value: FlagPrimitive): boolean {\n return typeof value === 'boolean';\n }\n\n trackFlag(_index: number, flag: FlagEntry): string {\n return flag.key;\n }\n\n onDeleteFlag(key: string): void {\n if (this.confirmDeleteKey === key) {\n this.deleteFlagRequest.emit(key);\n this.confirmDeleteKey = null;\n } else {\n this.confirmDeleteKey = key;\n }\n }\n\n onDeleteGroup(): void {\n if (this.confirmDeleteGroup) {\n this.deleteGroupRequest.emit();\n this.confirmDeleteGroup = false;\n } else {\n this.confirmDeleteGroup = true;\n }\n }\n\n private detectType(value: FlagPrimitive): 'string' | 'boolean' | 'number' {\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return 'number';\n return 'string';\n }\n\n private coerceValue(raw: string, type: 'string' | 'boolean' | 'number'): FlagPrimitive {\n const trimmed = raw.trim();\n if (trimmed === '' || trimmed.toLowerCase() === 'null') return null;\n switch (type) {\n case 'boolean':\n return trimmed.toLowerCase() === 'true';\n case 'number': {\n const n = Number(trimmed);\n return isNaN(n) ? trimmed : n;\n }\n default:\n return trimmed;\n }\n }\n}\n","<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { PendingChange, FlagPrimitive } from '../../models';\n\n@Component({\n selector: 'valar-changes-summary-modal',\n templateUrl: './changes-summary-modal.component.html',\n styleUrls: ['./changes-summary-modal.component.css'],\n standalone: false,\n})\nexport class ChangesSummaryModalComponent {\n @Input() changes: PendingChange[] = [];\n @Input() tenantId = '';\n @Output() confirm = new EventEmitter<void>();\n @Output() dismiss = new EventEmitter<void>();\n\n displayValue(value: FlagPrimitive): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return String(value);\n }\n\n typeLabel(value: FlagPrimitive): string {\n if (value === null) return 'null';\n return typeof value;\n }\n}\n","<!-- Backdrop -->\n<div class=\"modal-backdrop fade show\" (click)=\"dismiss.emit()\"></div>\n\n<!-- Modal -->\n<div class=\"modal fade show d-block\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"changesSummaryTitle\" aria-modal=\"true\">\n <div class=\"modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable\">\n <div class=\"modal-content\">\n <!-- Header -->\n <div class=\"modal-header bg-primary text-white\">\n <h5 class=\"modal-title\" id=\"changesSummaryTitle\">\n <i class=\"bi bi-clipboard-check me-2\" aria-hidden=\"true\"></i>\n Review Changes\n </h5>\n <button type=\"button\" class=\"btn-close btn-close-white\" aria-label=\"Close\" (click)=\"dismiss.emit()\"></button>\n </div>\n\n <!-- Body -->\n <div class=\"modal-body\">\n <p class=\"text-muted mb-3\">\n You're about to update <strong>{{ changes.length }}</strong>\n flag{{ changes.length !== 1 ? 's' : '' }} for tenant\n <code>{{ tenantId }}</code>.\n </p>\n\n <div class=\"table-responsive\">\n <table class=\"table table-sm table-bordered mb-0\" aria-label=\"Pending changes summary\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\">Group</th>\n <th scope=\"col\">Key</th>\n <th scope=\"col\">Old Value</th>\n <th scope=\"col\" class=\"text-center\" style=\"width: 40px\">\n <i class=\"bi bi-arrow-right\" aria-hidden=\"true\"></i>\n </th>\n <th scope=\"col\">New Value</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let change of changes\">\n <td>\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ change.group }}\n </td>\n <td><code>{{ change.key }}</code></td>\n <td>\n <span class=\"text-danger text-decoration-line-through\">\n {{ displayValue(change.oldValue) }}\n </span>\n <span class=\"badge bg-secondary ms-1\">{{ typeLabel(change.oldValue) }}</span>\n <span *ngIf=\"change.isNew\" class=\"badge bg-success ms-1\">NEW</span>\n </td>\n <td class=\"text-center text-muted\">\n <i class=\"bi bi-arrow-right\" aria-hidden=\"true\"></i>\n </td>\n <td>\n <span class=\"fw-semibold text-success\">\n {{ displayValue(change.newValue) }}\n </span>\n <span class=\"badge bg-secondary ms-1\">{{ typeLabel(change.newValue) }}</span>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"dismiss.emit()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Cancel\n </button>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"confirm.emit()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Looks Good\n </button>\n </div>\n </div>\n </div>\n</div>\n","import { Component, OnDestroy } from '@angular/core';\nimport { Subject, takeUntil, finalize } from 'rxjs';\nimport { FeatureFlagsApiService } from '../../services/feature-flags-api.service';\nimport {\n FeatureFlagsDocument,\n FlagGroup,\n FlagPrimitive,\n PendingChange,\n RESERVED_KEYS,\n} from '../../models';\n\nexport interface ParsedGroup {\n name: string;\n flags: { key: string; value: FlagPrimitive }[];\n}\n\n@Component({\n selector: 'valar-feature-flags-manager',\n templateUrl: './feature-flags-manager.component.html',\n styleUrls: ['./feature-flags-manager.component.css'],\n standalone: false,\n})\nexport class FeatureFlagsManagerComponent implements OnDestroy {\n tenantId = '';\n groups: ParsedGroup[] = [];\n loading = false;\n error: string | null = null;\n successMsg: string | null = null;\n loaded = false;\n\n // Add-group form\n showAddGroup = false;\n newGroupName = '';\n\n // Add-flag form (per group)\n addingFlagToGroup: string | null = null;\n newFlagKey = '';\n newFlagValue = '';\n newFlagType: 'string' | 'boolean' | 'number' = 'string';\n\n // ── Batch-edit state ──────────────────────────────────────────────\n /**\n * Pending changes keyed by \"group.key\".\n * Stores old/new values so we can display a diff summary.\n */\n pendingChanges = new Map<string, PendingChange>();\n showSummaryModal = false;\n\n private readonly destroy$ = new Subject<void>();\n\n constructor(private readonly api: FeatureFlagsApiService) {}\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n // ── Computed helpers ──────────────────────────────────────────────\n\n get hasPendingChanges(): boolean {\n return this.pendingChanges.size > 0;\n }\n\n get pendingChangeCount(): number {\n return this.pendingChanges.size;\n }\n\n get pendingChangesList(): PendingChange[] {\n return Array.from(this.pendingChanges.values());\n }\n\n /** Returns the set of dirty keys for a given group */\n dirtyKeysForGroup(groupName: string): Set<string> {\n const keys = new Set<string>();\n for (const [compositeKey] of this.pendingChanges) {\n const [g, k] = this.splitCompositeKey(compositeKey);\n if (g === groupName) keys.add(k);\n }\n return keys;\n }\n\n // ── Load ──────────────────────────────────────────────────────────\n\n loadFlags(): void {\n const id = this.tenantId.trim();\n if (!id) {\n this.error = 'Please enter a Tenant ID.';\n return;\n }\n this.clearMessages();\n this.pendingChanges.clear();\n this.loading = true;\n this.api.getByTenantId(id)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.loaded = true;\n if (!doc || this.groups.length === 0) {\n this.successMsg = 'No flags found for this tenant yet. Start adding some!';\n }\n },\n error: (err) => {\n this.error = err.error?.message ?? 'Failed to load flags.';\n },\n });\n }\n\n // ── Stage / unstage edits ─────────────────────────────────────────\n\n /** Called when a flag value is changed inline */\n onFlagChanged(groupName: string, event: { key: string; oldValue: FlagPrimitive; newValue: FlagPrimitive }): void {\n const compositeKey = this.makeCompositeKey(groupName, event.key);\n\n // If there's already a pending change, preserve the ORIGINAL old value\n const existing = this.pendingChanges.get(compositeKey);\n const originalOld = existing ? existing.oldValue : event.oldValue;\n\n // If user reverted back to original value, unstage it\n if (this.valuesEqual(originalOld, event.newValue)) {\n this.pendingChanges.delete(compositeKey);\n } else {\n this.pendingChanges.set(compositeKey, {\n group: groupName,\n key: event.key,\n oldValue: originalOld,\n newValue: event.newValue,\n isNew: existing?.isNew ?? false,\n });\n }\n }\n\n /** Revert a single flag's pending change */\n revertFlag(groupName: string, key: string): void {\n const compositeKey = this.makeCompositeKey(groupName, key);\n this.pendingChanges.delete(compositeKey);\n }\n\n /** Discard all pending changes */\n discardAll(): void {\n this.pendingChanges.clear();\n }\n\n // ── Submit (batch upsert) ─────────────────────────────────────────\n\n openSummaryModal(): void {\n this.showSummaryModal = true;\n }\n\n closeSummaryModal(): void {\n this.showSummaryModal = false;\n }\n\n /** Confirm and fire the API call with all pending changes */\n submitAll(): void {\n this.showSummaryModal = false;\n this.clearMessages();\n this.loading = true;\n\n // Build the upsert payload: { tenantId, group1: { k1: v1 }, group2: { k2: v2 } }\n const payload: Record<string, any> = { tenantId: this.tenantId };\n for (const change of this.pendingChanges.values()) {\n if (!payload[change.group]) {\n payload[change.group] = {};\n }\n payload[change.group][change.key] = change.newValue;\n }\n\n this.api.upsert(payload as any)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n const count = this.pendingChanges.size;\n this.pendingChanges.clear();\n this.parseDocument(doc);\n this.successMsg = `${count} flag${count !== 1 ? 's' : ''} updated successfully!`;\n },\n error: (err) => {\n this.error = err.error?.message ?? 'Failed to save changes.';\n },\n });\n }\n\n // ── Add group / add flag (staged as pending) ─────────────────────\n\n addGroup(): void {\n const name = this.newGroupName.trim();\n if (!name) return;\n this.showAddGroup = false;\n this.addingFlagToGroup = name;\n this.newGroupName = '';\n }\n\n /** Stage a new flag as a pending change */\n confirmAddFlag(groupName: string): void {\n const key = this.newFlagKey.trim();\n if (!key) return;\n const value = this.coerceValue(this.newFlagValue, this.newFlagType);\n\n const compositeKey = this.makeCompositeKey(groupName, key);\n this.pendingChanges.set(compositeKey, {\n group: groupName,\n key,\n oldValue: null,\n newValue: value,\n isNew: true,\n });\n\n this.addingFlagToGroup = null;\n this.newFlagKey = '';\n this.newFlagValue = '';\n this.newFlagType = 'string';\n }\n\n cancelAddFlag(): void {\n this.addingFlagToGroup = null;\n this.newFlagKey = '';\n this.newFlagValue = '';\n }\n\n // ── Deletes (immediate, not batched) ──────────────────────────────\n\n deleteFlag(groupName: string, flagKey: string): void {\n this.clearMessages();\n this.loading = true;\n // Also clear any pending change for this flag\n this.pendingChanges.delete(this.makeCompositeKey(groupName, flagKey));\n\n this.api.deleteFlag(this.tenantId, groupName, flagKey)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.successMsg = `Flag \"${groupName}.${flagKey}\" deleted.`;\n },\n error: (err) => this.error = err.error?.message ?? 'Failed to delete flag.',\n });\n }\n\n deleteGroup(groupName: string): void {\n this.clearMessages();\n this.loading = true;\n // Clear any pending changes for this group\n for (const [key] of this.pendingChanges) {\n if (key.startsWith(groupName + '::')) {\n this.pendingChanges.delete(key);\n }\n }\n\n this.api.deleteGroup(this.tenantId, groupName)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.successMsg = `Group \"${groupName}\" deleted.`;\n },\n error: (err) => this.error = err.error?.message ?? 'Failed to delete group.',\n });\n }\n\n // ── Helpers ───────────────────────────────────────────────────────\n\n trackGroup(_index: number, group: ParsedGroup): string {\n return group.name;\n }\n\n private parseDocument(doc: FeatureFlagsDocument | null): void {\n this.groups = [];\n if (!doc) return;\n for (const [key, value] of Object.entries(doc)) {\n if (RESERVED_KEYS.has(key)) continue;\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const flags = Object.entries(value as FlagGroup).map(([k, v]) => ({ key: k, value: v }));\n this.groups.push({ name: key, flags });\n }\n }\n this.groups.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n private coerceValue(raw: string, type: 'string' | 'boolean' | 'number'): FlagPrimitive {\n const trimmed = raw.trim();\n if (trimmed === '' || trimmed.toLowerCase() === 'null') return null;\n switch (type) {\n case 'boolean':\n return trimmed.toLowerCase() === 'true';\n case 'number': {\n const n = Number(trimmed);\n return isNaN(n) ? trimmed : n;\n }\n default:\n return trimmed;\n }\n }\n\n private clearMessages(): void {\n this.error = null;\n this.successMsg = null;\n }\n\n private makeCompositeKey(group: string, key: string): string {\n return `${group}::${key}`;\n }\n\n private splitCompositeKey(composite: string): [string, string] {\n const idx = composite.indexOf('::');\n return [composite.substring(0, idx), composite.substring(idx + 2)];\n }\n\n private valuesEqual(a: FlagPrimitive, b: FlagPrimitive): boolean {\n return a === b;\n }\n}\n","<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n\n <!-- Tenant ID Input -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <label for=\"tenantIdInput\" class=\"form-label fw-semibold\">Tenant ID</label>\n <div class=\"input-group\">\n <span class=\"input-group-text\" id=\"tenant-addon\">\n <i class=\"bi bi-building\" aria-hidden=\"true\"></i>\n </span>\n <input\n id=\"tenantIdInput\"\n type=\"text\"\n class=\"form-control\"\n placeholder=\"e.g. tenant_acme\"\n [(ngModel)]=\"tenantId\"\n (keyup.enter)=\"loadFlags()\"\n [attr.aria-describedby]=\"'tenant-addon'\"\n aria-label=\"Tenant ID\" />\n <button\n class=\"btn btn-primary\"\n [disabled]=\"loading || !tenantId.trim()\"\n (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading…' : 'Load Flags' }}\n </button>\n </div>\n <div class=\"form-text\">Enter a tenant ID and press Enter or click Load Flags.</div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"!newGroupName.trim()\">\n Next\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n","import { Injectable } from '@angular/core';\nimport { Observable, of, delay, throwError } from 'rxjs';\nimport {\n FeatureFlagsDocument,\n FlagGroup,\n RESERVED_KEYS,\n UpsertFlagsPayload,\n} from '../models';\n\n/**\n * In-memory mock of FeatureFlagsApiService.\n * Ships with 2 tenants pre-loaded so you can test the UI\n * without spinning up the backend.\n *\n * Use via `FeatureFlagsModule.forTesting()`.\n */\n@Injectable()\nexport class MockFeatureFlagsApiService {\n private readonly store = new Map<string, FeatureFlagsDocument>();\n\n constructor() {\n this.seedData();\n }\n\n // ────────────────────────── Public API (same shape as real service) ──\n\n getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null> {\n const doc = this.store.get(tenantId) ?? null;\n return of(this.clone(doc)).pipe(delay(250));\n }\n\n upsert(payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument> {\n const { tenantId, ...groups } = payload;\n if (!tenantId?.trim()) {\n return throwError(() => ({ error: { message: 'tenantId is required' } }));\n }\n\n let doc = this.store.get(tenantId);\n if (!doc) {\n doc = this.newDoc(tenantId);\n this.store.set(tenantId, doc);\n }\n\n // Merge groups via dot-path (mirrors the backend's $set behavior)\n for (const [groupName, groupFlags] of Object.entries(groups)) {\n if (RESERVED_KEYS.has(groupName)) continue;\n const existing = (doc[groupName] as FlagGroup) ?? {};\n doc[groupName] = { ...existing, ...(groupFlags as FlagGroup) };\n }\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument> {\n const doc = this.store.get(tenantId);\n if (!doc) {\n return throwError(() => ({\n error: { message: `No feature flags found for tenantId='${tenantId}'` },\n }));\n }\n\n const g = doc[group] as FlagGroup | undefined;\n if (g && key in g) {\n delete g[key];\n }\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument> {\n const doc = this.store.get(tenantId);\n if (!doc) {\n return throwError(() => ({\n error: { message: `No feature flags found for tenantId='${tenantId}'` },\n }));\n }\n\n delete doc[group];\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n // ────────────────────────── Seed data ────────────────────────────────\n\n private seedData(): void {\n this.store.set('tenant_acme', this.buildAcmeTenant());\n this.store.set('tenant_globex', this.buildGlobexTenant());\n }\n\n private buildAcmeTenant(): FeatureFlagsDocument {\n return {\n _id: '6650a1b2c3d4e5f6a7b8c9d0',\n tenantId: 'tenant_acme',\n createdAt: '2025-11-01T08:00:00.000Z',\n updatedAt: '2026-02-15T14:30:00.000Z',\n uiOptions: {\n darkMode: true,\n sidebarCollapsed: false,\n maxItemsPerPage: 25,\n accentColor: '#4f46e5',\n showBetaBanner: true,\n },\n checkout: {\n newCheckoutFlow: true,\n enableGiftCards: false,\n maxCartItems: 50,\n taxCalculation: 'inclusive',\n },\n notifications: {\n emailEnabled: true,\n smsEnabled: false,\n pushEnabled: true,\n dailyDigest: true,\n marketingOptIn: false,\n },\n search: {\n enableFuzzySearch: true,\n maxResults: 100,\n highlightMatches: true,\n searchDelay: 300,\n },\n } as unknown as FeatureFlagsDocument;\n }\n\n private buildGlobexTenant(): FeatureFlagsDocument {\n return {\n _id: '6650b2c3d4e5f6a7b8c9d0e1',\n tenantId: 'tenant_globex',\n createdAt: '2025-12-10T10:00:00.000Z',\n updatedAt: '2026-02-16T09:15:00.000Z',\n uiOptions: {\n darkMode: false,\n sidebarCollapsed: true,\n maxItemsPerPage: 10,\n accentColor: '#059669',\n showBetaBanner: false,\n },\n billing: {\n enableAutoPay: true,\n gracePeriodDays: 7,\n currency: 'USD',\n showInvoiceBreakdown: true,\n lateFeePercent: 1.5,\n },\n featureAccess: {\n analyticsV2: true,\n exportToCsv: true,\n bulkOperations: false,\n advancedReporting: true,\n apiRateLimit: 1000,\n },\n integrations: {\n slackEnabled: true,\n slackWebhookConfigured: true,\n jiraEnabled: false,\n githubEnabled: true,\n webhookRetries: 3,\n },\n } as unknown as FeatureFlagsDocument;\n }\n\n // ────────────────────────── Helpers ──────────────────────────────────\n\n private newDoc(tenantId: string): FeatureFlagsDocument {\n const now = new Date().toISOString();\n return { tenantId, createdAt: now, updatedAt: now };\n }\n\n /** Deep-clone to avoid leaking mutable references */\n private clone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n }\n}\n","import { ModuleWithProviders, NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { provideHttpClient } from '@angular/common/http';\nimport { FeatureFlagsManagerComponent } from './components/feature-flags-manager/feature-flags-manager.component';\nimport { FlagGroupCardComponent } from './components/flag-group-card/flag-group-card.component';\nimport { ChangesSummaryModalComponent } from './components/changes-summary-modal/changes-summary-modal.component';\nimport { FEATURE_FLAGS_CONFIG, FeatureFlagsConfig } from './models';\nimport { FeatureFlagsApiService } from './services/feature-flags-api.service';\nimport { MockFeatureFlagsApiService } from './services/mock-feature-flags-api.service';\n\n@NgModule({\n declarations: [\n FeatureFlagsManagerComponent,\n FlagGroupCardComponent,\n ChangesSummaryModalComponent,\n ],\n imports: [\n CommonModule,\n FormsModule,\n ],\n exports: [\n FeatureFlagsManagerComponent,\n FlagGroupCardComponent,\n ChangesSummaryModalComponent,\n ],\n})\nexport class FeatureFlagsModule {\n /**\n * Call forRoot() in the consuming app to provide\n * the API configuration.\n *\n * @example\n * FeatureFlagsModule.forRoot({\n * apiBaseUrl: 'http://localhost:3000',\n * apiVersion: 'v1',\n * })\n */\n static forRoot(config: FeatureFlagsConfig): ModuleWithProviders<FeatureFlagsModule> {\n return {\n ngModule: FeatureFlagsModule,\n providers: [\n { provide: FEATURE_FLAGS_CONFIG, useValue: config },\n FeatureFlagsApiService,\n provideHttpClient(),\n ],\n };\n }\n\n /**\n * Call forTesting() in the consuming app to use\n * in-memory mock data (no backend needed).\n *\n * Ships with 2 pre-loaded tenants:\n * - \"tenant_acme\" (Acme Corp)\n * - \"tenant_globex\" (Globex Inc)\n *\n * @example\n * FeatureFlagsModule.forTesting()\n */\n static forTesting(): ModuleWithProviders<FeatureFlagsModule> {\n return {\n ngModule: FeatureFlagsModule,\n providers: [\n { provide: FeatureFlagsApiService, useClass: MockFeatureFlagsApiService },\n ],\n };\n }\n}\n","/*\n * Public API Surface of valar-ui\n */\nimport '@angular/localize/init';\n\n// Feature Flags\nexport * from './lib/feature-flags/feature-flags.module';\nexport * from './lib/feature-flags/models';\nexport * from './lib/feature-flags/services/feature-flags-api.service';\nexport * from './lib/feature-flags/services/mock-feature-flags-api.service';\nexport * from './lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component';\nexport * from './lib/feature-flags/components/flag-group-card/flag-group-card.component';\nexport * from './lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["i1","i1.FeatureFlagsApiService","i2","i3","i4.FlagGroupCardComponent","i5.ChangesSummaryModalComponent"],"mappings":"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"}
1
+ {"version":3,"file":"pl4yzonellc-valar-ui.mjs","sources":["../../../projects/valar-ui/src/lib/feature-flags/models/feature-flags.model.ts","../../../projects/valar-ui/src/lib/feature-flags/services/feature-flags-api.service.ts","../../../projects/valar-ui/src/lib/feature-flags/components/flag-group-card/flag-group-card.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/flag-group-card/flag-group-card.component.html","../../../projects/valar-ui/src/lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component.html","../../../projects/valar-ui/src/lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component.ts","../../../projects/valar-ui/src/lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component.html","../../../projects/valar-ui/src/lib/feature-flags/services/mock-feature-flags-api.service.ts","../../../projects/valar-ui/src/lib/feature-flags/feature-flags.module.ts","../../../projects/valar-ui/src/public-api.ts","../../../projects/valar-ui/src/pl4yzonellc-valar-ui.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\n\n/**\n * Primitive types allowed as feature flag values.\n * Mirrors the backend's FlagPrimitive type.\n */\nexport type FlagPrimitive = boolean | string | number | null;\n\n/**\n * A group of feature flags: key → primitive value.\n */\nexport type FlagGroup = Record<string, FlagPrimitive>;\n\n/**\n * The full feature flags document for a tenant.\n * Dynamic group names sit at the top level alongside metadata.\n */\nexport interface FeatureFlagsDocument {\n _id?: string;\n tenantId: string;\n createdAt?: string;\n updatedAt?: string;\n [groupName: string]: FlagGroup | string | undefined;\n}\n\n/**\n * Shape of the upsert request body sent to the backend.\n * Service will inject tenantId + any number of group objects.\n */\nexport interface UpsertFlagsPayload {\n [groupName: string]: FlagGroup | string;\n}\n\n/**\n * Configuration for the FeatureFlags library module.\n * The consuming app provides this via forRoot().\n */\nexport interface FeatureFlagsConfig {\n /** Base URL of the Valar API, e.g. 'http://localhost:3000' */\n apiBaseUrl: string;\n /** API version prefix, defaults to 'v1' */\n apiVersion?: string;\n /** Root tenant id sent in request headers */\n rootTenantId: string;\n /** Tenant header name, defaults to 'x-tenant-id' */\n tenantHeaderName?: string;\n}\n\n/** Injection token for the library config */\nexport const FEATURE_FLAGS_CONFIG = new InjectionToken<FeatureFlagsConfig>(\n 'FEATURE_FLAGS_CONFIG'\n);\n\n/** Reserved keys that are NOT flag groups */\nexport const RESERVED_KEYS = new Set([\n '_id', 'tenantId', 'createdAt', 'updatedAt', '__v', 'id',\n]);\n\n/** A single staged change for the batch-edit workflow */\nexport interface PendingChange {\n group: string;\n key: string;\n oldValue: FlagPrimitive;\n newValue: FlagPrimitive;\n isNew?: boolean; // true if this is a brand-new flag\n}\n","import { HttpClient, HttpHeaders } from '@angular/common/http';\nimport { Inject, Injectable } from '@angular/core';\nimport { Observable, map } from 'rxjs';\nimport {\n FEATURE_FLAGS_CONFIG,\n FeatureFlagsConfig,\n FeatureFlagsDocument,\n UpsertFlagsPayload,\n} from '../models';\n\n/**\n * HTTP service that talks to the Valar feature-flags backend.\n * Base URL is injected via FEATURE_FLAGS_CONFIG so the consuming\n * app controls where requests go.\n */\n@Injectable()\nexport class FeatureFlagsApiService {\n private readonly baseUrl: string;\n private readonly rootTenantId: string;\n private readonly tenantHeaderName: string;\n\n constructor(\n private readonly http: HttpClient,\n @Inject(FEATURE_FLAGS_CONFIG) config: FeatureFlagsConfig,\n ) {\n const version = config.apiVersion ?? 'v1';\n this.baseUrl = `${config.apiBaseUrl}/${version}/feature-flags`;\n this.rootTenantId = config.rootTenantId;\n this.tenantHeaderName = config.tenantHeaderName ?? 'x-tenant-id';\n }\n\n /** GET /v1/feature-flags/tenant_<tenantId> */\n getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null> {\n return this.http.get<FeatureFlagsDocument | null>(this.tenantScopedBaseUrl(tenantId), this.requestOptions())\n .pipe(map(doc => doc ?? null));\n }\n\n /** PUT /v1/feature-flags (merge-upsert) */\n upsert(tenantId: string, payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument> {\n const requestPayload: UpsertFlagsPayload = {\n ...payload,\n tenantId: this.toTenantToken(tenantId),\n };\n return this.http.put<FeatureFlagsDocument>(this.baseUrl, requestPayload, this.requestOptions());\n }\n\n /** DELETE /v1/feature-flags/tenant_<tenantId>/:group/:key */\n deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument> {\n const url = [\n this.tenantScopedBaseUrl(tenantId),\n encodeURIComponent(group),\n encodeURIComponent(key),\n ].join('/');\n return this.http.delete<FeatureFlagsDocument>(url, this.requestOptions());\n }\n\n /** DELETE /v1/feature-flags/tenant_<tenantId>/:group */\n deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument> {\n const url = [\n this.tenantScopedBaseUrl(tenantId),\n encodeURIComponent(group),\n ].join('/');\n return this.http.delete<FeatureFlagsDocument>(url, this.requestOptions());\n }\n\n private tenantScopedBaseUrl(tenantId: string): string {\n return `${this.baseUrl}/${encodeURIComponent(this.toTenantToken(tenantId))}`;\n }\n\n private toTenantToken(tenantId: string): string {\n const normalized = tenantId.trim();\n if (normalized.startsWith('tenant_')) return normalized;\n return `tenant_${normalized}`;\n }\n\n private requestOptions(): { headers: HttpHeaders } {\n return {\n headers: new HttpHeaders({\n [this.tenantHeaderName]: this.rootTenantId,\n }),\n };\n }\n}\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { FlagPrimitive } from '../../models';\n\nexport interface FlagEntry {\n key: string;\n value: FlagPrimitive;\n}\n\nexport interface FlagChangedEvent {\n key: string;\n oldValue: FlagPrimitive;\n newValue: FlagPrimitive;\n}\n\n@Component({\n selector: 'valar-flag-group-card',\n templateUrl: './flag-group-card.component.html',\n styleUrls: ['./flag-group-card.component.css'],\n standalone: false,\n})\nexport class FlagGroupCardComponent {\n @Input() groupName = '';\n @Input() flags: FlagEntry[] = [];\n @Input() isNewGroup = false;\n @Input() dirtyKeys: Set<string> = new Set();\n @Input() isAddingFlag = false;\n @Input() newFlagKey = '';\n @Input() newFlagValue = '';\n @Input() newFlagType: 'string' | 'boolean' | 'number' = 'string';\n\n @Output() flagChanged = new EventEmitter<FlagChangedEvent>();\n @Output() revertFlag = new EventEmitter<string>();\n @Output() addFlagRequest = new EventEmitter<void>();\n @Output() cancelAddFlag = new EventEmitter<void>();\n @Output() confirmAddFlag = new EventEmitter<void>();\n @Output() newFlagKeyChange = new EventEmitter<string>();\n @Output() newFlagValueChange = new EventEmitter<string>();\n @Output() newFlagTypeChange = new EventEmitter<'string' | 'boolean' | 'number'>();\n @Output() deleteFlagRequest = new EventEmitter<string>();\n @Output() deleteGroupRequest = new EventEmitter<void>();\n\n collapsed = false;\n confirmDeleteGroup = false;\n confirmDeleteKey: string | null = null;\n\n /** Which flag key is currently in inline-edit mode */\n editingKey: string | null = null;\n editValue = '';\n editType: 'string' | 'boolean' | 'number' = 'string';\n\n isDirty(key: string): boolean {\n return this.dirtyKeys.has(key);\n }\n\n /** Enter inline-edit mode for a flag */\n startEdit(flag: FlagEntry): void {\n this.editingKey = flag.key;\n this.editType = this.detectType(flag.value);\n this.editValue = flag.value === null ? '' : String(flag.value);\n }\n\n /** Commit the inline edit — emits change to parent, stays in view mode */\n commitEdit(flag: FlagEntry): void {\n const newValue = this.coerceValue(this.editValue, this.editType);\n this.flagChanged.emit({ key: flag.key, oldValue: flag.value, newValue });\n this.editingKey = null;\n }\n\n /** Cancel inline edit without staging anything */\n cancelEdit(): void {\n this.editingKey = null;\n }\n\n displayValue(value: FlagPrimitive): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return String(value);\n }\n\n typeBadgeClass(value: FlagPrimitive): string {\n if (typeof value === 'boolean') return 'bg-info text-dark';\n if (typeof value === 'number') return 'bg-warning text-dark';\n if (value === null) return 'bg-secondary';\n return 'bg-primary';\n }\n\n typeLabel(value: FlagPrimitive): string {\n if (value === null) return 'null';\n return typeof value;\n }\n\n isBooleanFlag(value: FlagPrimitive): boolean {\n return typeof value === 'boolean';\n }\n\n trackFlag(_index: number, flag: FlagEntry): string {\n return flag.key;\n }\n\n onDeleteFlag(key: string): void {\n if (this.confirmDeleteKey === key) {\n this.deleteFlagRequest.emit(key);\n this.confirmDeleteKey = null;\n } else {\n this.confirmDeleteKey = key;\n }\n }\n\n onDeleteGroup(): void {\n if (this.confirmDeleteGroup) {\n this.deleteGroupRequest.emit();\n this.confirmDeleteGroup = false;\n } else {\n this.confirmDeleteGroup = true;\n }\n }\n\n private detectType(value: FlagPrimitive): 'string' | 'boolean' | 'number' {\n if (typeof value === 'boolean') return 'boolean';\n if (typeof value === 'number') return 'number';\n return 'string';\n }\n\n private coerceValue(raw: string, type: 'string' | 'boolean' | 'number'): FlagPrimitive {\n const trimmed = raw.trim();\n if (trimmed === '' || trimmed.toLowerCase() === 'null') return null;\n switch (type) {\n case 'boolean':\n return trimmed.toLowerCase() === 'true';\n case 'number': {\n const n = Number(trimmed);\n return isNaN(n) ? trimmed : n;\n }\n default:\n return trimmed;\n }\n }\n}\n","<div class=\"card shadow-sm\">\n <!-- Card Header -->\n <div class=\"card-header d-flex justify-content-between align-items-center\">\n <div class=\"d-flex align-items-center\">\n <button\n class=\"btn btn-sm btn-link text-decoration-none p-0 me-2\"\n (click)=\"collapsed = !collapsed\"\n [attr.aria-expanded]=\"!collapsed\"\n [attr.aria-controls]=\"'group-' + groupName\"\n aria-label=\"Toggle group\">\n <i class=\"bi\" [ngClass]=\"collapsed ? 'bi-chevron-right' : 'bi-chevron-down'\" aria-hidden=\"true\"></i>\n </button>\n <h6 class=\"mb-0 fw-bold\">\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ groupName }}\n </h6>\n <span *ngIf=\"isNewGroup\" class=\"badge bg-success ms-2\">NEW</span>\n <span class=\"badge bg-secondary ms-2\">{{ flags.length }}</span>\n <span *ngIf=\"dirtyKeys.size > 0\" class=\"badge bg-warning text-dark ms-1\" title=\"Unsaved changes\">\n {{ dirtyKeys.size }} edited\n </span>\n </div>\n <div class=\"btn-group btn-group-sm\">\n <button\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addFlagRequest.emit()\"\n title=\"Add flag to this group\"\n aria-label=\"Add flag\">\n <i class=\"bi bi-plus\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteGroup ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteGroup()\"\n [title]=\"confirmDeleteGroup ? 'Click again to confirm' : 'Delete entire group'\"\n [attr.aria-label]=\"confirmDeleteGroup ? 'Confirm delete group' : 'Delete group'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteGroup ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n <span *ngIf=\"confirmDeleteGroup\" class=\"ms-1\">Confirm?</span>\n </button>\n </div>\n </div>\n\n <!-- Card Body -->\n <div [id]=\"'group-' + groupName\" *ngIf=\"!collapsed\">\n <div class=\"table-responsive\">\n <table class=\"table table-hover table-sm mb-0\" [attr.aria-label]=\"'Flags in ' + groupName\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\" style=\"width: 28%\">Key</th>\n <th scope=\"col\" style=\"width: 12%\">Type</th>\n <th scope=\"col\" style=\"width: 38%\">Value</th>\n <th scope=\"col\" style=\"width: 22%\" class=\"text-end\">Actions</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let flag of flags; trackBy: trackFlag\"\n [class.row-dirty]=\"isDirty(flag.key)\">\n <!-- KEY -->\n <td class=\"align-middle\">\n <code>{{ flag.key }}</code>\n <i *ngIf=\"isDirty(flag.key)\" class=\"bi bi-pencil-fill text-warning ms-1 small\"\n title=\"Edited\" aria-hidden=\"true\"></i>\n </td>\n\n <!-- TYPE -->\n <td class=\"align-middle\">\n <span class=\"badge\" [ngClass]=\"typeBadgeClass(flag.value)\">\n {{ typeLabel(flag.value) }}\n </span>\n </td>\n\n <!-- VALUE (view or inline-edit) -->\n <td class=\"align-middle\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editMode\">\n <!-- Boolean as ON/OFF badge -->\n <span *ngIf=\"isBooleanFlag(flag.value); else plainValue\">\n <span class=\"badge\" [ngClass]=\"flag.value ? 'bg-success' : 'bg-danger'\">\n {{ flag.value ? 'ON' : 'OFF' }}\n </span>\n </span>\n <ng-template #plainValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\n </ng-container>\n\n <!-- Inline edit mode -->\n <ng-template #editMode>\n <!-- Boolean: simple toggle, no type dropdown -->\n <ng-container *ngIf=\"editType === 'boolean'; else nonBooleanEdit\">\n <select\n class=\"form-select form-select-sm\"\n style=\"max-width: 120px\"\n [(ngModel)]=\"editValue\"\n aria-label=\"Flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <!-- Non-boolean: type dropdown + value input -->\n <ng-template #nonBooleanEdit>\n <div class=\"input-group input-group-sm\">\n <select\n class=\"form-select\"\n style=\"max-width: 100px\"\n [(ngModel)]=\"editType\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n </select>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </div>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td class=\"align-middle text-end\">\n <ng-container *ngIf=\"editingKey !== flag.key; else editActions\">\n <button\n class=\"btn btn-sm btn-outline-secondary me-1\"\n (click)=\"startEdit(flag)\"\n title=\"Edit flag\"\n aria-label=\"Edit flag\">\n <i class=\"bi bi-pencil\" aria-hidden=\"true\"></i>\n </button>\n <button *ngIf=\"isDirty(flag.key)\"\n class=\"btn btn-sm btn-outline-warning me-1\"\n (click)=\"revertFlag.emit(flag.key)\"\n title=\"Revert change\"\n aria-label=\"Revert change\">\n <i class=\"bi bi-arrow-counterclockwise\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm\"\n [ngClass]=\"confirmDeleteKey === flag.key ? 'btn-danger' : 'btn-outline-danger'\"\n (click)=\"onDeleteFlag(flag.key)\"\n [title]=\"confirmDeleteKey === flag.key ? 'Click again to confirm' : 'Delete flag'\"\n [attr.aria-label]=\"confirmDeleteKey === flag.key ? 'Confirm delete' : 'Delete flag'\">\n <i class=\"bi\" [ngClass]=\"confirmDeleteKey === flag.key ? 'bi-exclamation-triangle' : 'bi-trash'\" aria-hidden=\"true\"></i>\n </button>\n </ng-container>\n <ng-template #editActions>\n <button\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"commitEdit(flag)\"\n title=\"Stage change\"\n aria-label=\"Stage change\">\n <i class=\"bi bi-check-lg\" aria-hidden=\"true\"></i>\n </button>\n <button\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"cancelEdit()\"\n title=\"Cancel\"\n aria-label=\"Cancel edit\">\n <i class=\"bi bi-x-lg\" aria-hidden=\"true\"></i>\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- Empty row -->\n <tr *ngIf=\"flags.length === 0 && !isAddingFlag\">\n <td colspan=\"4\" class=\"text-center text-muted py-3\">\n No flags in this group.\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n\n <!-- Add Flag Form -->\n <div *ngIf=\"isAddingFlag\" class=\"card-footer bg-light\">\n <h6 class=\"text-success mb-2\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Flag to \"{{ groupName }}\"\n </h6>\n <div class=\"row g-2 align-items-end\">\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Key</label>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"flagKey\"\n [ngModel]=\"newFlagKey\"\n (ngModelChange)=\"newFlagKeyChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag key\" />\n </div>\n <div class=\"col-md-2\">\n <label class=\"form-label form-label-sm\">Type</label>\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagType\"\n (ngModelChange)=\"newFlagTypeChange.emit($event)\"\n aria-label=\"New flag type\">\n <option value=\"string\">string</option>\n <option value=\"boolean\">boolean</option>\n <option value=\"number\">number</option>\n </select>\n </div>\n <div class=\"col-md-3\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTextInput\">\n <select\n class=\"form-select form-select-sm\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n aria-label=\"New flag value\">\n <option value=\"true\">true</option>\n <option value=\"false\">false</option>\n </select>\n </ng-container>\n <ng-template #newTextInput>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n placeholder=\"value\"\n [ngModel]=\"newFlagValue\"\n (ngModelChange)=\"newFlagValueChange.emit($event)\"\n (keyup.enter)=\"confirmAddFlag.emit()\"\n aria-label=\"New flag value\" />\n </ng-template>\n </div>\n <div class=\"col-md-4\">\n <button class=\"btn btn-success btn-sm me-1\" (click)=\"confirmAddFlag.emit()\" [disabled]=\"!newFlagKey.trim()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Stage\n </button>\n <button class=\"btn btn-outline-secondary btn-sm\" (click)=\"cancelAddFlag.emit()\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </div>\n</div>\n","import { Component, EventEmitter, Input, Output } from '@angular/core';\nimport { PendingChange, FlagPrimitive } from '../../models';\n\n@Component({\n selector: 'valar-changes-summary-modal',\n templateUrl: './changes-summary-modal.component.html',\n styleUrls: ['./changes-summary-modal.component.css'],\n standalone: false,\n})\nexport class ChangesSummaryModalComponent {\n @Input() changes: PendingChange[] = [];\n @Input() tenantId = '';\n @Output() confirm = new EventEmitter<void>();\n @Output() dismiss = new EventEmitter<void>();\n\n displayValue(value: FlagPrimitive): string {\n if (value === null) return 'null';\n if (typeof value === 'boolean') return value ? 'true' : 'false';\n return String(value);\n }\n\n typeLabel(value: FlagPrimitive): string {\n if (value === null) return 'null';\n return typeof value;\n }\n}\n","<!-- Backdrop -->\n<div class=\"modal-backdrop fade show\" (click)=\"dismiss.emit()\"></div>\n\n<!-- Modal -->\n<div class=\"modal fade show d-block\" tabindex=\"-1\" role=\"dialog\" aria-labelledby=\"changesSummaryTitle\" aria-modal=\"true\">\n <div class=\"modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable\">\n <div class=\"modal-content\">\n <!-- Header -->\n <div class=\"modal-header bg-primary text-white\">\n <h5 class=\"modal-title\" id=\"changesSummaryTitle\">\n <i class=\"bi bi-clipboard-check me-2\" aria-hidden=\"true\"></i>\n Review Changes\n </h5>\n <button type=\"button\" class=\"btn-close btn-close-white\" aria-label=\"Close\" (click)=\"dismiss.emit()\"></button>\n </div>\n\n <!-- Body -->\n <div class=\"modal-body\">\n <p class=\"text-muted mb-3\">\n You're about to update <strong>{{ changes.length }}</strong>\n flag{{ changes.length !== 1 ? 's' : '' }} for tenant\n <code>{{ tenantId }}</code>.\n </p>\n\n <div class=\"table-responsive\">\n <table class=\"table table-sm table-bordered mb-0\" aria-label=\"Pending changes summary\">\n <thead class=\"table-light\">\n <tr>\n <th scope=\"col\">Group</th>\n <th scope=\"col\">Key</th>\n <th scope=\"col\">Old Value</th>\n <th scope=\"col\" class=\"text-center\" style=\"width: 40px\">\n <i class=\"bi bi-arrow-right\" aria-hidden=\"true\"></i>\n </th>\n <th scope=\"col\">New Value</th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let change of changes\">\n <td>\n <i class=\"bi bi-folder2 me-1 text-primary\" aria-hidden=\"true\"></i>\n {{ change.group }}\n </td>\n <td><code>{{ change.key }}</code></td>\n <td>\n <span class=\"text-danger text-decoration-line-through\">\n {{ displayValue(change.oldValue) }}\n </span>\n <span class=\"badge bg-secondary ms-1\">{{ typeLabel(change.oldValue) }}</span>\n <span *ngIf=\"change.isNew\" class=\"badge bg-success ms-1\">NEW</span>\n </td>\n <td class=\"text-center text-muted\">\n <i class=\"bi bi-arrow-right\" aria-hidden=\"true\"></i>\n </td>\n <td>\n <span class=\"fw-semibold text-success\">\n {{ displayValue(change.newValue) }}\n </span>\n <span class=\"badge bg-secondary ms-1\">{{ typeLabel(change.newValue) }}</span>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n\n <!-- Footer -->\n <div class=\"modal-footer\">\n <button type=\"button\" class=\"btn btn-outline-secondary\" (click)=\"dismiss.emit()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Cancel\n </button>\n <button type=\"button\" class=\"btn btn-primary\" (click)=\"confirm.emit()\">\n <i class=\"bi bi-check-lg me-1\" aria-hidden=\"true\"></i>Looks Good\n </button>\n </div>\n </div>\n </div>\n</div>\n","import { Component, EventEmitter, Input, OnChanges, OnDestroy, Output, SimpleChanges } from '@angular/core';\nimport { Subject, takeUntil, finalize } from 'rxjs';\nimport { FeatureFlagsApiService } from '../../services/feature-flags-api.service';\nimport {\n FeatureFlagsDocument,\n FlagGroup,\n FlagPrimitive,\n PendingChange,\n RESERVED_KEYS,\n} from '../../models';\n\nexport interface ParsedGroup {\n name: string;\n flags: { key: string; value: FlagPrimitive }[];\n}\n\n@Component({\n selector: 'valar-feature-flags-manager',\n templateUrl: './feature-flags-manager.component.html',\n styleUrls: ['./feature-flags-manager.component.css'],\n standalone: false,\n})\nexport class FeatureFlagsManagerComponent implements OnDestroy, OnChanges {\n @Input() tenantId = '';\n @Output() tenantIdChange = new EventEmitter<string>();\n groups: ParsedGroup[] = [];\n loading = false;\n error: string | null = null;\n successMsg: string | null = null;\n loaded = false;\n\n // Add-group form\n showAddGroup = false;\n newGroupName = '';\n\n // Add-flag form (per group)\n addingFlagToGroup: string | null = null;\n newFlagKey = '';\n newFlagValue = '';\n newFlagType: 'string' | 'boolean' | 'number' = 'string';\n\n // ── Batch-edit state ──────────────────────────────────────────────\n /**\n * Pending changes keyed by \"group.key\".\n * Stores old/new values so we can display a diff summary.\n */\n pendingChanges = new Map<string, PendingChange>();\n showSummaryModal = false;\n private readonly newlyCreatedGroups = new Set<string>();\n\n private readonly destroy$ = new Subject<void>();\n\n constructor(private readonly api: FeatureFlagsApiService) {}\n\n ngOnChanges(changes: SimpleChanges): void {\n if (!changes['tenantId']) return;\n const id = this.tenantId.trim();\n if (!id) {\n this.groups = [];\n this.loaded = false;\n this.pendingChanges.clear();\n this.clearMessages();\n return;\n }\n this.tenantIdChange.emit(id);\n this.loadFlags();\n }\n\n ngOnDestroy(): void {\n this.destroy$.next();\n this.destroy$.complete();\n }\n\n // ── Computed helpers ──────────────────────────────────────────────\n\n get hasPendingChanges(): boolean {\n return this.pendingChanges.size > 0;\n }\n\n get pendingChangeCount(): number {\n return this.pendingChanges.size;\n }\n\n get pendingChangesList(): PendingChange[] {\n return Array.from(this.pendingChanges.values());\n }\n\n /** Returns the set of dirty keys for a given group */\n dirtyKeysForGroup(groupName: string): Set<string> {\n const keys = new Set<string>();\n for (const [compositeKey] of this.pendingChanges) {\n const [g, k] = this.splitCompositeKey(compositeKey);\n if (g === groupName) keys.add(k);\n }\n return keys;\n }\n\n // ── Load ──────────────────────────────────────────────────────────\n\n loadFlags(): void {\n const id = this.tenantId.trim();\n if (!id) {\n this.error = 'Tenant ID is required.';\n return;\n }\n this.clearMessages();\n this.pendingChanges.clear();\n this.loading = true;\n this.api.getByTenantId(id)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.reconcileNewGroupBadges();\n this.loaded = true;\n if (!doc || this.groups.length === 0) {\n this.successMsg = 'No flags found for this tenant yet. Start adding some!';\n }\n },\n error: (err) => {\n this.error = err.error?.message ?? 'Failed to load flags.';\n },\n });\n }\n\n // ── Stage / unstage edits ─────────────────────────────────────────\n\n /** Called when a flag value is changed inline */\n onFlagChanged(groupName: string, event: { key: string; oldValue: FlagPrimitive; newValue: FlagPrimitive }): void {\n const compositeKey = this.makeCompositeKey(groupName, event.key);\n\n // If there's already a pending change, preserve the ORIGINAL old value\n const existing = this.pendingChanges.get(compositeKey);\n const originalOld = existing ? existing.oldValue : event.oldValue;\n\n // If user reverted back to original value, unstage it\n if (this.valuesEqual(originalOld, event.newValue)) {\n this.pendingChanges.delete(compositeKey);\n } else {\n this.pendingChanges.set(compositeKey, {\n group: groupName,\n key: event.key,\n oldValue: originalOld,\n newValue: event.newValue,\n isNew: existing?.isNew ?? false,\n });\n }\n }\n\n /** Revert a single flag's pending change */\n revertFlag(groupName: string, key: string): void {\n const compositeKey = this.makeCompositeKey(groupName, key);\n this.pendingChanges.delete(compositeKey);\n }\n\n /** Discard all pending changes */\n discardAll(): void {\n this.pendingChanges.clear();\n }\n\n // ── Submit (batch upsert) ─────────────────────────────────────────\n\n openSummaryModal(): void {\n this.showSummaryModal = true;\n }\n\n closeSummaryModal(): void {\n this.showSummaryModal = false;\n }\n\n /** Confirm and fire the API call with all pending changes */\n submitAll(): void {\n this.showSummaryModal = false;\n this.clearMessages();\n this.loading = true;\n\n // Build the upsert payload: { group1: { k1: v1 }, group2: { k2: v2 } }\n const payload: Record<string, any> = {};\n for (const change of this.pendingChanges.values()) {\n if (!payload[change.group]) {\n payload[change.group] = {};\n }\n payload[change.group][change.key] = change.newValue;\n }\n\n this.api.upsert(this.tenantId, payload as any)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n const count = this.pendingChanges.size;\n this.pendingChanges.clear();\n this.parseDocument(doc);\n this.reconcileNewGroupBadges();\n this.successMsg = `${count} flag${count !== 1 ? 's' : ''} updated successfully!`;\n },\n error: (err) => {\n this.error = err.error?.message ?? 'Failed to save changes.';\n },\n });\n }\n\n // ── Add group / add flag (staged as pending) ─────────────────────\n\n addGroup(): void {\n const name = this.newGroupName.trim();\n if (!name) return;\n this.clearMessages();\n\n if (this.groups.some(g => g.name === name)) {\n this.showAddGroup = false;\n this.addingFlagToGroup = name;\n this.newGroupName = '';\n return;\n }\n\n this.loading = true;\n this.api.upsert(this.tenantId, { [name]: {} } as any)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.ensureGroupVisible(name);\n this.newlyCreatedGroups.add(name);\n this.loaded = true;\n this.successMsg = `Group \"${name}\" created.`;\n this.showAddGroup = false;\n this.addingFlagToGroup = name;\n this.newGroupName = '';\n },\n error: (err) => {\n this.error = err.error?.message ?? 'Failed to create group.';\n },\n });\n }\n\n /** Stage a new flag as a pending change */\n confirmAddFlag(groupName: string): void {\n const key = this.newFlagKey.trim();\n if (!key) return;\n const value = this.coerceValue(this.newFlagValue, this.newFlagType);\n\n const compositeKey = this.makeCompositeKey(groupName, key);\n this.pendingChanges.set(compositeKey, {\n group: groupName,\n key,\n oldValue: null,\n newValue: value,\n isNew: true,\n });\n\n this.addingFlagToGroup = null;\n this.newFlagKey = '';\n this.newFlagValue = '';\n this.newFlagType = 'string';\n }\n\n cancelAddFlag(): void {\n this.addingFlagToGroup = null;\n this.newFlagKey = '';\n this.newFlagValue = '';\n }\n\n // ── Deletes (immediate, not batched) ──────────────────────────────\n\n deleteFlag(groupName: string, flagKey: string): void {\n this.clearMessages();\n this.loading = true;\n // Also clear any pending change for this flag\n this.pendingChanges.delete(this.makeCompositeKey(groupName, flagKey));\n\n this.api.deleteFlag(this.tenantId, groupName, flagKey)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.reconcileNewGroupBadges();\n this.successMsg = `Flag \"${groupName}.${flagKey}\" deleted.`;\n },\n error: (err) => this.error = err.error?.message ?? 'Failed to delete flag.',\n });\n }\n\n deleteGroup(groupName: string): void {\n this.clearMessages();\n this.loading = true;\n // Clear any pending changes for this group\n for (const [key] of this.pendingChanges) {\n if (key.startsWith(groupName + '::')) {\n this.pendingChanges.delete(key);\n }\n }\n\n this.api.deleteGroup(this.tenantId, groupName)\n .pipe(takeUntil(this.destroy$), finalize(() => this.loading = false))\n .subscribe({\n next: (doc) => {\n this.parseDocument(doc);\n this.newlyCreatedGroups.delete(groupName);\n this.reconcileNewGroupBadges();\n this.successMsg = `Group \"${groupName}\" deleted.`;\n },\n error: (err) => this.error = err.error?.message ?? 'Failed to delete group.',\n });\n }\n\n // ── Helpers ───────────────────────────────────────────────────────\n\n trackGroup(_index: number, group: ParsedGroup): string {\n return group.name;\n }\n\n isNewGroup(group: ParsedGroup): boolean {\n if (!this.newlyCreatedGroups.has(group.name)) return false;\n const hasStagedFirstFlag = this.dirtyKeysForGroup(group.name).size > 0;\n return group.flags.length === 0 && !hasStagedFirstFlag;\n }\n\n private parseDocument(doc: FeatureFlagsDocument | null): void {\n this.groups = [];\n if (!doc) return;\n for (const [key, value] of Object.entries(doc)) {\n if (RESERVED_KEYS.has(key)) continue;\n if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n const flags = Object.entries(value as FlagGroup).map(([k, v]) => ({ key: k, value: v }));\n this.groups.push({ name: key, flags });\n }\n }\n this.groups.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n private coerceValue(raw: string, type: 'string' | 'boolean' | 'number'): FlagPrimitive {\n const trimmed = raw.trim();\n if (trimmed === '' || trimmed.toLowerCase() === 'null') return null;\n switch (type) {\n case 'boolean':\n return trimmed.toLowerCase() === 'true';\n case 'number': {\n const n = Number(trimmed);\n return isNaN(n) ? trimmed : n;\n }\n default:\n return trimmed;\n }\n }\n\n private clearMessages(): void {\n this.error = null;\n this.successMsg = null;\n }\n\n private makeCompositeKey(group: string, key: string): string {\n return `${group}::${key}`;\n }\n\n private splitCompositeKey(composite: string): [string, string] {\n const idx = composite.indexOf('::');\n return [composite.substring(0, idx), composite.substring(idx + 2)];\n }\n\n private valuesEqual(a: FlagPrimitive, b: FlagPrimitive): boolean {\n return a === b;\n }\n\n private ensureGroupVisible(groupName: string): void {\n if (this.groups.some(g => g.name === groupName)) return;\n this.groups.push({ name: groupName, flags: [] });\n this.groups.sort((a, b) => a.name.localeCompare(b.name));\n }\n\n private reconcileNewGroupBadges(): void {\n const existingGroupNames = new Set(this.groups.map(g => g.name));\n for (const groupName of Array.from(this.newlyCreatedGroups)) {\n const group = this.groups.find(g => g.name === groupName);\n const hasGroup = existingGroupNames.has(groupName);\n const hasFlags = !!group && group.flags.length > 0;\n if (!hasGroup || hasFlags) {\n this.newlyCreatedGroups.delete(groupName);\n }\n }\n }\n}\n","<div class=\"container-fluid py-4 pb-5\">\n <!-- Header -->\n <div class=\"d-flex align-items-center mb-4\">\n <h2 class=\"mb-0\">\n <i class=\"bi bi-toggles2 me-2\" aria-hidden=\"true\"></i>Feature Flags\n </h2>\n </div>\n\n <!-- Alerts -->\n <div *ngIf=\"error\" class=\"alert alert-danger alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-exclamation-triangle-fill me-2\" aria-hidden=\"true\"></i>{{ error }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"error = null\"></button>\n </div>\n <div *ngIf=\"successMsg\" class=\"alert alert-success alert-dismissible fade show\" role=\"alert\">\n <i class=\"bi bi-check-circle-fill me-2\" aria-hidden=\"true\"></i>{{ successMsg }}\n <button type=\"button\" class=\"btn-close\" aria-label=\"Close\" (click)=\"successMsg = null\"></button>\n </div>\n <!-- Tenant Context -->\n <div class=\"card shadow-sm mb-4\">\n <div class=\"card-body\">\n <div class=\"d-flex justify-content-between align-items-center\">\n <div>\n <label class=\"form-label fw-semibold mb-1\">Tenant ID</label>\n <div><code>{{ tenantId }}</code></div>\n </div>\n <button class=\"btn btn-primary\" [disabled]=\"loading\" (click)=\"loadFlags()\">\n <span *ngIf=\"loading\" class=\"spinner-border spinner-border-sm me-1\"\n role=\"status\" aria-hidden=\"true\"></span>\n {{ loading ? 'Loading...' : 'Reload Flags' }}\n </button>\n </div>\n <div class=\"form-text\">\n Tenant is provided by parent component input, while <code>x-tenant-id</code> comes from module config.\n </div>\n </div>\n </div>\n\n <!-- Content area (only after load) -->\n <div *ngIf=\"loaded\">\n <!-- Toolbar -->\n <div class=\"d-flex justify-content-between align-items-center mb-3\">\n <span class=\"text-muted\">\n {{ groups.length }} group{{ groups.length !== 1 ? 's' : '' }} found\n </span>\n <button class=\"btn btn-success btn-sm\" (click)=\"showAddGroup = true\" [disabled]=\"showAddGroup\">\n <i class=\"bi bi-plus-circle me-1\" aria-hidden=\"true\"></i>Add Group\n </button>\n </div>\n\n <!-- Add Group Form -->\n <div *ngIf=\"showAddGroup\" class=\"card border-success mb-3\">\n <div class=\"card-body\">\n <h6 class=\"card-title text-success\">\n <i class=\"bi bi-folder-plus me-1\" aria-hidden=\"true\"></i>New Group\n </h6>\n <div class=\"input-group input-group-sm\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"Group name (e.g. uiOptions)\"\n [(ngModel)]=\"newGroupName\"\n (keyup.enter)=\"addGroup()\"\n aria-label=\"New group name\" />\n <button class=\"btn btn-success\" (click)=\"addGroup()\" [disabled]=\"loading || !newGroupName.trim()\">\n {{ loading ? 'Creating...' : 'Create Group' }}\n </button>\n <button class=\"btn btn-outline-secondary\" (click)=\"showAddGroup = false; newGroupName = ''\">\n Cancel\n </button>\n </div>\n <div class=\"form-text\">Letters, numbers, _ and - only. Must start with a letter.</div>\n </div>\n </div>\n\n <!-- Flag Groups -->\n <div *ngFor=\"let group of groups; trackBy: trackGroup\" class=\"mb-3\">\n <valar-flag-group-card\n [groupName]=\"group.name\"\n [flags]=\"group.flags\"\n [isNewGroup]=\"isNewGroup(group)\"\n [dirtyKeys]=\"dirtyKeysForGroup(group.name)\"\n [isAddingFlag]=\"addingFlagToGroup === group.name\"\n [newFlagKey]=\"newFlagKey\"\n [newFlagValue]=\"newFlagValue\"\n [newFlagType]=\"newFlagType\"\n (flagChanged)=\"onFlagChanged(group.name, $event)\"\n (revertFlag)=\"revertFlag(group.name, $event)\"\n (addFlagRequest)=\"addingFlagToGroup = group.name\"\n (cancelAddFlag)=\"cancelAddFlag()\"\n (confirmAddFlag)=\"confirmAddFlag(group.name)\"\n (newFlagKeyChange)=\"newFlagKey = $event\"\n (newFlagValueChange)=\"newFlagValue = $event\"\n (newFlagTypeChange)=\"newFlagType = $event\"\n (deleteFlagRequest)=\"deleteFlag(group.name, $event)\"\n (deleteGroupRequest)=\"deleteGroup(group.name)\">\n </valar-flag-group-card>\n </div>\n\n <!-- Empty state -->\n <div *ngIf=\"groups.length === 0 && !showAddGroup\" class=\"text-center text-muted py-5\">\n <i class=\"bi bi-inbox display-1\" aria-hidden=\"true\"></i>\n <p class=\"mt-3\">No feature flag groups yet. Click <strong>Add Group</strong> to get started.</p>\n </div>\n </div>\n</div>\n\n<!-- Sticky bottom action bar (visible when there are pending changes) -->\n<div *ngIf=\"hasPendingChanges\" class=\"action-bar\">\n <div class=\"container-fluid d-flex justify-content-between align-items-center\">\n <span class=\"text-white\">\n <i class=\"bi bi-pencil-square me-1\" aria-hidden=\"true\"></i>\n <strong>{{ pendingChangeCount }}</strong>\n unsaved change{{ pendingChangeCount !== 1 ? 's' : '' }}\n </span>\n <div>\n <button class=\"btn btn-outline-light btn-sm me-2\" (click)=\"discardAll()\">\n <i class=\"bi bi-x-lg me-1\" aria-hidden=\"true\"></i>Discard All\n </button>\n <button class=\"btn btn-warning btn-sm fw-semibold\" (click)=\"openSummaryModal()\">\n <i class=\"bi bi-send me-1\" aria-hidden=\"true\"></i>Review &amp; Submit\n </button>\n </div>\n </div>\n</div>\n\n<!-- Changes Summary Modal -->\n<valar-changes-summary-modal\n *ngIf=\"showSummaryModal\"\n [changes]=\"pendingChangesList\"\n [tenantId]=\"tenantId\"\n (confirm)=\"submitAll()\"\n (dismiss)=\"closeSummaryModal()\">\n</valar-changes-summary-modal>\n\n","import { Injectable } from '@angular/core';\nimport { Observable, of, delay, throwError } from 'rxjs';\nimport {\n FeatureFlagsDocument,\n FlagGroup,\n RESERVED_KEYS,\n UpsertFlagsPayload,\n} from '../models';\n\n/**\n * In-memory mock of FeatureFlagsApiService.\n * Ships with 2 tenants pre-loaded so you can test the UI\n * without spinning up the backend.\n *\n * Use via `FeatureFlagsModule.forTesting()`.\n */\n@Injectable()\nexport class MockFeatureFlagsApiService {\n private readonly store = new Map<string, FeatureFlagsDocument>();\n\n constructor() {\n this.seedData();\n }\n\n // ────────────────────────── Public API (same shape as real service) ──\n\n getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null> {\n const doc = this.store.get(this.toTenantToken(tenantId)) ?? null;\n return of(this.clone(doc)).pipe(delay(250));\n }\n\n upsert(tenantId: string, payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument> {\n const tenantToken = this.toTenantToken(tenantId);\n let doc = this.store.get(tenantToken);\n if (!doc) {\n doc = this.newDoc(tenantToken);\n this.store.set(tenantToken, doc);\n }\n\n // Merge groups via dot-path (mirrors the backend's $set behavior)\n for (const [groupName, groupFlags] of Object.entries(payload)) {\n if (RESERVED_KEYS.has(groupName)) continue;\n const existing = (doc[groupName] as FlagGroup) ?? {};\n doc[groupName] = { ...existing, ...(groupFlags as FlagGroup) };\n }\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument> {\n const tenantToken = this.toTenantToken(tenantId);\n const doc = this.store.get(tenantToken);\n if (!doc) {\n return throwError(() => ({\n error: { message: `No feature flags found for tenantId='${tenantToken}'` },\n }));\n }\n\n const g = doc[group] as FlagGroup | undefined;\n if (g && key in g) {\n delete g[key];\n }\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument> {\n const tenantToken = this.toTenantToken(tenantId);\n const doc = this.store.get(tenantToken);\n if (!doc) {\n return throwError(() => ({\n error: { message: `No feature flags found for tenantId='${tenantToken}'` },\n }));\n }\n\n delete doc[group];\n doc.updatedAt = new Date().toISOString();\n\n return of(this.clone(doc)!).pipe(delay(200));\n }\n\n // ────────────────────────── Seed data ────────────────────────────────\n\n private seedData(): void {\n this.store.set('tenant_acme', this.buildAcmeTenant());\n this.store.set('tenant_globex', this.buildGlobexTenant());\n }\n\n private buildAcmeTenant(): FeatureFlagsDocument {\n return {\n _id: '6650a1b2c3d4e5f6a7b8c9d0',\n tenantId: 'tenant_acme',\n createdAt: '2025-11-01T08:00:00.000Z',\n updatedAt: '2026-02-15T14:30:00.000Z',\n uiOptions: {\n darkMode: true,\n sidebarCollapsed: false,\n maxItemsPerPage: 25,\n accentColor: '#4f46e5',\n showBetaBanner: true,\n },\n checkout: {\n newCheckoutFlow: true,\n enableGiftCards: false,\n maxCartItems: 50,\n taxCalculation: 'inclusive',\n },\n notifications: {\n emailEnabled: true,\n smsEnabled: false,\n pushEnabled: true,\n dailyDigest: true,\n marketingOptIn: false,\n },\n search: {\n enableFuzzySearch: true,\n maxResults: 100,\n highlightMatches: true,\n searchDelay: 300,\n },\n } as unknown as FeatureFlagsDocument;\n }\n\n private buildGlobexTenant(): FeatureFlagsDocument {\n return {\n _id: '6650b2c3d4e5f6a7b8c9d0e1',\n tenantId: 'tenant_globex',\n createdAt: '2025-12-10T10:00:00.000Z',\n updatedAt: '2026-02-16T09:15:00.000Z',\n uiOptions: {\n darkMode: false,\n sidebarCollapsed: true,\n maxItemsPerPage: 10,\n accentColor: '#059669',\n showBetaBanner: false,\n },\n billing: {\n enableAutoPay: true,\n gracePeriodDays: 7,\n currency: 'USD',\n showInvoiceBreakdown: true,\n lateFeePercent: 1.5,\n },\n featureAccess: {\n analyticsV2: true,\n exportToCsv: true,\n bulkOperations: false,\n advancedReporting: true,\n apiRateLimit: 1000,\n },\n integrations: {\n slackEnabled: true,\n slackWebhookConfigured: true,\n jiraEnabled: false,\n githubEnabled: true,\n webhookRetries: 3,\n },\n } as unknown as FeatureFlagsDocument;\n }\n\n // ────────────────────────── Helpers ──────────────────────────────────\n\n private newDoc(tenantId: string): FeatureFlagsDocument {\n const now = new Date().toISOString();\n return { tenantId, createdAt: now, updatedAt: now };\n }\n\n private toTenantToken(tenantId: string): string {\n const normalized = tenantId.trim();\n if (normalized.startsWith('tenant_')) return normalized;\n return `tenant_${normalized}`;\n }\n\n /** Deep-clone to avoid leaking mutable references */\n private clone<T>(obj: T): T {\n return JSON.parse(JSON.stringify(obj));\n }\n}\n","import { ModuleWithProviders, NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\nimport { provideHttpClient } from '@angular/common/http';\nimport { FeatureFlagsManagerComponent } from './components/feature-flags-manager/feature-flags-manager.component';\nimport { FlagGroupCardComponent } from './components/flag-group-card/flag-group-card.component';\nimport { ChangesSummaryModalComponent } from './components/changes-summary-modal/changes-summary-modal.component';\nimport { FEATURE_FLAGS_CONFIG, FeatureFlagsConfig } from './models';\nimport { FeatureFlagsApiService } from './services/feature-flags-api.service';\nimport { MockFeatureFlagsApiService } from './services/mock-feature-flags-api.service';\n\n@NgModule({\n declarations: [\n FeatureFlagsManagerComponent,\n FlagGroupCardComponent,\n ChangesSummaryModalComponent,\n ],\n imports: [\n CommonModule,\n FormsModule,\n ],\n exports: [\n FeatureFlagsManagerComponent,\n FlagGroupCardComponent,\n ChangesSummaryModalComponent,\n ],\n})\nexport class FeatureFlagsModule {\n /**\n * Call forRoot() in the consuming app to provide\n * the API configuration.\n *\n * @example\n * FeatureFlagsModule.forRoot({\n * apiBaseUrl: 'http://localhost:3000',\n * apiVersion: 'v1',\n * rootTenantId: 'root001', // sent in x-tenant-id header\n * })\n *\n * <valar-feature-flags-manager\n * [(tenantId)]=\"selectedTenantId\">\n * </valar-feature-flags-manager>\n */\n static forRoot(config: FeatureFlagsConfig): ModuleWithProviders<FeatureFlagsModule> {\n return {\n ngModule: FeatureFlagsModule,\n providers: [\n { provide: FEATURE_FLAGS_CONFIG, useValue: config },\n FeatureFlagsApiService,\n provideHttpClient(),\n ],\n };\n }\n\n /**\n * Call forTesting() in the consuming app to use\n * in-memory mock data (no backend needed).\n *\n * Ships with 2 pre-loaded tenants:\n * - \"tenant_acme\" (Acme Corp)\n * - \"tenant_globex\" (Globex Inc)\n *\n * @example\n * FeatureFlagsModule.forTesting()\n */\n static forTesting(): ModuleWithProviders<FeatureFlagsModule> {\n return {\n ngModule: FeatureFlagsModule,\n providers: [\n { provide: FeatureFlagsApiService, useClass: MockFeatureFlagsApiService },\n ],\n };\n }\n}\n","/*\n * Public API Surface of valar-ui\n */\nimport '@angular/localize/init';\n\n// Feature Flags\nexport * from './lib/feature-flags/feature-flags.module';\nexport * from './lib/feature-flags/models';\nexport * from './lib/feature-flags/services/feature-flags-api.service';\nexport * from './lib/feature-flags/services/mock-feature-flags-api.service';\nexport * from './lib/feature-flags/components/feature-flags-manager/feature-flags-manager.component';\nexport * from './lib/feature-flags/components/flag-group-card/flag-group-card.component';\nexport * from './lib/feature-flags/components/changes-summary-modal/changes-summary-modal.component';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":["i1","i1.FeatureFlagsApiService","i2","i3","i4.FlagGroupCardComponent","i5.ChangesSummaryModalComponent"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pl4yzonellc/valar-ui",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "peerDependencies": {
5
5
  "@angular/common": "^21.0.5",
6
6
  "@angular/core": "^21.0.5",
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, OnDestroy, EventEmitter, ModuleWithProviders } from '@angular/core';
2
+ import { InjectionToken, OnDestroy, OnChanges, EventEmitter, SimpleChanges, ModuleWithProviders } from '@angular/core';
3
3
  import { HttpClient } from '@angular/common/http';
4
4
  import { Observable } from 'rxjs';
5
5
  import * as i4 from '@angular/common';
@@ -27,10 +27,9 @@ interface FeatureFlagsDocument {
27
27
  }
28
28
  /**
29
29
  * Shape of the upsert request body sent to the backend.
30
- * tenantId + any number of group objects.
30
+ * Service will inject tenantId + any number of group objects.
31
31
  */
32
32
  interface UpsertFlagsPayload {
33
- tenantId: string;
34
33
  [groupName: string]: FlagGroup | string;
35
34
  }
36
35
  /**
@@ -42,6 +41,10 @@ interface FeatureFlagsConfig {
42
41
  apiBaseUrl: string;
43
42
  /** API version prefix, defaults to 'v1' */
44
43
  apiVersion?: string;
44
+ /** Root tenant id sent in request headers */
45
+ rootTenantId: string;
46
+ /** Tenant header name, defaults to 'x-tenant-id' */
47
+ tenantHeaderName?: string;
45
48
  }
46
49
  /** Injection token for the library config */
47
50
  declare const FEATURE_FLAGS_CONFIG: InjectionToken<FeatureFlagsConfig>;
@@ -64,15 +67,20 @@ interface PendingChange {
64
67
  declare class FeatureFlagsApiService {
65
68
  private readonly http;
66
69
  private readonly baseUrl;
70
+ private readonly rootTenantId;
71
+ private readonly tenantHeaderName;
67
72
  constructor(http: HttpClient, config: FeatureFlagsConfig);
68
- /** GET /v1/feature-flags/:tenantId */
73
+ /** GET /v1/feature-flags/tenant_<tenantId> */
69
74
  getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null>;
70
75
  /** PUT /v1/feature-flags (merge-upsert) */
71
- upsert(payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument>;
72
- /** DELETE /v1/feature-flags/:tenantId/:group/:key */
76
+ upsert(tenantId: string, payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument>;
77
+ /** DELETE /v1/feature-flags/tenant_<tenantId>/:group/:key */
73
78
  deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument>;
74
- /** DELETE /v1/feature-flags/:tenantId/:group */
79
+ /** DELETE /v1/feature-flags/tenant_<tenantId>/:group */
75
80
  deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument>;
81
+ private tenantScopedBaseUrl;
82
+ private toTenantToken;
83
+ private requestOptions;
76
84
  static ɵfac: i0.ɵɵFactoryDeclaration<FeatureFlagsApiService, never>;
77
85
  static ɵprov: i0.ɵɵInjectableDeclaration<FeatureFlagsApiService>;
78
86
  }
@@ -84,9 +92,10 @@ interface ParsedGroup {
84
92
  value: FlagPrimitive;
85
93
  }[];
86
94
  }
87
- declare class FeatureFlagsManagerComponent implements OnDestroy {
95
+ declare class FeatureFlagsManagerComponent implements OnDestroy, OnChanges {
88
96
  private readonly api;
89
97
  tenantId: string;
98
+ tenantIdChange: EventEmitter<string>;
90
99
  groups: ParsedGroup[];
91
100
  loading: boolean;
92
101
  error: string | null;
@@ -104,8 +113,10 @@ declare class FeatureFlagsManagerComponent implements OnDestroy {
104
113
  */
105
114
  pendingChanges: Map<string, PendingChange>;
106
115
  showSummaryModal: boolean;
116
+ private readonly newlyCreatedGroups;
107
117
  private readonly destroy$;
108
118
  constructor(api: FeatureFlagsApiService);
119
+ ngOnChanges(changes: SimpleChanges): void;
109
120
  ngOnDestroy(): void;
110
121
  get hasPendingChanges(): boolean;
111
122
  get pendingChangeCount(): number;
@@ -134,14 +145,17 @@ declare class FeatureFlagsManagerComponent implements OnDestroy {
134
145
  deleteFlag(groupName: string, flagKey: string): void;
135
146
  deleteGroup(groupName: string): void;
136
147
  trackGroup(_index: number, group: ParsedGroup): string;
148
+ isNewGroup(group: ParsedGroup): boolean;
137
149
  private parseDocument;
138
150
  private coerceValue;
139
151
  private clearMessages;
140
152
  private makeCompositeKey;
141
153
  private splitCompositeKey;
142
154
  private valuesEqual;
155
+ private ensureGroupVisible;
156
+ private reconcileNewGroupBadges;
143
157
  static ɵfac: i0.ɵɵFactoryDeclaration<FeatureFlagsManagerComponent, never>;
144
- static ɵcmp: i0.ɵɵComponentDeclaration<FeatureFlagsManagerComponent, "valar-feature-flags-manager", never, {}, {}, never, never, false, never>;
158
+ static ɵcmp: i0.ɵɵComponentDeclaration<FeatureFlagsManagerComponent, "valar-feature-flags-manager", never, { "tenantId": { "alias": "tenantId"; "required": false; }; }, { "tenantIdChange": "tenantIdChange"; }, never, never, false, never>;
145
159
  }
146
160
 
147
161
  interface FlagEntry {
@@ -156,6 +170,7 @@ interface FlagChangedEvent {
156
170
  declare class FlagGroupCardComponent {
157
171
  groupName: string;
158
172
  flags: FlagEntry[];
173
+ isNewGroup: boolean;
159
174
  dirtyKeys: Set<string>;
160
175
  isAddingFlag: boolean;
161
176
  newFlagKey: string;
@@ -195,7 +210,7 @@ declare class FlagGroupCardComponent {
195
210
  private detectType;
196
211
  private coerceValue;
197
212
  static ɵfac: i0.ɵɵFactoryDeclaration<FlagGroupCardComponent, never>;
198
- static ɵcmp: i0.ɵɵComponentDeclaration<FlagGroupCardComponent, "valar-flag-group-card", never, { "groupName": { "alias": "groupName"; "required": false; }; "flags": { "alias": "flags"; "required": false; }; "dirtyKeys": { "alias": "dirtyKeys"; "required": false; }; "isAddingFlag": { "alias": "isAddingFlag"; "required": false; }; "newFlagKey": { "alias": "newFlagKey"; "required": false; }; "newFlagValue": { "alias": "newFlagValue"; "required": false; }; "newFlagType": { "alias": "newFlagType"; "required": false; }; }, { "flagChanged": "flagChanged"; "revertFlag": "revertFlag"; "addFlagRequest": "addFlagRequest"; "cancelAddFlag": "cancelAddFlag"; "confirmAddFlag": "confirmAddFlag"; "newFlagKeyChange": "newFlagKeyChange"; "newFlagValueChange": "newFlagValueChange"; "newFlagTypeChange": "newFlagTypeChange"; "deleteFlagRequest": "deleteFlagRequest"; "deleteGroupRequest": "deleteGroupRequest"; }, never, never, false, never>;
213
+ static ɵcmp: i0.ɵɵComponentDeclaration<FlagGroupCardComponent, "valar-flag-group-card", never, { "groupName": { "alias": "groupName"; "required": false; }; "flags": { "alias": "flags"; "required": false; }; "isNewGroup": { "alias": "isNewGroup"; "required": false; }; "dirtyKeys": { "alias": "dirtyKeys"; "required": false; }; "isAddingFlag": { "alias": "isAddingFlag"; "required": false; }; "newFlagKey": { "alias": "newFlagKey"; "required": false; }; "newFlagValue": { "alias": "newFlagValue"; "required": false; }; "newFlagType": { "alias": "newFlagType"; "required": false; }; }, { "flagChanged": "flagChanged"; "revertFlag": "revertFlag"; "addFlagRequest": "addFlagRequest"; "cancelAddFlag": "cancelAddFlag"; "confirmAddFlag": "confirmAddFlag"; "newFlagKeyChange": "newFlagKeyChange"; "newFlagValueChange": "newFlagValueChange"; "newFlagTypeChange": "newFlagTypeChange"; "deleteFlagRequest": "deleteFlagRequest"; "deleteGroupRequest": "deleteGroupRequest"; }, never, never, false, never>;
199
214
  }
200
215
 
201
216
  declare class ChangesSummaryModalComponent {
@@ -218,7 +233,12 @@ declare class FeatureFlagsModule {
218
233
  * FeatureFlagsModule.forRoot({
219
234
  * apiBaseUrl: 'http://localhost:3000',
220
235
  * apiVersion: 'v1',
236
+ * rootTenantId: 'root001', // sent in x-tenant-id header
221
237
  * })
238
+ *
239
+ * <valar-feature-flags-manager
240
+ * [(tenantId)]="selectedTenantId">
241
+ * </valar-feature-flags-manager>
222
242
  */
223
243
  static forRoot(config: FeatureFlagsConfig): ModuleWithProviders<FeatureFlagsModule>;
224
244
  /**
@@ -249,13 +269,14 @@ declare class MockFeatureFlagsApiService {
249
269
  private readonly store;
250
270
  constructor();
251
271
  getByTenantId(tenantId: string): Observable<FeatureFlagsDocument | null>;
252
- upsert(payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument>;
272
+ upsert(tenantId: string, payload: UpsertFlagsPayload): Observable<FeatureFlagsDocument>;
253
273
  deleteFlag(tenantId: string, group: string, key: string): Observable<FeatureFlagsDocument>;
254
274
  deleteGroup(tenantId: string, group: string): Observable<FeatureFlagsDocument>;
255
275
  private seedData;
256
276
  private buildAcmeTenant;
257
277
  private buildGlobexTenant;
258
278
  private newDoc;
279
+ private toTenantToken;
259
280
  /** Deep-clone to avoid leaking mutable references */
260
281
  private clone;
261
282
  static ɵfac: i0.ɵɵFactoryDeclaration<MockFeatureFlagsApiService, never>;