@pl4yzonellc/valar-ui 1.0.4 → 1.0.5
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.
|
@@ -99,6 +99,7 @@ class FlagGroupCardComponent {
|
|
|
99
99
|
this.newFlagKey = '';
|
|
100
100
|
this.newFlagValue = '';
|
|
101
101
|
this.newFlagType = 'string';
|
|
102
|
+
this.newFlagObjectEntries = [];
|
|
102
103
|
this.flagChanged = new EventEmitter();
|
|
103
104
|
this.revertFlag = new EventEmitter();
|
|
104
105
|
this.addFlagRequest = new EventEmitter();
|
|
@@ -107,6 +108,7 @@ class FlagGroupCardComponent {
|
|
|
107
108
|
this.newFlagKeyChange = new EventEmitter();
|
|
108
109
|
this.newFlagValueChange = new EventEmitter();
|
|
109
110
|
this.newFlagTypeChange = new EventEmitter();
|
|
111
|
+
this.newFlagObjectEntriesChange = new EventEmitter();
|
|
110
112
|
this.deleteFlagRequest = new EventEmitter();
|
|
111
113
|
this.deleteGroupRequest = new EventEmitter();
|
|
112
114
|
this.collapsed = false;
|
|
@@ -116,6 +118,7 @@ class FlagGroupCardComponent {
|
|
|
116
118
|
this.editingKey = null;
|
|
117
119
|
this.editValue = '';
|
|
118
120
|
this.editType = 'string';
|
|
121
|
+
this.editObjectEntries = [];
|
|
119
122
|
}
|
|
120
123
|
isDirty(key) {
|
|
121
124
|
return this.dirtyKeys.has(key);
|
|
@@ -124,21 +127,30 @@ class FlagGroupCardComponent {
|
|
|
124
127
|
startEdit(flag) {
|
|
125
128
|
this.editingKey = flag.key;
|
|
126
129
|
this.editType = this.detectType(flag.value);
|
|
130
|
+
if (this.isObjectFlag(flag.value)) {
|
|
131
|
+
this.editObjectEntries = this.toObjectEntries(flag.value);
|
|
132
|
+
this.editValue = '';
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.editObjectEntries = [];
|
|
127
136
|
this.editValue = flag.value === null ? '' : String(flag.value);
|
|
128
137
|
}
|
|
129
138
|
/** Commit the inline edit — emits change to parent, stays in view mode */
|
|
130
139
|
commitEdit(flag) {
|
|
131
|
-
const newValue = this.coerceValue(this.editValue, this.editType);
|
|
140
|
+
const newValue = this.coerceValue(this.editValue, this.editType, this.editObjectEntries);
|
|
132
141
|
this.flagChanged.emit({ key: flag.key, oldValue: flag.value, newValue });
|
|
133
142
|
this.editingKey = null;
|
|
134
143
|
}
|
|
135
144
|
/** Cancel inline edit without staging anything */
|
|
136
145
|
cancelEdit() {
|
|
137
146
|
this.editingKey = null;
|
|
147
|
+
this.editObjectEntries = [];
|
|
138
148
|
}
|
|
139
149
|
displayValue(value) {
|
|
140
150
|
if (value === null)
|
|
141
151
|
return 'null';
|
|
152
|
+
if (this.isObjectFlag(value))
|
|
153
|
+
return JSON.stringify(value);
|
|
142
154
|
if (typeof value === 'boolean')
|
|
143
155
|
return value ? 'true' : 'false';
|
|
144
156
|
return String(value);
|
|
@@ -150,16 +162,56 @@ class FlagGroupCardComponent {
|
|
|
150
162
|
return 'bg-warning text-dark';
|
|
151
163
|
if (value === null)
|
|
152
164
|
return 'bg-secondary';
|
|
165
|
+
if (this.isObjectFlag(value))
|
|
166
|
+
return 'bg-dark';
|
|
153
167
|
return 'bg-primary';
|
|
154
168
|
}
|
|
155
169
|
typeLabel(value) {
|
|
156
170
|
if (value === null)
|
|
157
171
|
return 'null';
|
|
172
|
+
if (this.isObjectFlag(value))
|
|
173
|
+
return 'object';
|
|
158
174
|
return typeof value;
|
|
159
175
|
}
|
|
160
176
|
isBooleanFlag(value) {
|
|
161
177
|
return typeof value === 'boolean';
|
|
162
178
|
}
|
|
179
|
+
isObjectFlag(value) {
|
|
180
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
181
|
+
}
|
|
182
|
+
onEditTypeChange(type) {
|
|
183
|
+
this.editType = type;
|
|
184
|
+
if (type === 'object') {
|
|
185
|
+
this.editValue = '';
|
|
186
|
+
this.editObjectEntries = this.editObjectEntries.length > 0 ? this.editObjectEntries : [this.createObjectEntry()];
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.editObjectEntries = [];
|
|
190
|
+
}
|
|
191
|
+
onNewFlagTypeChange(type) {
|
|
192
|
+
this.newFlagTypeChange.emit(type);
|
|
193
|
+
if (type === 'object' && this.newFlagObjectEntries.length === 0) {
|
|
194
|
+
this.newFlagObjectEntriesChange.emit([this.createObjectEntry()]);
|
|
195
|
+
}
|
|
196
|
+
if (type !== 'object' && this.newFlagObjectEntries.length > 0) {
|
|
197
|
+
this.newFlagObjectEntriesChange.emit([]);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
addEditObjectField() {
|
|
201
|
+
this.editObjectEntries = [...this.editObjectEntries, this.createObjectEntry()];
|
|
202
|
+
}
|
|
203
|
+
removeEditObjectField(index) {
|
|
204
|
+
this.editObjectEntries = this.editObjectEntries.filter((_, entryIndex) => entryIndex !== index);
|
|
205
|
+
}
|
|
206
|
+
addNewObjectField() {
|
|
207
|
+
this.newFlagObjectEntriesChange.emit([...this.newFlagObjectEntries, this.createObjectEntry()]);
|
|
208
|
+
}
|
|
209
|
+
removeNewObjectField(index) {
|
|
210
|
+
this.newFlagObjectEntriesChange.emit(this.newFlagObjectEntries.filter((_, entryIndex) => entryIndex !== index));
|
|
211
|
+
}
|
|
212
|
+
trackObjectField(index) {
|
|
213
|
+
return index;
|
|
214
|
+
}
|
|
163
215
|
trackFlag(_index, flag) {
|
|
164
216
|
return flag.key;
|
|
165
217
|
}
|
|
@@ -186,10 +238,21 @@ class FlagGroupCardComponent {
|
|
|
186
238
|
return 'boolean';
|
|
187
239
|
if (typeof value === 'number')
|
|
188
240
|
return 'number';
|
|
241
|
+
if (this.isObjectFlag(value))
|
|
242
|
+
return 'object';
|
|
189
243
|
return 'string';
|
|
190
244
|
}
|
|
191
|
-
coerceValue(raw, type) {
|
|
245
|
+
coerceValue(raw, type, objectEntries = []) {
|
|
192
246
|
const trimmed = raw.trim();
|
|
247
|
+
if (type === 'object') {
|
|
248
|
+
return objectEntries.reduce((acc, entry) => {
|
|
249
|
+
const key = entry.key.trim();
|
|
250
|
+
if (!key)
|
|
251
|
+
return acc;
|
|
252
|
+
acc[key] = entry.value;
|
|
253
|
+
return acc;
|
|
254
|
+
}, {});
|
|
255
|
+
}
|
|
193
256
|
if (trimmed === '' || trimmed.toLowerCase() === 'null')
|
|
194
257
|
return null;
|
|
195
258
|
switch (type) {
|
|
@@ -203,12 +266,19 @@ class FlagGroupCardComponent {
|
|
|
203
266
|
return trimmed;
|
|
204
267
|
}
|
|
205
268
|
}
|
|
269
|
+
toObjectEntries(value) {
|
|
270
|
+
const entries = Object.entries(value).map(([key, entryValue]) => ({ key, value: entryValue }));
|
|
271
|
+
return entries.length > 0 ? entries : [this.createObjectEntry()];
|
|
272
|
+
}
|
|
273
|
+
createObjectEntry() {
|
|
274
|
+
return { key: '', value: '' };
|
|
275
|
+
}
|
|
206
276
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FlagGroupCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
207
|
-
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"] }] }); }
|
|
277
|
+
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", newFlagObjectEntries: "newFlagObjectEntries" }, outputs: { flagChanged: "flagChanged", revertFlag: "revertFlag", addFlagRequest: "addFlagRequest", cancelAddFlag: "cancelAddFlag", confirmAddFlag: "confirmAddFlag", newFlagKeyChange: "newFlagKeyChange", newFlagValueChange: "newFlagValueChange", newFlagTypeChange: "newFlagTypeChange", newFlagObjectEntriesChange: "newFlagObjectEntriesChange", 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 <pre *ngIf=\"isObjectFlag(flag.value); else scalarValue\" class=\"mb-0 object-value\">{{ displayValue(flag.value) }}</pre>\n <ng-template #scalarValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\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 (ngModelChange)=\"onEditTypeChange($event)\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n <option value=\"object\">object</option>\n </select>\n <ng-container *ngIf=\"editType === 'object'; else editScalarInput\">\n <div class=\"object-editor flex-fill\">\n <div *ngFor=\"let entry of editObjectEntries; trackBy: trackObjectField; let i = index\"\n class=\"input-group input-group-sm mb-2\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child key\"\n [(ngModel)]=\"entry.key\"\n aria-label=\"Object child key\" />\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child value\"\n [(ngModel)]=\"entry.value\"\n aria-label=\"Object child value\" />\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"removeEditObjectField(i)\"\n aria-label=\"Remove object child\">\n <i class=\"bi bi-dash-lg\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addEditObjectField()\">\n <i class=\"bi bi-plus-lg me-1\" aria-hidden=\"true\"></i>Add Child\n </button>\n </div>\n </ng-container>\n <ng-template #editScalarInput>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </ng-template>\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)=\"onNewFlagTypeChange($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 <option value=\"object\">object</option>\n </select>\n </div>\n <div class=\"col-md-5\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTypedInput\">\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 #newTypedInput>\n <div *ngIf=\"newFlagType === 'object'; else newTextInput\" class=\"object-editor\">\n <div *ngFor=\"let entry of newFlagObjectEntries; trackBy: trackObjectField; let i = index\"\n class=\"input-group input-group-sm mb-2\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child key\"\n [(ngModel)]=\"entry.key\"\n aria-label=\"New object child key\" />\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child value\"\n [(ngModel)]=\"entry.value\"\n aria-label=\"New object child value\" />\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"removeNewObjectField(i)\"\n aria-label=\"Remove new object child\">\n <i class=\"bi bi-dash-lg\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button type=\"button\" class=\"btn btn-outline-primary btn-sm\" (click)=\"addNewObjectField()\">\n <i class=\"bi bi-plus-lg me-1\" aria-hidden=\"true\"></i>Add Child\n </button>\n </div>\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 </ng-template>\n </div>\n <div class=\"col-md-2\">\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}.object-value{white-space:pre-wrap;word-break:break-word;font-size:.8rem}.object-editor{min-width:0}\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"] }] }); }
|
|
208
278
|
}
|
|
209
279
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FlagGroupCardComponent, decorators: [{
|
|
210
280
|
type: Component,
|
|
211
|
-
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"] }]
|
|
281
|
+
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 <pre *ngIf=\"isObjectFlag(flag.value); else scalarValue\" class=\"mb-0 object-value\">{{ displayValue(flag.value) }}</pre>\n <ng-template #scalarValue>\n <span class=\"text-break\">{{ displayValue(flag.value) }}</span>\n </ng-template>\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 (ngModelChange)=\"onEditTypeChange($event)\"\n aria-label=\"Flag type\">\n <option value=\"string\">string</option>\n <option value=\"number\">number</option>\n <option value=\"object\">object</option>\n </select>\n <ng-container *ngIf=\"editType === 'object'; else editScalarInput\">\n <div class=\"object-editor flex-fill\">\n <div *ngFor=\"let entry of editObjectEntries; trackBy: trackObjectField; let i = index\"\n class=\"input-group input-group-sm mb-2\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child key\"\n [(ngModel)]=\"entry.key\"\n aria-label=\"Object child key\" />\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child value\"\n [(ngModel)]=\"entry.value\"\n aria-label=\"Object child value\" />\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"removeEditObjectField(i)\"\n aria-label=\"Remove object child\">\n <i class=\"bi bi-dash-lg\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button\n type=\"button\"\n class=\"btn btn-outline-primary btn-sm\"\n (click)=\"addEditObjectField()\">\n <i class=\"bi bi-plus-lg me-1\" aria-hidden=\"true\"></i>Add Child\n </button>\n </div>\n </ng-container>\n <ng-template #editScalarInput>\n <input\n type=\"text\"\n class=\"form-control\"\n [(ngModel)]=\"editValue\"\n (keyup.enter)=\"commitEdit(flag)\"\n aria-label=\"Flag value\" />\n </ng-template>\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)=\"onNewFlagTypeChange($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 <option value=\"object\">object</option>\n </select>\n </div>\n <div class=\"col-md-5\">\n <label class=\"form-label form-label-sm\">Value</label>\n <ng-container *ngIf=\"newFlagType === 'boolean'; else newTypedInput\">\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 #newTypedInput>\n <div *ngIf=\"newFlagType === 'object'; else newTextInput\" class=\"object-editor\">\n <div *ngFor=\"let entry of newFlagObjectEntries; trackBy: trackObjectField; let i = index\"\n class=\"input-group input-group-sm mb-2\">\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child key\"\n [(ngModel)]=\"entry.key\"\n aria-label=\"New object child key\" />\n <input\n type=\"text\"\n class=\"form-control\"\n placeholder=\"child value\"\n [(ngModel)]=\"entry.value\"\n aria-label=\"New object child value\" />\n <button\n type=\"button\"\n class=\"btn btn-outline-danger\"\n (click)=\"removeNewObjectField(i)\"\n aria-label=\"Remove new object child\">\n <i class=\"bi bi-dash-lg\" aria-hidden=\"true\"></i>\n </button>\n </div>\n <button type=\"button\" class=\"btn btn-outline-primary btn-sm\" (click)=\"addNewObjectField()\">\n <i class=\"bi bi-plus-lg me-1\" aria-hidden=\"true\"></i>Add Child\n </button>\n </div>\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 </ng-template>\n </div>\n <div class=\"col-md-2\">\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}.object-value{white-space:pre-wrap;word-break:break-word;font-size:.8rem}.object-editor{min-width:0}\n"] }]
|
|
212
282
|
}], propDecorators: { groupName: [{
|
|
213
283
|
type: Input
|
|
214
284
|
}], flags: [{
|
|
@@ -225,6 +295,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
|
|
|
225
295
|
type: Input
|
|
226
296
|
}], newFlagType: [{
|
|
227
297
|
type: Input
|
|
298
|
+
}], newFlagObjectEntries: [{
|
|
299
|
+
type: Input
|
|
228
300
|
}], flagChanged: [{
|
|
229
301
|
type: Output
|
|
230
302
|
}], revertFlag: [{
|
|
@@ -241,6 +313,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImpor
|
|
|
241
313
|
type: Output
|
|
242
314
|
}], newFlagTypeChange: [{
|
|
243
315
|
type: Output
|
|
316
|
+
}], newFlagObjectEntriesChange: [{
|
|
317
|
+
type: Output
|
|
244
318
|
}], deleteFlagRequest: [{
|
|
245
319
|
type: Output
|
|
246
320
|
}], deleteGroupRequest: [{
|
|
@@ -257,6 +331,8 @@ class ChangesSummaryModalComponent {
|
|
|
257
331
|
displayValue(value) {
|
|
258
332
|
if (value === null)
|
|
259
333
|
return 'null';
|
|
334
|
+
if (this.isObjectValue(value))
|
|
335
|
+
return JSON.stringify(value);
|
|
260
336
|
if (typeof value === 'boolean')
|
|
261
337
|
return value ? 'true' : 'false';
|
|
262
338
|
return String(value);
|
|
@@ -264,8 +340,13 @@ class ChangesSummaryModalComponent {
|
|
|
264
340
|
typeLabel(value) {
|
|
265
341
|
if (value === null)
|
|
266
342
|
return 'null';
|
|
343
|
+
if (this.isObjectValue(value))
|
|
344
|
+
return 'object';
|
|
267
345
|
return typeof value;
|
|
268
346
|
}
|
|
347
|
+
isObjectValue(value) {
|
|
348
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
349
|
+
}
|
|
269
350
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: ChangesSummaryModalComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
270
351
|
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.5", type: ChangesSummaryModalComponent, isStandalone: false, selector: "valar-changes-summary-modal", inputs: { changes: "changes", tenantId: "tenantId" }, outputs: { confirm: "confirm", dismiss: "dismiss" }, ngImport: i0, template: "<!-- 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", styles: [":host{display:block}.modal-backdrop{z-index:1040}.modal{z-index:1050}.text-decoration-line-through{text-decoration:line-through}\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"] }] }); }
|
|
271
352
|
}
|
|
@@ -300,6 +381,7 @@ class FeatureFlagsManagerComponent {
|
|
|
300
381
|
this.newFlagKey = '';
|
|
301
382
|
this.newFlagValue = '';
|
|
302
383
|
this.newFlagType = 'string';
|
|
384
|
+
this.newFlagObjectEntries = [];
|
|
303
385
|
// ── Batch-edit state ──────────────────────────────────────────────
|
|
304
386
|
/**
|
|
305
387
|
* Pending changes keyed by "group.key".
|
|
@@ -475,7 +557,7 @@ class FeatureFlagsManagerComponent {
|
|
|
475
557
|
const key = this.newFlagKey.trim();
|
|
476
558
|
if (!key)
|
|
477
559
|
return;
|
|
478
|
-
const value = this.coerceValue(this.newFlagValue, this.newFlagType);
|
|
560
|
+
const value = this.coerceValue(this.newFlagValue, this.newFlagType, this.newFlagObjectEntries);
|
|
479
561
|
const compositeKey = this.makeCompositeKey(groupName, key);
|
|
480
562
|
this.pendingChanges.set(compositeKey, {
|
|
481
563
|
group: groupName,
|
|
@@ -488,11 +570,14 @@ class FeatureFlagsManagerComponent {
|
|
|
488
570
|
this.newFlagKey = '';
|
|
489
571
|
this.newFlagValue = '';
|
|
490
572
|
this.newFlagType = 'string';
|
|
573
|
+
this.newFlagObjectEntries = [];
|
|
491
574
|
}
|
|
492
575
|
cancelAddFlag() {
|
|
493
576
|
this.addingFlagToGroup = null;
|
|
494
577
|
this.newFlagKey = '';
|
|
495
578
|
this.newFlagValue = '';
|
|
579
|
+
this.newFlagType = 'string';
|
|
580
|
+
this.newFlagObjectEntries = [];
|
|
496
581
|
}
|
|
497
582
|
// ── Deletes (immediate, not batched) ──────────────────────────────
|
|
498
583
|
deleteFlag(groupName, flagKey) {
|
|
@@ -556,8 +641,17 @@ class FeatureFlagsManagerComponent {
|
|
|
556
641
|
}
|
|
557
642
|
this.groups.sort((a, b) => a.name.localeCompare(b.name));
|
|
558
643
|
}
|
|
559
|
-
coerceValue(raw, type) {
|
|
644
|
+
coerceValue(raw, type, objectEntries = []) {
|
|
560
645
|
const trimmed = raw.trim();
|
|
646
|
+
if (type === 'object') {
|
|
647
|
+
return objectEntries.reduce((acc, entry) => {
|
|
648
|
+
const key = entry.key.trim();
|
|
649
|
+
if (!key)
|
|
650
|
+
return acc;
|
|
651
|
+
acc[key] = entry.value;
|
|
652
|
+
return acc;
|
|
653
|
+
}, {});
|
|
654
|
+
}
|
|
561
655
|
if (trimmed === '' || trimmed.toLowerCase() === 'null')
|
|
562
656
|
return null;
|
|
563
657
|
switch (type) {
|
|
@@ -583,7 +677,7 @@ class FeatureFlagsManagerComponent {
|
|
|
583
677
|
return [composite.substring(0, idx), composite.substring(idx + 2)];
|
|
584
678
|
}
|
|
585
679
|
valuesEqual(a, b) {
|
|
586
|
-
return a === b;
|
|
680
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
587
681
|
}
|
|
588
682
|
ensureGroupVisible(groupName) {
|
|
589
683
|
if (this.groups.some(g => g.name === groupName))
|
|
@@ -603,11 +697,11 @@ class FeatureFlagsManagerComponent {
|
|
|
603
697
|
}
|
|
604
698
|
}
|
|
605
699
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsManagerComponent, deps: [{ token: FeatureFlagsApiService }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
606
|
-
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 & 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"] }] }); }
|
|
700
|
+
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 [newFlagObjectEntries]=\"newFlagObjectEntries\"\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 (newFlagObjectEntriesChange)=\"newFlagObjectEntries = $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 & 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", "newFlagObjectEntries"], outputs: ["flagChanged", "revertFlag", "addFlagRequest", "cancelAddFlag", "confirmAddFlag", "newFlagKeyChange", "newFlagValueChange", "newFlagTypeChange", "newFlagObjectEntriesChange", "deleteFlagRequest", "deleteGroupRequest"] }, { kind: "component", type: ChangesSummaryModalComponent, selector: "valar-changes-summary-modal", inputs: ["changes", "tenantId"], outputs: ["confirm", "dismiss"] }] }); }
|
|
607
701
|
}
|
|
608
702
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.5", ngImport: i0, type: FeatureFlagsManagerComponent, decorators: [{
|
|
609
703
|
type: Component,
|
|
610
|
-
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 & 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"] }]
|
|
704
|
+
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 [newFlagObjectEntries]=\"newFlagObjectEntries\"\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 (newFlagObjectEntriesChange)=\"newFlagObjectEntries = $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 & 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"] }]
|
|
611
705
|
}], ctorParameters: () => [{ type: FeatureFlagsApiService }], propDecorators: { tenantId: [{
|
|
612
706
|
type: Input
|
|
613
707
|
}], tenantIdChange: [{
|