@cqa-lib/cqa-ui 1.1.528 → 1.1.530

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/esm2020/lib/audit-log-drawer/audit-log-drawer.component.mjs +146 -0
  2. package/esm2020/lib/audit-log-drawer/audit-log-drawer.models.mjs +2 -0
  3. package/esm2020/lib/audit-log-drawer/audit-log-drawer.service.mjs +84 -0
  4. package/esm2020/lib/audit-log-drawer/audit-log-entry-card.component.mjs +30 -0
  5. package/esm2020/lib/dialog/dialog.component.mjs +15 -3
  6. package/esm2020/lib/dialog/dialog.models.mjs +1 -1
  7. package/esm2020/lib/manage-columns-dialog/manage-columns-dialog.component.mjs +133 -0
  8. package/esm2020/lib/manage-columns-dialog/manage-columns-dialog.models.mjs +2 -0
  9. package/esm2020/lib/templates/modular-table-template/dialogs/move-to-folder-dialog.component.mjs +38 -13
  10. package/esm2020/lib/templates/modular-table-template/dialogs/new-folder-dialog.component.mjs +79 -87
  11. package/esm2020/lib/templates/modular-table-template/folder-sidebar/folder-sidebar.component.mjs +21 -4
  12. package/esm2020/lib/templates/modular-table-template/modular-table-template.component.mjs +3 -1
  13. package/esm2020/lib/templates/modular-table-template/modular-table-template.models.mjs +3 -1
  14. package/esm2020/lib/ui-kit.module.mjs +16 -1
  15. package/esm2020/public-api.mjs +7 -1
  16. package/fesm2015/cqa-lib-cqa-ui.mjs +536 -105
  17. package/fesm2015/cqa-lib-cqa-ui.mjs.map +1 -1
  18. package/fesm2020/cqa-lib-cqa-ui.mjs +531 -103
  19. package/fesm2020/cqa-lib-cqa-ui.mjs.map +1 -1
  20. package/lib/audit-log-drawer/audit-log-drawer.component.d.ts +34 -0
  21. package/lib/audit-log-drawer/audit-log-drawer.models.d.ts +43 -0
  22. package/lib/audit-log-drawer/audit-log-drawer.service.d.ts +13 -0
  23. package/lib/audit-log-drawer/audit-log-entry-card.component.d.ts +10 -0
  24. package/lib/dialog/dialog.component.d.ts +1 -0
  25. package/lib/dialog/dialog.models.d.ts +4 -0
  26. package/lib/manage-columns-dialog/manage-columns-dialog.component.d.ts +32 -0
  27. package/lib/manage-columns-dialog/manage-columns-dialog.models.d.ts +11 -0
  28. package/lib/templates/modular-table-template/dialogs/move-to-folder-dialog.component.d.ts +12 -2
  29. package/lib/templates/modular-table-template/dialogs/new-folder-dialog.component.d.ts +23 -30
  30. package/lib/templates/modular-table-template/folder-sidebar/folder-sidebar.component.d.ts +4 -0
  31. package/lib/templates/modular-table-template/modular-table-template.models.d.ts +2 -0
  32. package/lib/ui-kit.module.d.ts +92 -89
  33. package/package.json +1 -1
  34. package/public-api.d.ts +6 -0
  35. package/styles.css +1 -1
@@ -48,7 +48,7 @@ import { DndModule } from 'ngx-drag-drop';
48
48
  import * as jquery from 'jquery';
49
49
  import * as momentImport from 'moment';
50
50
  import 'daterangepicker';
51
- import { filter } from 'rxjs/operators';
51
+ import { filter, takeUntil } from 'rxjs/operators';
52
52
  import { Subject, BehaviorSubject } from 'rxjs';
53
53
  import * as i1$7 from 'ngx-monaco-editor';
54
54
  import { MonacoEditorModule } from 'ngx-monaco-editor';
@@ -948,6 +948,18 @@ class DialogComponent {
948
948
  return 'filled';
949
949
  }
950
950
  }
