@myrmidon/paged-data-browsers 5.0.2 → 5.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -14
- package/fesm2022/myrmidon-paged-data-browsers.mjs +831 -66
- package/fesm2022/myrmidon-paged-data-browsers.mjs.map +1 -1
- package/index.d.ts +255 -10
- package/package.json +1 -1
|
@@ -12,10 +12,14 @@ import * as i5 from '@angular/material/tooltip';
|
|
|
12
12
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
13
13
|
import { ColorToContrastPipe, StringToColorPipe } from '@myrmidon/ngx-tools';
|
|
14
14
|
import { BehaviorSubject, of, tap, forkJoin } from 'rxjs';
|
|
15
|
+
import { map } from 'rxjs/operators';
|
|
15
16
|
|
|
16
17
|
class CompactPagerComponent {
|
|
17
18
|
constructor() {
|
|
18
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The current paging information.
|
|
21
|
+
*/
|
|
22
|
+
this.paging = input({ pageNumber: 0, pageCount: 0, total: 0 }, ...(ngDevMode ? [{ debugName: "paging" }] : []));
|
|
19
23
|
/**
|
|
20
24
|
* Emits the new paging information when the user changes the page.
|
|
21
25
|
*/
|
|
@@ -48,12 +52,12 @@ class CompactPagerComponent {
|
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
52
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.
|
|
55
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
56
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: CompactPagerComponent, isStandalone: true, selector: "pdb-compact-pager", inputs: { paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pagingChange: "pagingChange" }, ngImport: i0, template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }] }); }
|
|
53
57
|
}
|
|
54
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
58
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: CompactPagerComponent, decorators: [{
|
|
55
59
|
type: Component,
|
|
56
|
-
args: [{ selector: 'pdb-compact-pager', imports: [CommonModule, MatButtonModule, MatIconModule], template: "@if (paging().pageCount) {\
|
|
60
|
+
args: [{ selector: 'pdb-compact-pager', imports: [CommonModule, MatButtonModule, MatIconModule], template: "@if (paging().pageCount) {\n <div class=\"form-row\">\n <span id=\"pages\">{{ paging().pageNumber }}/{{ paging().pageCount }}</span>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onFirst()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>first_page</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onPrevious()\"\n [disabled]=\"paging().pageNumber < 2\"\n >\n <mat-icon>navigate_before</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onNext()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>navigate_next</mat-icon>\n </button>\n <button\n type=\"button\"\n mat-icon-button\n (click)=\"onLast()\"\n [disabled]=\"paging().pageNumber === paging().pageCount\"\n >\n <mat-icon>last_page</mat-icon>\n </button>\n <span id=\"total\">{{ paging().total }} </span>\n </div>\n}\n", styles: ["#pages,#total{color:silver}.form-row{display:flex;gap:2px;align-items:center;flex-wrap:wrap}.form-row *{flex:0 0 auto}\n"] }]
|
|
57
61
|
}] });
|
|
58
62
|
|
|
59
63
|
class RangeViewComponent {
|
|
@@ -61,19 +65,19 @@ class RangeViewComponent {
|
|
|
61
65
|
/**
|
|
62
66
|
* The domain of the range view (start, limit).
|
|
63
67
|
*/
|
|
64
|
-
this.domain = input([0, 100]);
|
|
68
|
+
this.domain = input([0, 100], ...(ngDevMode ? [{ debugName: "domain" }] : []));
|
|
65
69
|
/**
|
|
66
70
|
* The range of the range view (start, limit).
|
|
67
71
|
*/
|
|
68
|
-
this.range = input([0, 100]);
|
|
72
|
+
this.range = input([0, 100], ...(ngDevMode ? [{ debugName: "range" }] : []));
|
|
69
73
|
/**
|
|
70
74
|
* The width of the component.
|
|
71
75
|
*/
|
|
72
|
-
this.width = input(100);
|
|
76
|
+
this.width = input(100, ...(ngDevMode ? [{ debugName: "width" }] : []));
|
|
73
77
|
/**
|
|
74
78
|
* The height of the component.
|
|
75
79
|
*/
|
|
76
|
-
this.height = input(5);
|
|
80
|
+
this.height = input(5, ...(ngDevMode ? [{ debugName: "height" }] : []));
|
|
77
81
|
this.scaledRange = computed(() => {
|
|
78
82
|
const domain = this.domain();
|
|
79
83
|
const range = this.range();
|
|
@@ -85,14 +89,14 @@ class RangeViewComponent {
|
|
|
85
89
|
(rangeStart / domainWidth) * width,
|
|
86
90
|
((rangeStart + rangeWidth) / domainWidth) * width,
|
|
87
91
|
];
|
|
88
|
-
});
|
|
92
|
+
}, ...(ngDevMode ? [{ debugName: "scaledRange" }] : []));
|
|
89
93
|
}
|
|
90
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
91
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.
|
|
94
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
95
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.1", type: RangeViewComponent, isStandalone: true, selector: "pdb-range-view", inputs: { domain: { classPropertyName: "domain", publicName: "domain", isSignal: true, isRequired: false, transformFunction: null }, range: { classPropertyName: "range", publicName: "range", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }); }
|
|
92
96
|
}
|
|
93
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
97
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: RangeViewComponent, decorators: [{
|
|
94
98
|
type: Component,
|
|
95
|
-
args: [{ selector: 'pdb-range-view', template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\
|
|
99
|
+
args: [{ selector: 'pdb-range-view', template: "<svg [attr.width]=\"width()\" [attr.height]=\"height()\">\n <rect id=\"rdomain\" [attr.width]=\"width()\" [attr.height]=\"height()\" />\n <rect\n id=\"rrange\"\n [attr.x]=\"scaledRange()[0]\"\n [attr.y]=\"0\"\n [attr.width]=\"scaledRange()[1] - scaledRange()[0]\"\n [attr.height]=\"height()\"\n />\n</svg>\n", styles: ["#rdomain{fill:#d3d3d3;stroke-width:3;stroke:#c1ba9b}#rrange{fill:#91aad3;stroke-width:3}\n"] }]
|
|
96
100
|
}], ctorParameters: () => [] });
|
|
97
101
|
|
|
98
102
|
/**
|
|
@@ -109,15 +113,15 @@ class BrowserTreeNodeComponent {
|
|
|
109
113
|
/**
|
|
110
114
|
* The node to display.
|
|
111
115
|
*/
|
|
112
|
-
this.node = input();
|
|
116
|
+
this.node = input(...(ngDevMode ? [undefined, { debugName: "node" }] : []));
|
|
113
117
|
/**
|
|
114
118
|
* The paging information for the node's children.
|
|
115
119
|
*/
|
|
116
|
-
this.paging = input();
|
|
120
|
+
this.paging = input(...(ngDevMode ? [undefined, { debugName: "paging" }] : []));
|
|
117
121
|
/**
|
|
118
122
|
* True to show debug information.
|
|
119
123
|
*/
|
|
120
|
-
this.debug = input();
|
|
124
|
+
this.debug = input(...(ngDevMode ? [undefined, { debugName: "debug" }] : []));
|
|
121
125
|
/**
|
|
122
126
|
* True to hide the node's loc and label. This is useful if you want to
|
|
123
127
|
* provide your own view for the node, between the expansion toggle and
|
|
@@ -127,27 +131,27 @@ class BrowserTreeNodeComponent {
|
|
|
127
131
|
* the view, then you can just add your own content to this component's
|
|
128
132
|
* template, without setting this property to true.
|
|
129
133
|
*/
|
|
130
|
-
this.hideLabel = input();
|
|
134
|
+
this.hideLabel = input(...(ngDevMode ? [undefined, { debugName: "hideLabel" }] : []));
|
|
131
135
|
/**
|
|
132
136
|
* True to hide the node's location (y and x).
|
|
133
137
|
*/
|
|
134
|
-
this.hideLoc = input();
|
|
138
|
+
this.hideLoc = input(...(ngDevMode ? [undefined, { debugName: "hideLoc" }] : []));
|
|
135
139
|
/**
|
|
136
140
|
* True to hide the node's paging control unless hovered.
|
|
137
141
|
*/
|
|
138
|
-
this.hidePaging = input();
|
|
142
|
+
this.hidePaging = input(...(ngDevMode ? [undefined, { debugName: "hidePaging" }] : []));
|
|
139
143
|
/**
|
|
140
144
|
* True to hide the node's filter edit button.
|
|
141
145
|
*/
|
|
142
|
-
this.hideFilter = input();
|
|
146
|
+
this.hideFilter = input(...(ngDevMode ? [undefined, { debugName: "hideFilter" }] : []));
|
|
143
147
|
/**
|
|
144
148
|
* The indent size for the node's children.
|
|
145
149
|
*/
|
|
146
|
-
this.indentSize = input(30);
|
|
150
|
+
this.indentSize = input(30, ...(ngDevMode ? [{ debugName: "indentSize" }] : []));
|
|
147
151
|
/**
|
|
148
152
|
* The width of the range view.
|
|
149
153
|
*/
|
|
150
|
-
this.rangeWidth = input(250);
|
|
154
|
+
this.rangeWidth = input(250, ...(ngDevMode ? [{ debugName: "rangeWidth" }] : []));
|
|
151
155
|
/**
|
|
152
156
|
* Emits when the user wants to toggle the expanded state of the node.
|
|
153
157
|
*/
|
|
@@ -178,14 +182,14 @@ class BrowserTreeNodeComponent {
|
|
|
178
182
|
this.editNodeFilterRequest.emit(this.node());
|
|
179
183
|
}
|
|
180
184
|
}
|
|
181
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.
|
|
182
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.
|
|
183
|
-
// ngx-tools
|
|
184
|
-
ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }, { kind: "component", type:
|
|
185
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
186
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.1", type: BrowserTreeNodeComponent, isStandalone: true, selector: "pdb-browser-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: false, transformFunction: null }, paging: { classPropertyName: "paging", publicName: "paging", isSignal: true, isRequired: false, transformFunction: null }, debug: { classPropertyName: "debug", publicName: "debug", isSignal: true, isRequired: false, transformFunction: null }, hideLabel: { classPropertyName: "hideLabel", publicName: "hideLabel", isSignal: true, isRequired: false, transformFunction: null }, hideLoc: { classPropertyName: "hideLoc", publicName: "hideLoc", isSignal: true, isRequired: false, transformFunction: null }, hidePaging: { classPropertyName: "hidePaging", publicName: "hidePaging", isSignal: true, isRequired: false, transformFunction: null }, hideFilter: { classPropertyName: "hideFilter", publicName: "hideFilter", isSignal: true, isRequired: false, transformFunction: null }, indentSize: { classPropertyName: "indentSize", publicName: "indentSize", isSignal: true, isRequired: false, transformFunction: null }, rangeWidth: { classPropertyName: "rangeWidth", publicName: "rangeWidth", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { toggleExpandedRequest: "toggleExpandedRequest", changePageRequest: "changePageRequest", editNodeFilterRequest: "editNodeFilterRequest" }, ngImport: i0, template: "@if (node()) {\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\">\n <!-- pager -->\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\n <pdb-compact-pager\n [paging]=\"paging()!\"\n (pagingChange)=\"onPagingChange(node()!, $event)\"\n />\n <pdb-range-view\n [width]=\"rangeWidth()\"\n [domain]=\"[0, paging()!.pageCount]\"\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\n />\n </div>\n }\n <!-- node -->\n <div class=\"form-row\">\n <!-- expand/collapse button -->\n <button\n type=\"button\"\n mat-icon-button\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\n i18n-matTooltip\n [disabled]=\"node()!.hasChildren === false\"\n (click)=\"onToggleExpanded()\"\n >\n <mat-icon class=\"mat-primary\" i18n>\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\n chevron_left } @else { chevron_right } } @else { stop }\n </mat-icon>\n </button>\n\n <!-- tag -->\n @if (!hideLabel()) {\n <span\n class=\"tag\"\n [ngStyle]=\"{\n 'background-color': (node()!.tag | stringToColor),\n color: node()!.tag | stringToColor | colorToContrast\n }\"\n >{{ node()!.tag }}</span\n >\n\n <!-- loc and label -->\n @if (!hideLoc()) {\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\n {{ node()!.label }}\n }\n\n <!-- PROJECTED NODE -->\n <ng-content></ng-content>\n\n <!-- debug -->\n @if (debug()) {\n <span class=\"debug\"\n >#{{ node()!.id }}\n <span\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\n node()!.paging.total\n }})</span\n ></span\n >\n }\n\n <!-- filter -->\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button\n type=\"button\"\n mat-icon-button\n matTooltip=\"Add filter\"\n i18n-matTooltip\n (click)=\"onEditFilter()\"\n >\n <mat-icon>filter_list</mat-icon>\n </button>\n </div>\n } @if (node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\n </button>\n </div>\n } }\n </div>\n</div>\n}\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1$1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: MatBadgeModule }, { kind: "directive", type: i2$1.MatBadge, selector: "[matBadge]", inputs: ["matBadgeColor", "matBadgeOverlap", "matBadgeDisabled", "matBadgePosition", "matBadge", "matBadgeDescription", "matBadgeSize", "matBadgeHidden"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "component", type:
|
|
185
187
|
// local
|
|
186
|
-
CompactPagerComponent, selector: "pdb-compact-pager", inputs: ["paging"], outputs: ["pagingChange"] }, { kind: "component", type: RangeViewComponent, selector: "pdb-range-view", inputs: ["domain", "range", "width", "height"] }
|
|
188
|
+
CompactPagerComponent, selector: "pdb-compact-pager", inputs: ["paging"], outputs: ["pagingChange"] }, { kind: "component", type: RangeViewComponent, selector: "pdb-range-view", inputs: ["domain", "range", "width", "height"] }, { kind: "pipe", type:
|
|
189
|
+
// ngx-tools
|
|
190
|
+
ColorToContrastPipe, name: "colorToContrast" }, { kind: "pipe", type: StringToColorPipe, name: "stringToColor" }] }); }
|
|
187
191
|
}
|
|
188
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.
|
|
192
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.1", ngImport: i0, type: BrowserTreeNodeComponent, decorators: [{
|
|
189
193
|
type: Component,
|
|
190
194
|
args: [{ selector: 'pdb-browser-tree-node', imports: [
|
|
191
195
|
CommonModule,
|
|
@@ -199,7 +203,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImpor
|
|
|
199
203
|
// local
|
|
200
204
|
CompactPagerComponent,
|
|
201
205
|
RangeViewComponent,
|
|
202
|
-
], template: "@if (node()) {\
|
|
206
|
+
], template: "@if (node()) {\n<div id=\"node\" [style.margin-left.px]=\"(node()!.y - 1) * indentSize()\">\n <!-- pager -->\n @if (node()!.expanded && paging() && paging()!.pageCount > 1) {\n <div id=\"pager\" [style.display]=\"hidePaging() ? 'inherit' : 'block'\">\n <pdb-compact-pager\n [paging]=\"paging()!\"\n (pagingChange)=\"onPagingChange(node()!, $event)\"\n />\n <pdb-range-view\n [width]=\"rangeWidth()\"\n [domain]=\"[0, paging()!.pageCount]\"\n [range]=\"[paging()!.pageNumber - 1, paging()!.pageNumber]\"\n />\n </div>\n }\n <!-- node -->\n <div class=\"form-row\">\n <!-- expand/collapse button -->\n <button\n type=\"button\"\n mat-icon-button\n [matTooltip]=\"node()?.expanded ? 'Collapse' : 'Expand'\"\n i18n-matTooltip\n [disabled]=\"node()!.hasChildren === false\"\n (click)=\"onToggleExpanded()\"\n >\n <mat-icon class=\"mat-primary\" i18n>\n @if (node()!.hasChildren === true) { @if (node()!.expanded) {\n chevron_left } @else { chevron_right } } @else { stop }\n </mat-icon>\n </button>\n\n <!-- tag -->\n @if (!hideLabel()) {\n <span\n class=\"tag\"\n [ngStyle]=\"{\n 'background-color': (node()!.tag | stringToColor),\n color: node()!.tag | stringToColor | colorToContrast\n }\"\n >{{ node()!.tag }}</span\n >\n\n <!-- loc and label -->\n @if (!hideLoc()) {\n <span class=\"loc\">{{ node()!.y }}.{{ node()!.x }}</span> - }\n {{ node()!.label }}\n }\n\n <!-- PROJECTED NODE -->\n <ng-content></ng-content>\n\n <!-- debug -->\n @if (debug()) {\n <span class=\"debug\"\n >#{{ node()!.id }}\n <span\n >| {{ node()!.paging.pageNumber }}/{{ node()!.paging.pageCount }} ({{\n node()!.paging.total\n }})</span\n ></span\n >\n }\n\n <!-- filter -->\n @if (!hideFilter()){ @if (!node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button\n type=\"button\"\n mat-icon-button\n matTooltip=\"Add filter\"\n i18n-matTooltip\n (click)=\"onEditFilter()\"\n >\n <mat-icon>filter_list</mat-icon>\n </button>\n </div>\n } @if (node()?.filter && node()?.y) {\n <div class=\"muted\">\n <button type=\"button\" mat-icon-button (click)=\"onEditFilter()\">\n <mat-icon [matBadge]=\"node()?.filter ? 'F' : ''\">filter_alt</mat-icon>\n </button>\n </div>\n } }\n </div>\n</div>\n}\n", styles: [":root{--browser-tree-node-margin-bottom: 4px;--browser-tree-node-padding: 4px 6px;--browser-tree-node-border: 1px solid #98a8d4;--browser-tree-node-border-radius: 6px;--browser-tree-node-hover-bg-color: #d6dee9}#node{margin-bottom:var(--browser-tree-node-margin-bottom);padding:var(--browser-tree-node-padding);border:var(--browser-tree-node-border);border-radius:var(--browser-tree-node-border-radius)}#node:hover{background-color:var(--browser-tree-node-hover-bg-color)}.form-row{display:flex;gap:8px;align-items:center}.form-row *{flex:0 0 auto}.form-row span{flex:0 1 auto;white-space:normal}#node #pager{display:none}#node:hover #pager{display:block}span.loc{font-size:.85em;color:#666;vertical-align:middle}span.tag{border:1px solid #aaa;border-radius:4px;padding:0 4px}fieldset{border:1px solid silver;border-radius:6px;padding:4px 6px;margin:4px 0}legend{color:silver}.muted{opacity:.3}.muted:hover{opacity:1}.debug{font-size:.85em;color:#9c3d3e}\n"] }]
|
|
203
207
|
}] });
|
|
204
208
|
|
|
205
209
|
/**
|
|
@@ -499,11 +503,13 @@ class PagedListStore {
|
|
|
499
503
|
|
|
500
504
|
/**
|
|
501
505
|
* A store for the node browser component. This store is used to keep a
|
|
502
|
-
* list of nodes, and to load them from the
|
|
503
|
-
* node. Every tree node in the list
|
|
504
|
-
* page count and total items, plus
|
|
506
|
+
* list of nodes, and to load them from the injected PagedTreeStoreService<F>.
|
|
507
|
+
* It also keeps the root node(s) in memory. Every tree node in the list
|
|
508
|
+
* is extended with page number, page count and total items, plus
|
|
509
|
+
* expansion-related metadata.
|
|
505
510
|
* The store keeps a flat list of these tree nodes, allowing users to
|
|
506
|
-
* expand and collapse them.
|
|
511
|
+
* expand and collapse them. Each node has its children paged, and optionally
|
|
512
|
+
* filtered.
|
|
507
513
|
* F is the type of the filter object, E is the type of the paged tree nodes.
|
|
508
514
|
*/
|
|
509
515
|
class PagedTreeStore {
|
|
@@ -539,7 +545,7 @@ class PagedTreeStore {
|
|
|
539
545
|
this._hasMockRoot = options.hasMockRoot || false;
|
|
540
546
|
}
|
|
541
547
|
/**
|
|
542
|
-
* Gets the global filter,
|
|
548
|
+
* Gets the global filter, optionally overridden with values
|
|
543
549
|
* from the specified node's filter.
|
|
544
550
|
* @param node The optional node.
|
|
545
551
|
* @returns The filter.
|
|
@@ -567,7 +573,8 @@ class PagedTreeStore {
|
|
|
567
573
|
return this._nodes$.value;
|
|
568
574
|
}
|
|
569
575
|
/**
|
|
570
|
-
* Get the root node of the tree.
|
|
576
|
+
* Get the root node of the tree. Given that the store keeps a flat
|
|
577
|
+
* list, this is the first node in the list, if any.
|
|
571
578
|
* @returns The root node of the tree or undefined if empty.
|
|
572
579
|
*/
|
|
573
580
|
getRootNode() {
|
|
@@ -596,8 +603,11 @@ class PagedTreeStore {
|
|
|
596
603
|
* @returns Observable of the page of nodes.
|
|
597
604
|
*/
|
|
598
605
|
getPageFromCacheOrServer(filter, pageNumber) {
|
|
606
|
+
// try to get the page from cache
|
|
599
607
|
const key = this.buildCacheKey(pageNumber, filter);
|
|
600
608
|
const pageInCache = this._cache.get(key);
|
|
609
|
+
// if in cache, just return it; else, get it from the server and
|
|
610
|
+
// store it in cache
|
|
601
611
|
if (pageInCache) {
|
|
602
612
|
return of(pageInCache);
|
|
603
613
|
}
|
|
@@ -653,10 +663,11 @@ class PagedTreeStore {
|
|
|
653
663
|
this._service
|
|
654
664
|
.getNodes({
|
|
655
665
|
...filter,
|
|
656
|
-
parentId: undefined,
|
|
666
|
+
parentId: undefined, // root have no parent
|
|
657
667
|
}, 1, this._pageSize, this._hasMockRoot)
|
|
658
668
|
.subscribe({
|
|
659
669
|
next: (page) => {
|
|
670
|
+
// update the nodes
|
|
660
671
|
this._nodes$.next(this.createPageNodes(page));
|
|
661
672
|
// get the children of each node thus calculating their hasChildren property
|
|
662
673
|
const childrenObservables = this._nodes$.value.map((node) => this.getPageFromCacheOrServer({ ...filter, parentId: node.id }, 1));
|
|
@@ -702,13 +713,17 @@ class PagedTreeStore {
|
|
|
702
713
|
*/
|
|
703
714
|
expand(id) {
|
|
704
715
|
return new Promise((resolve, reject) => {
|
|
716
|
+
// get the node to expand
|
|
705
717
|
const node = this._nodes$.value.find((n) => n.id === id);
|
|
718
|
+
// if no node, or no children, or already expanded, do nothing
|
|
706
719
|
if (!node || node.hasChildren === false || node.expanded) {
|
|
707
720
|
resolve(false);
|
|
708
721
|
}
|
|
722
|
+
// get the first page of children for this node
|
|
709
723
|
this.getPageFromCacheOrServer({ ...this.getFilter(node), parentId: id }, 1).subscribe((page) => {
|
|
710
724
|
// no children, set hasChildren to false
|
|
711
725
|
if (!page.total) {
|
|
726
|
+
// no children, next time we won't try to expand
|
|
712
727
|
node.hasChildren = false;
|
|
713
728
|
resolve(false);
|
|
714
729
|
}
|
|
@@ -776,6 +791,11 @@ class PagedTreeStore {
|
|
|
776
791
|
}
|
|
777
792
|
return true;
|
|
778
793
|
}
|
|
794
|
+
/**
|
|
795
|
+
* Get the children of the node with the specified ID.
|
|
796
|
+
* @param id The ID of the node whose children you want to get.
|
|
797
|
+
* @returns An array of child nodes.
|
|
798
|
+
*/
|
|
779
799
|
getChildren(id) {
|
|
780
800
|
const node = this._nodes$.value.find((n) => n.id === id);
|
|
781
801
|
if (!node || node.hasChildren === false) {
|
|
@@ -856,7 +876,8 @@ class PagedTreeStore {
|
|
|
856
876
|
return Promise.resolve(true);
|
|
857
877
|
}
|
|
858
878
|
/**
|
|
859
|
-
* Change the page including the
|
|
879
|
+
* Change the page including the children of the parent node with
|
|
880
|
+
* the specified ID.
|
|
860
881
|
* @param parentId The ID of the parent node whose children are inside the page
|
|
861
882
|
* you want to change.
|
|
862
883
|
* @param pageNumber The new page number.
|
|
@@ -866,45 +887,64 @@ class PagedTreeStore {
|
|
|
866
887
|
return new Promise((resolve, reject) => {
|
|
867
888
|
// get the parent node
|
|
868
889
|
const parentNode = this._nodes$.value.find((n) => n.id === parentId);
|
|
890
|
+
// if parent not found, do nothing
|
|
869
891
|
if (!parentNode) {
|
|
870
892
|
resolve(false);
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
// parent should be expanded
|
|
896
|
+
if (!parentNode.expanded) {
|
|
897
|
+
resolve(false);
|
|
898
|
+
return;
|
|
871
899
|
}
|
|
872
900
|
// get the page
|
|
873
|
-
this.getPageFromCacheOrServer({ ...this.getFilter(parentNode), parentId }, pageNumber).subscribe(
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
901
|
+
this.getPageFromCacheOrServer({ ...this.getFilter(parentNode), parentId }, pageNumber).subscribe({
|
|
902
|
+
next: (page) => {
|
|
903
|
+
// if page is empty, collapse the parent
|
|
904
|
+
if (!page.total) {
|
|
905
|
+
parentNode.hasChildren = false;
|
|
906
|
+
parentNode.expanded = false;
|
|
907
|
+
this._nodes$.next(this._nodes$.value);
|
|
908
|
+
resolve(false);
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
879
911
|
this._dirty = true;
|
|
880
|
-
// remove all the nodes in the same page of node
|
|
881
|
-
// with all their descendants
|
|
882
912
|
const nodes = this._nodes$.value;
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const oldPageNr = nodes[start].paging.pageNumber;
|
|
888
|
-
while (start > 0 &&
|
|
889
|
-
nodes[start - 1].parentId === parentId &&
|
|
890
|
-
nodes[start - 1].paging.pageNumber === oldPageNr) {
|
|
891
|
-
start--;
|
|
913
|
+
const parentIndex = nodes.indexOf(parentNode);
|
|
914
|
+
if (parentIndex === -1) {
|
|
915
|
+
reject(`Parent node ID ${parentId} not found in store`);
|
|
916
|
+
return;
|
|
892
917
|
}
|
|
893
|
-
|
|
894
|
-
//
|
|
918
|
+
const pageNodes = this.createPageNodes(page);
|
|
919
|
+
// find the range of children to replace
|
|
920
|
+
let start = parentIndex + 1;
|
|
895
921
|
let end = start;
|
|
896
|
-
|
|
897
|
-
while (end < nodes.length &&
|
|
922
|
+
// find all current children of this parent (at any page)
|
|
923
|
+
while (end < nodes.length &&
|
|
924
|
+
nodes[end].parentId === parentId &&
|
|
925
|
+
nodes[end].y === parentNode.y + 1) {
|
|
926
|
+
// skip descendants of children
|
|
927
|
+
const childY = nodes[end].y;
|
|
898
928
|
end++;
|
|
929
|
+
while (end < nodes.length && nodes[end].y > childY) {
|
|
930
|
+
end++;
|
|
931
|
+
}
|
|
899
932
|
}
|
|
900
|
-
// replace
|
|
901
|
-
nodes.splice(start, end - start);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
933
|
+
// replace the children with the new page
|
|
934
|
+
nodes.splice(start, end - start, ...pageNodes);
|
|
935
|
+
// update parent paging info
|
|
936
|
+
parentNode.paging = {
|
|
937
|
+
pageNumber: page.pageNumber,
|
|
938
|
+
pageCount: page.pageCount,
|
|
939
|
+
total: page.total,
|
|
940
|
+
};
|
|
905
941
|
this._nodes$.next(nodes);
|
|
906
942
|
resolve(true);
|
|
907
|
-
}
|
|
943
|
+
},
|
|
944
|
+
error: (error) => {
|
|
945
|
+
console.error('Error changing page:', error);
|
|
946
|
+
reject(error);
|
|
947
|
+
},
|
|
908
948
|
});
|
|
909
949
|
});
|
|
910
950
|
}
|
|
@@ -932,6 +972,731 @@ class PagedTreeStore {
|
|
|
932
972
|
const key = this.buildCacheKey(pageNumber, filter);
|
|
933
973
|
return this._cache.has(key);
|
|
934
974
|
}
|
|
975
|
+
/**
|
|
976
|
+
* Remove all hilite properties from all nodes in the store.
|
|
977
|
+
*/
|
|
978
|
+
removeHilites() {
|
|
979
|
+
const nodes = this._nodes$.value;
|
|
980
|
+
nodes.forEach((node) => {
|
|
981
|
+
node.hilite = undefined;
|
|
982
|
+
});
|
|
983
|
+
this._nodes$.next(nodes);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Find all nodes whose labels include the specified search text,
|
|
987
|
+
* highlight them, and ensure they are visible by expanding their ancestors.
|
|
988
|
+
* @param searchText The text to search for in node labels (case-insensitive).
|
|
989
|
+
* @returns Promise that resolves when all matching nodes are highlighted and visible.
|
|
990
|
+
*/
|
|
991
|
+
async findLabels(searchText) {
|
|
992
|
+
if (!searchText || searchText.trim().length === 0) {
|
|
993
|
+
this.removeHilites();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
// first remove all existing hilites
|
|
997
|
+
this.removeHilites();
|
|
998
|
+
const searchLower = searchText.toLowerCase();
|
|
999
|
+
const matchingNodes = [];
|
|
1000
|
+
// find all matching nodes (including those not currently visible):
|
|
1001
|
+
// search through all possible nodes, not just visible ones
|
|
1002
|
+
await this.searchAllNodes(searchLower, matchingNodes);
|
|
1003
|
+
// highlight the matching nodes
|
|
1004
|
+
matchingNodes.forEach((node) => {
|
|
1005
|
+
node.hilite = true;
|
|
1006
|
+
});
|
|
1007
|
+
// ensure all matching nodes are visible by expanding their ancestor paths
|
|
1008
|
+
const expansionPromises = [];
|
|
1009
|
+
for (const node of matchingNodes) {
|
|
1010
|
+
expansionPromises.push(this.ensureNodeVisible(node.id));
|
|
1011
|
+
}
|
|
1012
|
+
await Promise.all(expansionPromises);
|
|
1013
|
+
// update the nodes observable
|
|
1014
|
+
this._nodes$.next(this._nodes$.value);
|
|
1015
|
+
}
|
|
1016
|
+
/**
|
|
1017
|
+
* Search through all nodes recursively to find matches, including nodes
|
|
1018
|
+
* that are not currently loaded/visible.
|
|
1019
|
+
* @param searchLower The search text in lowercase.
|
|
1020
|
+
* @param matchingNodes Array to collect matching nodes.
|
|
1021
|
+
*/
|
|
1022
|
+
async searchAllNodes(searchLower, matchingNodes) {
|
|
1023
|
+
// start with root nodes
|
|
1024
|
+
const rootNodes = this._nodes$.value.filter((n) => n.parentId === undefined);
|
|
1025
|
+
for (const rootNode of rootNodes) {
|
|
1026
|
+
await this.searchNodeAndDescendants(rootNode, searchLower, matchingNodes);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Recursively search a node and all its descendants for label matches.
|
|
1031
|
+
* @param node The node to search.
|
|
1032
|
+
* @param searchLower The search text in lowercase.
|
|
1033
|
+
* @param matchingNodes Array to collect matching nodes.
|
|
1034
|
+
*/
|
|
1035
|
+
async searchNodeAndDescendants(node, searchLower, matchingNodes) {
|
|
1036
|
+
// check if current node matches
|
|
1037
|
+
if (node.label.toLowerCase().includes(searchLower)) {
|
|
1038
|
+
matchingNodes.push(node);
|
|
1039
|
+
}
|
|
1040
|
+
// if node has children, we need to load them to search through them
|
|
1041
|
+
if (node.hasChildren) {
|
|
1042
|
+
// Expand the node if not already expanded to load its children
|
|
1043
|
+
if (!node.expanded) {
|
|
1044
|
+
await this.expand(node.id);
|
|
1045
|
+
}
|
|
1046
|
+
// get all direct children and search them recursively
|
|
1047
|
+
const children = this.getChildren(node.id);
|
|
1048
|
+
for (const child of children) {
|
|
1049
|
+
await this.searchNodeAndDescendants(child, searchLower, matchingNodes);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Ensure a node is visible by expanding all its ancestors.
|
|
1055
|
+
* @param nodeId The ID of the node to make visible.
|
|
1056
|
+
*/
|
|
1057
|
+
async ensureNodeVisible(nodeId) {
|
|
1058
|
+
const node = this._nodes$.value.find((n) => n.id === nodeId);
|
|
1059
|
+
if (!node) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
// find the path from root to this node
|
|
1063
|
+
const ancestorPath = [];
|
|
1064
|
+
let currentNode = node;
|
|
1065
|
+
while (currentNode && currentNode.parentId !== undefined) {
|
|
1066
|
+
ancestorPath.unshift(currentNode.parentId);
|
|
1067
|
+
currentNode = this._nodes$.value.find((n) => n.id === currentNode.parentId);
|
|
1068
|
+
}
|
|
1069
|
+
// expand all ancestors in order
|
|
1070
|
+
for (const ancestorId of ancestorPath) {
|
|
1071
|
+
const ancestor = this._nodes$.value.find((n) => n.id === ancestorId);
|
|
1072
|
+
if (ancestor && ancestor.hasChildren && !ancestor.expanded) {
|
|
1073
|
+
await this.expand(ancestorId);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
get _isEditable() {
|
|
1078
|
+
// check if the service supports editing
|
|
1079
|
+
return 'saveChanges' in this._service;
|
|
1080
|
+
}
|
|
1081
|
+
get _editableService() {
|
|
1082
|
+
return this._isEditable
|
|
1083
|
+
? this._service
|
|
1084
|
+
: undefined;
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* Add a child node to the specified parent.
|
|
1088
|
+
* @param parentId The ID of the parent node.
|
|
1089
|
+
* @param child The child node to add (without ID).
|
|
1090
|
+
* @param first If true, add as first child; otherwise add as last child.
|
|
1091
|
+
* @returns The added node with temporary ID, or undefined if service doesn't support editing.
|
|
1092
|
+
*/
|
|
1093
|
+
addChild(parentId, child, first = false) {
|
|
1094
|
+
return new Promise((resolve, reject) => {
|
|
1095
|
+
const editableService = this._editableService;
|
|
1096
|
+
if (!editableService) {
|
|
1097
|
+
resolve(undefined);
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
const parent = this._nodes$.value.find((n) => n.id === parentId);
|
|
1101
|
+
if (!parent) {
|
|
1102
|
+
reject(`Parent node with ID ${parentId} not found`);
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
// calculate position
|
|
1106
|
+
const siblings = this.getChildren(parentId);
|
|
1107
|
+
const position = first ? 1 : siblings.length + 1;
|
|
1108
|
+
const y = parent.y + 1;
|
|
1109
|
+
// adjust x values of existing siblings if inserting at beginning
|
|
1110
|
+
if (first && siblings.length > 0) {
|
|
1111
|
+
siblings.forEach((sibling) => {
|
|
1112
|
+
sibling.x++;
|
|
1113
|
+
editableService.updateNode(sibling.id, { x: sibling.x });
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
const newChild = {
|
|
1117
|
+
...child,
|
|
1118
|
+
parentId,
|
|
1119
|
+
y,
|
|
1120
|
+
x: position,
|
|
1121
|
+
};
|
|
1122
|
+
const addedNode = editableService.addNode(newChild, parentId, position);
|
|
1123
|
+
// update parent hasChildren if it was false
|
|
1124
|
+
if (parent.hasChildren === false) {
|
|
1125
|
+
parent.hasChildren = true;
|
|
1126
|
+
editableService.updateNode(parentId, { hasChildren: true });
|
|
1127
|
+
}
|
|
1128
|
+
// if parent is expanded, add the node to the visible list
|
|
1129
|
+
if (parent.expanded) {
|
|
1130
|
+
const nodes = this._nodes$.value;
|
|
1131
|
+
const parentIndex = nodes.indexOf(parent);
|
|
1132
|
+
if (parentIndex !== -1) {
|
|
1133
|
+
const insertIndex = first
|
|
1134
|
+
? parentIndex + 1
|
|
1135
|
+
: parentIndex + siblings.length + 1;
|
|
1136
|
+
const pagedNode = {
|
|
1137
|
+
...addedNode,
|
|
1138
|
+
paging: parent.paging,
|
|
1139
|
+
expanded: false,
|
|
1140
|
+
};
|
|
1141
|
+
nodes.splice(insertIndex, 0, pagedNode);
|
|
1142
|
+
this._nodes$.next(nodes);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
this._dirty = true;
|
|
1146
|
+
this.invalidateCache(parentId);
|
|
1147
|
+
resolve(addedNode);
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
/**
|
|
1151
|
+
* Add a sibling node next to the anchor node.
|
|
1152
|
+
* @param anchorId The ID of the anchor node.
|
|
1153
|
+
* @param sibling The sibling node to add (without ID).
|
|
1154
|
+
* @param before If true, add before anchor; otherwise add after anchor.
|
|
1155
|
+
* @returns The added node with temporary ID, or undefined if service doesn't support editing.
|
|
1156
|
+
*/
|
|
1157
|
+
addSibling(anchorId, sibling, before = false) {
|
|
1158
|
+
return new Promise((resolve, reject) => {
|
|
1159
|
+
const editableService = this._editableService;
|
|
1160
|
+
if (!editableService) {
|
|
1161
|
+
resolve(undefined);
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1164
|
+
const anchor = this._nodes$.value.find((n) => n.id === anchorId);
|
|
1165
|
+
if (!anchor) {
|
|
1166
|
+
reject(`Anchor node with ID ${anchorId} not found`);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
// get all siblings of the anchor
|
|
1170
|
+
const siblings = this._nodes$.value.filter((n) => n.parentId === anchor.parentId && n.y === anchor.y);
|
|
1171
|
+
siblings.sort((a, b) => a.x - b.x);
|
|
1172
|
+
const anchorIndex = siblings.findIndex((s) => s.id === anchorId);
|
|
1173
|
+
const insertPosition = before ? anchor.x : anchor.x + 1;
|
|
1174
|
+
// adjust x values of siblings that come after the insertion point
|
|
1175
|
+
siblings.slice(before ? anchorIndex : anchorIndex + 1).forEach((s) => {
|
|
1176
|
+
s.x++;
|
|
1177
|
+
editableService.updateNode(s.id, { x: s.x });
|
|
1178
|
+
});
|
|
1179
|
+
const newSibling = {
|
|
1180
|
+
...sibling,
|
|
1181
|
+
parentId: anchor.parentId,
|
|
1182
|
+
y: anchor.y,
|
|
1183
|
+
x: insertPosition,
|
|
1184
|
+
};
|
|
1185
|
+
const addedNode = editableService.addNode(newSibling, anchor.parentId);
|
|
1186
|
+
// add to visible list if the parent is expanded
|
|
1187
|
+
const nodes = this._nodes$.value;
|
|
1188
|
+
const anchorVisibleIndex = nodes.indexOf(anchor);
|
|
1189
|
+
if (anchorVisibleIndex !== -1) {
|
|
1190
|
+
const insertIndex = before
|
|
1191
|
+
? anchorVisibleIndex
|
|
1192
|
+
: anchorVisibleIndex + 1;
|
|
1193
|
+
const pagedNode = {
|
|
1194
|
+
...addedNode,
|
|
1195
|
+
paging: anchor.paging,
|
|
1196
|
+
expanded: false,
|
|
1197
|
+
};
|
|
1198
|
+
nodes.splice(insertIndex, 0, pagedNode);
|
|
1199
|
+
this._nodes$.next(nodes);
|
|
1200
|
+
}
|
|
1201
|
+
this._dirty = true;
|
|
1202
|
+
this.invalidateCache(anchor.parentId);
|
|
1203
|
+
resolve(addedNode);
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Remove a node from the tree.
|
|
1208
|
+
* We only track the deletion and update visible siblings.
|
|
1209
|
+
* Coordinate adjustments for non-visible nodes are handled by applyLocalChanges.
|
|
1210
|
+
*/
|
|
1211
|
+
removeNode(nodeId) {
|
|
1212
|
+
return new Promise((resolve, reject) => {
|
|
1213
|
+
const editableService = this._editableService;
|
|
1214
|
+
if (!editableService) {
|
|
1215
|
+
resolve(false);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const node = this._nodes$.value.find((n) => n.id === nodeId);
|
|
1219
|
+
if (!node) {
|
|
1220
|
+
resolve(false);
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const parentId = node.parentId;
|
|
1224
|
+
const nodeY = node.y;
|
|
1225
|
+
const nodeX = node.x;
|
|
1226
|
+
// CRITICAL: ensure the node is in the editable service's cache
|
|
1227
|
+
// before removing it, so its original data is preserved
|
|
1228
|
+
if (!editableService.getNode(nodeId)) {
|
|
1229
|
+
// pass the complete node including any extended properties (like 'key'):
|
|
1230
|
+
// this preserves all data needed for persistence
|
|
1231
|
+
editableService.updateNode(nodeId, node);
|
|
1232
|
+
console.log('Added node to cache before deletion:', nodeId);
|
|
1233
|
+
}
|
|
1234
|
+
// remove all descendants first
|
|
1235
|
+
const descendants = this.getDescendants(nodeId);
|
|
1236
|
+
descendants.forEach((descendant) => {
|
|
1237
|
+
// ensure each descendant is in cache before removing
|
|
1238
|
+
if (!editableService.getNode(descendant.id)) {
|
|
1239
|
+
editableService.updateNode(descendant.id, descendant);
|
|
1240
|
+
console.log('Added descendant to cache before deletion:', descendant.id);
|
|
1241
|
+
}
|
|
1242
|
+
editableService.removeNode(descendant.id);
|
|
1243
|
+
});
|
|
1244
|
+
// remove the node itself
|
|
1245
|
+
console.log('Removing node from editable service:', nodeId);
|
|
1246
|
+
editableService.removeNode(nodeId);
|
|
1247
|
+
// update ONLY visible siblings' x coordinates
|
|
1248
|
+
// (Non-visible siblings will get adjusted by applyLocalChanges)
|
|
1249
|
+
const visibleSiblings = this._nodes$.value.filter((n) => n.parentId === parentId && n.y === nodeY && n.x > nodeX);
|
|
1250
|
+
visibleSiblings.forEach((sibling) => {
|
|
1251
|
+
sibling.x--;
|
|
1252
|
+
// this will succeed because these nodes ARE in the local cache
|
|
1253
|
+
editableService.updateNode(sibling.id, { x: sibling.x });
|
|
1254
|
+
});
|
|
1255
|
+
// remove from visible list
|
|
1256
|
+
const nodes = this._nodes$.value;
|
|
1257
|
+
const nodeIndex = nodes.indexOf(node);
|
|
1258
|
+
if (nodeIndex !== -1) {
|
|
1259
|
+
let endIndex = nodeIndex + 1;
|
|
1260
|
+
while (endIndex < nodes.length && nodes[endIndex].y > nodeY) {
|
|
1261
|
+
endIndex++;
|
|
1262
|
+
}
|
|
1263
|
+
nodes.splice(nodeIndex, endIndex - nodeIndex);
|
|
1264
|
+
}
|
|
1265
|
+
// update parent hasChildren if needed
|
|
1266
|
+
if (parentId !== undefined) {
|
|
1267
|
+
const parent = this._nodes$.value.find((n) => n.id === parentId);
|
|
1268
|
+
if (parent && parent.paging.total === 1) {
|
|
1269
|
+
// this was the last child
|
|
1270
|
+
parent.hasChildren = false;
|
|
1271
|
+
parent.expanded = false;
|
|
1272
|
+
editableService.updateNode(parentId, { hasChildren: false });
|
|
1273
|
+
this._nodes$.next(nodes);
|
|
1274
|
+
this._dirty = true;
|
|
1275
|
+
this.invalidateCache(parentId);
|
|
1276
|
+
resolve(true);
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
this._nodes$.next(nodes);
|
|
1281
|
+
this._dirty = true;
|
|
1282
|
+
this.invalidateCache(parentId);
|
|
1283
|
+
// reload the parent's current page to show the updated state
|
|
1284
|
+
if (parentId !== undefined) {
|
|
1285
|
+
const parent = this._nodes$.value.find((n) => n.id === parentId);
|
|
1286
|
+
if (parent?.expanded && parent?.paging) {
|
|
1287
|
+
this.changePage(parentId, parent.paging.pageNumber)
|
|
1288
|
+
.then(() => resolve(true))
|
|
1289
|
+
.catch(reject);
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
resolve(true);
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* Replace a node with a new one.
|
|
1298
|
+
* @param oldNodeId The ID of the node to replace.
|
|
1299
|
+
* @param newNode The new node data (without ID).
|
|
1300
|
+
* @param keepDescendants If true, keep descendants; otherwise remove them.
|
|
1301
|
+
* @returns Promise that resolves to the new node, or undefined if service
|
|
1302
|
+
* doesn't support editing.
|
|
1303
|
+
*/
|
|
1304
|
+
replaceNode(oldNodeId, newNode, keepDescendants = true) {
|
|
1305
|
+
return new Promise((resolve, reject) => {
|
|
1306
|
+
const editableService = this._editableService;
|
|
1307
|
+
if (!editableService) {
|
|
1308
|
+
resolve(undefined);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const oldNode = this._nodes$.value.find((n) => n.id === oldNodeId);
|
|
1312
|
+
if (!oldNode) {
|
|
1313
|
+
reject(`Node with ID ${oldNodeId} not found`);
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const updatedNode = {
|
|
1317
|
+
...newNode,
|
|
1318
|
+
id: oldNodeId,
|
|
1319
|
+
parentId: oldNode.parentId,
|
|
1320
|
+
y: oldNode.y,
|
|
1321
|
+
x: oldNode.x,
|
|
1322
|
+
};
|
|
1323
|
+
editableService.updateNode(oldNodeId, updatedNode);
|
|
1324
|
+
// handle descendants
|
|
1325
|
+
if (!keepDescendants) {
|
|
1326
|
+
const descendants = this.getDescendants(oldNodeId);
|
|
1327
|
+
descendants.forEach((descendant) => {
|
|
1328
|
+
editableService.removeNode(descendant.id);
|
|
1329
|
+
});
|
|
1330
|
+
// update hasChildren
|
|
1331
|
+
editableService.updateNode(oldNodeId, { hasChildren: false });
|
|
1332
|
+
}
|
|
1333
|
+
// update in visible list
|
|
1334
|
+
const nodes = this._nodes$.value;
|
|
1335
|
+
const nodeIndex = nodes.findIndex((n) => n.id === oldNodeId);
|
|
1336
|
+
if (nodeIndex !== -1) {
|
|
1337
|
+
const pagedNode = nodes[nodeIndex];
|
|
1338
|
+
Object.assign(pagedNode, updatedNode);
|
|
1339
|
+
if (!keepDescendants) {
|
|
1340
|
+
pagedNode.hasChildren = false;
|
|
1341
|
+
pagedNode.expanded = false;
|
|
1342
|
+
// remove descendants from visible list
|
|
1343
|
+
let endIndex = nodeIndex + 1;
|
|
1344
|
+
while (endIndex < nodes.length && nodes[endIndex].y > oldNode.y) {
|
|
1345
|
+
endIndex++;
|
|
1346
|
+
}
|
|
1347
|
+
if (endIndex > nodeIndex + 1) {
|
|
1348
|
+
nodes.splice(nodeIndex + 1, endIndex - nodeIndex - 1);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
this._nodes$.next(nodes);
|
|
1352
|
+
}
|
|
1353
|
+
this._dirty = true;
|
|
1354
|
+
this.invalidateCache(oldNode.parentId);
|
|
1355
|
+
resolve(updatedNode);
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
/**
|
|
1359
|
+
* Get all descendants of a node.
|
|
1360
|
+
*/
|
|
1361
|
+
getDescendants(nodeId) {
|
|
1362
|
+
const node = this._nodes$.value.find((n) => n.id === nodeId);
|
|
1363
|
+
if (!node)
|
|
1364
|
+
return [];
|
|
1365
|
+
const descendants = [];
|
|
1366
|
+
const nodes = this._nodes$.value;
|
|
1367
|
+
const nodeIndex = nodes.indexOf(node);
|
|
1368
|
+
if (nodeIndex !== -1) {
|
|
1369
|
+
let i = nodeIndex + 1;
|
|
1370
|
+
while (i < nodes.length && nodes[i].y > node.y) {
|
|
1371
|
+
descendants.push(nodes[i]);
|
|
1372
|
+
i++;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
return descendants;
|
|
1376
|
+
}
|
|
1377
|
+
/**
|
|
1378
|
+
* Invalidate cache for a specific parent and its ancestors.
|
|
1379
|
+
*/
|
|
1380
|
+
invalidateCache(parentId) {
|
|
1381
|
+
// clear cache entries that might be affected by the change
|
|
1382
|
+
this._cache.clear();
|
|
1383
|
+
// if we have a specific parent, we could be more selective about
|
|
1384
|
+
// cache invalidation but for now, clearing all is safer
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Save all pending changes if the service supports it.
|
|
1388
|
+
* Preserves the current expansion state after saving.
|
|
1389
|
+
* @returns Promise that resolves when save is complete,
|
|
1390
|
+
* with ID mappings for new nodes.
|
|
1391
|
+
*/
|
|
1392
|
+
saveChanges() {
|
|
1393
|
+
return new Promise((resolve, reject) => {
|
|
1394
|
+
const editableService = this._editableService;
|
|
1395
|
+
if (!editableService) {
|
|
1396
|
+
resolve(undefined);
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
// capture current expansion state before saving
|
|
1400
|
+
const expandedNodeIds = this._nodes$.value
|
|
1401
|
+
.filter((n) => n.expanded)
|
|
1402
|
+
.map((n) => n.id);
|
|
1403
|
+
editableService.saveChanges().subscribe({
|
|
1404
|
+
next: (idMap) => {
|
|
1405
|
+
// update temporary IDs to permanent IDs in the visible list
|
|
1406
|
+
if (idMap.size > 0) {
|
|
1407
|
+
const nodes = this._nodes$.value;
|
|
1408
|
+
// update node IDs
|
|
1409
|
+
nodes.forEach((node) => {
|
|
1410
|
+
const newId = idMap.get(node.id);
|
|
1411
|
+
if (newId !== undefined) {
|
|
1412
|
+
node.id = newId;
|
|
1413
|
+
}
|
|
1414
|
+
});
|
|
1415
|
+
// update parent IDs for children of replaced nodes
|
|
1416
|
+
nodes.forEach((node) => {
|
|
1417
|
+
if (node.parentId !== undefined) {
|
|
1418
|
+
const newParentId = idMap.get(node.parentId);
|
|
1419
|
+
if (newParentId !== undefined) {
|
|
1420
|
+
node.parentId = newParentId;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
});
|
|
1424
|
+
this._nodes$.next(nodes);
|
|
1425
|
+
}
|
|
1426
|
+
// clear cache to force fresh data
|
|
1427
|
+
this._cache.clear();
|
|
1428
|
+
this._dirty = false;
|
|
1429
|
+
// restore expansion state by re-expanding previously expanded nodes
|
|
1430
|
+
const restoreExpansion = async () => {
|
|
1431
|
+
for (const oldId of expandedNodeIds) {
|
|
1432
|
+
const newId = idMap.get(oldId) ?? oldId;
|
|
1433
|
+
const node = this._nodes$.value.find((n) => n.id === newId);
|
|
1434
|
+
if (node && !node.expanded) {
|
|
1435
|
+
await this.expand(newId);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
};
|
|
1439
|
+
restoreExpansion().then(() => resolve(idMap));
|
|
1440
|
+
},
|
|
1441
|
+
error: (error) => reject(error),
|
|
1442
|
+
});
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Check if there are unsaved changes.
|
|
1447
|
+
*/
|
|
1448
|
+
hasUnsavedChanges() {
|
|
1449
|
+
return this._editableService?.hasChanges() || false;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Clear all unsaved changes.
|
|
1453
|
+
*/
|
|
1454
|
+
clearUnsavedChanges() {
|
|
1455
|
+
this._editableService?.clearChanges();
|
|
1456
|
+
this._dirty = true;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Types of operations that can be performed on tree nodes.
|
|
1462
|
+
*/
|
|
1463
|
+
var ChangeOperationType;
|
|
1464
|
+
(function (ChangeOperationType) {
|
|
1465
|
+
ChangeOperationType["ADD"] = "add";
|
|
1466
|
+
ChangeOperationType["REMOVE"] = "remove";
|
|
1467
|
+
ChangeOperationType["UPDATE"] = "update";
|
|
1468
|
+
})(ChangeOperationType || (ChangeOperationType = {}));
|
|
1469
|
+
/**
|
|
1470
|
+
* Base implementation of EditablePagedTreeStoreService that handles change tracking
|
|
1471
|
+
* and provides common editing functionality. Implementers only need to override
|
|
1472
|
+
* the abstract methods for actual data persistence.
|
|
1473
|
+
*/
|
|
1474
|
+
class EditablePagedTreeStoreServiceBase {
|
|
1475
|
+
constructor() {
|
|
1476
|
+
this._changes = [];
|
|
1477
|
+
this._nextTempId = -1;
|
|
1478
|
+
this._nodes = new Map();
|
|
1479
|
+
this._removedNodes = new Set();
|
|
1480
|
+
this._hasChanges$ = new BehaviorSubject(false);
|
|
1481
|
+
/**
|
|
1482
|
+
* Observable that emits when the change state changes.
|
|
1483
|
+
*/
|
|
1484
|
+
this.hasChanges$ = this._hasChanges$.asObservable();
|
|
1485
|
+
}
|
|
1486
|
+
/**
|
|
1487
|
+
* Get nodes, including any local changes that haven't been saved yet.
|
|
1488
|
+
*/
|
|
1489
|
+
getNodes(filter, pageNumber, pageSize, hasMockRoot) {
|
|
1490
|
+
return this.fetchNodes(filter, pageNumber, pageSize, hasMockRoot).pipe(map((page) => this.applyLocalChanges(page, filter)));
|
|
1491
|
+
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Apply local changes to a page of nodes.
|
|
1494
|
+
* This includes applying x-coordinate adjustments for siblings of deleted nodes.
|
|
1495
|
+
*/
|
|
1496
|
+
applyLocalChanges(page, filter) {
|
|
1497
|
+
let items = [...page.items];
|
|
1498
|
+
// Step 1: Remove deleted nodes from the current page
|
|
1499
|
+
items = items.filter((node) => !this._removedNodes.has(node.id));
|
|
1500
|
+
// Step 2: Calculate x-coordinate adjustments for this page based on deletions
|
|
1501
|
+
// For each removed node that would affect this page, adjust x coordinates
|
|
1502
|
+
const xAdjustments = new Map(); // nodeId -> adjustment amount
|
|
1503
|
+
this._removedNodes.forEach((removedId) => {
|
|
1504
|
+
const removeChange = this._changes.find((c) => c.type === ChangeOperationType.REMOVE && c.id === removedId);
|
|
1505
|
+
if (removeChange?.originalNode) {
|
|
1506
|
+
const removedNode = removeChange.originalNode;
|
|
1507
|
+
// For each item in current page that was a sibling after the removed node
|
|
1508
|
+
items.forEach((item) => {
|
|
1509
|
+
if (item.parentId === removedNode.parentId &&
|
|
1510
|
+
item.y === removedNode.y &&
|
|
1511
|
+
item.x > removedNode.x) {
|
|
1512
|
+
// This sibling should have its x decreased
|
|
1513
|
+
const currentAdjustment = xAdjustments.get(item.id) || 0;
|
|
1514
|
+
xAdjustments.set(item.id, currentAdjustment - 1);
|
|
1515
|
+
}
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
});
|
|
1519
|
+
// Step 3: Apply updates to existing nodes, including coordinate adjustments
|
|
1520
|
+
items = items.map((node) => {
|
|
1521
|
+
let result = { ...node };
|
|
1522
|
+
// First apply any explicit updates from cache
|
|
1523
|
+
const updated = this._nodes.get(node.id);
|
|
1524
|
+
if (updated && !this._removedNodes.has(node.id)) {
|
|
1525
|
+
result = { ...result, ...updated };
|
|
1526
|
+
}
|
|
1527
|
+
// Then apply calculated x-coordinate adjustments from deletions
|
|
1528
|
+
const xAdjustment = xAdjustments.get(node.id);
|
|
1529
|
+
if (xAdjustment !== undefined && xAdjustment !== 0) {
|
|
1530
|
+
result.x = result.x + xAdjustment;
|
|
1531
|
+
}
|
|
1532
|
+
return result;
|
|
1533
|
+
});
|
|
1534
|
+
// Step 4: Add new nodes that belong to this specific page
|
|
1535
|
+
const pageParentId = filter.parentId;
|
|
1536
|
+
const newNodesForThisPage = Array.from(this._nodes.values()).filter((node) => node.id < 0 && // temporary ID (newly added)
|
|
1537
|
+
!this._removedNodes.has(node.id) && // not removed
|
|
1538
|
+
node.parentId === pageParentId && // belongs to the same parent
|
|
1539
|
+
this.matchesFilter(node, filter) && // matches the filter
|
|
1540
|
+
node.label !== undefined && // has all required properties
|
|
1541
|
+
node.x !== undefined &&
|
|
1542
|
+
node.y !== undefined);
|
|
1543
|
+
items.push(...newNodesForThisPage);
|
|
1544
|
+
// Step 5: Sort items by their position (x coordinate)
|
|
1545
|
+
items.sort((a, b) => a.x - b.x);
|
|
1546
|
+
// Step 6: Adjust total count to reflect deletions and additions for this parent
|
|
1547
|
+
let totalAdjustment = 0;
|
|
1548
|
+
// Count deletions that affect this parent
|
|
1549
|
+
this._removedNodes.forEach((removedId) => {
|
|
1550
|
+
const change = this._changes.find((c) => c.type === ChangeOperationType.REMOVE && c.id === removedId);
|
|
1551
|
+
if (change?.originalNode?.parentId === pageParentId) {
|
|
1552
|
+
totalAdjustment--;
|
|
1553
|
+
}
|
|
1554
|
+
});
|
|
1555
|
+
// Count additions for this parent
|
|
1556
|
+
totalAdjustment += newNodesForThisPage.length;
|
|
1557
|
+
const adjustedTotal = Math.max(0, page.total + totalAdjustment);
|
|
1558
|
+
const adjustedPageCount = Math.ceil(adjustedTotal / (page.pageSize || 20));
|
|
1559
|
+
return {
|
|
1560
|
+
...page,
|
|
1561
|
+
items: items,
|
|
1562
|
+
total: adjustedTotal,
|
|
1563
|
+
pageCount: adjustedPageCount,
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Check if a node matches the given filter.
|
|
1568
|
+
*/
|
|
1569
|
+
matchesFilter(node, filter) {
|
|
1570
|
+
if (filter.parentId !== undefined && node.parentId !== filter.parentId) {
|
|
1571
|
+
return false;
|
|
1572
|
+
}
|
|
1573
|
+
if (filter.tags &&
|
|
1574
|
+
filter.tags.length > 0 &&
|
|
1575
|
+
(!node.tag || !filter.tags.includes(node.tag))) {
|
|
1576
|
+
return false;
|
|
1577
|
+
}
|
|
1578
|
+
return true;
|
|
1579
|
+
}
|
|
1580
|
+
/**
|
|
1581
|
+
* Add a new node to the tree.
|
|
1582
|
+
*/
|
|
1583
|
+
addNode(node, parentId, position) {
|
|
1584
|
+
const newNode = {
|
|
1585
|
+
...node,
|
|
1586
|
+
id: this._nextTempId--,
|
|
1587
|
+
parentId,
|
|
1588
|
+
};
|
|
1589
|
+
this._nodes.set(newNode.id, newNode);
|
|
1590
|
+
this._changes.push({
|
|
1591
|
+
type: ChangeOperationType.ADD,
|
|
1592
|
+
id: newNode.id,
|
|
1593
|
+
node: newNode,
|
|
1594
|
+
parentId,
|
|
1595
|
+
position,
|
|
1596
|
+
});
|
|
1597
|
+
this.updateHasChanges();
|
|
1598
|
+
return newNode;
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Remove a node from the tree.
|
|
1602
|
+
*/
|
|
1603
|
+
removeNode(id) {
|
|
1604
|
+
const node = this._nodes.get(id);
|
|
1605
|
+
if (node || !this._removedNodes.has(id)) {
|
|
1606
|
+
this._removedNodes.add(id);
|
|
1607
|
+
this._changes.push({
|
|
1608
|
+
type: ChangeOperationType.REMOVE,
|
|
1609
|
+
id,
|
|
1610
|
+
originalNode: node,
|
|
1611
|
+
});
|
|
1612
|
+
this.updateHasChanges();
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Update an existing node.
|
|
1617
|
+
* If the node is in cache, update it with the partial updates.
|
|
1618
|
+
* If not in cache and updates is a complete TreeNode, add it to cache.
|
|
1619
|
+
* Otherwise skip (normal for pagination).
|
|
1620
|
+
*/
|
|
1621
|
+
updateNode(id, updates) {
|
|
1622
|
+
const existing = this._nodes.get(id);
|
|
1623
|
+
if (!existing) {
|
|
1624
|
+
// Check if updates contains a complete TreeNode (used for deletion prep)
|
|
1625
|
+
const isCompleteNode = updates.label !== undefined &&
|
|
1626
|
+
updates.y !== undefined &&
|
|
1627
|
+
updates.x !== undefined;
|
|
1628
|
+
if (isCompleteNode) {
|
|
1629
|
+
// Add the complete node to cache (used before deletion)
|
|
1630
|
+
const completeNode = { ...updates, id };
|
|
1631
|
+
this._nodes.set(id, completeNode);
|
|
1632
|
+
// Don't add a change operation - this is just caching for later operations
|
|
1633
|
+
}
|
|
1634
|
+
// Otherwise silently skip - normal for pagination
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const updated = { ...existing, ...updates, id };
|
|
1638
|
+
this._nodes.set(id, updated);
|
|
1639
|
+
this._changes.push({
|
|
1640
|
+
type: ChangeOperationType.UPDATE,
|
|
1641
|
+
id,
|
|
1642
|
+
node: updated,
|
|
1643
|
+
originalNode: existing,
|
|
1644
|
+
});
|
|
1645
|
+
this.updateHasChanges();
|
|
1646
|
+
}
|
|
1647
|
+
/**
|
|
1648
|
+
* Save all pending changes.
|
|
1649
|
+
*/
|
|
1650
|
+
saveChanges() {
|
|
1651
|
+
if (this._changes.length === 0) {
|
|
1652
|
+
return of(new Map());
|
|
1653
|
+
}
|
|
1654
|
+
return this.persistChanges([...this._changes]).pipe(map((idMap) => {
|
|
1655
|
+
// update temporary IDs with permanent ones
|
|
1656
|
+
idMap.forEach((permanentId, tempId) => {
|
|
1657
|
+
const node = this._nodes.get(tempId);
|
|
1658
|
+
if (node) {
|
|
1659
|
+
this._nodes.delete(tempId);
|
|
1660
|
+
this._nodes.set(permanentId, { ...node, id: permanentId });
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
// clear changes after successful save
|
|
1664
|
+
this._changes = [];
|
|
1665
|
+
this._removedNodes.clear();
|
|
1666
|
+
this.updateHasChanges();
|
|
1667
|
+
return idMap;
|
|
1668
|
+
}));
|
|
1669
|
+
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Check if there are unsaved changes.
|
|
1672
|
+
*/
|
|
1673
|
+
hasChanges() {
|
|
1674
|
+
return this._changes.length > 0;
|
|
1675
|
+
}
|
|
1676
|
+
/**
|
|
1677
|
+
* Clear all pending changes.
|
|
1678
|
+
*/
|
|
1679
|
+
clearChanges() {
|
|
1680
|
+
this._changes = [];
|
|
1681
|
+
this._nodes.clear();
|
|
1682
|
+
this._removedNodes.clear();
|
|
1683
|
+
this.updateHasChanges();
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Get all pending changes.
|
|
1687
|
+
*/
|
|
1688
|
+
getChanges() {
|
|
1689
|
+
return [...this._changes];
|
|
1690
|
+
}
|
|
1691
|
+
/**
|
|
1692
|
+
* Get a node by ID, including local changes.
|
|
1693
|
+
*/
|
|
1694
|
+
getNode(id) {
|
|
1695
|
+
return this._nodes.get(id);
|
|
1696
|
+
}
|
|
1697
|
+
updateHasChanges() {
|
|
1698
|
+
this._hasChanges$.next(this._changes.length > 0);
|
|
1699
|
+
}
|
|
935
1700
|
}
|
|
936
1701
|
|
|
937
1702
|
/*
|
|
@@ -942,5 +1707,5 @@ class PagedTreeStore {
|
|
|
942
1707
|
* Generated bundle index. Do not edit.
|
|
943
1708
|
*/
|
|
944
1709
|
|
|
945
|
-
export { BrowserTreeNodeComponent, CompactPagerComponent, DEFAULT_PAGED_LIST_STORE_OPTIONS, LRUCache, PagedListStore, PagedTreeStore, RangeViewComponent };
|
|
1710
|
+
export { BrowserTreeNodeComponent, ChangeOperationType, CompactPagerComponent, DEFAULT_PAGED_LIST_STORE_OPTIONS, EditablePagedTreeStoreServiceBase, LRUCache, PagedListStore, PagedTreeStore, RangeViewComponent };
|
|
946
1711
|
//# sourceMappingURL=myrmidon-paged-data-browsers.mjs.map
|