diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index aae6d957c..c1f4c46aa 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -107,6 +107,7 @@ import {I18nModule} from "./core/i18n/i18n.module";
import {OriginDestinationComponent} from "./services/analytics/origin-destination/components/origin-destination.component";
import {SbbToggleModule} from "@sbb-esta/angular/toggle";
import {ToggleSwitchButtonComponent} from "./view/toggle-switch-button/toggle-switch-button.component";
+import {GlobalNodesManagementComponent} from "./view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component";
@NgModule({
declarations: [
@@ -152,6 +153,7 @@ import {ToggleSwitchButtonComponent} from "./view/toggle-switch-button/toggle-sw
NavigationBarComponent,
EditorPropertiesViewComponent,
EditorEditToolsViewComponent,
+ GlobalNodesManagementComponent,
FilterableLabelDialogComponent,
FilterableLabelFormComponent,
NoteDialogComponent,
diff --git a/src/app/view/editor-edit-tools-view-component/editor-edit-tools-view.component.html b/src/app/view/editor-edit-tools-view-component/editor-edit-tools-view.component.html
index b69a0ab74..2eff04cda 100644
--- a/src/app/view/editor-edit-tools-view-component/editor-edit-tools-view.component.html
+++ b/src/app/view/editor-edit-tools-view-component/editor-edit-tools-view.component.html
@@ -29,6 +29,13 @@
{{ "app.view.editor-edit-tools-view-component.edit" | t
+
+ {{
+ "app.view.editor-edit-tools-view-component.nodes" | translate
+ }}
+
+
+
+
+
+
+
+ @if (matchingNodes.length) {
+
+
+
+ @for (column of ["nodes-expanded", "nodes"]; track column) {
+ |
+ {{ "app.view.editor-edit-tools-view-component." + column | translate }}
+ |
+ }
+
+ @if (matchingNodes.length > 1) {
+
+ |
+
+ |
+
+ }
+
+
+ @for (node of matchingNodes; track node.getId()) {
+
+ |
+
+ |
+ {{ node.getBetriebspunktName() }} |
+ {{ node.getFullName() }} |
+
+ }
+
+
+ } @else {
+
{{ "app.view.editor-edit-tools-view-component.nodes-no-result" | translate }}
+ }
+
+
diff --git a/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.scss b/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.scss
new file mode 100644
index 000000000..202cd1940
--- /dev/null
+++ b/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.scss
@@ -0,0 +1,60 @@
+.global-nodes-management {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ align-items: stretch;
+
+ .search {
+ position: relative;
+
+ input {
+ width: 100%;
+ }
+ .sbb-icon {
+ position: absolute;
+ right: 5px;
+ top: 50%;
+ transform: translateY(-50%);
+ }
+ }
+
+ .nodes-list {
+ overflow-x: auto;
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+
+ th,
+ td {
+ font-family: var(--sbb-font-light);
+ font-weight: var(--sbb-font-weight-normal);
+ padding: 8px 0;
+ white-space: nowrap;
+ vertical-align: middle;
+ }
+
+ th {
+ text-align: start;
+ }
+ tbody tr {
+ border-top: var(--sbb-border-width-thin) solid var(--sbb-expansion-panel-border-color-open);
+ }
+ td.offset {
+ padding-left: 10px;
+ }
+ tbody td:last-child {
+ width: 99%;
+ }
+ thead th:not(:last-child),
+ tbody td:not(:last-child) {
+ padding-right: 13px;
+ }
+
+ .sbb-checkbox {
+ display: block;
+ }
+ }
+ }
+}
diff --git a/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.ts b/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.ts
new file mode 100644
index 000000000..72e01f8b2
--- /dev/null
+++ b/src/app/view/editor-edit-tools-view-component/global-nodes-management/global-nodes-management.component.ts
@@ -0,0 +1,106 @@
+import {Component} from "@angular/core";
+
+import {NodeService} from "../../../services/data/node.service";
+import {Node} from "../../../models/node.model";
+import {DataService} from "../../../services/data/data.service";
+import {UiInteractionService} from "../../../services/ui/ui.interaction.service";
+import {ConfirmationDialogParameter} from "../../dialogs/confirmation-dialog/confirmation-dialog.component";
+
+function normalizeStr(str: string): string {
+ return str
+ .toLowerCase()
+ .trim()
+ .normalize("NFD")
+ .replace(/[\u0300-\u036f]/g, "");
+}
+
+@Component({
+ selector: "sbb-global-nodes-management",
+ templateUrl: "./global-nodes-management.component.html",
+ styleUrl: "./global-nodes-management.component.scss",
+})
+export class GlobalNodesManagementComponent {
+ query: string;
+ allNodes: Node[];
+ matchingNodes: Node[];
+
+ constructor(
+ private dataService: DataService,
+ private nodeService: NodeService,
+ private uiInteractionService: UiInteractionService,
+ ) {
+ this.query = "";
+ this.allNodes = this.nodeService.getNodes();
+ this.nodeService.nodes.subscribe((nodes) => this.updateState({nodes}));
+ }
+
+ updateState({nodes = this.allNodes, query = this.query}: {nodes?: Node[]; query?: string}) {
+ // Save state locally
+ this.query = query;
+ this.allNodes = nodes;
+
+ const normalizedQuery = normalizeStr(this.query);
+
+ this.matchingNodes = normalizedQuery
+ ? this.allNodes.filter(
+ (node) =>
+ normalizeStr(node.getFullName()).includes(normalizedQuery) ||
+ normalizeStr(node.getBetriebspunktName()).includes(normalizedQuery),
+ )
+ : this.allNodes;
+ }
+
+ getGlobalCheckboxStatus(): boolean | undefined {
+ let allCollapsed = true;
+ let noneCollapsed = true;
+ this.matchingNodes.every((node) => {
+ const isCollapsed = node.getIsCollapsed();
+ allCollapsed = allCollapsed && isCollapsed;
+ noneCollapsed = noneCollapsed && !isCollapsed;
+
+ // If both allCollapsed and noneCollapsed fail, stop iterating
+ return allCollapsed || noneCollapsed;
+ });
+
+ if (allCollapsed) return false;
+ if (noneCollapsed) return true;
+ return undefined;
+ }
+
+ toggleIsCollapsed(node: Node, isCollapsed: boolean) {
+ node.setIsCollapsed(isCollapsed);
+ this.dataService.triggerViewUpdate();
+ }
+ onClickGlobalCheckbox(event: MouseEvent) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const currentGlobalCheckboxStatus = this.getGlobalCheckboxStatus();
+ const newCheckboxStatus = !currentGlobalCheckboxStatus;
+ const newIsCollapsed = !newCheckboxStatus;
+
+ const allNodesImpacted = this.allNodes.length === this.matchingNodes.length;
+ const impactedNodesCount = this.matchingNodes.length;
+
+ const dialogTitle = $localize`:@@app.view.editor-edit-tools-view-component.global-nodes-management:Global nodes management`;
+ const dialogContent = newIsCollapsed
+ ? allNodesImpacted
+ ? $localize`:@@app.view.editor-edit-tools-view-component.confirm-collapse-all:Are you sure you want to collapse all nodes?`
+ : $localize`:@@app.view.editor-edit-tools-view-component.confirm-collapse-matching:Are you sure you want to collapse the ${impactedNodesCount}:count: nodes matching "${this.query}:query:"?`
+ : allNodesImpacted
+ ? $localize`:@@app.view.editor-edit-tools-view-component.confirm-expand-all:Are you sure you want to expand all nodes?`
+ : $localize`:@@app.view.editor-edit-tools-view-component.confirm-expand-matching:Are you sure you want to expand the ${impactedNodesCount}:count: nodes matching "${this.query}:query:"?`;
+ const confirmationDialogParameter = new ConfirmationDialogParameter(dialogTitle, dialogContent);
+
+ this.uiInteractionService
+ .showConfirmationDiagramDialog(confirmationDialogParameter)
+ .subscribe((confirmed: boolean) => {
+ if (confirmed) {
+ this.matchingNodes.forEach((node) => {
+ node.setIsCollapsed(newIsCollapsed);
+ });
+ this.dataService.triggerViewUpdate();
+ }
+ });
+ }
+}
diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json
index 12223bbc5..63fd0221b 100644
--- a/src/assets/i18n/en.json
+++ b/src/assets/i18n/en.json
@@ -337,7 +337,15 @@
"trainruns": "Trainruns",
"notes": "Notes",
"nodes": "Nodes"
- }
+ },
+ "nodes-search-placeholder": "Search for names or short names",
+ "nodes-expanded": "Expanded",
+ "nodes-no-result": "There is no node matching the query.",
+ "global-nodes-management": "Global nodes management",
+ "confirm-expand-all": "Are you sure you want to expand all nodes?",
+ "confirm-expand-matching": "Are you sure you want to expand the {$count} nodes matching \"{$query}\"?",
+ "confirm-collapse-all": "Are you sure you want to collapse all nodes?",
+ "confirm-collapse-matching": "Are you sure you want to collapse the {$count} nodes matching \"{$query}\"?"
},
"editor-filter-view": {
"filter": "Filter",
diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json
index b6d1979be..4b5fb98fb 100644
--- a/src/assets/i18n/fr.json
+++ b/src/assets/i18n/fr.json
@@ -336,7 +336,15 @@
"trainruns": "Trajets de train",
"notes": "Notes",
"nodes": "Noeuds"
- }
+ },
+ "nodes-search-placeholder": "Rechercher par nom ou trigramme",
+ "nodes-expanded": "Développés",
+ "nodes-no-result": "Aucun noeud ne correspond à cette recherche.",
+ "global-nodes-management": "Gestion globale des noeuds",
+ "confirm-expand-all": "Êtes-vous sûr(e) de vouloir développer tous les noeuds ?",
+ "confirm-expand-matching": "Êtes-vous sûr(e) de vouloir développer les {$count} noeuds qui contiennent «{$query}» ?",
+ "confirm-collapse-all": "Êtes-vous sûr(e) de vouloir réduire tous les noeuds ?",
+ "confirm-collapse-matching": "Êtes-vous sûr(e) de vouloir réduire les {$count} noeuds qui contiennent «{$query}» ?"
},
"editor-filter-view": {
"filter": "Filtres",