951
+ isButtonDisabled(button) {
952
+ const d = button.disabled;
953
+ if (typeof d === 'function') {
954
+ try {
955
+ return !!d(this.dialogRef);
956
+ }
957
+ catch {
958
+ return false;
959
+ }
960
+ }
961
+ return !!d;
962
+ }
951
963
  buttonHostClasses(button) {
952
964
  const role = this.normalizeRole(button.role);
953
965
  if (role === 'warn') {
@@ -975,10 +987,10 @@ class DialogComponent {
975
987
  }
976
988
  }
977
989
  DialogComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: DialogComponent, deps: [{ token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
978
- DialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: DialogComponent, selector: "cqa-dialog", host: { classAttribute: "cqa-ui-root" }, viewQueries: [{ propertyName: "portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], ngImport: i0, template: "<div class=\"cqa-ui-root\">\n <div class=\"cqa-flex cqa-w-full cqa-justify-center cqa-px-4 sm:cqa-px-6\">\n <div [ngClass]=\"panelClassList\" [ngStyle]=\"panelStyles\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-5\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-3\">\n <h2 class=\"cqa-text-lg cqa-font-semibold cqa-text-dialog\">\n {{ config.title }}\n </h2>\n\n <p *ngIf=\"config.description\" class=\"cqa-text-sm cqa-leading-6 cqa-text-dialog-secondary\">\n {{ config.description }}\n </p>\n\n <div *ngIf=\"config.warning\"\n class=\"cqa-rounded-xl cqa-border cqa-border-red-200 cqa-bg-red-50 cqa-px-4 cqa-py-3 cqa-text-sm cqa-leading-5 cqa-text-red-700\">\n {{ config.warning }}\n </div>\n </div>\n\n <div class=\"cqa-text-sm cqa-text-dialog\" [class.hidden]=\"!contentAttached\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n\n <div class=\"cqa-mt-4 cqa-flex cqa-flex-wrap cqa-gap-3\" [ngClass]=\"buttonAlignmentClass\">\n <cqa-button *ngFor=\"let button of config.buttons\" type=\"button\" [variant]=\"buttonVariant(button)\"\n [ngClass]=\"buttonHostClasses(button)\" (clicked)=\"onButtonClick(button)\">\n {{ button.label }}\n </cqa-button>\n </div>\n </div>\n </div>\n </div>\n</div>", components: [{ type: ButtonComponent, selector: "cqa-button", inputs: ["variant", "btnSize", "disabled", "loading", "icon", "iconPosition", "fullWidth", "iconColor", "type", "text", "customClass", "inlineStyles", "tooltip", "tooltipPosition"], outputs: ["clicked"] }], directives: [{ type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
990
+ DialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: DialogComponent, selector: "cqa-dialog", host: { classAttribute: "cqa-ui-root" }, viewQueries: [{ propertyName: "portalOutlet", first: true, predicate: CdkPortalOutlet, descendants: true, static: true }], ngImport: i0, template: "<div class=\"cqa-ui-root\">\n <div class=\"cqa-flex cqa-w-full cqa-justify-center cqa-px-4 sm:cqa-px-6\">\n <div [ngClass]=\"panelClassList\" [ngStyle]=\"panelStyles\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-5\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-3\">\n <h2 class=\"cqa-text-lg cqa-font-semibold cqa-text-dialog\">\n {{ config.title }}\n </h2>\n\n <p *ngIf=\"config.description\" class=\"cqa-text-sm cqa-leading-6 cqa-text-dialog-secondary\">\n {{ config.description }}\n </p>\n\n <div *ngIf=\"config.warning\"\n class=\"cqa-rounded-xl cqa-border cqa-border-red-200 cqa-bg-red-50 cqa-px-4 cqa-py-3 cqa-text-sm cqa-leading-5 cqa-text-red-700\">\n {{ config.warning }}\n </div>\n </div>\n\n <div class=\"cqa-text-sm cqa-text-dialog\" [class.hidden]=\"!contentAttached\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n\n <div class=\"cqa-mt-4 cqa-flex cqa-flex-wrap cqa-gap-3\" [ngClass]=\"buttonAlignmentClass\">\n <cqa-button *ngFor=\"let button of config.buttons\" type=\"button\" [variant]=\"buttonVariant(button)\"\n [disabled]=\"isButtonDisabled(button)\"\n [ngClass]=\"buttonHostClasses(button)\" (clicked)=\"onButtonClick(button)\">\n {{ button.label }}\n </cqa-button>\n </div>\n </div>\n </div>\n </div>\n</div>", components: [{ type: ButtonComponent, selector: "cqa-button", inputs: ["variant", "btnSize", "disabled", "loading", "icon", "iconPosition", "fullWidth", "iconColor", "type", "text", "customClass", "inlineStyles", "tooltip", "tooltipPosition"], outputs: ["clicked"] }], directives: [{ type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i3.CdkPortalOutlet, selector: "[cdkPortalOutlet]", inputs: ["cdkPortalOutlet"], outputs: ["attached"], exportAs: ["cdkPortalOutlet"] }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
979
991
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: DialogComponent, decorators: [{
980
992
  type: Component,
981
- args: [{ selector: 'cqa-dialog', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'cqa-ui-root' }, template: "<div class=\"cqa-ui-root\">\n <div class=\"cqa-flex cqa-w-full cqa-justify-center cqa-px-4 sm:cqa-px-6\">\n <div [ngClass]=\"panelClassList\" [ngStyle]=\"panelStyles\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-5\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-3\">\n <h2 class=\"cqa-text-lg cqa-font-semibold cqa-text-dialog\">\n {{ config.title }}\n </h2>\n\n <p *ngIf=\"config.description\" class=\"cqa-text-sm cqa-leading-6 cqa-text-dialog-secondary\">\n {{ config.description }}\n </p>\n\n <div *ngIf=\"config.warning\"\n class=\"cqa-rounded-xl cqa-border cqa-border-red-200 cqa-bg-red-50 cqa-px-4 cqa-py-3 cqa-text-sm cqa-leading-5 cqa-text-red-700\">\n {{ config.warning }}\n </div>\n </div>\n\n <div class=\"cqa-text-sm cqa-text-dialog\" [class.hidden]=\"!contentAttached\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n\n <div class=\"cqa-mt-4 cqa-flex cqa-flex-wrap cqa-gap-3\" [ngClass]=\"buttonAlignmentClass\">\n <cqa-button *ngFor=\"let button of config.buttons\" type=\"button\" [variant]=\"buttonVariant(button)\"\n [ngClass]=\"buttonHostClasses(button)\" (clicked)=\"onButtonClick(button)\">\n {{ button.label }}\n </cqa-button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: [] }]
993
+ args: [{ selector: 'cqa-dialog', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'cqa-ui-root' }, template: "<div class=\"cqa-ui-root\">\n <div class=\"cqa-flex cqa-w-full cqa-justify-center cqa-px-4 sm:cqa-px-6\">\n <div [ngClass]=\"panelClassList\" [ngStyle]=\"panelStyles\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-5\">\n <div class=\"cqa-flex cqa-flex-col cqa-gap-3\">\n <h2 class=\"cqa-text-lg cqa-font-semibold cqa-text-dialog\">\n {{ config.title }}\n </h2>\n\n <p *ngIf=\"config.description\" class=\"cqa-text-sm cqa-leading-6 cqa-text-dialog-secondary\">\n {{ config.description }}\n </p>\n\n <div *ngIf=\"config.warning\"\n class=\"cqa-rounded-xl cqa-border cqa-border-red-200 cqa-bg-red-50 cqa-px-4 cqa-py-3 cqa-text-sm cqa-leading-5 cqa-text-red-700\">\n {{ config.warning }}\n </div>\n </div>\n\n <div class=\"cqa-text-sm cqa-text-dialog\" [class.hidden]=\"!contentAttached\">\n <ng-template cdkPortalOutlet></ng-template>\n </div>\n\n <div class=\"cqa-mt-4 cqa-flex cqa-flex-wrap cqa-gap-3\" [ngClass]=\"buttonAlignmentClass\">\n <cqa-button *ngFor=\"let button of config.buttons\" type=\"button\" [variant]=\"buttonVariant(button)\"\n [disabled]=\"isButtonDisabled(button)\"\n [ngClass]=\"buttonHostClasses(button)\" (clicked)=\"onButtonClick(button)\">\n {{ button.label }}\n </cqa-button>\n </div>\n </div>\n </div>\n </div>\n</div>", styles: [] }]
982
994
  }], ctorParameters: function () { return [{ type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { portalOutlet: [{
983
995
  type: ViewChild,
984
996
  args: [CdkPortalOutlet, { static: true }]
@@ -7776,6 +7788,8 @@ const DEFAULT_MODULAR_LABELS = {
7776
7788
  newFolderDialogColorLabel: 'Folder color (optional)',
7777
7789
  newFolderDialogCancel: 'Cancel',
7778
7790
  newFolderDialogConfirm: 'Create folder',
7791
+ newFolderDialogNameRequiredError: 'Folder name is required',
7792
+ newFolderDialogNameMaxLengthError: 'Folder name cannot exceed 20 characters',
7779
7793
  folderMenuCreateSubfolder: 'Create subfolder',
7780
7794
  folderMenuRename: 'Rename',
7781
7795
  folderMenuMove: 'Move folder',
@@ -7831,6 +7845,14 @@ class MoveToFolderDialogComponent {
7831
7845
  this.currentFolderId = null;
7832
7846
  /** Controls which folder is visually selected. Two-way-bind friendly via `pickedFolderIdChange`. */
7833
7847
  this.pickedFolderId = null;
7848
+ /** Disables the top-level "Unorganised / no folder" row. Decoupled from `currentFolderId`
7849
+ * so consumers that reuse this picker for non-move flows (e.g. parent-folder selection
7850
+ * inside the New Folder dialog) can keep the root option clickable. The move flows wire
7851
+ * this to `currentFolderId == null`. */
7852
+ this.rootDisabled = false;
7853
+ /** CSS height for the scrollable picker container. Lets embedders shrink the picker when
7854
+ * it shares space with other fields (e.g. inside the New Folder dialog). */
7855
+ this.pickerHeight = '360px';
7834
7856
  /** Fires whenever the user picks a destination. */
7835
7857
  this.folderPicked = new EventEmitter();
7836
7858
  /** Companion output for banana-in-the-box `[(pickedFolderId)]` binding. */
@@ -7842,7 +7864,7 @@ class MoveToFolderDialogComponent {
7842
7864
  this.seedExpandedIds();
7843
7865
  }
7844
7866
  ngOnChanges(changes) {
7845
- if (changes['initialExpandedIds'] || changes['currentFolderId'] || changes['folders']) {
7867
+ if (changes['initialExpandedIds'] || changes['currentFolderId'] || changes['pickedFolderId'] || changes['folders']) {
7846
7868
  this.seedExpandedIds();
7847
7869
  }
7848
7870
  }
@@ -7852,9 +7874,13 @@ class MoveToFolderDialogComponent {
7852
7874
  return;
7853
7875
  }
7854
7876
  this.expandedIds = new Set();
7855
- if (this.currentFolderId != null) {
7877
+ // Prefer currentFolderId (the disabled "self" row) so the user lands on it; fall back
7878
+ // to pickedFolderId so non-move flows (e.g. parent picker) still open with the
7879
+ // pre-selected node visible.
7880
+ const seed = this.currentFolderId != null ? this.currentFolderId : this.pickedFolderId;
7881
+ if (seed != null) {
7856
7882
  const trail = [];
7857
- this.collectAncestors(this.folders, this.currentFolderId, trail);
7883
+ this.collectAncestors(this.folders, seed, trail);
7858
7884
  trail.forEach(id => this.expandedIds.add(id));
7859
7885
  }
7860
7886
  }
@@ -7893,6 +7919,9 @@ class MoveToFolderDialogComponent {
7893
7919
  isPicked(id) {
7894
7920
  return this.pickedFolderId === id;
7895
7921
  }
7922
+ get isUnorganisedDisabled() {
7923
+ return this.rootDisabled;
7924
+ }
7896
7925
  pick(id) {
7897
7926
  this.pickedFolderId = id;
7898
7927
  this.folderPicked.emit(id);
@@ -7911,7 +7940,7 @@ class MoveToFolderDialogComponent {
7911
7940
  }
7912
7941
  }
7913
7942
  MoveToFolderDialogComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: MoveToFolderDialogComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
7914
- MoveToFolderDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: MoveToFolderDialogComponent, selector: "cqa-move-to-folder-dialog", inputs: { folders: "folders", labels: "labels", currentFolderId: "currentFolderId", pickedFolderId: "pickedFolderId", initialExpandedIds: "initialExpandedIds" }, outputs: { folderPicked: "folderPicked", pickedFolderIdChange: "pickedFolderIdChange" }, host: { classAttribute: "cqa-ui-root" }, usesOnChanges: true, ngImport: i0, template: `
7943
+ MoveToFolderDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: MoveToFolderDialogComponent, selector: "cqa-move-to-folder-dialog", inputs: { folders: "folders", labels: "labels", currentFolderId: "currentFolderId", pickedFolderId: "pickedFolderId", initialExpandedIds: "initialExpandedIds", rootDisabled: "rootDisabled", pickerHeight: "pickerHeight" }, outputs: { folderPicked: "folderPicked", pickedFolderIdChange: "pickedFolderIdChange" }, host: { classAttribute: "cqa-ui-root" }, usesOnChanges: true, ngImport: i0, template: `
7915
7944
  <!-- Reusable outlined folder icon. Stroke uses currentColor so the row's text color drives the icon. -->
7916
7945
  <ng-template #folderIconTpl>
7917
7946
  <svg width="17" height="15" viewBox="0 0 17 15" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" class="cqa-shrink-0">
@@ -7926,16 +7955,19 @@ MoveToFolderDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.
7926
7955
  </svg>
7927
7956
  </ng-template>
7928
7957
 
7929
- <div class="cqa-flex cqa-flex-col cqa-border-solid cqa-border cqa-border-[#E2E2E3] cqa-rounded-[10px] cqa-py-2 cqa-w-full cqa-h-[360px] cqa-overflow-y-auto cqa-bg-white cqa-scrollbar-thin">
7958
+ <div class="cqa-flex cqa-flex-col cqa-border-solid cqa-border cqa-border-[#E2E2E3] cqa-rounded-[10px] cqa-py-2 cqa-w-full cqa-overflow-y-auto cqa-bg-white cqa-scrollbar-thin" [style.height]="pickerHeight">
7930
7959
  <!-- Unorganised (root / no folder) -->
7931
7960
  <div
7932
7961
  role="button"
7933
7962
  tabindex="0"
7934
7963
  class="cqa-flex cqa-items-center cqa-gap-2.5 cqa-px-4 cqa-py-2 cqa-cursor-pointer cqa-transition-colors cqa-text-sm"
7935
7964
  [ngClass]="isPicked(null) ? 'cqa-bg-indigo-50 cqa-text-indigo-700' : 'cqa-text-neutral-800 hover:cqa-bg-neutral-50'"
7936
- (click)="pick(null)"
7937
- (keydown.enter)="pick(null)"
7938
- (keydown.space)="$event.preventDefault(); pick(null)"
7965
+ [class.cqa-opacity-40]="isUnorganisedDisabled"
7966
+ [class.cqa-cursor-not-allowed]="isUnorganisedDisabled"
7967
+ [attr.aria-disabled]="isUnorganisedDisabled || null"
7968
+ (click)="!isUnorganisedDisabled && pick(null)"
7969
+ (keydown.enter)="!isUnorganisedDisabled && pick(null)"
7970
+ (keydown.space)="$event.preventDefault(); !isUnorganisedDisabled && pick(null)"
7939
7971
  >
7940
7972
  <!-- chevron-slot placeholder so all rows line up -->
7941
7973
  <span class="cqa-inline-block cqa-w-5 cqa-h-5 cqa-shrink-0"></span>
@@ -7996,16 +8028,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
7996
8028
  </svg>
7997
8029
  </ng-template>
7998
8030
 
7999
- <div class="cqa-flex cqa-flex-col cqa-border-solid cqa-border cqa-border-[#E2E2E3] cqa-rounded-[10px] cqa-py-2 cqa-w-full cqa-h-[360px] cqa-overflow-y-auto cqa-bg-white cqa-scrollbar-thin">
8031
+ <div class="cqa-flex cqa-flex-col cqa-border-solid cqa-border cqa-border-[#E2E2E3] cqa-rounded-[10px] cqa-py-2 cqa-w-full cqa-overflow-y-auto cqa-bg-white cqa-scrollbar-thin" [style.height]="pickerHeight">
8000
8032
  <!-- Unorganised (root / no folder) -->
8001
8033
  <div
8002
8034
  role="button"
8003
8035
  tabindex="0"
8004
8036
  class="cqa-flex cqa-items-center cqa-gap-2.5 cqa-px-4 cqa-py-2 cqa-cursor-pointer cqa-transition-colors cqa-text-sm"
8005
8037
  [ngClass]="isPicked(null) ? 'cqa-bg-indigo-50 cqa-text-indigo-700' : 'cqa-text-neutral-800 hover:cqa-bg-neutral-50'"
8006
- (click)="pick(null)"
8007
- (keydown.enter)="pick(null)"
8008
- (keydown.space)="$event.preventDefault(); pick(null)"
8038
+ [class.cqa-opacity-40]="isUnorganisedDisabled"
8039
+ [class.cqa-cursor-not-allowed]="isUnorganisedDisabled"
8040
+ [attr.aria-disabled]="isUnorganisedDisabled || null"
8041
+ (click)="!isUnorganisedDisabled && pick(null)"
8042
+ (keydown.enter)="!isUnorganisedDisabled && pick(null)"
8043
+ (keydown.space)="$event.preventDefault(); !isUnorganisedDisabled && pick(null)"
8009
8044
  >
8010
8045
  <!-- chevron-slot placeholder so all rows line up -->
8011
8046
  <span class="cqa-inline-block cqa-w-5 cqa-h-5 cqa-shrink-0"></span>
@@ -8059,6 +8094,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
8059
8094
  type: Input
8060
8095
  }], initialExpandedIds: [{
8061
8096
  type: Input
8097
+ }], rootDisabled: [{
8098
+ type: Input
8099
+ }], pickerHeight: [{
8100
+ type: Input
8062
8101
  }], folderPicked: [{
8063
8102
  type: Output
8064
8103
  }], pickedFolderIdChange: [{
@@ -8066,6 +8105,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
8066
8105
  }] } });
8067
8106
 
8068
8107
  const DEFAULT_FOLDER_COLOR = '#99999E';
8108
+ const FOLDER_NAME_MAX_LENGTH = 20;
8069
8109
  const PRESET_FOLDER_COLORS = [
8070
8110
  DEFAULT_FOLDER_COLOR,
8071
8111
  '#4F46E5',
@@ -8080,8 +8120,10 @@ const PRESET_FOLDER_COLORS = [
8080
8120
  * Body of the "New Folder" dialog.
8081
8121
  *
8082
8122
  * Uses reusable library components (`cqa-custom-input` for the name field,
8083
- * `cqa-dynamic-select` for the parent dropdown) so hosts can drop this dialog
8084
- * into their own modal shell without worrying about styling drift.
8123
+ * `cqa-move-to-folder-dialog` as an embedded tree picker for the parent folder)
8124
+ * so hosts can drop this dialog into their own modal shell without worrying
8125
+ * about styling drift. The tree picker handles arbitrarily deep nesting via
8126
+ * expand/collapse rather than a flat dropdown of "A › B › C ›…" paths.
8085
8127
  *
8086
8128
  * Two integration paths:
8087
8129
  *
@@ -8096,16 +8138,19 @@ const PRESET_FOLDER_COLORS = [
8096
8138
  class NewFolderDialogComponent {
8097
8139
  constructor(cdr) {
8098
8140
  this.cdr = cdr;
8099
- // NOTE: These inputs are declared as setters (instead of plain @Input fields)
8100
- // because `DialogService.open(...)` assigns the `inputs` map via direct
8101
- // property assignment *after* `attachComponent` has already run the
8102
- // component's `ngOnInit`. Direct assignment bypasses `ngOnChanges`, so without
8103
- // setters the `parentSelectConfig` would be built from a default empty folders
8104
- // array and the parent-folder dropdown would render empty / appear broken.
8141
+ // NOTE: `folders` and `labels` are declared as setters (instead of plain @Input fields)
8142
+ // because `DialogService.open(...)` assigns the `inputs` map via direct property
8143
+ // assignment *after* `attachComponent` has already run the component's lifecycle
8144
+ // hooks direct assignment bypasses `ngOnChanges`, so setters keep derived state
8145
+ // (e.g. `parentPickerLabels`) in sync no matter the assignment order.
8105
8146
  this._folders = [];
8106
8147
  this._labels = { ...DEFAULT_MODULAR_LABELS };
8107
8148
  this.name = '';
8108
- this._parentId = null;
8149
+ /** Tracks whether the user has interacted with the name field, so we don't surface
8150
+ * the "required" error on first paint when the dialog opens with an empty name. */
8151
+ this.nameTouched = false;
8152
+ this.maxNameLength = FOLDER_NAME_MAX_LENGTH;
8153
+ this.parentId = null;
8109
8154
  this.color = DEFAULT_FOLDER_COLOR;
8110
8155
  this.presetColors = PRESET_FOLDER_COLORS;
8111
8156
  this.rainbowBorder = 'conic-gradient(from 0deg, #ef4444, #f59e0b, #eab308, #10b981, #06b6d4, #4f46e5, #8b5cf6, #ec4899, #ef4444)';
@@ -8114,79 +8159,35 @@ class NewFolderDialogComponent {
8114
8159
  this.colorChange = new EventEmitter();
8115
8160
  this.submitted = new EventEmitter();
8116
8161
  this.cancelled = new EventEmitter();
8117
- /** Reactive form used by cqa-dynamic-select. Untyped to match the project's Angular 13.4 forms API. */
8118
- this.parentForm = new FormGroup({ parentId: new FormControl(null) });
8119
- this.parentOptions = [];
8120
- this.rebuildSelectConfig(); // ensure config exists even before first input set
8162
+ /** Labels passed to the embedded tree picker. Re-uses the move-dialog surface but
8163
+ * rewrites the root row's label so it reads "(no parent)" instead of "Unorganised"
8164
+ * for the create-folder context. */
8165
+ this.parentPickerLabels = this.buildParentPickerLabels();
8121
8166
  }
8122
8167
  set folders(value) {
8123
8168
  this._folders = value || [];
8124
- this.rebuildOptions();
8125
- this.rebuildSelectConfig();
8169
+ this.cdr.markForCheck();
8126
8170
  }
8127
8171
  get folders() { return this._folders; }
8128
8172
  set labels(value) {
8129
8173
  this._labels = value || { ...DEFAULT_MODULAR_LABELS };
8130
- this.rebuildSelectConfig();
8131
- }
8132
- get labels() { return this._labels; }
8133
- set parentId(value) {
8134
- this._parentId = value ?? null;
8135
- this.syncParentControl();
8136
- }
8137
- get parentId() { return this._parentId; }
8138
- ngOnInit() {
8139
- // Setters above already rebuilt config once folders/labels were assigned. This
8140
- // just ensures the form control matches the current parentId value in case
8141
- // ordering of input assignment differs.
8142
- this.syncParentControl();
8143
- }
8144
- rebuildSelectConfig() {
8145
- this.parentSelectConfig = {
8146
- key: 'parentId',
8147
- label: this.labels.newFolderDialogParentLabel,
8148
- placeholder: this.labels.newFolderDialogParentNone,
8149
- searchable: false,
8150
- multiple: false,
8151
- closeOnSelect: true,
8152
- options: [
8153
- // `getOptionValue` in cqa-dynamic-select returns `id ?? value`, so using
8154
- // `value: null` (with no id) yields a stable `null` resolved value that
8155
- // matches the FormControl's null state.
8156
- { value: null, name: this.labels.newFolderDialogParentNone },
8157
- ...this.parentOptions.map(o => ({ id: o.id, name: o.label })),
8158
- ],
8159
- };
8160
- }
8161
- rebuildOptions() {
8162
- const opts = [];
8163
- const walk = (nodes, trail) => {
8164
- for (const n of nodes || []) {
8165
- const label = [...trail, n.name].join(' › ');
8166
- opts.push({ id: n.id, label });
8167
- if (n.children?.length)
8168
- walk(n.children, [...trail, n.name]);
8169
- }
8170
- };
8171
- walk(this.folders, []);
8172
- this.parentOptions = opts;
8174
+ this.parentPickerLabels = this.buildParentPickerLabels();
8173
8175
  this.cdr.markForCheck();
8174
8176
  }
8175
- syncParentControl() {
8176
- // Keep the reactive form in sync with the `parentId` input without bouncing events.
8177
- this.parentForm.get('parentId')?.setValue(this.parentId ?? null, { emitEvent: false });
8177
+ get labels() { return this._labels; }
8178
+ buildParentPickerLabels() {
8179
+ return { ...this._labels, moveDialogRoot: this._labels.newFolderDialogParentNone };
8178
8180
  }
8179
8181
  onNameChange(value) {
8180
8182
  this.name = value;
8183
+ this.nameTouched = true;
8181
8184
  this.nameChange.emit(value);
8182
8185
  }
8183
- onParentSelectionChange(event) {
8184
- // cqa-dynamic-select resolves options to `id ?? value`; option ids in the
8185
- // parent tree are numeric so we coerce defensively.
8186
- const raw = event?.value;
8186
+ onParentPicked(id) {
8187
+ // The tree picker emits `null` for the "(no parent)" sentinel; numeric otherwise.
8187
8188
  let next = null;
8188
- if (raw != null) {
8189
- const coerced = typeof raw === 'number' ? raw : Number(raw);
8189
+ if (id != null) {
8190
+ const coerced = typeof id === 'number' ? id : Number(id);
8190
8191
  if (Number.isFinite(coerced))
8191
8192
  next = coerced;
8192
8193
  }
@@ -8213,9 +8214,26 @@ class NewFolderDialogComponent {
8213
8214
  if (target)
8214
8215
  this.onColorChange(target.value);
8215
8216
  }
8216
- /** Returns true when the current state is a valid submission (non-empty, trimmed name). */
8217
+ /** Errors surfaced under the name field. Only computed once the user has touched
8218
+ * the input so the dialog doesn't open with a "required" error already showing. */
8219
+ get nameErrors() {
8220
+ if (!this.nameTouched)
8221
+ return [];
8222
+ const trimmed = (this.name || '').trim();
8223
+ const errors = [];
8224
+ if (!trimmed) {
8225
+ errors.push(this.labels.newFolderDialogNameRequiredError);
8226
+ }
8227
+ else if (trimmed.length > this.maxNameLength) {
8228
+ errors.push(this.labels.newFolderDialogNameMaxLengthError);
8229
+ }
8230
+ return errors;
8231
+ }
8232
+ /** Returns true when the current state is a valid submission (non-empty trimmed name
8233
+ * within the max length). Used by the host to gate the dialog's Create button. */
8217
8234
  get isValid() {
8218
- return !!(this.name || '').trim();
8235
+ const trimmed = (this.name || '').trim();
8236
+ return trimmed.length > 0 && trimmed.length <= this.maxNameLength;
8219
8237
  }
8220
8238
  submit() {
8221
8239
  if (!this.isValid)
@@ -8236,18 +8254,25 @@ NewFolderDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0
8236
8254
  [required]="true"
8237
8255
  [placeholder]="labels.newFolderDialogNamePlaceholder"
8238
8256
  [value]="name"
8257
+ [errors]="nameErrors"
8239
8258
  [fullWidth]="true"
8240
8259
  (valueChange)="onNameChange($event)"
8241
8260
  (enterPressed)="submit()"
8242
8261
  ></cqa-custom-input>
8243
8262
 
8244
- <!-- Parent folder -->
8245
- <cqa-dynamic-select
8246
- class="cqa-block cqa-w-full"
8247
- [form]="parentForm"
8248
- [config]="parentSelectConfig"
8249
- (selectionChange)="onParentSelectionChange($event)"
8250
- ></cqa-dynamic-select>
8263
+ <!-- Parent folder — tree picker (handles arbitrarily deep nesting via expand/collapse) -->
8264
+ <div class="cqa-flex cqa-flex-col cqa-gap-2">
8265
+ <label class="cqa-text-sm cqa-font-medium cqa-text-neutral-700">
8266
+ {{ labels.newFolderDialogParentLabel }}
8267
+ </label>
8268
+ <cqa-move-to-folder-dialog
8269
+ [folders]="folders"
8270
+ [labels]="parentPickerLabels"
8271
+ [pickedFolderId]="parentId"
8272
+ [pickerHeight]="'240px'"
8273
+ (pickedFolderIdChange)="onParentPicked($event)"
8274
+ ></cqa-move-to-folder-dialog>
8275
+ </div>
8251
8276
 
8252
8277
  <!-- Folder color (optional) -->
8253
8278
  <div class="cqa-flex cqa-flex-col cqa-gap-2">
@@ -8306,7 +8331,7 @@ NewFolderDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0
8306
8331
  </div>
8307
8332
  </div>
8308
8333
  </div>
8309
- `, isInline: true, components: [{ type: CustomInputComponent, selector: "cqa-custom-input", inputs: ["inputId", "label", "type", "placeholder", "value", "disabled", "errors", "required", "ariaLabel", "size", "fullWidth", "maxLength", "showCharCount", "inputInlineStyle", "labelInlineStyle"], outputs: ["valueChange", "blurred", "focused", "enterPressed"] }, { type: DynamicSelectFieldComponent, selector: "cqa-dynamic-select", inputs: ["form", "config"], outputs: ["selectionChange", "selectClick", "searchChange", "loadMore", "addCustomValue"] }], directives: [{ type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8334
+ `, isInline: true, components: [{ type: CustomInputComponent, selector: "cqa-custom-input", inputs: ["inputId", "label", "type", "placeholder", "value", "disabled", "errors", "required", "ariaLabel", "size", "fullWidth", "maxLength", "showCharCount", "inputInlineStyle", "labelInlineStyle"], outputs: ["valueChange", "blurred", "focused", "enterPressed"] }, { type: MoveToFolderDialogComponent, selector: "cqa-move-to-folder-dialog", inputs: ["folders", "labels", "currentFolderId", "pickedFolderId", "initialExpandedIds", "rootDisabled", "pickerHeight"], outputs: ["folderPicked", "pickedFolderIdChange"] }], directives: [{ type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
8310
8335
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: NewFolderDialogComponent, decorators: [{
8311
8336
  type: Component,
8312
8337
  args: [{ selector: 'cqa-new-folder-dialog', template: `
@@ -8317,18 +8342,25 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
8317
8342
  [required]="true"
8318
8343
  [placeholder]="labels.newFolderDialogNamePlaceholder"
8319
8344
  [value]="name"
8345
+ [errors]="nameErrors"
8320
8346
  [fullWidth]="true"
8321
8347
  (valueChange)="onNameChange($event)"
8322
8348
  (enterPressed)="submit()"
8323
8349
  ></cqa-custom-input>
8324
8350
 
8325
- <!-- Parent folder -->
8326
- <cqa-dynamic-select
8327
- class="cqa-block cqa-w-full"
8328
- [form]="parentForm"
8329
- [config]="parentSelectConfig"
8330
- (selectionChange)="onParentSelectionChange($event)"
8331
- ></cqa-dynamic-select>
8351
+ <!-- Parent folder — tree picker (handles arbitrarily deep nesting via expand/collapse) -->
8352
+ <div class="cqa-flex cqa-flex-col cqa-gap-2">
8353
+ <label class="cqa-text-sm cqa-font-medium cqa-text-neutral-700">
8354
+ {{ labels.newFolderDialogParentLabel }}
8355
+ </label>
8356
+ <cqa-move-to-folder-dialog
8357
+ [folders]="folders"
8358
+ [labels]="parentPickerLabels"
8359
+ [pickedFolderId]="parentId"
8360
+ [pickerHeight]="'240px'"
8361
+ (pickedFolderIdChange)="onParentPicked($event)"
8362
+ ></cqa-move-to-folder-dialog>
8363
+ </div>
8332
8364
 
8333
8365
  <!-- Folder color (optional) -->
8334
8366
  <div class="cqa-flex cqa-flex-col cqa-gap-2">
@@ -8887,6 +8919,8 @@ class FolderSidebarComponent {
8887
8919
  this.searchValue = '';
8888
8920
  this.renamingId = null;
8889
8921
  this.renameDraft = '';
8922
+ /** Inline rename uses the same name rules as the New Folder dialog. */
8923
+ this.renameMaxLength = 20;
8890
8924
  /** Id of the folder whose context menu is open, or null when closed. */
8891
8925
  this.contextMenuFolderId = null;
8892
8926
  /** Viewport-anchored position (client coordinates) for the floating menu. */
@@ -8972,9 +9006,20 @@ class FolderSidebarComponent {
8972
9006
  this.cdr.markForCheck();
8973
9007
  this.focusRenameInput(n.id);
8974
9008
  }
9009
+ /** True when the active rename draft passes the name rules (non-empty, within max length). */
9010
+ get isRenameDraftValid() {
9011
+ const trimmed = (this.renameDraft || '').trim();
9012
+ return trimmed.length > 0 && trimmed.length <= this.renameMaxLength;
9013
+ }
8975
9014
  commitRename(n) {
9015
+ // Discard invalid drafts (empty / too long) instead of committing a bad name.
9016
+ // The user sees the red border while editing; on blur we revert to the original.
9017
+ if (!this.isRenameDraftValid) {
9018
+ this.cancelRename();
9019
+ return;
9020
+ }
8976
9021
  const name = (this.renameDraft || '').trim();
8977
- if (name && name !== n.name) {
9022
+ if (name !== n.name) {
8978
9023
  // Emit the full renamed node (with the new name) + parent context so hosts can
8979
9024
  // call `PUT /test_case_folders/{id}` without re-resolving from their cached tree.
8980
9025
  const folder = { ...n, name };
@@ -8997,6 +9042,10 @@ class FolderSidebarComponent {
8997
9042
  event.stopPropagation();
8998
9043
  if (event.key === 'Enter') {
8999
9044
  event.preventDefault();
9045
+ // Don't swallow the user's edit on Enter when the draft is invalid — keep
9046
+ // the input open (red border already signals the issue) and let them fix it.
9047
+ if (!this.isRenameDraftValid)
9048
+ return;
9000
9049
  this.commitRename(n);
9001
9050
  }
9002
9051
  else if (event.key === 'Escape') {
@@ -9258,10 +9307,10 @@ class FolderSidebarComponent {
9258
9307
  }
9259
9308
  }
9260
9309
  FolderSidebarComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: FolderSidebarComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
9261
- FolderSidebarComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: FolderSidebarComponent, selector: "cqa-folder-sidebar", inputs: { folders: "folders", selectedFolderId: "selectedFolderId", expandedFolderIds: "expandedFolderIds", unorganisedCount: "unorganisedCount", allowCreate: "allowCreate", allowRename: "allowRename", allowDelete: "allowDelete", allowMove: "allowMove", allowDuplicate: "allowDuplicate", allowDrop: "allowDrop", showCounts: "showCounts", collapsed: "collapsed", labels: "labels" }, outputs: { folderSelected: "folderSelected", folderExpansionToggled: "folderExpansionToggled", folderCreated: "folderCreated", folderCreateRequested: "folderCreateRequested", folderRenamed: "folderRenamed", folderDeleted: "folderDeleted", folderMoveRequested: "folderMoveRequested", folderDuplicateRequested: "folderDuplicateRequested", testsDropped: "testsDropped", folderDropped: "folderDropped", collapsedChange: "collapsedChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()", "document:dragend": "onDocumentDragEnd()" }, classAttribute: "cqa-ui-root" }, ngImport: i0, template: "<!-- Reusable folder icon. Render via <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: node.color }\"></ng-container>. -->\n<ng-template #folderIcon let-color=\"color\">\n <span class=\"cqa-inline-flex cqa-items-center cqa-justify-center cqa-w-4 cqa-h-4 cqa-shrink-0\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M12.1916 9.85824C12.1916 10.1677 12.0687 10.4644 11.8499 10.6832C11.6311 10.902 11.3343 11.0249 11.0249 11.0249H1.69157C1.38215 11.0249 1.0854 10.902 0.866611 10.6832C0.647819 10.4644 0.524902 10.1677 0.524902 9.85824V1.69157C0.524902 1.38215 0.647819 1.0854 0.866611 0.866611C1.0854 0.647819 1.38215 0.524902 1.69157 0.524902H4.60824L5.7749 2.2749H11.0249C11.3343 2.2749 11.6311 2.39782 11.8499 2.61661C12.0687 2.8354 12.1916 3.13215 12.1916 3.44157V9.85824Z\" [attr.stroke]=\"color || '#99999E'\" stroke-width=\"1.05\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n</ng-template>\n\n<aside\n class=\"cqa-flex cqa-flex-col cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-lg cqa-h-full cqa-min-h-0\"\n [class.cqa-w-[240px]]=\"!collapsed\"\n [class.cqa-w-[48px]]=\"collapsed\"\n style=\"transition: width 150ms ease;\"\n>\n <!-- Header -->\n <div class=\"cqa-flex cqa-items-center cqa-justify-between cqa-px-3 cqa-py-3\">\n <span *ngIf=\"!collapsed\" class=\"cqa-text-sm cqa-font-semibold cqa-text-neutral-900\">\n {{ labels.folders }}\n </span>\n <div class=\"cqa-flex cqa-items-center cqa-gap-1\" [class.cqa-w-full]=\"collapsed\" [class.cqa-justify-center]=\"collapsed\">\n <button\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"togglePanel()\"\n [attr.aria-label]=\"collapsed ? 'Expand folders panel' : 'Collapse folders panel'\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">\n {{ collapsed ? 'chevron_right' : 'keyboard_double_arrow_left' }}\n </mat-icon>\n </button>\n <button\n *ngIf=\"!collapsed && allowCreate\"\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"requestCreate(null)\"\n [attr.aria-label]=\"labels.newFolder\"\n [title]=\"labels.newFolder\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">add</mat-icon>\n </button>\n </div>\n </div>\n\n <ng-container *ngIf=\"!collapsed\">\n <!-- Search -->\n <div class=\"cqa-px-3 cqa-pb-2\">\n <cqa-search-bar\n size=\"sm\"\n [fullWidth]=\"true\"\n [value]=\"searchValue\"\n [placeholder]=\"labels.searchFoldersPlaceholder\"\n [showClear]=\"true\"\n (valueChange)=\"searchValue = $event\"\n (cleared)=\"searchValue = ''\"\n ></cqa-search-bar>\n </div>\n\n <!-- Tree (folders + Unorganised tab) -->\n <div role=\"tree\" class=\"cqa-flex-1 cqa-overflow-y-auto cqa-py-1\">\n <ng-container *ngFor=\"let row of rows; let i = index; trackBy: trackByRow\">\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-expanded]=\"row.hasChildren ? isExpanded(row.node.id) : null\"\n [attr.aria-selected]=\"isSelected(row.node.id)\"\n [attr.data-folder-row-id]=\"row.node.id\"\n [cqaFolderDrop]=\"row.node.id\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(row.node.id, $event)\"\n (folderDropped)=\"onFolderRowDropped(row.node.id, $event)\"\n [cqaFolderDrag]=\"row.node.id\"\n [dragEnabled]=\"allowDrop\"\n (dragstart)=\"onFolderDragStart(row.node)\"\n (dragend)=\"onFolderDragEnd()\"\n (dragover)=\"onFolderRowDragOver(row)\"\n (dragleave)=\"onFolderRowDragLeave(row)\"\n (click)=\"onSelect(row.node)\"\n (contextmenu)=\"openContextMenu(row.node, $event)\"\n (keydown)=\"onRowKeydown($event, row, i)\"\n class=\"cqa-group cqa-flex cqa-items-center cqa-gap-1 cqa-pr-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(row.node.id) || contextMenuFolderId === row.node.id\"\n [style.paddingLeft.px]=\"8 + row.depth * 16\"\n [attr.title]=\"row.node.totalCount != null ? (row.node.totalCount + ' total including subfolders') : null\"\n >\n <span\n *ngIf=\"isSelected(row.node.id)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <button\n type=\"button\"\n class=\"cqa-p-0 cqa-text-neutral-400\"\n [style.visibility]=\"row.hasChildren ? 'visible' : 'hidden'\"\n (click)=\"onToggle(row.node, $event)\"\n [attr.aria-label]=\"isExpanded(row.node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">\n {{ isExpanded(row.node.id) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: row.node.color }\"></ng-container>\n\n <ng-container *ngIf=\"renamingId === row.node.id; else nameTpl\">\n <input\n type=\"text\"\n size=\"1\"\n [attr.data-folder-rename-input]=\"row.node.id\"\n class=\"cqa-flex-1 cqa-min-w-0 cqa-w-full cqa-px-2 cqa-py-0.5 cqa-rounded cqa-border cqa-border-neutral-300 cqa-bg-white cqa-text-sm cqa-shadow-sm focus:cqa-outline-none focus:cqa-border-indigo-400 focus:cqa-ring-1 focus:cqa-ring-indigo-200\"\n [(ngModel)]=\"renameDraft\"\n (keydown)=\"onRenameKey($event, row.node)\"\n (keypress)=\"$event.stopPropagation()\"\n (keyup)=\"$event.stopPropagation()\"\n (blur)=\"commitRename(row.node)\"\n (click)=\"$event.stopPropagation()\"\n />\n </ng-container>\n <ng-template #nameTpl>\n <span\n class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-800 cqa-truncate\"\n (dblclick)=\"beginRename(row.node, $event)\"\n >{{ row.node.name }}</span>\n </ng-template>\n\n <!-- Count shown at rest; hidden on row hover when delete is allowed -->\n <span\n *ngIf=\"showCounts && row.node.count != null\"\n class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums cqa-ml-1\"\n [class.group-hover:cqa-hidden]=\"allowDelete\"\n >{{ row.node.count }}</span>\n\n <button\n *ngIf=\"hasAnyContextAction\"\n type=\"button\"\n class=\"cqa-p-0.5 cqa-rounded hover:cqa-bg-neutral-200 cqa-text-neutral-500\"\n [class.cqa-hidden]=\"contextMenuFolderId !== row.node.id\"\n [class.group-hover:cqa-inline-flex]=\"true\"\n [class.cqa-inline-flex]=\"contextMenuFolderId === row.node.id\"\n (click)=\"openContextMenu(row.node, $event)\"\n [attr.aria-label]=\"'Open actions for ' + row.node.name\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"contextMenuFolderId === row.node.id\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">more_horiz</mat-icon>\n </button>\n </div>\n </ng-container>\n\n <!-- Divider between folder tree and Unorganised tab -->\n <div\n aria-hidden=\"true\"\n class=\"cqa-mx-3\"\n style=\"height: 1px; background-color: #E5E7EB; margin-top: 6px; margin-bottom: 6px;\"\n ></div>\n\n <!-- Unorganised \u2014 flows with the tree, behaves like a tab item -->\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-selected]=\"isSelected(null)\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-px-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(null)\"\n [cqaFolderDrop]=\"null\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(null, $event)\"\n (folderDropped)=\"onFolderRowDropped(null, $event)\"\n (click)=\"onSelectUnorganised()\"\n >\n <span\n *ngIf=\"isSelected(null)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <mat-icon class=\"cqa-text-neutral-500\" style=\"font-size:16px;width:16px;height:16px\">inbox</mat-icon>\n <span class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-700\">{{ labels.unorganised }}</span>\n <span *ngIf=\"showCounts\" class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums\">{{ unorganisedCount }}</span>\n </div>\n </div>\n\n <!-- Folder context menu (right-click / ellipsis). Anchored to viewport coords. -->\n <div\n *ngIf=\"contextMenuFolderId !== null\"\n role=\"menu\"\n class=\"cqa-fixed cqa-z-50 cqa-min-w-[180px] cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-md cqa-shadow-lg cqa-py-1\"\n [style.left.px]=\"contextMenuPosition.x\"\n [style.top.px]=\"contextMenuPosition.y\"\n (click)=\"$event.stopPropagation()\"\n (contextmenu)=\"$event.preventDefault(); $event.stopPropagation()\"\n >\n <ng-container *ngIf=\"folderById(contextMenuFolderId) as menuNode\">\n <button\n *ngIf=\"allowCreate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextCreateSubfolder(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">create_new_folder</mat-icon>\n <span>{{ labels.folderMenuCreateSubfolder }}</span>\n </button>\n <button\n *ngIf=\"allowRename\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextRename(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">edit</mat-icon>\n <span>{{ labels.folderMenuRename }}</span>\n </button>\n <button\n *ngIf=\"allowMove\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextMove(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">drive_file_move</mat-icon>\n <span>{{ labels.folderMenuMove }}</span>\n </button>\n <button\n *ngIf=\"allowDuplicate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextDuplicate(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">content_copy</mat-icon>\n <span>{{ labels.folderMenuDuplicate }}</span>\n </button>\n <div *ngIf=\"allowDelete && (allowCreate || allowRename || allowMove || allowDuplicate)\" class=\"cqa-h-px cqa-bg-neutral-200 cqa-my-1\"></div>\n <button\n *ngIf=\"allowDelete\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-red-600 hover:cqa-bg-red-50 cqa-text-left\"\n (click)=\"onContextDelete(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">delete_outline</mat-icon>\n <span>{{ labels.folderMenuDelete }}</span>\n </button>\n </ng-container>\n </div>\n\n </ng-container>\n</aside>\n", components: [{ type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: SearchBarComponent, selector: "cqa-search-bar", inputs: ["placeholder", "value", "disabled", "showClear", "ariaLabel", "autoFocus", "size", "fullWidth"], outputs: ["valueChange", "search", "cleared"] }], directives: [{ type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: FolderDropDirective, selector: "[cqaFolderDrop]", inputs: ["cqaFolderDrop", "dropEnabled"], outputs: ["testsDropped", "folderDropped"] }, { type: FolderDragDirective, selector: "[cqaFolderDrag]", inputs: ["cqaFolderDrag", "dragEnabled"] }, { type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet"] }, { type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9310
+ FolderSidebarComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: FolderSidebarComponent, selector: "cqa-folder-sidebar", inputs: { folders: "folders", selectedFolderId: "selectedFolderId", expandedFolderIds: "expandedFolderIds", unorganisedCount: "unorganisedCount", allowCreate: "allowCreate", allowRename: "allowRename", allowDelete: "allowDelete", allowMove: "allowMove", allowDuplicate: "allowDuplicate", allowDrop: "allowDrop", showCounts: "showCounts", collapsed: "collapsed", labels: "labels" }, outputs: { folderSelected: "folderSelected", folderExpansionToggled: "folderExpansionToggled", folderCreated: "folderCreated", folderCreateRequested: "folderCreateRequested", folderRenamed: "folderRenamed", folderDeleted: "folderDeleted", folderMoveRequested: "folderMoveRequested", folderDuplicateRequested: "folderDuplicateRequested", testsDropped: "testsDropped", folderDropped: "folderDropped", collapsedChange: "collapsedChange" }, host: { listeners: { "document:click": "onDocumentClick()", "document:keydown.escape": "onEscape()", "document:dragend": "onDocumentDragEnd()" }, classAttribute: "cqa-ui-root" }, ngImport: i0, template: "<!-- Reusable folder icon. Render via <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: node.color }\"></ng-container>. -->\n<ng-template #folderIcon let-color=\"color\">\n <span class=\"cqa-inline-flex cqa-items-center cqa-justify-center cqa-w-4 cqa-h-4 cqa-shrink-0\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M12.1916 9.85824C12.1916 10.1677 12.0687 10.4644 11.8499 10.6832C11.6311 10.902 11.3343 11.0249 11.0249 11.0249H1.69157C1.38215 11.0249 1.0854 10.902 0.866611 10.6832C0.647819 10.4644 0.524902 10.1677 0.524902 9.85824V1.69157C0.524902 1.38215 0.647819 1.0854 0.866611 0.866611C1.0854 0.647819 1.38215 0.524902 1.69157 0.524902H4.60824L5.7749 2.2749H11.0249C11.3343 2.2749 11.6311 2.39782 11.8499 2.61661C12.0687 2.8354 12.1916 3.13215 12.1916 3.44157V9.85824Z\" [attr.stroke]=\"color || '#99999E'\" stroke-width=\"1.05\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n</ng-template>\n\n<aside\n class=\"cqa-flex cqa-flex-col cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-lg cqa-h-full cqa-min-h-0\"\n [class.cqa-w-[240px]]=\"!collapsed\"\n [class.cqa-w-[48px]]=\"collapsed\"\n style=\"transition: width 150ms ease;\"\n>\n <!-- Header -->\n <div class=\"cqa-flex cqa-items-center cqa-justify-between cqa-px-3 cqa-py-3\">\n <span *ngIf=\"!collapsed\" class=\"cqa-text-sm cqa-font-semibold cqa-text-neutral-900\">\n {{ labels.folders }}\n </span>\n <div class=\"cqa-flex cqa-items-center cqa-gap-1\" [class.cqa-w-full]=\"collapsed\" [class.cqa-justify-center]=\"collapsed\">\n <button\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"togglePanel()\"\n [attr.aria-label]=\"collapsed ? 'Expand folders panel' : 'Collapse folders panel'\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">\n {{ collapsed ? 'chevron_right' : 'keyboard_double_arrow_left' }}\n </mat-icon>\n </button>\n <button\n *ngIf=\"!collapsed && allowCreate\"\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"requestCreate(null)\"\n [attr.aria-label]=\"labels.newFolder\"\n [title]=\"labels.newFolder\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">add</mat-icon>\n </button>\n </div>\n </div>\n\n <ng-container *ngIf=\"!collapsed\">\n <!-- Search -->\n <div class=\"cqa-px-3 cqa-pb-2\">\n <cqa-search-bar\n size=\"sm\"\n [fullWidth]=\"true\"\n [value]=\"searchValue\"\n [placeholder]=\"labels.searchFoldersPlaceholder\"\n [showClear]=\"true\"\n (valueChange)=\"searchValue = $event\"\n (cleared)=\"searchValue = ''\"\n ></cqa-search-bar>\n </div>\n\n <!-- Tree (folders only \u2014 Unorganised is pinned below the scroll area) -->\n <div role=\"tree\" class=\"cqa-flex-1 cqa-min-h-0 cqa-max-h-[60vh] cqa-overflow-y-auto cqa-py-1 cqa-scrollbar-thin cqa-scrollbar-track-transparent cqa-scrollbar-thumb-[#E5E7EB] cqa-scrollbar-thumb-rounded-full cqa-scrollbar-thumb-hover:cqa-bg-[#D1D5DB]\">\n <ng-container *ngFor=\"let row of rows; let i = index; trackBy: trackByRow\">\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-expanded]=\"row.hasChildren ? isExpanded(row.node.id) : null\"\n [attr.aria-selected]=\"isSelected(row.node.id)\"\n [attr.data-folder-row-id]=\"row.node.id\"\n [cqaFolderDrop]=\"row.node.id\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(row.node.id, $event)\"\n (folderDropped)=\"onFolderRowDropped(row.node.id, $event)\"\n [cqaFolderDrag]=\"row.node.id\"\n [dragEnabled]=\"allowDrop\"\n (dragstart)=\"onFolderDragStart(row.node)\"\n (dragend)=\"onFolderDragEnd()\"\n (dragover)=\"onFolderRowDragOver(row)\"\n (dragleave)=\"onFolderRowDragLeave(row)\"\n (click)=\"onSelect(row.node)\"\n (contextmenu)=\"openContextMenu(row.node, $event)\"\n (keydown)=\"onRowKeydown($event, row, i)\"\n class=\"cqa-group cqa-flex cqa-items-center cqa-gap-1 cqa-pr-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(row.node.id) || contextMenuFolderId === row.node.id\"\n [style.paddingLeft.px]=\"8 + row.depth * 16\"\n [attr.title]=\"row.node.totalCount != null ? (row.node.totalCount + ' total including subfolders') : null\"\n >\n <span\n *ngIf=\"isSelected(row.node.id)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <button\n type=\"button\"\n class=\"cqa-p-0 cqa-text-neutral-400\"\n [style.visibility]=\"row.hasChildren ? 'visible' : 'hidden'\"\n (click)=\"onToggle(row.node, $event)\"\n [attr.aria-label]=\"isExpanded(row.node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">\n {{ isExpanded(row.node.id) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: row.node.color }\"></ng-container>\n\n <ng-container *ngIf=\"renamingId === row.node.id; else nameTpl\">\n <input\n type=\"text\"\n size=\"1\"\n [attr.data-folder-rename-input]=\"row.node.id\"\n [attr.maxlength]=\"renameMaxLength\"\n class=\"cqa-flex-1 cqa-min-w-0 cqa-w-full cqa-px-2 cqa-py-0.5 cqa-rounded cqa-border cqa-bg-white cqa-text-sm cqa-shadow-sm focus:cqa-outline-none focus:cqa-ring-1\"\n [ngClass]=\"isRenameDraftValid\n ? 'cqa-border-neutral-300 focus:cqa-border-indigo-400 focus:cqa-ring-indigo-200'\n : 'cqa-border-[#EF4444] focus:cqa-border-[#EF4444] focus:cqa-ring-[#FCA5A5]'\"\n [attr.aria-invalid]=\"!isRenameDraftValid\"\n [(ngModel)]=\"renameDraft\"\n (keydown)=\"onRenameKey($event, row.node)\"\n (keypress)=\"$event.stopPropagation()\"\n (keyup)=\"$event.stopPropagation()\"\n (blur)=\"commitRename(row.node)\"\n (click)=\"$event.stopPropagation()\"\n />\n </ng-container>\n <ng-template #nameTpl>\n <span\n class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-800 cqa-truncate\"\n (dblclick)=\"beginRename(row.node, $event)\"\n >{{ row.node.name }}</span>\n </ng-template>\n\n <!-- Count shown at rest; hidden on row hover when delete is allowed -->\n <span\n *ngIf=\"showCounts && row.node.count != null\"\n class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums cqa-ml-1\"\n [class.group-hover:cqa-hidden]=\"allowDelete\"\n >{{ row.node.count }}</span>\n\n <button\n *ngIf=\"hasAnyContextAction\"\n type=\"button\"\n class=\"cqa-p-0.5 cqa-rounded hover:cqa-bg-neutral-200 cqa-text-neutral-500\"\n [class.cqa-hidden]=\"contextMenuFolderId !== row.node.id\"\n [class.group-hover:cqa-inline-flex]=\"true\"\n [class.cqa-inline-flex]=\"contextMenuFolderId === row.node.id\"\n (click)=\"openContextMenu(row.node, $event)\"\n [attr.aria-label]=\"'Open actions for ' + row.node.name\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"contextMenuFolderId === row.node.id\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">more_horiz</mat-icon>\n </button>\n </div>\n </ng-container>\n </div>\n\n <!-- Divider between folder tree and pinned Unorganised tab -->\n <div\n aria-hidden=\"true\"\n class=\"cqa-mx-3 cqa-shrink-0\"\n style=\"height: 1px; background-color: #E5E7EB; margin-top: 6px; margin-bottom: 6px;\"\n ></div>\n\n <!-- Unorganised \u2014 pinned below the scrollable folder tree so it stays visible regardless of folder count -->\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-selected]=\"isSelected(null)\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-px-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative cqa-shrink-0\"\n [class.cqa-bg-indigo-50]=\"isSelected(null)\"\n [cqaFolderDrop]=\"null\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(null, $event)\"\n (folderDropped)=\"onFolderRowDropped(null, $event)\"\n (click)=\"onSelectUnorganised()\"\n >\n <span\n *ngIf=\"isSelected(null)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <mat-icon class=\"cqa-text-neutral-500\" style=\"font-size:16px;width:16px;height:16px\">inbox</mat-icon>\n <span class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-700\">{{ labels.unorganised }}</span>\n <span *ngIf=\"showCounts\" class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums\">{{ unorganisedCount }}</span>\n </div>\n\n <!-- Folder context menu (right-click / ellipsis). Anchored to viewport coords. -->\n <div\n *ngIf=\"contextMenuFolderId !== null\"\n role=\"menu\"\n class=\"cqa-fixed cqa-z-50 cqa-min-w-[180px] cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-md cqa-shadow-lg cqa-py-1\"\n [style.left.px]=\"contextMenuPosition.x\"\n [style.top.px]=\"contextMenuPosition.y\"\n (click)=\"$event.stopPropagation()\"\n (contextmenu)=\"$event.preventDefault(); $event.stopPropagation()\"\n >\n <ng-container *ngIf=\"folderById(contextMenuFolderId) as menuNode\">\n <button\n *ngIf=\"allowCreate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextCreateSubfolder(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">create_new_folder</mat-icon>\n <span>{{ labels.folderMenuCreateSubfolder }}</span>\n </button>\n <button\n *ngIf=\"allowRename\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextRename(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">edit</mat-icon>\n <span>{{ labels.folderMenuRename }}</span>\n </button>\n <button\n *ngIf=\"allowMove\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextMove(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">drive_file_move</mat-icon>\n <span>{{ labels.folderMenuMove }}</span>\n </button>\n <button\n *ngIf=\"allowDuplicate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextDuplicate(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">content_copy</mat-icon>\n <span>{{ labels.folderMenuDuplicate }}</span>\n </button>\n <div *ngIf=\"allowDelete && (allowCreate || allowRename || allowMove || allowDuplicate)\" class=\"cqa-h-px cqa-bg-neutral-200 cqa-my-1\"></div>\n <button\n *ngIf=\"allowDelete\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-red-600 hover:cqa-bg-red-50 cqa-text-left\"\n (click)=\"onContextDelete(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">delete_outline</mat-icon>\n <span>{{ labels.folderMenuDelete }}</span>\n </button>\n </ng-container>\n </div>\n\n </ng-container>\n</aside>\n", components: [{ type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: SearchBarComponent, selector: "cqa-search-bar", inputs: ["placeholder", "value", "disabled", "showClear", "ariaLabel", "autoFocus", "size", "fullWidth"], outputs: ["valueChange", "search", "cleared"] }], directives: [{ type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: FolderDropDirective, selector: "[cqaFolderDrop]", inputs: ["cqaFolderDrop", "dropEnabled"], outputs: ["testsDropped", "folderDropped"] }, { type: FolderDragDirective, selector: "[cqaFolderDrag]", inputs: ["cqaFolderDrag", "dragEnabled"] }, { type: i2.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet"] }, { type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { type: i1$1.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
9262
9311
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: FolderSidebarComponent, decorators: [{
9263
9312
  type: Component,
9264
- args: [{ selector: 'cqa-folder-sidebar', host: { class: 'cqa-ui-root' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Reusable folder icon. Render via <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: node.color }\"></ng-container>. -->\n<ng-template #folderIcon let-color=\"color\">\n <span class=\"cqa-inline-flex cqa-items-center cqa-justify-center cqa-w-4 cqa-h-4 cqa-shrink-0\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M12.1916 9.85824C12.1916 10.1677 12.0687 10.4644 11.8499 10.6832C11.6311 10.902 11.3343 11.0249 11.0249 11.0249H1.69157C1.38215 11.0249 1.0854 10.902 0.866611 10.6832C0.647819 10.4644 0.524902 10.1677 0.524902 9.85824V1.69157C0.524902 1.38215 0.647819 1.0854 0.866611 0.866611C1.0854 0.647819 1.38215 0.524902 1.69157 0.524902H4.60824L5.7749 2.2749H11.0249C11.3343 2.2749 11.6311 2.39782 11.8499 2.61661C12.0687 2.8354 12.1916 3.13215 12.1916 3.44157V9.85824Z\" [attr.stroke]=\"color || '#99999E'\" stroke-width=\"1.05\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n</ng-template>\n\n<aside\n class=\"cqa-flex cqa-flex-col cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-lg cqa-h-full cqa-min-h-0\"\n [class.cqa-w-[240px]]=\"!collapsed\"\n [class.cqa-w-[48px]]=\"collapsed\"\n style=\"transition: width 150ms ease;\"\n>\n <!-- Header -->\n <div class=\"cqa-flex cqa-items-center cqa-justify-between cqa-px-3 cqa-py-3\">\n <span *ngIf=\"!collapsed\" class=\"cqa-text-sm cqa-font-semibold cqa-text-neutral-900\">\n {{ labels.folders }}\n </span>\n <div class=\"cqa-flex cqa-items-center cqa-gap-1\" [class.cqa-w-full]=\"collapsed\" [class.cqa-justify-center]=\"collapsed\">\n <button\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"togglePanel()\"\n [attr.aria-label]=\"collapsed ? 'Expand folders panel' : 'Collapse folders panel'\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">\n {{ collapsed ? 'chevron_right' : 'keyboard_double_arrow_left' }}\n </mat-icon>\n </button>\n <button\n *ngIf=\"!collapsed && allowCreate\"\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"requestCreate(null)\"\n [attr.aria-label]=\"labels.newFolder\"\n [title]=\"labels.newFolder\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">add</mat-icon>\n </button>\n </div>\n </div>\n\n <ng-container *ngIf=\"!collapsed\">\n <!-- Search -->\n <div class=\"cqa-px-3 cqa-pb-2\">\n <cqa-search-bar\n size=\"sm\"\n [fullWidth]=\"true\"\n [value]=\"searchValue\"\n [placeholder]=\"labels.searchFoldersPlaceholder\"\n [showClear]=\"true\"\n (valueChange)=\"searchValue = $event\"\n (cleared)=\"searchValue = ''\"\n ></cqa-search-bar>\n </div>\n\n <!-- Tree (folders + Unorganised tab) -->\n <div role=\"tree\" class=\"cqa-flex-1 cqa-overflow-y-auto cqa-py-1\">\n <ng-container *ngFor=\"let row of rows; let i = index; trackBy: trackByRow\">\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-expanded]=\"row.hasChildren ? isExpanded(row.node.id) : null\"\n [attr.aria-selected]=\"isSelected(row.node.id)\"\n [attr.data-folder-row-id]=\"row.node.id\"\n [cqaFolderDrop]=\"row.node.id\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(row.node.id, $event)\"\n (folderDropped)=\"onFolderRowDropped(row.node.id, $event)\"\n [cqaFolderDrag]=\"row.node.id\"\n [dragEnabled]=\"allowDrop\"\n (dragstart)=\"onFolderDragStart(row.node)\"\n (dragend)=\"onFolderDragEnd()\"\n (dragover)=\"onFolderRowDragOver(row)\"\n (dragleave)=\"onFolderRowDragLeave(row)\"\n (click)=\"onSelect(row.node)\"\n (contextmenu)=\"openContextMenu(row.node, $event)\"\n (keydown)=\"onRowKeydown($event, row, i)\"\n class=\"cqa-group cqa-flex cqa-items-center cqa-gap-1 cqa-pr-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(row.node.id) || contextMenuFolderId === row.node.id\"\n [style.paddingLeft.px]=\"8 + row.depth * 16\"\n [attr.title]=\"row.node.totalCount != null ? (row.node.totalCount + ' total including subfolders') : null\"\n >\n <span\n *ngIf=\"isSelected(row.node.id)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <button\n type=\"button\"\n class=\"cqa-p-0 cqa-text-neutral-400\"\n [style.visibility]=\"row.hasChildren ? 'visible' : 'hidden'\"\n (click)=\"onToggle(row.node, $event)\"\n [attr.aria-label]=\"isExpanded(row.node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">\n {{ isExpanded(row.node.id) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: row.node.color }\"></ng-container>\n\n <ng-container *ngIf=\"renamingId === row.node.id; else nameTpl\">\n <input\n type=\"text\"\n size=\"1\"\n [attr.data-folder-rename-input]=\"row.node.id\"\n class=\"cqa-flex-1 cqa-min-w-0 cqa-w-full cqa-px-2 cqa-py-0.5 cqa-rounded cqa-border cqa-border-neutral-300 cqa-bg-white cqa-text-sm cqa-shadow-sm focus:cqa-outline-none focus:cqa-border-indigo-400 focus:cqa-ring-1 focus:cqa-ring-indigo-200\"\n [(ngModel)]=\"renameDraft\"\n (keydown)=\"onRenameKey($event, row.node)\"\n (keypress)=\"$event.stopPropagation()\"\n (keyup)=\"$event.stopPropagation()\"\n (blur)=\"commitRename(row.node)\"\n (click)=\"$event.stopPropagation()\"\n />\n </ng-container>\n <ng-template #nameTpl>\n <span\n class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-800 cqa-truncate\"\n (dblclick)=\"beginRename(row.node, $event)\"\n >{{ row.node.name }}</span>\n </ng-template>\n\n <!-- Count shown at rest; hidden on row hover when delete is allowed -->\n <span\n *ngIf=\"showCounts && row.node.count != null\"\n class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums cqa-ml-1\"\n [class.group-hover:cqa-hidden]=\"allowDelete\"\n >{{ row.node.count }}</span>\n\n <button\n *ngIf=\"hasAnyContextAction\"\n type=\"button\"\n class=\"cqa-p-0.5 cqa-rounded hover:cqa-bg-neutral-200 cqa-text-neutral-500\"\n [class.cqa-hidden]=\"contextMenuFolderId !== row.node.id\"\n [class.group-hover:cqa-inline-flex]=\"true\"\n [class.cqa-inline-flex]=\"contextMenuFolderId === row.node.id\"\n (click)=\"openContextMenu(row.node, $event)\"\n [attr.aria-label]=\"'Open actions for ' + row.node.name\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"contextMenuFolderId === row.node.id\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">more_horiz</mat-icon>\n </button>\n </div>\n </ng-container>\n\n <!-- Divider between folder tree and Unorganised tab -->\n <div\n aria-hidden=\"true\"\n class=\"cqa-mx-3\"\n style=\"height: 1px; background-color: #E5E7EB; margin-top: 6px; margin-bottom: 6px;\"\n ></div>\n\n <!-- Unorganised \u2014 flows with the tree, behaves like a tab item -->\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-selected]=\"isSelected(null)\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-px-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(null)\"\n [cqaFolderDrop]=\"null\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(null, $event)\"\n (folderDropped)=\"onFolderRowDropped(null, $event)\"\n (click)=\"onSelectUnorganised()\"\n >\n <span\n *ngIf=\"isSelected(null)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <mat-icon class=\"cqa-text-neutral-500\" style=\"font-size:16px;width:16px;height:16px\">inbox</mat-icon>\n <span class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-700\">{{ labels.unorganised }}</span>\n <span *ngIf=\"showCounts\" class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums\">{{ unorganisedCount }}</span>\n </div>\n </div>\n\n <!-- Folder context menu (right-click / ellipsis). Anchored to viewport coords. -->\n <div\n *ngIf=\"contextMenuFolderId !== null\"\n role=\"menu\"\n class=\"cqa-fixed cqa-z-50 cqa-min-w-[180px] cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-md cqa-shadow-lg cqa-py-1\"\n [style.left.px]=\"contextMenuPosition.x\"\n [style.top.px]=\"contextMenuPosition.y\"\n (click)=\"$event.stopPropagation()\"\n (contextmenu)=\"$event.preventDefault(); $event.stopPropagation()\"\n >\n <ng-container *ngIf=\"folderById(contextMenuFolderId) as menuNode\">\n <button\n *ngIf=\"allowCreate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextCreateSubfolder(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">create_new_folder</mat-icon>\n <span>{{ labels.folderMenuCreateSubfolder }}</span>\n </button>\n <button\n *ngIf=\"allowRename\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextRename(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">edit</mat-icon>\n <span>{{ labels.folderMenuRename }}</span>\n </button>\n <button\n *ngIf=\"allowMove\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextMove(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">drive_file_move</mat-icon>\n <span>{{ labels.folderMenuMove }}</span>\n </button>\n <button\n *ngIf=\"allowDuplicate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextDuplicate(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">content_copy</mat-icon>\n <span>{{ labels.folderMenuDuplicate }}</span>\n </button>\n <div *ngIf=\"allowDelete && (allowCreate || allowRename || allowMove || allowDuplicate)\" class=\"cqa-h-px cqa-bg-neutral-200 cqa-my-1\"></div>\n <button\n *ngIf=\"allowDelete\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-red-600 hover:cqa-bg-red-50 cqa-text-left\"\n (click)=\"onContextDelete(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">delete_outline</mat-icon>\n <span>{{ labels.folderMenuDelete }}</span>\n </button>\n </ng-container>\n </div>\n\n </ng-container>\n</aside>\n", styles: [] }]
9313
+ args: [{ selector: 'cqa-folder-sidebar', host: { class: 'cqa-ui-root' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Reusable folder icon. Render via <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: node.color }\"></ng-container>. -->\n<ng-template #folderIcon let-color=\"color\">\n <span class=\"cqa-inline-flex cqa-items-center cqa-justify-center cqa-w-4 cqa-h-4 cqa-shrink-0\">\n <svg width=\"13\" height=\"12\" viewBox=\"0 0 13 12\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" aria-hidden=\"true\">\n <path d=\"M12.1916 9.85824C12.1916 10.1677 12.0687 10.4644 11.8499 10.6832C11.6311 10.902 11.3343 11.0249 11.0249 11.0249H1.69157C1.38215 11.0249 1.0854 10.902 0.866611 10.6832C0.647819 10.4644 0.524902 10.1677 0.524902 9.85824V1.69157C0.524902 1.38215 0.647819 1.0854 0.866611 0.866611C1.0854 0.647819 1.38215 0.524902 1.69157 0.524902H4.60824L5.7749 2.2749H11.0249C11.3343 2.2749 11.6311 2.39782 11.8499 2.61661C12.0687 2.8354 12.1916 3.13215 12.1916 3.44157V9.85824Z\" [attr.stroke]=\"color || '#99999E'\" stroke-width=\"1.05\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </span>\n</ng-template>\n\n<aside\n class=\"cqa-flex cqa-flex-col cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-lg cqa-h-full cqa-min-h-0\"\n [class.cqa-w-[240px]]=\"!collapsed\"\n [class.cqa-w-[48px]]=\"collapsed\"\n style=\"transition: width 150ms ease;\"\n>\n <!-- Header -->\n <div class=\"cqa-flex cqa-items-center cqa-justify-between cqa-px-3 cqa-py-3\">\n <span *ngIf=\"!collapsed\" class=\"cqa-text-sm cqa-font-semibold cqa-text-neutral-900\">\n {{ labels.folders }}\n </span>\n <div class=\"cqa-flex cqa-items-center cqa-gap-1\" [class.cqa-w-full]=\"collapsed\" [class.cqa-justify-center]=\"collapsed\">\n <button\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"togglePanel()\"\n [attr.aria-label]=\"collapsed ? 'Expand folders panel' : 'Collapse folders panel'\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">\n {{ collapsed ? 'chevron_right' : 'keyboard_double_arrow_left' }}\n </mat-icon>\n </button>\n <button\n *ngIf=\"!collapsed && allowCreate\"\n type=\"button\"\n class=\"cqa-p-1 cqa-rounded hover:cqa-bg-neutral-100 cqa-text-neutral-500\"\n (click)=\"requestCreate(null)\"\n [attr.aria-label]=\"labels.newFolder\"\n [title]=\"labels.newFolder\"\n >\n <mat-icon style=\"font-size:18px;width:18px;height:18px\">add</mat-icon>\n </button>\n </div>\n </div>\n\n <ng-container *ngIf=\"!collapsed\">\n <!-- Search -->\n <div class=\"cqa-px-3 cqa-pb-2\">\n <cqa-search-bar\n size=\"sm\"\n [fullWidth]=\"true\"\n [value]=\"searchValue\"\n [placeholder]=\"labels.searchFoldersPlaceholder\"\n [showClear]=\"true\"\n (valueChange)=\"searchValue = $event\"\n (cleared)=\"searchValue = ''\"\n ></cqa-search-bar>\n </div>\n\n <!-- Tree (folders only \u2014 Unorganised is pinned below the scroll area) -->\n <div role=\"tree\" class=\"cqa-flex-1 cqa-min-h-0 cqa-max-h-[60vh] cqa-overflow-y-auto cqa-py-1 cqa-scrollbar-thin cqa-scrollbar-track-transparent cqa-scrollbar-thumb-[#E5E7EB] cqa-scrollbar-thumb-rounded-full cqa-scrollbar-thumb-hover:cqa-bg-[#D1D5DB]\">\n <ng-container *ngFor=\"let row of rows; let i = index; trackBy: trackByRow\">\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-expanded]=\"row.hasChildren ? isExpanded(row.node.id) : null\"\n [attr.aria-selected]=\"isSelected(row.node.id)\"\n [attr.data-folder-row-id]=\"row.node.id\"\n [cqaFolderDrop]=\"row.node.id\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(row.node.id, $event)\"\n (folderDropped)=\"onFolderRowDropped(row.node.id, $event)\"\n [cqaFolderDrag]=\"row.node.id\"\n [dragEnabled]=\"allowDrop\"\n (dragstart)=\"onFolderDragStart(row.node)\"\n (dragend)=\"onFolderDragEnd()\"\n (dragover)=\"onFolderRowDragOver(row)\"\n (dragleave)=\"onFolderRowDragLeave(row)\"\n (click)=\"onSelect(row.node)\"\n (contextmenu)=\"openContextMenu(row.node, $event)\"\n (keydown)=\"onRowKeydown($event, row, i)\"\n class=\"cqa-group cqa-flex cqa-items-center cqa-gap-1 cqa-pr-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative\"\n [class.cqa-bg-indigo-50]=\"isSelected(row.node.id) || contextMenuFolderId === row.node.id\"\n [style.paddingLeft.px]=\"8 + row.depth * 16\"\n [attr.title]=\"row.node.totalCount != null ? (row.node.totalCount + ' total including subfolders') : null\"\n >\n <span\n *ngIf=\"isSelected(row.node.id)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <button\n type=\"button\"\n class=\"cqa-p-0 cqa-text-neutral-400\"\n [style.visibility]=\"row.hasChildren ? 'visible' : 'hidden'\"\n (click)=\"onToggle(row.node, $event)\"\n [attr.aria-label]=\"isExpanded(row.node.id) ? 'Collapse' : 'Expand'\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">\n {{ isExpanded(row.node.id) ? 'expand_more' : 'chevron_right' }}\n </mat-icon>\n </button>\n <ng-container *ngTemplateOutlet=\"folderIcon; context: { color: row.node.color }\"></ng-container>\n\n <ng-container *ngIf=\"renamingId === row.node.id; else nameTpl\">\n <input\n type=\"text\"\n size=\"1\"\n [attr.data-folder-rename-input]=\"row.node.id\"\n [attr.maxlength]=\"renameMaxLength\"\n class=\"cqa-flex-1 cqa-min-w-0 cqa-w-full cqa-px-2 cqa-py-0.5 cqa-rounded cqa-border cqa-bg-white cqa-text-sm cqa-shadow-sm focus:cqa-outline-none focus:cqa-ring-1\"\n [ngClass]=\"isRenameDraftValid\n ? 'cqa-border-neutral-300 focus:cqa-border-indigo-400 focus:cqa-ring-indigo-200'\n : 'cqa-border-[#EF4444] focus:cqa-border-[#EF4444] focus:cqa-ring-[#FCA5A5]'\"\n [attr.aria-invalid]=\"!isRenameDraftValid\"\n [(ngModel)]=\"renameDraft\"\n (keydown)=\"onRenameKey($event, row.node)\"\n (keypress)=\"$event.stopPropagation()\"\n (keyup)=\"$event.stopPropagation()\"\n (blur)=\"commitRename(row.node)\"\n (click)=\"$event.stopPropagation()\"\n />\n </ng-container>\n <ng-template #nameTpl>\n <span\n class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-800 cqa-truncate\"\n (dblclick)=\"beginRename(row.node, $event)\"\n >{{ row.node.name }}</span>\n </ng-template>\n\n <!-- Count shown at rest; hidden on row hover when delete is allowed -->\n <span\n *ngIf=\"showCounts && row.node.count != null\"\n class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums cqa-ml-1\"\n [class.group-hover:cqa-hidden]=\"allowDelete\"\n >{{ row.node.count }}</span>\n\n <button\n *ngIf=\"hasAnyContextAction\"\n type=\"button\"\n class=\"cqa-p-0.5 cqa-rounded hover:cqa-bg-neutral-200 cqa-text-neutral-500\"\n [class.cqa-hidden]=\"contextMenuFolderId !== row.node.id\"\n [class.group-hover:cqa-inline-flex]=\"true\"\n [class.cqa-inline-flex]=\"contextMenuFolderId === row.node.id\"\n (click)=\"openContextMenu(row.node, $event)\"\n [attr.aria-label]=\"'Open actions for ' + row.node.name\"\n [attr.aria-haspopup]=\"'menu'\"\n [attr.aria-expanded]=\"contextMenuFolderId === row.node.id\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">more_horiz</mat-icon>\n </button>\n </div>\n </ng-container>\n </div>\n\n <!-- Divider between folder tree and pinned Unorganised tab -->\n <div\n aria-hidden=\"true\"\n class=\"cqa-mx-3 cqa-shrink-0\"\n style=\"height: 1px; background-color: #E5E7EB; margin-top: 6px; margin-bottom: 6px;\"\n ></div>\n\n <!-- Unorganised \u2014 pinned below the scrollable folder tree so it stays visible regardless of folder count -->\n <div\n role=\"treeitem\"\n tabindex=\"0\"\n [attr.aria-selected]=\"isSelected(null)\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-px-3 cqa-py-1.5 cqa-cursor-pointer hover:cqa-bg-neutral-50 focus:cqa-outline-none focus:cqa-bg-neutral-100 cqa-relative cqa-shrink-0\"\n [class.cqa-bg-indigo-50]=\"isSelected(null)\"\n [cqaFolderDrop]=\"null\"\n [dropEnabled]=\"allowDrop\"\n (testsDropped)=\"rowDropped(null, $event)\"\n (folderDropped)=\"onFolderRowDropped(null, $event)\"\n (click)=\"onSelectUnorganised()\"\n >\n <span\n *ngIf=\"isSelected(null)\"\n class=\"cqa-absolute cqa-left-0 cqa-top-0 cqa-bottom-0 cqa-w-[3px] cqa-bg-indigo-600\"\n ></span>\n <mat-icon class=\"cqa-text-neutral-500\" style=\"font-size:16px;width:16px;height:16px\">inbox</mat-icon>\n <span class=\"cqa-flex-1 cqa-text-sm cqa-text-neutral-700\">{{ labels.unorganised }}</span>\n <span *ngIf=\"showCounts\" class=\"cqa-text-xs cqa-text-neutral-400 cqa-tabular-nums\">{{ unorganisedCount }}</span>\n </div>\n\n <!-- Folder context menu (right-click / ellipsis). Anchored to viewport coords. -->\n <div\n *ngIf=\"contextMenuFolderId !== null\"\n role=\"menu\"\n class=\"cqa-fixed cqa-z-50 cqa-min-w-[180px] cqa-bg-white cqa-border cqa-border-neutral-200 cqa-rounded-md cqa-shadow-lg cqa-py-1\"\n [style.left.px]=\"contextMenuPosition.x\"\n [style.top.px]=\"contextMenuPosition.y\"\n (click)=\"$event.stopPropagation()\"\n (contextmenu)=\"$event.preventDefault(); $event.stopPropagation()\"\n >\n <ng-container *ngIf=\"folderById(contextMenuFolderId) as menuNode\">\n <button\n *ngIf=\"allowCreate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextCreateSubfolder(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">create_new_folder</mat-icon>\n <span>{{ labels.folderMenuCreateSubfolder }}</span>\n </button>\n <button\n *ngIf=\"allowRename\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextRename(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">edit</mat-icon>\n <span>{{ labels.folderMenuRename }}</span>\n </button>\n <button\n *ngIf=\"allowMove\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextMove(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">drive_file_move</mat-icon>\n <span>{{ labels.folderMenuMove }}</span>\n </button>\n <button\n *ngIf=\"allowDuplicate\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-neutral-800 hover:cqa-bg-neutral-100 cqa-text-left\"\n (click)=\"onContextDuplicate(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">content_copy</mat-icon>\n <span>{{ labels.folderMenuDuplicate }}</span>\n </button>\n <div *ngIf=\"allowDelete && (allowCreate || allowRename || allowMove || allowDuplicate)\" class=\"cqa-h-px cqa-bg-neutral-200 cqa-my-1\"></div>\n <button\n *ngIf=\"allowDelete\"\n type=\"button\"\n role=\"menuitem\"\n class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-w-full cqa-px-3 cqa-py-1.5 cqa-text-sm cqa-text-red-600 hover:cqa-bg-red-50 cqa-text-left\"\n (click)=\"onContextDelete(menuNode)\"\n >\n <mat-icon style=\"font-size:16px;width:16px;height:16px\">delete_outline</mat-icon>\n <span>{{ labels.folderMenuDelete }}</span>\n </button>\n </ng-container>\n </div>\n\n </ng-container>\n</aside>\n", styles: [] }]
9265
9314
  }], ctorParameters: function () { return [{ type: i0.ChangeDetectorRef }]; }, propDecorators: { folders: [{
9266
9315
  type: Input
9267
9316
  }], selectedFolderId: [{
@@ -10377,6 +10426,8 @@ class ModularTableTemplateComponent {
10377
10426
  folders: this.folders,
10378
10427
  labels: this.modularLabels,
10379
10428
  currentFolderId: this.selectedFolderId,
10429
+ // When moving FROM Unorganised, disable the Unorganised destination row.
10430
+ rootDisabled: this.selectedFolderId == null,
10380
10431
  },
10381
10432
  },
10382
10433
  buttons: [
@@ -50760,6 +50811,295 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
50760
50811
  type: Input
50761
50812
  }] } });
50762
50813
 
50814
+ class ManageColumnsDialogComponent {
50815
+ constructor() {
50816
+ this.columns = [];
50817
+ this.lockedColumns = [];
50818
+ this.items = [];
50819
+ this.newColName = '';
50820
+ this.columnsError = null;
50821
+ this.trackByUid = (_, item) => item.uid;
50822
+ this.nextUid = 1;
50823
+ }
50824
+ ngOnInit() {
50825
+ const initial = Array.isArray(this.columns) ? this.columns : [];
50826
+ this.items = initial.map(name => ({
50827
+ uid: this.nextUid++,
50828
+ originalName: name,
50829
+ name,
50830
+ }));
50831
+ }
50832
+ isLocked(item) {
50833
+ if (!this.lockedColumns || this.lockedColumns.length === 0) {
50834
+ return false;
50835
+ }
50836
+ if (item.originalName != null && this.lockedColumns.indexOf(item.originalName) !== -1) {
50837
+ return true;
50838
+ }
50839
+ return this.lockedColumns.indexOf(item.name) !== -1;
50840
+ }
50841
+ onDndDrop(event) {
50842
+ const dragged = event.data;
50843
+ if (!dragged || event.index == null) {
50844
+ return;
50845
+ }
50846
+ const from = this.items.findIndex(it => it.uid === dragged.uid);
50847
+ if (from < 0) {
50848
+ return;
50849
+ }
50850
+ const next = this.items.slice();
50851
+ next.splice(from, 1);
50852
+ const to = event.index > from ? event.index - 1 : event.index;
50853
+ next.splice(to, 0, dragged);
50854
+ this.items = next;
50855
+ }
50856
+ onMove(index, dir) {
50857
+ const target = index + dir;
50858
+ if (target < 0 || target >= this.items.length) {
50859
+ return;
50860
+ }
50861
+ const next = this.items.slice();
50862
+ [next[index], next[target]] = [next[target], next[index]];
50863
+ this.items = next;
50864
+ }
50865
+ onRename(index, value) {
50866
+ if (!this.items[index]) {
50867
+ return;
50868
+ }
50869
+ this.items = this.items.map((it, i) => i === index ? { ...it, name: value } : it);
50870
+ this.columnsError = null;
50871
+ }
50872
+ onRemove(index) {
50873
+ const item = this.items[index];
50874
+ if (!item || this.isLocked(item)) {
50875
+ return;
50876
+ }
50877
+ this.items = this.items.filter((_, i) => i !== index);
50878
+ this.columnsError = null;
50879
+ }
50880
+ onNewColNameChange(value) {
50881
+ this.newColName = value;
50882
+ }
50883
+ onAdd() {
50884
+ const trimmed = (this.newColName || '').trim();
50885
+ if (!trimmed || this.itemsHasName(trimmed)) {
50886
+ return;
50887
+ }
50888
+ this.items = [...this.items, {
50889
+ uid: this.nextUid++,
50890
+ originalName: null,
50891
+ name: trimmed,
50892
+ }];
50893
+ this.newColName = '';
50894
+ this.columnsError = null;
50895
+ }
50896
+ get canAdd() {
50897
+ const trimmed = (this.newColName || '').trim();
50898
+ if (!trimmed) {
50899
+ return false;
50900
+ }
50901
+ return !this.itemsHasName(trimmed);
50902
+ }
50903
+ getValue() {
50904
+ const trimmed = this.items.map(it => ({
50905
+ originalName: it.originalName,
50906
+ name: (it.name || '').trim(),
50907
+ }));
50908
+ if (trimmed.length === 0) {
50909
+ this.columnsError = 'At least one column is required.';
50910
+ return null;
50911
+ }
50912
+ if (trimmed.some(c => c.name.length === 0)) {
50913
+ this.columnsError = 'Column names cannot be empty.';
50914
+ return null;
50915
+ }
50916
+ const lowered = trimmed.map(c => c.name.toLowerCase());
50917
+ if (new Set(lowered).size !== trimmed.length) {
50918
+ this.columnsError = 'Column names must be unique.';
50919
+ return null;
50920
+ }
50921
+ this.columnsError = null;
50922
+ return { columns: trimmed };
50923
+ }
50924
+ itemsHasName(candidate) {
50925
+ const lowered = candidate.toLowerCase();
50926
+ return this.items.some(it => (it.name || '').trim().toLowerCase() === lowered);
50927
+ }
50928
+ }
50929
+ ManageColumnsDialogComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: ManageColumnsDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
50930
+ ManageColumnsDialogComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: ManageColumnsDialogComponent, selector: "cqa-manage-columns-dialog", inputs: { columns: "columns", lockedColumns: "lockedColumns" }, host: { styleAttribute: "display:block;width:100%;", classAttribute: "cqa-ui-root" }, ngImport: i0, template: "<div class=\"cqa-flex cqa-flex-col cqa-gap-3 cqa-w-full\">\n\n <div class=\"manage-cols-drop-list cqa-flex cqa-flex-col cqa-gap-2\"\n [dndDropzone]=\"['col']\"\n dndEffectAllowed=\"move\"\n dndDragoverClass=\"dndDragover\"\n (dndDrop)=\"onDndDrop($event)\">\n <div dndPlaceholderRef class=\"manage-cols-placeholder\">Drop here</div>\n <div *ngFor=\"let item of items; let i = index; trackBy: trackByUid\"\n class=\"manage-cols-row cqa-flex cqa-items-center cqa-gap-2 cqa-px-2 cqa-py-1.5 cqa-rounded-md cqa-border cqa-border-gray-200 cqa-bg-gray-50\">\n <span class=\"manage-cols-handle cqa-flex cqa-items-center cqa-justify-center cqa-text-gray-400\"\n [dndDraggable]=\"item\"\n [dndDisableIf]=\"isLocked(item)\"\n dndType=\"col\"\n dndEffectAllowed=\"move\">\n <mat-icon style=\"font-size:18px;width:18px;height:18px;line-height:18px;\">drag_indicator</mat-icon>\n </span>\n <cqa-custom-input\n class=\"cqa-flex-1\"\n [value]=\"item.name\"\n [fullWidth]=\"true\"\n [disabled]=\"isLocked(item)\"\n inputInlineStyle=\"font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\"\n (valueChange)=\"onRename(i, $event)\">\n </cqa-custom-input>\n <button type=\"button\" class=\"manage-cols-icon-btn\"\n title=\"Move up\"\n [disabled]=\"i === 0\"\n (click)=\"onMove(i, -1)\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"manage-cols-icon-btn\"\n title=\"Move down\"\n [disabled]=\"i === items.length - 1\"\n (click)=\"onMove(i, 1)\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button type=\"button\" class=\"manage-cols-icon-btn manage-cols-icon-btn-danger\"\n title=\"Delete column\"\n [disabled]=\"isLocked(item)\"\n (click)=\"onRemove(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n\n <div class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-pt-3 cqa-mt-1 cqa-border-t cqa-border-dashed cqa-border-gray-200\">\n <cqa-custom-input\n class=\"cqa-flex-1\"\n [value]=\"newColName\"\n [fullWidth]=\"true\"\n placeholder=\"New column name (e.g. currency)\"\n inputInlineStyle=\"font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\"\n (valueChange)=\"onNewColNameChange($event)\"\n (enterPressed)=\"onAdd()\">\n </cqa-custom-input>\n <cqa-button\n variant=\"outlined\"\n btnSize=\"sm\"\n icon=\"add\"\n text=\"Add column\"\n [disabled]=\"!canAdd\"\n (clicked)=\"onAdd()\">\n </cqa-button>\n </div>\n\n <span *ngIf=\"columnsError\" class=\"cqa-text-xs cqa-text-red-600\">{{ columnsError }}</span>\n\n <div class=\"cqa-px-3 cqa-py-2 cqa-rounded-md cqa-bg-primary-surface cqa-border cqa-border-success-100 cqa-text-xs cqa-text-gray-700\">\n <strong class=\"cqa-text-gray-900\">Heads up:</strong>\n Deleting a column removes its data from every environment. Renaming keeps the data in place.\n </div>\n\n</div>\n", components: [{ type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: CustomInputComponent, selector: "cqa-custom-input", inputs: ["inputId", "label", "type", "placeholder", "value", "disabled", "errors", "required", "ariaLabel", "size", "fullWidth", "maxLength", "showCharCount", "inputInlineStyle", "labelInlineStyle"], outputs: ["valueChange", "blurred", "focused", "enterPressed"] }, { type: ButtonComponent, selector: "cqa-button", inputs: ["variant", "btnSize", "disabled", "loading", "icon", "iconPosition", "fullWidth", "iconColor", "type", "text", "customClass", "inlineStyles", "tooltip", "tooltipPosition"], outputs: ["clicked"] }], directives: [{ type: i5.DndDropzoneDirective, selector: "[dndDropzone]", inputs: ["dndDropzone", "dndEffectAllowed", "dndAllowExternal", "dndHorizontal", "dndDragoverClass", "dndDropzoneDisabledClass", "dndDisableIf", "dndDisableDropIf"], outputs: ["dndDragover", "dndDrop"] }, { type: i5.DndPlaceholderRefDirective, selector: "[dndPlaceholderRef]" }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i5.DndDraggableDirective, selector: "[dndDraggable]", inputs: ["dndDraggable", "dndEffectAllowed", "dndType", "dndDraggingClass", "dndDraggingSourceClass", "dndDraggableDisabledClass", "dndDragImageOffsetFunction", "dndDisableIf", "dndDisableDragIf"], outputs: ["dndStart", "dndDrag", "dndEnd", "dndMoved", "dndCopied", "dndLinked", "dndCanceled"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
50931
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: ManageColumnsDialogComponent, decorators: [{
50932
+ type: Component,
50933
+ args: [{ selector: 'cqa-manage-columns-dialog', host: { class: 'cqa-ui-root', style: 'display:block;width:100%;' }, template: "<div class=\"cqa-flex cqa-flex-col cqa-gap-3 cqa-w-full\">\n\n <div class=\"manage-cols-drop-list cqa-flex cqa-flex-col cqa-gap-2\"\n [dndDropzone]=\"['col']\"\n dndEffectAllowed=\"move\"\n dndDragoverClass=\"dndDragover\"\n (dndDrop)=\"onDndDrop($event)\">\n <div dndPlaceholderRef class=\"manage-cols-placeholder\">Drop here</div>\n <div *ngFor=\"let item of items; let i = index; trackBy: trackByUid\"\n class=\"manage-cols-row cqa-flex cqa-items-center cqa-gap-2 cqa-px-2 cqa-py-1.5 cqa-rounded-md cqa-border cqa-border-gray-200 cqa-bg-gray-50\">\n <span class=\"manage-cols-handle cqa-flex cqa-items-center cqa-justify-center cqa-text-gray-400\"\n [dndDraggable]=\"item\"\n [dndDisableIf]=\"isLocked(item)\"\n dndType=\"col\"\n dndEffectAllowed=\"move\">\n <mat-icon style=\"font-size:18px;width:18px;height:18px;line-height:18px;\">drag_indicator</mat-icon>\n </span>\n <cqa-custom-input\n class=\"cqa-flex-1\"\n [value]=\"item.name\"\n [fullWidth]=\"true\"\n [disabled]=\"isLocked(item)\"\n inputInlineStyle=\"font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\"\n (valueChange)=\"onRename(i, $event)\">\n </cqa-custom-input>\n <button type=\"button\" class=\"manage-cols-icon-btn\"\n title=\"Move up\"\n [disabled]=\"i === 0\"\n (click)=\"onMove(i, -1)\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"manage-cols-icon-btn\"\n title=\"Move down\"\n [disabled]=\"i === items.length - 1\"\n (click)=\"onMove(i, 1)\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n <button type=\"button\" class=\"manage-cols-icon-btn manage-cols-icon-btn-danger\"\n title=\"Delete column\"\n [disabled]=\"isLocked(item)\"\n (click)=\"onRemove(i)\">\n <mat-icon>delete</mat-icon>\n </button>\n </div>\n </div>\n\n <div class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-pt-3 cqa-mt-1 cqa-border-t cqa-border-dashed cqa-border-gray-200\">\n <cqa-custom-input\n class=\"cqa-flex-1\"\n [value]=\"newColName\"\n [fullWidth]=\"true\"\n placeholder=\"New column name (e.g. currency)\"\n inputInlineStyle=\"font-family: 'Inter', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', monospace;\"\n (valueChange)=\"onNewColNameChange($event)\"\n (enterPressed)=\"onAdd()\">\n </cqa-custom-input>\n <cqa-button\n variant=\"outlined\"\n btnSize=\"sm\"\n icon=\"add\"\n text=\"Add column\"\n [disabled]=\"!canAdd\"\n (clicked)=\"onAdd()\">\n </cqa-button>\n </div>\n\n <span *ngIf=\"columnsError\" class=\"cqa-text-xs cqa-text-red-600\">{{ columnsError }}</span>\n\n <div class=\"cqa-px-3 cqa-py-2 cqa-rounded-md cqa-bg-primary-surface cqa-border cqa-border-success-100 cqa-text-xs cqa-text-gray-700\">\n <strong class=\"cqa-text-gray-900\">Heads up:</strong>\n Deleting a column removes its data from every environment. Renaming keeps the data in place.\n </div>\n\n</div>\n" }]
50934
+ }], propDecorators: { columns: [{
50935
+ type: Input
50936
+ }], lockedColumns: [{
50937
+ type: Input
50938
+ }] } });
50939
+
50940
+ const ALL_FILTER_VALUE = '__all__';
50941
+
50942
+ class AuditLogEntryCardComponent {
50943
+ get userShortName() {
50944
+ const u = this.entry?.user || '';
50945
+ const idx = u.indexOf('@');
50946
+ return idx > 0 ? u.substring(0, idx) : u;
50947
+ }
50948
+ get etypeClass() {
50949
+ switch (this.entry?.entityType) {
50950
+ case 'Environment Variable': return 'cqa-audit-log-etype-ev';
50951
+ case 'Test Data Profile': return 'cqa-audit-log-etype-tdp';
50952
+ case 'Global Data': return 'cqa-audit-log-etype-gd';
50953
+ default: return '';
50954
+ }
50955
+ }
50956
+ get hasDiff() {
50957
+ return this.entry?.before != null || this.entry?.after != null;
50958
+ }
50959
+ }
50960
+ AuditLogEntryCardComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogEntryCardComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
50961
+ AuditLogEntryCardComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: AuditLogEntryCardComponent, selector: "cqa-audit-log-entry-card", inputs: { entry: "entry" }, host: { classAttribute: "cqa-ui-root cqa-audit-log-row" }, ngImport: i0, template: "<div class=\"cqa-audit-log-meta cqa-font-inter\">\n <div class=\"cqa-audit-log-ts\">{{ entry?.ts }}</div>\n <div class=\"cqa-audit-log-user\">{{ userShortName }}</div>\n</div>\n<div class=\"cqa-min-w-0\">\n <div class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-flex-wrap cqa-mb-1\">\n <span class=\"cqa-audit-log-etype\" [ngClass]=\"etypeClass\">{{ entry?.entityType }}</span>\n <span class=\"cqa-text-[13px] cqa-font-medium cqa-text-[#0F172A]\">{{ entry?.entityName }}</span>\n </div>\n <div class=\"cqa-text-xs cqa-text-[#64748B]\">{{ entry?.change }}</div>\n <div *ngIf=\"hasDiff\" class=\"cqa-audit-log-diff\">\n <span class=\"cqa-audit-log-before\">{{ entry?.before || '\u2014' }}</span>\n <span class=\"cqa-audit-log-arrow\">\u2192</span>\n <span class=\"cqa-audit-log-after\">{{ entry?.after || '\u2014' }}</span>\n </div>\n</div>\n", directives: [{ type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] });
50962
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogEntryCardComponent, decorators: [{
50963
+ type: Component,
50964
+ args: [{ selector: 'cqa-audit-log-entry-card', host: { class: 'cqa-ui-root cqa-audit-log-row' }, template: "<div class=\"cqa-audit-log-meta cqa-font-inter\">\n <div class=\"cqa-audit-log-ts\">{{ entry?.ts }}</div>\n <div class=\"cqa-audit-log-user\">{{ userShortName }}</div>\n</div>\n<div class=\"cqa-min-w-0\">\n <div class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-flex-wrap cqa-mb-1\">\n <span class=\"cqa-audit-log-etype\" [ngClass]=\"etypeClass\">{{ entry?.entityType }}</span>\n <span class=\"cqa-text-[13px] cqa-font-medium cqa-text-[#0F172A]\">{{ entry?.entityName }}</span>\n </div>\n <div class=\"cqa-text-xs cqa-text-[#64748B]\">{{ entry?.change }}</div>\n <div *ngIf=\"hasDiff\" class=\"cqa-audit-log-diff\">\n <span class=\"cqa-audit-log-before\">{{ entry?.before || '\u2014' }}</span>\n <span class=\"cqa-audit-log-arrow\">\u2192</span>\n <span class=\"cqa-audit-log-after\">{{ entry?.after || '\u2014' }}</span>\n </div>\n</div>\n" }]
50965
+ }], propDecorators: { entry: [{
50966
+ type: Input
50967
+ }] } });
50968
+
50969
+ class AuditLogDrawerComponent {
50970
+ constructor() {
50971
+ this.entries = [];
50972
+ this.entityTypeOptions = [];
50973
+ this.envOptions = [];
50974
+ this.userOptions = [];
50975
+ this.defaultRange = '7d';
50976
+ this.isLoading = false;
50977
+ this.filterChange = new EventEmitter();
50978
+ this.close = new EventEmitter();
50979
+ this.ALL = ALL_FILTER_VALUE;
50980
+ this.filtersForm = new FormGroup({
50981
+ entityType: new FormControl(ALL_FILTER_VALUE),
50982
+ env: new FormControl(ALL_FILTER_VALUE),
50983
+ user: new FormControl(ALL_FILTER_VALUE),
50984
+ range: new FormControl('7d'),
50985
+ });
50986
+ this.trackById = (_, e) => e.id;
50987
+ this.destroy$ = new Subject();
50988
+ this.rangeMs = {
50989
+ '24h': 24 * 60 * 60 * 1000,
50990
+ '7d': 7 * 24 * 60 * 60 * 1000,
50991
+ '30d': 30 * 24 * 60 * 60 * 1000,
50992
+ 'all': Number.POSITIVE_INFINITY,
50993
+ };
50994
+ }
50995
+ ngOnInit() {
50996
+ this.filtersForm.patchValue({ range: this.defaultRange });
50997
+ this.entityConfig = this.buildSelectConfig('entityType', 'All entity types', this.entityTypeOptions);
50998
+ this.envConfig = this.buildSelectConfig('env', 'All environments', this.envOptions);
50999
+ this.userConfig = this.buildSelectConfig('user', 'All users', this.userOptions);
51000
+ this.rangeConfig = this.buildSelectConfig('range', 'Last 7 days', [
51001
+ { value: '24h', label: 'Last 24h' },
51002
+ { value: '7d', label: 'Last 7 days' },
51003
+ { value: '30d', label: 'Last 30 days' },
51004
+ { value: 'all', label: 'All time' },
51005
+ ], false);
51006
+ this.filtersForm.valueChanges
51007
+ .pipe(takeUntil(this.destroy$))
51008
+ .subscribe(() => this.filterChange.emit(this.currentFilterState()));
51009
+ }
51010
+ ngOnDestroy() {
51011
+ this.destroy$.next();
51012
+ this.destroy$.complete();
51013
+ }
51014
+ get filteredEntries() {
51015
+ const f = this.currentFilterState();
51016
+ const cutoff = this.rangeMs[f.range];
51017
+ const now = Date.now();
51018
+ return (this.entries || []).filter(e => {
51019
+ if (f.entityType !== ALL_FILTER_VALUE && e.entityType !== f.entityType) {
51020
+ return false;
51021
+ }
51022
+ if (f.env !== ALL_FILTER_VALUE && !this.entryMatchesEnv(e, f.env)) {
51023
+ return false;
51024
+ }
51025
+ if (f.user !== ALL_FILTER_VALUE && e.user !== f.user) {
51026
+ return false;
51027
+ }
51028
+ if (cutoff !== Number.POSITIVE_INFINITY) {
51029
+ const ts = this.parseTs(e.ts);
51030
+ if (ts != null && now - ts > cutoff) {
51031
+ return false;
51032
+ }
51033
+ }
51034
+ return true;
51035
+ });
51036
+ }
51037
+ onClose() {
51038
+ this.close.emit();
51039
+ }
51040
+ entryMatchesEnv(entry, env) {
51041
+ return (entry.entityName || '').toLowerCase().includes(env.toLowerCase());
51042
+ }
51043
+ parseTs(ts) {
51044
+ if (!ts) {
51045
+ return null;
51046
+ }
51047
+ const t = Date.parse(ts);
51048
+ return Number.isNaN(t) ? null : t;
51049
+ }
51050
+ currentFilterState() {
51051
+ const v = this.filtersForm.value;
51052
+ return {
51053
+ entityType: v.entityType ?? ALL_FILTER_VALUE,
51054
+ env: v.env ?? ALL_FILTER_VALUE,
51055
+ user: v.user ?? ALL_FILTER_VALUE,
51056
+ range: v.range ?? '7d',
51057
+ };
51058
+ }
51059
+ buildSelectConfig(key, placeholder, items, includeAll = true) {
51060
+ const options = [];
51061
+ if (includeAll) {
51062
+ options.push({ id: ALL_FILTER_VALUE, value: ALL_FILTER_VALUE, name: placeholder, label: placeholder });
51063
+ }
51064
+ for (const it of items) {
51065
+ options.push({ id: it.value, value: it.value, name: it.label, label: it.label });
51066
+ }
51067
+ return {
51068
+ key,
51069
+ label: '',
51070
+ placeholder,
51071
+ multiple: false,
51072
+ searchable: false,
51073
+ options,
51074
+ };
51075
+ }
51076
+ }
51077
+ AuditLogDrawerComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogDrawerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
51078
+ AuditLogDrawerComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: AuditLogDrawerComponent, selector: "cqa-audit-log-drawer", inputs: { entries: "entries", entityTypeOptions: "entityTypeOptions", envOptions: "envOptions", userOptions: "userOptions", defaultRange: "defaultRange", isLoading: "isLoading" }, outputs: { filterChange: "filterChange", close: "close" }, host: { styleAttribute: "display:flex;flex-direction:column;height:100%;width:560px;max-width:100vw;background:#fff;box-shadow:-8px 0 32px rgba(15,23,42,.12);", classAttribute: "cqa-ui-root" }, ngImport: i0, template: "<header class=\"cqa-flex cqa-items-start cqa-justify-between cqa-gap-3 cqa-px-[22px] cqa-py-[18px] cqa-border-b cqa-border-[#E5E7EB] cqa-flex-shrink-0 cqa-font-inter\">\n <div class=\"cqa-min-w-0\">\n <h3 class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-m-0 cqa-text-[15px] cqa-font-semibold cqa-text-[#0F172A]\">\n <mat-icon class=\"cqa-text-primary\" style=\"font-size:18px;width:18px;height:18px;line-height:18px;color:#3F43EE;\">history</mat-icon>\n Audit log\n </h3>\n <p class=\"cqa-text-xs cqa-text-[#64748B] cqa-mt-0.5 cqa-mb-0\">\n All changes across environments, profiles, and globals. Read-only, append-only.\n </p>\n </div>\n <button type=\"button\" class=\"cqa-audit-log-close-btn\" (click)=\"onClose()\" aria-label=\"Close audit log\">\n <mat-icon>close</mat-icon>\n </button>\n</header>\n\n<section class=\"cqa-grid cqa-gap-2 cqa-px-[22px] cqa-py-3 cqa-border-b cqa-border-[#E5E7EB] cqa-flex-shrink-0\"\n style=\"grid-template-columns: repeat(4, minmax(0, 1fr)); background:#FAFBFC;\">\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"entityConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"envConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"userConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"rangeConfig\"></cqa-dynamic-select>\n</section>\n\n<div class=\"cqa-flex-1 cqa-overflow-y-auto\">\n <cqa-full-table-loader *ngIf=\"isLoading\"></cqa-full-table-loader>\n <ng-container *ngIf=\"!isLoading\">\n <cqa-empty-state\n *ngIf=\"filteredEntries.length === 0; else listTpl\"\n title=\"No entries match your filters\"\n description=\"Adjust the filters above to widen the search.\">\n </cqa-empty-state>\n <ng-template #listTpl>\n <cqa-audit-log-entry-card\n *ngFor=\"let e of filteredEntries; trackBy: trackById\"\n [entry]=\"e\">\n </cqa-audit-log-entry-card>\n </ng-template>\n </ng-container>\n</div>\n", components: [{ type: i1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { type: DynamicSelectFieldComponent, selector: "cqa-dynamic-select", inputs: ["form", "config"], outputs: ["selectionChange", "selectClick", "searchChange", "loadMore", "addCustomValue"] }, { type: FullTableLoaderComponent, selector: "cqa-full-table-loader", inputs: ["label"] }, { type: EmptyStateComponent, selector: "cqa-empty-state", inputs: ["preset", "imageUrl", "title", "description", "actions"], outputs: ["actionClick"] }, { type: AuditLogEntryCardComponent, selector: "cqa-audit-log-entry-card", inputs: ["entry"] }], directives: [{ type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i2.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }] });
51079
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogDrawerComponent, decorators: [{
51080
+ type: Component,
51081
+ args: [{ selector: 'cqa-audit-log-drawer', host: {
51082
+ class: 'cqa-ui-root',
51083
+ style: 'display:flex;flex-direction:column;height:100%;width:560px;max-width:100vw;background:#fff;box-shadow:-8px 0 32px rgba(15,23,42,.12);',
51084
+ }, template: "<header class=\"cqa-flex cqa-items-start cqa-justify-between cqa-gap-3 cqa-px-[22px] cqa-py-[18px] cqa-border-b cqa-border-[#E5E7EB] cqa-flex-shrink-0 cqa-font-inter\">\n <div class=\"cqa-min-w-0\">\n <h3 class=\"cqa-flex cqa-items-center cqa-gap-2 cqa-m-0 cqa-text-[15px] cqa-font-semibold cqa-text-[#0F172A]\">\n <mat-icon class=\"cqa-text-primary\" style=\"font-size:18px;width:18px;height:18px;line-height:18px;color:#3F43EE;\">history</mat-icon>\n Audit log\n </h3>\n <p class=\"cqa-text-xs cqa-text-[#64748B] cqa-mt-0.5 cqa-mb-0\">\n All changes across environments, profiles, and globals. Read-only, append-only.\n </p>\n </div>\n <button type=\"button\" class=\"cqa-audit-log-close-btn\" (click)=\"onClose()\" aria-label=\"Close audit log\">\n <mat-icon>close</mat-icon>\n </button>\n</header>\n\n<section class=\"cqa-grid cqa-gap-2 cqa-px-[22px] cqa-py-3 cqa-border-b cqa-border-[#E5E7EB] cqa-flex-shrink-0\"\n style=\"grid-template-columns: repeat(4, minmax(0, 1fr)); background:#FAFBFC;\">\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"entityConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"envConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"userConfig\"></cqa-dynamic-select>\n <cqa-dynamic-select [form]=\"filtersForm\" [config]=\"rangeConfig\"></cqa-dynamic-select>\n</section>\n\n<div class=\"cqa-flex-1 cqa-overflow-y-auto\">\n <cqa-full-table-loader *ngIf=\"isLoading\"></cqa-full-table-loader>\n <ng-container *ngIf=\"!isLoading\">\n <cqa-empty-state\n *ngIf=\"filteredEntries.length === 0; else listTpl\"\n title=\"No entries match your filters\"\n description=\"Adjust the filters above to widen the search.\">\n </cqa-empty-state>\n <ng-template #listTpl>\n <cqa-audit-log-entry-card\n *ngFor=\"let e of filteredEntries; trackBy: trackById\"\n [entry]=\"e\">\n </cqa-audit-log-entry-card>\n </ng-template>\n </ng-container>\n</div>\n" }]
51085
+ }], propDecorators: { entries: [{
51086
+ type: Input
51087
+ }], entityTypeOptions: [{
51088
+ type: Input
51089
+ }], envOptions: [{
51090
+ type: Input
51091
+ }], userOptions: [{
51092
+ type: Input
51093
+ }], defaultRange: [{
51094
+ type: Input
51095
+ }], isLoading: [{
51096
+ type: Input
51097
+ }], filterChange: [{
51098
+ type: Output
51099
+ }], close: [{
51100
+ type: Output
51101
+ }] } });
51102
+
50763
51103
  class AssignEnvironmentsDialogComponent {
50764
51104
  constructor(cdr) {
50765
51105
  this.cdr = cdr;
@@ -51017,6 +51357,9 @@ UiKitModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "1
51017
51357
  NewEnvironmentDialogComponent,
51018
51358
  NewEnvironmentVariableDialogComponent,
51019
51359
  NewTestDataProfileDialogComponent,
51360
+ ManageColumnsDialogComponent,
51361
+ AuditLogDrawerComponent,
51362
+ AuditLogEntryCardComponent,
51020
51363
  AssignEnvironmentsDialogComponent,
51021
51364
  PermissionToggleComponent,
51022
51365
  ExportCodeModalComponent,
@@ -51207,6 +51550,9 @@ UiKitModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "12.0.0", version: "1
51207
51550
  NewEnvironmentDialogComponent,
51208
51551
  NewEnvironmentVariableDialogComponent,
51209
51552
  NewTestDataProfileDialogComponent,
51553
+ ManageColumnsDialogComponent,
51554
+ AuditLogDrawerComponent,
51555
+ AuditLogEntryCardComponent,
51210
51556
  AssignEnvironmentsDialogComponent,
51211
51557
  PermissionToggleComponent,
51212
51558
  ExportCodeModalComponent,
@@ -51442,6 +51788,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
51442
51788
  NewEnvironmentDialogComponent,
51443
51789
  NewEnvironmentVariableDialogComponent,
51444
51790
  NewTestDataProfileDialogComponent,
51791
+ ManageColumnsDialogComponent,
51792
+ AuditLogDrawerComponent,
51793
+ AuditLogEntryCardComponent,
51445
51794
  AssignEnvironmentsDialogComponent,
51446
51795
  PermissionToggleComponent,
51447
51796
  ExportCodeModalComponent,
@@ -51638,6 +51987,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
51638
51987
  NewEnvironmentDialogComponent,
51639
51988
  NewEnvironmentVariableDialogComponent,
51640
51989
  NewTestDataProfileDialogComponent,
51990
+ ManageColumnsDialogComponent,
51991
+ AuditLogDrawerComponent,
51992
+ AuditLogEntryCardComponent,
51641
51993
  AssignEnvironmentsDialogComponent,
51642
51994
  PermissionToggleComponent,
51643
51995
  ExportCodeModalComponent,
@@ -51746,6 +52098,82 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImpor
51746
52098
  }]
51747
52099
  }], ctorParameters: function () { return [{ type: i1.MatIconRegistry }]; } });
51748
52100
 
52101
+ class AuditLogDrawerService {
52102
+ constructor(overlay, injector) {
52103
+ this.overlay = overlay;
52104
+ this.injector = injector;
52105
+ this.currentOverlayRef = null;
52106
+ }
52107
+ open(inputs) {
52108
+ if (this.currentOverlayRef) {
52109
+ this.currentOverlayRef.dispose();
52110
+ this.currentOverlayRef = null;
52111
+ }
52112
+ const positionStrategy = this.overlay.position()
52113
+ .global()
52114
+ .right('0')
52115
+ .top('0')
52116
+ .bottom('0');
52117
+ const overlayRef = this.overlay.create(new OverlayConfig({
52118
+ hasBackdrop: true,
52119
+ backdropClass: ['cqa-audit-log-drawer-backdrop'],
52120
+ scrollStrategy: this.overlay.scrollStrategies.block(),
52121
+ positionStrategy,
52122
+ panelClass: ['cqa-audit-log-drawer-panel', 'cqa-ui-root'],
52123
+ maxWidth: '100vw',
52124
+ height: '100%',
52125
+ }));
52126
+ const portal = new ComponentPortal(AuditLogDrawerComponent, null, this.injector);
52127
+ const componentRef = overlayRef.attach(portal);
52128
+ const instance = componentRef.instance;
52129
+ instance.entries = inputs.entries ?? [];
52130
+ instance.entityTypeOptions = inputs.entityTypeOptions ?? [];
52131
+ instance.envOptions = inputs.envOptions ?? [];
52132
+ instance.userOptions = inputs.userOptions ?? [];
52133
+ instance.defaultRange = inputs.defaultRange ?? '7d';
52134
+ instance.isLoading = !!inputs.isLoading;
52135
+ componentRef.changeDetectorRef.markForCheck();
52136
+ const filterChange = new Subject();
52137
+ const afterClosed = new Subject();
52138
+ instance.filterChange.subscribe((s) => filterChange.next(s));
52139
+ const closeAll = () => {
52140
+ if (!this.currentOverlayRef) {
52141
+ return;
52142
+ }
52143
+ overlayRef.dispose();
52144
+ this.currentOverlayRef = null;
52145
+ afterClosed.next();
52146
+ afterClosed.complete();
52147
+ filterChange.complete();
52148
+ };
52149
+ instance.close.subscribe(() => closeAll());
52150
+ overlayRef.backdropClick().subscribe(() => closeAll());
52151
+ overlayRef.keydownEvents()
52152
+ .pipe(filter((e) => e.key === 'Escape' || e.key === 'Esc'))
52153
+ .subscribe(() => closeAll());
52154
+ this.currentOverlayRef = overlayRef;
52155
+ return {
52156
+ filterChange$: filterChange.asObservable(),
52157
+ afterClosed$: afterClosed.asObservable(),
52158
+ close: () => closeAll(),
52159
+ updateEntries: (entries) => {
52160
+ instance.entries = entries ?? [];
52161
+ componentRef.changeDetectorRef.markForCheck();
52162
+ },
52163
+ updateLoading: (isLoading) => {
52164
+ instance.isLoading = !!isLoading;
52165
+ componentRef.changeDetectorRef.markForCheck();
52166
+ },
52167
+ };
52168
+ }
52169
+ }
52170
+ AuditLogDrawerService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogDrawerService, deps: [{ token: i1$6.Overlay }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable });
52171
+ AuditLogDrawerService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogDrawerService, providedIn: 'root' });
52172
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: AuditLogDrawerService, decorators: [{
52173
+ type: Injectable,
52174
+ args: [{ providedIn: 'root' }]
52175
+ }], ctorParameters: function () { return [{ type: i1$6.Overlay }, { type: i0.Injector }]; } });
52176
+
51749
52177
  /**
51750
52178
  * Opens the Step Details Drawer (Edit In Depth) as a right-side drawer overlay.
51751
52179
  * Matches the behavior of Element popup - opens as a right-side drawer panel.
@@ -52340,5 +52768,5 @@ function buildTestCaseDetailsFromApi(data, options) {
52340
52768
  * Generated bundle index. Do not edit.
52341
52769
  */
52342
52770
 
52343
- export { ADVANCED_SUBFIELDS_BY_TYPE, ADVANCED_TOGGLE_KEYS, AIActionStepComponent, AIAgentStepComponent, API_EDIT_STEP_LABELS, ActionMenuButtonComponent, AddPrerequisiteCasesSectionComponent, AdvancedVariablesFormComponent, AiDebugAlertComponent, AiLogsWithReasoningComponent, AiPromptCardComponent, AiReasoningComponent, ApiEditStepComponent, ApiMockingCardComponent, ApiStepComponent, AssignEnvironmentsDialogComponent, AutocompleteComponent, BadgeComponent, BasicStepComponent, BreakpointsModalComponent, ButtonComponent, CUSTOM_EDIT_STEP_DATA, CUSTOM_EDIT_STEP_EDIT_IN_DEPTH, CUSTOM_EDIT_STEP_REF, CUSTOM_ELEMENT_POPUP_REF, CaptureVideoDialogComponent, ChangeHistoryComponent, ChartCardComponent, CodeEditorComponent, ColumnVisibilityComponent, CompareRunsComponent, ConditionBranchEditorComponent, ConditionDebugStepComponent, ConditionStepComponent, ConfigurationCardComponent, ConsoleAlertComponent, CoverageModuleCardComponent, CreateStepGroupComponent, CustomEditStepComponent, CustomEditStepRef, CustomEditStepService, CustomInputComponent, CustomTextareaComponent, CustomToggleComponent, DEFAULT_FOLDER_COLOR, DEFAULT_METADATA_COLOR, DEFAULT_MODULAR_CONFIG, DEFAULT_MODULAR_LABELS, DEFAULT_PRIORITY_COLOR_CONFIG, DEFAULT_REORDER_LABELS, DEFAULT_STATUS_COLOR_CONFIG, DIALOG_DATA, DIALOG_REF, DashboardHeaderComponent, DaterangepickerComponent, DaterangepickerDirective, DbQueryExecutionItemComponent, DbVerificationStepComponent, DeleteFolderDialogComponent, DeleteStepsComponent, DetailDrawerComponent, DetailDrawerTabComponent, DetailDrawerTabContentDirective, DetailSidePanelComponent, DialogComponent, DialogRef, DialogService, DocumentVerificationStepComponent, DropdownButtonComponent, DynamicCellContainerDirective, DynamicCellTemplateDirective, DynamicFilterComponent, DynamicHeaderTemplateDirective, DynamicSelectFieldComponent, DynamicTableComponent, ELEMENT_POPUP_DATA, ELEMENT_POPUP_EDIT_IN_DEPTH, EMPTY_STATE_IMAGES, EMPTY_STATE_PRESETS, ENVIRONMENT_ACCENT_COLORS, ElementFormComponent, ElementListComponent, ElementPopupComponent, ElementPopupRef, ElementPopupService, EmptyStateComponent, ErrorModalComponent, ExecutionResultModalComponent, ExportCodeModalComponent, FOLDER_DRAG_MIME, FailedStepCardComponent, FailedStepComponent, FailedTestCasesCardComponent, FileDownloadStepComponent, FileUploadComponent, FolderDragDirective, FolderDropDirective, FolderSidebarComponent, FullTableLoaderComponent, HeatErrorMapCellComponent, InsightCardComponent, ItemListComponent, IterationsLoopComponent, JumpToStepModalComponent, LiveConversationComponent, LiveExecutionStepComponent, LoopStepComponent, MONACO_LANGUAGE_MAP, MainStepCollapseComponent, MetricsCardComponent, MixedVariableInputComponent, ModularTableTemplateComponent, MoveToFolderDialogComponent, NetworkRequestComponent, NewEnvironmentDialogComponent, NewEnvironmentVariableDialogComponent, NewFolderDialogComponent, NewGlobalVariableDialogComponent, NewTestDataProfileDialogComponent, NewVersionHistoryDetailComponent, OtherButtonComponent, PRIORITY_COLORS, PaginationComponent, PermissionToggleComponent, ProgressIndicatorComponent, ProgressTextCardComponent, QuestionnaireListComponent, RESULT_COLORS, ROW_DRAG_MIME, RadioCardGroupComponent, RecordingBannerComponent, ReviewRecordedStepsModalComponent, RowDragDirective, RunExecutionAlertComponent, RunHistoryCardComponent, STATUS_COLORS, STEP_DETAILS_DRAWER_DATA, STEP_DETAILS_DRAWER_REF, STEP_DETAILS_FIELDS_BY_TYPE, STEP_DETAILS_FIELD_META, STEP_DETAILS_MODAL_DATA, STEP_DETAILS_MODAL_REF, SearchBarComponent, SegmentControlComponent, SelectedFiltersComponent, SelfHealAnalysisComponent, SessionChangesModalComponent, SessionRestorationDialogComponent, SimulatorComponent, StepBuilderActionComponent, StepBuilderAiAgentComponent, StepBuilderConditionComponent, StepBuilderCustomCodeComponent, StepBuilderDatabaseComponent, StepBuilderDocumentComponent, StepBuilderDocumentGenerationTemplateStepComponent, StepBuilderGroupComponent, StepBuilderLoopComponent, StepBuilderRecordStepComponent, StepDetailsDrawerComponent, StepDetailsDrawerRef, StepDetailsDrawerService, StepDetailsModalComponent, StepDetailsModalRef, StepDetailsModalService, StepGroupComponent, StepProgressCardComponent, StepRendererComponent, StepStatusCardComponent, StepTypes, StepperComponent, SubStepsConfirmationDialogComponent, TEST_CASE_DETAILS_FIELD_MAP, TEST_CASE_DETAILS_SELECT_KEYS, TEST_DATA_MODAL_DATA, TEST_DATA_MODAL_EDIT_IN_DEPTH, TEST_DATA_MODAL_REF, TableActionToolbarComponent, TableDataLoaderComponent, TableTemplateComponent, TailwindOverlayContainer, TemplateVariablesFormComponent, TestCaseAiAgentStepComponent, TestCaseAiVerifyStepComponent, TestCaseApiStepComponent, TestCaseConditionStepComponent, TestCaseCustomCodeStepComponent, TestCaseDatabaseStepComponent, TestCaseDetailsComponent, TestCaseDetailsEditComponent, TestCaseDetailsRendererComponent, TestCaseLinkCellComponent, TestCaseLoopStepComponent, TestCaseNormalStepComponent, TestCaseRestoreSessionStepComponent, TestCaseScreenshotStepComponent, TestCaseScrollStepComponent, TestCaseStepGroupComponent, TestCaseUploadStepComponent, TestCaseVerifyUrlStepComponent, TestDataModalComponent, TestDataModalRef, TestDataModalService, TestDistributionCardComponent, UiKitModule, UpdatedFailedStepComponent, VersionHistoryCompareComponent, VersionHistoryDetailComponent, VersionHistoryListComponent, VersionHistoryRestoreConfirmComponent, ViewCompareButtonComponent, ViewMoreFailedStepButtonComponent, VisualComparisonComponent, VisualDifferenceModalComponent, WorkspaceSelectorComponent, buildTestCaseDetailsFromApi, getDynamicFieldsFromLegacyConfig, getEmptyStatePreset, getMetadataColor, getMetadataValueStyle, getStepDetailsStepType, humanizeVariableKey, isAiAgentStepConfig, isAiVerifyStepConfig, isApiStepConfig, isConditionStepConfig, isCustomCodeStepConfig, isDatabaseStepConfig, isLoopStepConfig, isNormalStepConfig, isRestoreSessionStepConfig, isScreenshotStepConfig, isScrollStepConfig, isStepGroupConfig, isUploadStepConfig, isVerifyUrlStepConfig, mapApiVariablesToDynamicFields };
52771
+ export { ADVANCED_SUBFIELDS_BY_TYPE, ADVANCED_TOGGLE_KEYS, AIActionStepComponent, AIAgentStepComponent, ALL_FILTER_VALUE, API_EDIT_STEP_LABELS, ActionMenuButtonComponent, AddPrerequisiteCasesSectionComponent, AdvancedVariablesFormComponent, AiDebugAlertComponent, AiLogsWithReasoningComponent, AiPromptCardComponent, AiReasoningComponent, ApiEditStepComponent, ApiMockingCardComponent, ApiStepComponent, AssignEnvironmentsDialogComponent, AuditLogDrawerComponent, AuditLogDrawerService, AuditLogEntryCardComponent, AutocompleteComponent, BadgeComponent, BasicStepComponent, BreakpointsModalComponent, ButtonComponent, CUSTOM_EDIT_STEP_DATA, CUSTOM_EDIT_STEP_EDIT_IN_DEPTH, CUSTOM_EDIT_STEP_REF, CUSTOM_ELEMENT_POPUP_REF, CaptureVideoDialogComponent, ChangeHistoryComponent, ChartCardComponent, CodeEditorComponent, ColumnVisibilityComponent, CompareRunsComponent, ConditionBranchEditorComponent, ConditionDebugStepComponent, ConditionStepComponent, ConfigurationCardComponent, ConsoleAlertComponent, CoverageModuleCardComponent, CreateStepGroupComponent, CustomEditStepComponent, CustomEditStepRef, CustomEditStepService, CustomInputComponent, CustomTextareaComponent, CustomToggleComponent, DEFAULT_FOLDER_COLOR, DEFAULT_METADATA_COLOR, DEFAULT_MODULAR_CONFIG, DEFAULT_MODULAR_LABELS, DEFAULT_PRIORITY_COLOR_CONFIG, DEFAULT_REORDER_LABELS, DEFAULT_STATUS_COLOR_CONFIG, DIALOG_DATA, DIALOG_REF, DashboardHeaderComponent, DaterangepickerComponent, DaterangepickerDirective, DbQueryExecutionItemComponent, DbVerificationStepComponent, DeleteFolderDialogComponent, DeleteStepsComponent, DetailDrawerComponent, DetailDrawerTabComponent, DetailDrawerTabContentDirective, DetailSidePanelComponent, DialogComponent, DialogRef, DialogService, DocumentVerificationStepComponent, DropdownButtonComponent, DynamicCellContainerDirective, DynamicCellTemplateDirective, DynamicFilterComponent, DynamicHeaderTemplateDirective, DynamicSelectFieldComponent, DynamicTableComponent, ELEMENT_POPUP_DATA, ELEMENT_POPUP_EDIT_IN_DEPTH, EMPTY_STATE_IMAGES, EMPTY_STATE_PRESETS, ENVIRONMENT_ACCENT_COLORS, ElementFormComponent, ElementListComponent, ElementPopupComponent, ElementPopupRef, ElementPopupService, EmptyStateComponent, ErrorModalComponent, ExecutionResultModalComponent, ExportCodeModalComponent, FOLDER_DRAG_MIME, FOLDER_NAME_MAX_LENGTH, FailedStepCardComponent, FailedStepComponent, FailedTestCasesCardComponent, FileDownloadStepComponent, FileUploadComponent, FolderDragDirective, FolderDropDirective, FolderSidebarComponent, FullTableLoaderComponent, HeatErrorMapCellComponent, InsightCardComponent, ItemListComponent, IterationsLoopComponent, JumpToStepModalComponent, LiveConversationComponent, LiveExecutionStepComponent, LoopStepComponent, MONACO_LANGUAGE_MAP, MainStepCollapseComponent, ManageColumnsDialogComponent, MetricsCardComponent, MixedVariableInputComponent, ModularTableTemplateComponent, MoveToFolderDialogComponent, NetworkRequestComponent, NewEnvironmentDialogComponent, NewEnvironmentVariableDialogComponent, NewFolderDialogComponent, NewGlobalVariableDialogComponent, NewTestDataProfileDialogComponent, NewVersionHistoryDetailComponent, OtherButtonComponent, PRIORITY_COLORS, PaginationComponent, PermissionToggleComponent, ProgressIndicatorComponent, ProgressTextCardComponent, QuestionnaireListComponent, RESULT_COLORS, ROW_DRAG_MIME, RadioCardGroupComponent, RecordingBannerComponent, ReviewRecordedStepsModalComponent, RowDragDirective, RunExecutionAlertComponent, RunHistoryCardComponent, STATUS_COLORS, STEP_DETAILS_DRAWER_DATA, STEP_DETAILS_DRAWER_REF, STEP_DETAILS_FIELDS_BY_TYPE, STEP_DETAILS_FIELD_META, STEP_DETAILS_MODAL_DATA, STEP_DETAILS_MODAL_REF, SearchBarComponent, SegmentControlComponent, SelectedFiltersComponent, SelfHealAnalysisComponent, SessionChangesModalComponent, SessionRestorationDialogComponent, SimulatorComponent, StepBuilderActionComponent, StepBuilderAiAgentComponent, StepBuilderConditionComponent, StepBuilderCustomCodeComponent, StepBuilderDatabaseComponent, StepBuilderDocumentComponent, StepBuilderDocumentGenerationTemplateStepComponent, StepBuilderGroupComponent, StepBuilderLoopComponent, StepBuilderRecordStepComponent, StepDetailsDrawerComponent, StepDetailsDrawerRef, StepDetailsDrawerService, StepDetailsModalComponent, StepDetailsModalRef, StepDetailsModalService, StepGroupComponent, StepProgressCardComponent, StepRendererComponent, StepStatusCardComponent, StepTypes, StepperComponent, SubStepsConfirmationDialogComponent, TEST_CASE_DETAILS_FIELD_MAP, TEST_CASE_DETAILS_SELECT_KEYS, TEST_DATA_MODAL_DATA, TEST_DATA_MODAL_EDIT_IN_DEPTH, TEST_DATA_MODAL_REF, TableActionToolbarComponent, TableDataLoaderComponent, TableTemplateComponent, TailwindOverlayContainer, TemplateVariablesFormComponent, TestCaseAiAgentStepComponent, TestCaseAiVerifyStepComponent, TestCaseApiStepComponent, TestCaseConditionStepComponent, TestCaseCustomCodeStepComponent, TestCaseDatabaseStepComponent, TestCaseDetailsComponent, TestCaseDetailsEditComponent, TestCaseDetailsRendererComponent, TestCaseLinkCellComponent, TestCaseLoopStepComponent, TestCaseNormalStepComponent, TestCaseRestoreSessionStepComponent, TestCaseScreenshotStepComponent, TestCaseScrollStepComponent, TestCaseStepGroupComponent, TestCaseUploadStepComponent, TestCaseVerifyUrlStepComponent, TestDataModalComponent, TestDataModalRef, TestDataModalService, TestDistributionCardComponent, UiKitModule, UpdatedFailedStepComponent, VersionHistoryCompareComponent, VersionHistoryDetailComponent, VersionHistoryListComponent, VersionHistoryRestoreConfirmComponent, ViewCompareButtonComponent, ViewMoreFailedStepButtonComponent, VisualComparisonComponent, VisualDifferenceModalComponent, WorkspaceSelectorComponent, buildTestCaseDetailsFromApi, getDynamicFieldsFromLegacyConfig, getEmptyStatePreset, getMetadataColor, getMetadataValueStyle, getStepDetailsStepType, humanizeVariableKey, isAiAgentStepConfig, isAiVerifyStepConfig, isApiStepConfig, isConditionStepConfig, isCustomCodeStepConfig, isDatabaseStepConfig, isLoopStepConfig, isNormalStepConfig, isRestoreSessionStepConfig, isScreenshotStepConfig, isScrollStepConfig, isStepGroupConfig, isUploadStepConfig, isVerifyUrlStepConfig, mapApiVariablesToDynamicFields };
52344
52772
  //# sourceMappingURL=cqa-lib-cqa-ui.mjs.map