@meshmakers/octo-ui 3.4.280 → 3.4.310

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.
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { inject, ChangeDetectionStrategy, Component, Injectable, Input, EventEmitter, Output, ElementRef, forwardRef, ViewChild, signal, computed, input, model, output, effect, untracked, Directive, InjectionToken, HostListener, makeEnvironmentProviders } from '@angular/core';
3
3
  import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
4
- import { AttributeSelectorService, AttributeValueTypeDto as AttributeValueTypeDto$1, CkTypeSelectorService, CkTypeAttributeService, GetEntitiesByCkTypeDtoGQL, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, SearchFilterTypesDto, SortOrdersDto, FieldFilterOperatorsDto, GetCkTypesDtoGQL, GetCkModelByIdDtoGQL, LevelMetaData, RtAssociationMetaData, CkTypeMetaData, GraphDirectionDto, AssociationModOptionsDto, GraphQL, DeleteStrategiesDto, CommunicationService, provideOctoServices } from '@meshmakers/octo-services';
4
+ import { AttributeSelectorService, AttributeValueTypeDto as AttributeValueTypeDto$1, CkTypeSelectorService, CkTypeAttributeService, GetEntitiesByCkTypeDtoGQL, RuntimeEntitySelectDataSource, RuntimeEntityDialogDataSource, SearchFilterTypesDto, SortOrdersDto, FieldFilterOperatorsDto, GetCkTypesDtoGQL, GetCkModelByIdDtoGQL, CkTypeMetaData, GraphDirectionDto, AssociationModOptionsDto, GraphQL, DeleteStrategiesDto, CommunicationService, provideOctoServices } from '@meshmakers/octo-services';
5
5
  import { Roles, provideMmSharedAuth } from '@meshmakers/shared-auth';
6
6
  import { WindowStateService, EntitySelectInputComponent, DataSourceTyped, HierarchyDataSourceBase, NotificationDisplayService, TreeComponent, ListViewComponent, FetchResultTyped, DataSourceBase, InputService, BaseTreeDetailComponent, ConfirmationService, provideMmSharedUi } from '@meshmakers/shared-ui';
7
7
  import * as i1$2 from '@angular/common';
@@ -19,12 +19,12 @@ import * as i5$1 from '@progress/kendo-angular-icons';
19
19
  import { IconsModule, SVGIconModule, SVGIconComponent } from '@progress/kendo-angular-icons';
20
20
  import * as i5 from '@progress/kendo-angular-inputs';
21
21
  import { InputsModule, TextBoxModule, KENDO_INPUTS, SwitchModule } from '@progress/kendo-angular-inputs';
22
- import { searchIcon, sortAscSmallIcon, sortDescSmallIcon, filterClearIcon, fileIcon, folderIcon, calendarIcon, checkboxCheckedIcon, listUnorderedIcon, chevronRightIcon, chevronDownIcon, downloadIcon, windowIcon, arrowRightIcon, arrowLeftIcon, chevronDoubleRightIcon, chevronDoubleLeftIcon, arrowUpIcon, arrowDownIcon, pencilIcon, trashIcon, copyIcon, folderMoreIcon, folderOpenIcon, gearIcon, plusIcon, minusIcon, dollarIcon, hyperlinkOpenIcon, infoCircleIcon, eyeIcon, arrowRotateCcwIcon, locationsIcon, arrowRotateCwIcon, xIcon, checkCircleIcon, exclamationCircleIcon, xCircleIcon, warningCircleIcon, gridIcon, linkIcon } from '@progress/kendo-svg-icons';
22
+ import { searchIcon, sortAscSmallIcon, sortDescSmallIcon, filterClearIcon, fileIcon, folderIcon, calendarIcon, checkboxCheckedIcon, listUnorderedIcon, chevronRightIcon, chevronDownIcon, downloadIcon, windowIcon, arrowRightIcon, arrowLeftIcon, chevronDoubleRightIcon, chevronDoubleLeftIcon, arrowUpIcon, arrowDownIcon, pencilIcon, trashIcon, copyIcon, folderMoreIcon, gearIcon, folderOpenIcon, plusIcon, minusIcon, dollarIcon, hyperlinkOpenIcon, infoCircleIcon, eyeIcon, arrowRotateCcwIcon, locationsIcon, arrowRotateCwIcon, xIcon, checkCircleIcon, exclamationCircleIcon, xCircleIcon, warningCircleIcon, gridIcon, linkIcon } from '@progress/kendo-svg-icons';
23
23
  import { Subject, firstValueFrom, Subscription, of, forkJoin, map as map$1, takeUntil as takeUntil$1, catchError as catchError$1, defer, from, finalize, switchMap as switchMap$1, startWith } from 'rxjs';
24
24
  import { debounceTime, distinctUntilChanged, map, switchMap, tap, catchError, takeUntil } from 'rxjs/operators';
25
25
  import { LoaderModule, BadgeModule } from '@progress/kendo-angular-indicators';
26
26
  import * as i1$3 from 'apollo-angular';
27
- import { gql } from 'apollo-angular';
27
+ import { gql, Apollo } from 'apollo-angular';
28
28
  import { isCompositeFilterDescriptor } from '@progress/kendo-data-query';
29
29
  import { TreeItemDataTyped } from '@meshmakers/shared-services';
30
30
  import * as i7 from '@progress/kendo-angular-dateinputs';
@@ -5488,6 +5488,66 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5488
5488
  }]
5489
5489
  }], ctorParameters: () => [{ type: i1$3.Apollo }] });
5490
5490
 
5491
+ const GetCkTypeAssociationRolesDocumentDto = gql `
5492
+ query getCkTypeAssociationRoles($ckTypeId: String!) {
5493
+ constructionKit {
5494
+ types(rtCkId: $ckTypeId, first: 1) {
5495
+ items {
5496
+ rtCkTypeId
5497
+ associations {
5498
+ in {
5499
+ all {
5500
+ roleId {
5501
+ fullName
5502
+ semanticVersionedFullName
5503
+ }
5504
+ rtRoleId
5505
+ navigationPropertyName
5506
+ multiplicity
5507
+ targetCkTypeId {
5508
+ fullName
5509
+ }
5510
+ rtTargetCkTypeId
5511
+ rtOriginCkTypeId
5512
+ }
5513
+ }
5514
+ out {
5515
+ all {
5516
+ roleId {
5517
+ fullName
5518
+ semanticVersionedFullName
5519
+ }
5520
+ rtRoleId
5521
+ navigationPropertyName
5522
+ multiplicity
5523
+ targetCkTypeId {
5524
+ fullName
5525
+ }
5526
+ rtTargetCkTypeId
5527
+ rtOriginCkTypeId
5528
+ }
5529
+ }
5530
+ }
5531
+ }
5532
+ }
5533
+ }
5534
+ }
5535
+ `;
5536
+ class GetCkTypeAssociationRolesDtoGQL extends i1$3.Query {
5537
+ document = GetCkTypeAssociationRolesDocumentDto;
5538
+ constructor(apollo) {
5539
+ super(apollo);
5540
+ }
5541
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, deps: [{ token: i1$3.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
5542
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, providedIn: 'root' });
5543
+ }
5544
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, decorators: [{
5545
+ type: Injectable,
5546
+ args: [{
5547
+ providedIn: 'root'
5548
+ }]
5549
+ }], ctorParameters: () => [{ type: i1$3.Apollo }] });
5550
+
5491
5551
  const GetRuntimeEntityAssociationsByIdDocumentDto = gql `
5492
5552
  query getRuntimeEntityAssociationsById($rtId: OctoObjectId!, $ckTypeId: String!, $direction: GraphDirection!, $after: String, $first: Int, $roleId: String, $relatedRtCkId: RtCkTypeId, $relatedRtId: OctoObjectId) {
5493
5553
  runtime {
@@ -5534,39 +5594,32 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5534
5594
  }]
5535
5595
  }], ctorParameters: () => [{ type: i1$3.Apollo }] });
5536
5596
 
5537
- const GetTreeNodesDocumentDto = gql `
5538
- query getTreeNodes($rtId: OctoObjectId!, $ckTypeId: String!, $directRoleId: String!, $directTargetCkTypeId: String!, $indirectRoleId: String!, $indirectTargetCkTypeId: String!) {
5597
+ const GetTreeAssociationTargetsDocumentDto = gql `
5598
+ query getTreeAssociationTargets($rtId: OctoObjectId!, $ckTypeId: String!, $roleId: String!, $targetCkTypeId: String!, $direction: GraphDirection!, $first: Int) {
5539
5599
  runtime {
5540
- runtimeEntities(rtId: $rtId, ckId: $ckTypeId) {
5600
+ runtimeEntities(rtId: $rtId, ckId: $ckTypeId, first: 1) {
5541
5601
  items {
5542
- rtId
5543
- ckTypeId
5544
- attributes(attributeNames: ["name", "description"]) {
5545
- items {
5546
- attributeName
5547
- value
5548
- }
5549
- }
5550
5602
  associations {
5551
- targets(roleId: $directRoleId, ckId: $directTargetCkTypeId, direction: INBOUND) {
5603
+ targets(
5604
+ roleId: $roleId
5605
+ ckId: $targetCkTypeId
5606
+ direction: $direction
5607
+ first: $first
5608
+ ) {
5609
+ totalCount
5552
5610
  items {
5553
5611
  rtId
5554
5612
  ckTypeId
5555
- attributes(attributeNames: ["name", "description"]) {
5613
+ rtWellKnownName
5614
+ attributes(
5615
+ attributeNames: ["name", "displayName", "description"]
5616
+ resolveEnumValuesToNames: true
5617
+ ) {
5556
5618
  items {
5557
5619
  attributeName
5558
5620
  value
5559
5621
  }
5560
5622
  }
5561
- associations {
5562
- targets(
5563
- roleId: $indirectRoleId
5564
- ckId: $indirectTargetCkTypeId
5565
- direction: INBOUND
5566
- ) {
5567
- totalCount
5568
- }
5569
- }
5570
5623
  }
5571
5624
  }
5572
5625
  }
@@ -5575,15 +5628,15 @@ const GetTreeNodesDocumentDto = gql `
5575
5628
  }
5576
5629
  }
5577
5630
  `;
5578
- class GetTreeNodesDtoGQL extends i1$3.Query {
5579
- document = GetTreeNodesDocumentDto;
5631
+ class GetTreeAssociationTargetsDtoGQL extends i1$3.Query {
5632
+ document = GetTreeAssociationTargetsDocumentDto;
5580
5633
  constructor(apollo) {
5581
5634
  super(apollo);
5582
5635
  }
5583
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeNodesDtoGQL, deps: [{ token: i1$3.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
5584
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeNodesDtoGQL, providedIn: 'root' });
5636
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeAssociationTargetsDtoGQL, deps: [{ token: i1$3.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
5637
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeAssociationTargetsDtoGQL, providedIn: 'root' });
5585
5638
  }
5586
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeNodesDtoGQL, decorators: [{
5639
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetTreeAssociationTargetsDtoGQL, decorators: [{
5587
5640
  type: Injectable,
5588
5641
  args: [{
5589
5642
  providedIn: 'root'
@@ -5963,6 +6016,475 @@ const article = {
5963
6016
  viewBox: '0 -960 960 960'
5964
6017
  };
5965
6018
 
6019
+ /** Runtime CK type id of the per-tenant tree navigation configuration singleton. */
6020
+ const CONFIG_CK_TYPE_ID = 'System.UI/TreeNavigationConfiguration';
6021
+ /** rtWellKnownName of the singleton instance. */
6022
+ const CONFIG_WELL_KNOWN_NAME = 'TreeNavigation';
6023
+ /** Matches every source CK type. */
6024
+ const WILDCARD = '*';
6025
+ const CONFIG_TYPE_EXISTS_QUERY = gql `
6026
+ query treeNavigationConfigTypeExists {
6027
+ constructionKit {
6028
+ types(rtCkId: "System.UI/TreeNavigationConfiguration", first: 1) {
6029
+ items {
6030
+ rtCkTypeId
6031
+ }
6032
+ }
6033
+ }
6034
+ }
6035
+ `;
6036
+ const CONFIG_QUERY = gql `
6037
+ query getTreeNavigationConfiguration {
6038
+ runtime {
6039
+ systemUITreeNavigationConfiguration(
6040
+ first: 1
6041
+ fieldFilter: [
6042
+ {
6043
+ attributePath: "rtWellKnownName"
6044
+ operator: EQUALS
6045
+ comparisonValue: "TreeNavigation"
6046
+ }
6047
+ ]
6048
+ ) {
6049
+ items {
6050
+ rtId
6051
+ roles {
6052
+ sourceCkTypeId
6053
+ roleId
6054
+ visible
6055
+ displayName
6056
+ sortIndex
6057
+ grouped
6058
+ icon
6059
+ }
6060
+ }
6061
+ }
6062
+ }
6063
+ }
6064
+ `;
6065
+ /**
6066
+ * Loads the singleton's `perspectives` array (AB#4263). Kept SEPARATE from
6067
+ * CONFIG_QUERY on purpose: the `perspectives` field only exists in the tenant
6068
+ * schema when System.UI >= 2.3.0. On a tenant still on System.UI 2.2.0 the type
6069
+ * IS present (so the type-exists probe passes) but this field is not, which would
6070
+ * raise a GraphQL validation error for the whole operation. Isolating it here
6071
+ * lets `fetchPerspectives` swallow that error so the 2.2.0 roles feature keeps
6072
+ * working and perspectives simply come back empty.
6073
+ */
6074
+ const CONFIG_PERSPECTIVES_QUERY = gql `
6075
+ query getTreeNavigationPerspectives {
6076
+ runtime {
6077
+ systemUITreeNavigationConfiguration(
6078
+ first: 1
6079
+ fieldFilter: [
6080
+ {
6081
+ attributePath: "rtWellKnownName"
6082
+ operator: EQUALS
6083
+ comparisonValue: "TreeNavigation"
6084
+ }
6085
+ ]
6086
+ ) {
6087
+ items {
6088
+ rtId
6089
+ perspectives {
6090
+ key
6091
+ displayName
6092
+ sortIndex
6093
+ icon
6094
+ rootMode
6095
+ rootCkTypeId
6096
+ primaryRoleId
6097
+ primaryDirection
6098
+ secondaryRoleIds
6099
+ }
6100
+ }
6101
+ }
6102
+ }
6103
+ }
6104
+ `;
6105
+ const ROLE_SUGGESTIONS_QUERY = gql `
6106
+ query treeNavigationRoleSuggestions($ckTypeId: String!) {
6107
+ constructionKit {
6108
+ types(rtCkId: $ckTypeId, first: 1) {
6109
+ items {
6110
+ associations {
6111
+ in {
6112
+ all {
6113
+ rtRoleId
6114
+ navigationPropertyName
6115
+ }
6116
+ }
6117
+ }
6118
+ }
6119
+ }
6120
+ }
6121
+ }
6122
+ `;
6123
+ const CREATE_CONFIG_MUTATION = gql `
6124
+ mutation createTreeNavigationConfiguration(
6125
+ $entities: [SystemUITreeNavigationConfigurationInput!]!
6126
+ ) {
6127
+ runtime {
6128
+ systemUITreeNavigationConfigurations {
6129
+ create(entities: $entities) {
6130
+ rtId
6131
+ }
6132
+ }
6133
+ }
6134
+ }
6135
+ `;
6136
+ const UPDATE_CONFIG_MUTATION = gql `
6137
+ mutation updateTreeNavigationConfiguration(
6138
+ $entities: [SystemUITreeNavigationConfigurationInputUpdate!]!
6139
+ ) {
6140
+ runtime {
6141
+ systemUITreeNavigationConfigurations {
6142
+ update(entities: $entities) {
6143
+ rtId
6144
+ }
6145
+ }
6146
+ }
6147
+ }
6148
+ `;
6149
+ /**
6150
+ * Loads the optional per-tenant `System.UI/TreeNavigationConfiguration` singleton
6151
+ * and resolves per-association overrides for the entity trees.
6152
+ *
6153
+ * Design notes:
6154
+ * - The config entity is OPTIONAL. When the CK type is not installed on the
6155
+ * tenant (System.UI < 2.2.0) or no instance exists, every lookup returns
6156
+ * undefined and the trees fall back to pure auto-discovery.
6157
+ * - The singleton field (`systemUITreeNavigationConfiguration`) only exists in
6158
+ * the tenant schema when the CK type is installed, so querying it blindly
6159
+ * would raise a GraphQL validation error (and a user-facing toast). We first
6160
+ * probe the CK schema with the always-valid `constructionKit.types` query and
6161
+ * only run the singleton query when the type is present.
6162
+ * - Uses inline `gql` (not codegen) so the feature is decoupled from a schema
6163
+ * re-introspection that includes the new CK type.
6164
+ */
6165
+ class TreeNavigationConfigService {
6166
+ apollo = inject(Apollo);
6167
+ /** key `${sourceCkTypeId}::${roleId}` -> override; cached for the session. */
6168
+ overridesPromise;
6169
+ /** Configured extra perspectives; cached for the session. */
6170
+ perspectivesPromise;
6171
+ /**
6172
+ * Resolves the override for one (source type, role) pair, preferring an exact
6173
+ * source-type match over a wildcard (`*`) rule. Returns undefined when nothing
6174
+ * is configured.
6175
+ */
6176
+ async resolve(sourceCkTypeId, roleId) {
6177
+ const overrides = await this.getOverrides();
6178
+ return (overrides.get(`${sourceCkTypeId}::${roleId}`) ??
6179
+ overrides.get(`${WILDCARD}::${roleId}`));
6180
+ }
6181
+ /** Forces a reload on next access (e.g. after a tenant switch). */
6182
+ reset() {
6183
+ this.overridesPromise = undefined;
6184
+ this.perspectivesPromise = undefined;
6185
+ }
6186
+ /**
6187
+ * Returns the configured extra tree perspectives (AB#4263), sorted by
6188
+ * sortIndex then displayName. Returns [] when the CK type is absent
6189
+ * (System.UI < 2.3.0) or none are configured. The built-in spatial
6190
+ * perspective is synthesized by the data source and is NOT included here.
6191
+ */
6192
+ perspectives() {
6193
+ if (!this.perspectivesPromise) {
6194
+ this.perspectivesPromise = this.loadPerspectives().catch((error) => {
6195
+ console.error('Error loading tree navigation perspectives', error);
6196
+ this.perspectivesPromise = undefined;
6197
+ return [];
6198
+ });
6199
+ }
6200
+ return this.perspectivesPromise;
6201
+ }
6202
+ async loadPerspectives() {
6203
+ // Skip the singleton entirely when the CK type is not installed at all.
6204
+ if (!(await this.probeTypePresent())) {
6205
+ return [];
6206
+ }
6207
+ const rawPerspectives = await this.fetchPerspectivesRaw();
6208
+ return rawPerspectives
6209
+ .map((raw) => this.toPerspectiveDefinition(raw))
6210
+ .filter((p) => p !== null)
6211
+ .sort((a, b) => (a.sortIndex ?? Number.MAX_SAFE_INTEGER) -
6212
+ (b.sortIndex ?? Number.MAX_SAFE_INTEGER) ||
6213
+ a.displayName.localeCompare(b.displayName));
6214
+ }
6215
+ /** Maps a raw perspective record to a definition, dropping invalid rows. */
6216
+ toPerspectiveDefinition(raw) {
6217
+ const key = raw?.key?.trim();
6218
+ if (!key) {
6219
+ return null;
6220
+ }
6221
+ const rootMode = raw?.rootMode === 'Type' ? 'Type' : 'Spatial';
6222
+ const secondary = (raw?.secondaryRoleIds ?? [])
6223
+ .map((r) => (typeof r === 'string' ? r : ''))
6224
+ .filter((r) => r.length > 0);
6225
+ return {
6226
+ key,
6227
+ displayName: raw?.displayName?.trim() || key,
6228
+ sortIndex: raw?.sortIndex ?? undefined,
6229
+ icon: raw?.icon ?? undefined,
6230
+ rootMode,
6231
+ rootCkTypeId: raw?.rootCkTypeId ?? undefined,
6232
+ primaryRoleId: raw?.primaryRoleId ?? undefined,
6233
+ primaryDirection: raw?.primaryDirection === 'Outbound' ? 'Outbound' : undefined,
6234
+ secondaryRoleIds: secondary.length > 0 ? secondary : undefined,
6235
+ };
6236
+ }
6237
+ /**
6238
+ * Returns the inbound association roles declared on a CK type, for the role
6239
+ * autocomplete in the settings editor. Returns `{ roleId, label }` where the
6240
+ * label is the friendly inbound name plus the role id. Empty for `*` or an
6241
+ * unknown type (orphan roles can still be typed as custom values).
6242
+ */
6243
+ async getRoleSuggestions(ckTypeId) {
6244
+ if (!ckTypeId || ckTypeId === WILDCARD) {
6245
+ return [];
6246
+ }
6247
+ try {
6248
+ const result = await firstValueFrom(this.apollo.query({
6249
+ query: ROLE_SUGGESTIONS_QUERY,
6250
+ variables: { ckTypeId },
6251
+ fetchPolicy: 'network-only',
6252
+ }));
6253
+ const all = result.data?.constructionKit?.types?.items?.[0]?.associations?.in
6254
+ ?.all ?? [];
6255
+ const byRole = new Map();
6256
+ for (const role of all) {
6257
+ const roleId = String(role?.rtRoleId ?? '');
6258
+ if (!roleId || byRole.has(roleId)) {
6259
+ continue;
6260
+ }
6261
+ const nav = role?.navigationPropertyName ?? '';
6262
+ byRole.set(roleId, nav ? `${nav} (${roleId})` : roleId);
6263
+ }
6264
+ return [...byRole.entries()].map(([roleId, label]) => ({ roleId, label }));
6265
+ }
6266
+ catch (error) {
6267
+ console.error('Error loading role suggestions for', ckTypeId, error);
6268
+ return [];
6269
+ }
6270
+ }
6271
+ getOverrides() {
6272
+ if (!this.overridesPromise) {
6273
+ this.overridesPromise = this.load().catch((error) => {
6274
+ console.error('Error loading tree navigation configuration', error);
6275
+ // Drop the cache so a later expand can retry, fall back to no overrides.
6276
+ this.overridesPromise = undefined;
6277
+ return new Map();
6278
+ });
6279
+ }
6280
+ return this.overridesPromise;
6281
+ }
6282
+ async load() {
6283
+ const map = new Map();
6284
+ const { rawRoles } = await this.fetchSingleton();
6285
+ for (const raw of rawRoles) {
6286
+ if (!raw?.sourceCkTypeId || !raw?.roleId) {
6287
+ continue;
6288
+ }
6289
+ map.set(`${raw.sourceCkTypeId}::${raw.roleId}`, {
6290
+ visible: raw.visible ?? undefined,
6291
+ displayName: raw.displayName ?? undefined,
6292
+ sortIndex: raw.sortIndex ?? undefined,
6293
+ grouped: raw.grouped ?? undefined,
6294
+ icon: raw.icon ?? undefined,
6295
+ });
6296
+ }
6297
+ return map;
6298
+ }
6299
+ /**
6300
+ * Loads the full configuration for editing (settings page). Returns whether
6301
+ * the CK type is installed (so the page can show a clear "upgrade System.UI"
6302
+ * hint), the singleton rtId (null when not created yet), and the role rules.
6303
+ */
6304
+ async loadConfig() {
6305
+ const { typePresent, rtId, rawRoles } = await this.fetchSingleton();
6306
+ const rawPerspectives = typePresent ? await this.fetchPerspectivesRaw() : [];
6307
+ const roles = [];
6308
+ for (const raw of rawRoles) {
6309
+ if (!raw?.sourceCkTypeId || !raw?.roleId) {
6310
+ continue;
6311
+ }
6312
+ roles.push({
6313
+ sourceCkTypeId: raw.sourceCkTypeId,
6314
+ roleId: raw.roleId,
6315
+ visible: raw.visible ?? undefined,
6316
+ displayName: raw.displayName ?? undefined,
6317
+ sortIndex: raw.sortIndex ?? undefined,
6318
+ grouped: raw.grouped ?? undefined,
6319
+ icon: raw.icon ?? undefined,
6320
+ });
6321
+ }
6322
+ const perspectives = rawPerspectives
6323
+ .map((raw) => this.toPerspectiveDefinition(raw))
6324
+ .filter((p) => p !== null);
6325
+ return { typePresent, rtId, roles, perspectives };
6326
+ }
6327
+ /**
6328
+ * Creates or updates the singleton with the given rules and perspectives, then
6329
+ * invalidates the resolve/perspective cache so the trees pick up the change on
6330
+ * the next expand. Returns the singleton rtId.
6331
+ */
6332
+ async saveConfig(rtId, roles, perspectives = []) {
6333
+ const cleanRoles = roles
6334
+ .filter((r) => r.sourceCkTypeId && r.roleId)
6335
+ .map((r) => this.toRoleInput(r));
6336
+ const cleanPerspectives = perspectives
6337
+ .filter((p) => p.key)
6338
+ .map((p) => this.toPerspectiveInput(p));
6339
+ // Only send `perspectives` when non-empty: on a System.UI 2.2.0 tenant the
6340
+ // input type has no `perspectives` field, so including it (even as []) would
6341
+ // fail validation and break roles saving. Trade-off: clearing the last
6342
+ // perspective on 2.3.0 is not persisted via save (edge case).
6343
+ let savedRtId;
6344
+ if (rtId) {
6345
+ const item = { roles: cleanRoles };
6346
+ if (cleanPerspectives.length > 0) {
6347
+ item['perspectives'] = cleanPerspectives;
6348
+ }
6349
+ const result = await firstValueFrom(this.apollo.mutate({
6350
+ mutation: UPDATE_CONFIG_MUTATION,
6351
+ variables: {
6352
+ entities: [{ rtId, item }],
6353
+ },
6354
+ fetchPolicy: 'no-cache',
6355
+ }));
6356
+ savedRtId =
6357
+ result.data?.runtime?.systemUITreeNavigationConfigurations?.update?.[0]
6358
+ ?.rtId ?? rtId;
6359
+ }
6360
+ else {
6361
+ const result = await firstValueFrom(this.apollo.mutate({
6362
+ mutation: CREATE_CONFIG_MUTATION,
6363
+ variables: {
6364
+ entities: [
6365
+ {
6366
+ rtWellKnownName: CONFIG_WELL_KNOWN_NAME,
6367
+ name: 'Tree Navigation',
6368
+ roles: cleanRoles,
6369
+ ...(cleanPerspectives.length > 0
6370
+ ? { perspectives: cleanPerspectives }
6371
+ : {}),
6372
+ },
6373
+ ],
6374
+ },
6375
+ fetchPolicy: 'no-cache',
6376
+ }));
6377
+ const created = result.data?.runtime?.systemUITreeNavigationConfigurations?.create?.[0]
6378
+ ?.rtId;
6379
+ if (!created) {
6380
+ throw new Error('createTreeNavigationConfiguration returned no entity');
6381
+ }
6382
+ savedRtId = created;
6383
+ }
6384
+ this.reset();
6385
+ return savedRtId;
6386
+ }
6387
+ /** Drops undefined fields so the record-array input only carries set values. */
6388
+ toRoleInput(role) {
6389
+ const input = {
6390
+ sourceCkTypeId: role.sourceCkTypeId,
6391
+ roleId: role.roleId,
6392
+ };
6393
+ if (role.visible !== undefined)
6394
+ input['visible'] = role.visible;
6395
+ if (role.displayName !== undefined && role.displayName !== '') {
6396
+ input['displayName'] = role.displayName;
6397
+ }
6398
+ if (role.sortIndex !== undefined && role.sortIndex !== null) {
6399
+ input['sortIndex'] = role.sortIndex;
6400
+ }
6401
+ if (role.grouped !== undefined)
6402
+ input['grouped'] = role.grouped;
6403
+ if (role.icon !== undefined && role.icon !== '')
6404
+ input['icon'] = role.icon;
6405
+ return input;
6406
+ }
6407
+ /** Drops undefined/empty fields so the perspective input only carries set values. */
6408
+ toPerspectiveInput(p) {
6409
+ const input = {
6410
+ key: p.key,
6411
+ displayName: p.displayName || p.key,
6412
+ rootMode: p.rootMode,
6413
+ };
6414
+ if (p.sortIndex !== undefined && p.sortIndex !== null) {
6415
+ input['sortIndex'] = p.sortIndex;
6416
+ }
6417
+ if (p.icon)
6418
+ input['icon'] = p.icon;
6419
+ if (p.rootCkTypeId)
6420
+ input['rootCkTypeId'] = p.rootCkTypeId;
6421
+ if (p.primaryRoleId)
6422
+ input['primaryRoleId'] = p.primaryRoleId;
6423
+ if (p.primaryDirection)
6424
+ input['primaryDirection'] = p.primaryDirection;
6425
+ if (p.secondaryRoleIds && p.secondaryRoleIds.length > 0) {
6426
+ input['secondaryRoleIds'] = p.secondaryRoleIds;
6427
+ }
6428
+ return input;
6429
+ }
6430
+ /**
6431
+ * Probes whether the `System.UI/TreeNavigationConfiguration` CK type is
6432
+ * installed on the tenant (System.UI >= 2.2.0). Uses the always-valid
6433
+ * `constructionKit.types` query so it never raises a schema validation error.
6434
+ */
6435
+ async probeTypePresent() {
6436
+ const exists = await firstValueFrom(this.apollo.query({ query: CONFIG_TYPE_EXISTS_QUERY, fetchPolicy: 'network-only' }));
6437
+ return (exists.data?.constructionKit?.types?.items?.length ?? 0) > 0;
6438
+ }
6439
+ /**
6440
+ * Probes the CK schema and (when present) loads the singleton's raw roles.
6441
+ * Perspectives are loaded separately (see fetchPerspectivesRaw) so a System.UI
6442
+ * 2.2.0 tenant (type present but no `perspectives` field yet) keeps its roles.
6443
+ */
6444
+ async fetchSingleton() {
6445
+ if (!(await this.probeTypePresent())) {
6446
+ return { typePresent: false, rtId: null, rawRoles: [] };
6447
+ }
6448
+ const result = await firstValueFrom(this.apollo.query({ query: CONFIG_QUERY, fetchPolicy: 'network-only' }));
6449
+ const item = result.data?.runtime?.systemUITreeNavigationConfiguration?.items?.[0];
6450
+ return {
6451
+ typePresent: true,
6452
+ rtId: item?.rtId ?? null,
6453
+ rawRoles: item?.roles ?? [],
6454
+ };
6455
+ }
6456
+ /**
6457
+ * Loads the singleton's raw perspectives, tolerating the System.UI 2.2.0 case
6458
+ * where the `perspectives` field does not yet exist (GraphQL validation error).
6459
+ * Any failure yields an empty list so roles loading is never affected.
6460
+ */
6461
+ async fetchPerspectivesRaw() {
6462
+ try {
6463
+ const result = await firstValueFrom(this.apollo.query({ query: CONFIG_PERSPECTIVES_QUERY, fetchPolicy: 'network-only' }));
6464
+ return (result.data?.runtime?.systemUITreeNavigationConfiguration?.items?.[0]
6465
+ ?.perspectives ?? []);
6466
+ }
6467
+ catch {
6468
+ // System.UI 2.2.0: `perspectives` field absent → validation error. Degrade
6469
+ // to no perspectives; the roles query above is unaffected.
6470
+ return [];
6471
+ }
6472
+ }
6473
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: TreeNavigationConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6474
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: TreeNavigationConfigService, providedIn: 'root' });
6475
+ }
6476
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: TreeNavigationConfigService, decorators: [{
6477
+ type: Injectable,
6478
+ args: [{
6479
+ providedIn: 'root',
6480
+ }]
6481
+ }] });
6482
+ const TREE_NAVIGATION_CONFIG_CONSTANTS = {
6483
+ CONFIG_CK_TYPE_ID,
6484
+ CONFIG_WELL_KNOWN_NAME,
6485
+ WILDCARD,
6486
+ };
6487
+
5966
6488
  /**
5967
6489
  * Performs helpful Octomesh Types operations, like checks, comparisons etc.
5968
6490
  */
@@ -5988,9 +6510,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
5988
6510
  }]
5989
6511
  }] });
5990
6512
 
6513
+ /** Well-known role id of the hierarchical parent-child association. */
6514
+ const PARENT_CHILD_ROLE_ID = 'System/ParentChild';
6515
+ /**
6516
+ * Target CK types that cannot be navigated in the tree. `System/Entity` is the
6517
+ * universal abstract base type and has no defining collection root, so the
6518
+ * generic `System/Related` association (target `System/Entity`, present as an
6519
+ * inbound role on every type) makes `targets(ckId: "System/Entity")` throw
6520
+ * server-side ("has no defining collection root"). Skipping it here keeps the
6521
+ * tree from firing a guaranteed-failing query for every entity.
6522
+ */
6523
+ const NON_NAVIGABLE_TARGET_CK_TYPES = new Set(['System/Entity']);
6524
+ /** Key of the built-in spatial perspective (all Basic/Tree roots, as before). */
6525
+ const SPATIAL_PERSPECTIVE_KEY = 'Spatial';
6526
+ /**
6527
+ * The always-available built-in perspective. It reproduces the pre-AB#4263
6528
+ * behaviour (roots = all Basic/Tree entities) and is synthesized rather than
6529
+ * stored, so a zero-config tenant still has exactly one perspective.
6530
+ */
6531
+ const BUILT_IN_SPATIAL_PERSPECTIVE = {
6532
+ key: SPATIAL_PERSPECTIVE_KEY,
6533
+ displayName: 'Spatial',
6534
+ rootMode: 'Spatial',
6535
+ sortIndex: 0,
6536
+ };
6537
+ /**
6538
+ * Loads all runtime instances of a CK type as the roots of a `Type` perspective
6539
+ * (AB#4263). Inline gql (like TreeNavigationConfigService) to stay decoupled
6540
+ * from a schema re-introspection; the selection mirrors getTrees so the existing
6541
+ * root-node rendering keeps working.
6542
+ */
6543
+ const GET_ROOT_ENTITIES_BY_CK_TYPE = gql `
6544
+ query getRuntimeEntitiesByCkType($ckTypeId: String!, $first: Int!) {
6545
+ runtime {
6546
+ runtimeEntities(ckId: $ckTypeId, first: $first) {
6547
+ items {
6548
+ rtId
6549
+ ckTypeId
6550
+ rtWellKnownName
6551
+ attributes(attributeNames: ["name", "displayName", "description"]) {
6552
+ items {
6553
+ attributeName
6554
+ value
6555
+ }
6556
+ }
6557
+ }
6558
+ }
6559
+ }
6560
+ }
6561
+ `;
5991
6562
  class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
5992
6563
  getTreesDtoGQL = inject(GetTreesDtoGQL);
5993
- getTreeNodesDtoGQL = inject(GetTreeNodesDtoGQL);
6564
+ getCkTypeAssociationRolesDtoGQL = inject(GetCkTypeAssociationRolesDtoGQL);
6565
+ getTreeAssociationTargetsDtoGQL = inject(GetTreeAssociationTargetsDtoGQL);
5994
6566
  getCkModelsGQL = inject(GetCkModelsDtoGQL);
5995
6567
  getCkTypesGQL = inject(GetCkTypesDtoGQL);
5996
6568
  getCkModelByIdDtoGQL = inject(GetCkModelByIdDtoGQL);
@@ -5999,6 +6571,94 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
5999
6571
  updateRuntimeEntitiesDtoGQL = inject(UpdateRuntimeEntitiesDtoGQL);
6000
6572
  updateTreeNodesDtoGQL = inject(UpdateTreeNodesDtoGQL);
6001
6573
  typeHelperService = inject(TypeHelperService);
6574
+ treeNavConfig = inject(TreeNavigationConfigService);
6575
+ apollo = inject(Apollo);
6576
+ /** Key of the currently active tree perspective (AB#4263). */
6577
+ activePerspectiveKey = SPATIAL_PERSPECTIVE_KEY;
6578
+ /**
6579
+ * The perspective resolved during the last `fetchRootNodes()`. Used by
6580
+ * `fetchChildren()` to apply the root-level whitelist for `Type` perspectives.
6581
+ */
6582
+ activePerspective = BUILT_IN_SPATIAL_PERSPECTIVE;
6583
+ /**
6584
+ * rtIds of the current perspective's root nodes. The primary/secondary role
6585
+ * whitelist is applied ONLY to the direct children of these roots — deeper
6586
+ * nodes keep full auto-discovery (whitelist-at-root-only).
6587
+ */
6588
+ perspectiveRootRtIds = new Set();
6589
+ /**
6590
+ * Selects the active perspective by key. The host is responsible for reloading
6591
+ * the tree afterwards (e.g. `treeDetail.refreshTree()`), which re-runs
6592
+ * `fetchRootNodes()` and re-resolves the perspective.
6593
+ */
6594
+ setActivePerspective(key) {
6595
+ this.activePerspectiveKey = key || SPATIAL_PERSPECTIVE_KEY;
6596
+ }
6597
+ /** The currently selected perspective key. */
6598
+ getActivePerspectiveKey() {
6599
+ return this.activePerspectiveKey;
6600
+ }
6601
+ /**
6602
+ * Returns all selectable perspectives: the built-in spatial one first, then the
6603
+ * per-tenant configured perspectives, de-duplicated by key (a configured
6604
+ * `Spatial` overrides the built-in). With no configuration this is a single
6605
+ * entry, so the host can hide the switcher.
6606
+ */
6607
+ async getPerspectives() {
6608
+ let configured = [];
6609
+ try {
6610
+ configured = await this.treeNavConfig.perspectives();
6611
+ }
6612
+ catch (error) {
6613
+ console.error('Error loading perspectives', error);
6614
+ }
6615
+ const byKey = new Map();
6616
+ byKey.set(BUILT_IN_SPATIAL_PERSPECTIVE.key, BUILT_IN_SPATIAL_PERSPECTIVE);
6617
+ for (const p of configured) {
6618
+ byKey.set(p.key, p);
6619
+ }
6620
+ return [...byKey.values()].sort((a, b) => (a.sortIndex ?? Number.MAX_SAFE_INTEGER) -
6621
+ (b.sortIndex ?? Number.MAX_SAFE_INTEGER) ||
6622
+ a.displayName.localeCompare(b.displayName));
6623
+ }
6624
+ /**
6625
+ * The role-id whitelist of the active perspective (primary + secondary), or
6626
+ * null when the perspective is not a `Type` perspective or declares no roles.
6627
+ */
6628
+ activePerspectiveWhitelist() {
6629
+ const p = this.activePerspective;
6630
+ if (p.rootMode !== 'Type') {
6631
+ return null;
6632
+ }
6633
+ const allowed = new Set();
6634
+ if (p.primaryRoleId) {
6635
+ allowed.add(p.primaryRoleId);
6636
+ }
6637
+ for (const roleId of p.secondaryRoleIds ?? []) {
6638
+ allowed.add(roleId);
6639
+ }
6640
+ return allowed.size > 0 ? allowed : null;
6641
+ }
6642
+ /**
6643
+ * The set of role ids the given entity's direct children are restricted to,
6644
+ * or null for full auto-discovery. Non-null only when the entity is a root of
6645
+ * the active `Type` perspective (whitelist-at-root-only).
6646
+ */
6647
+ rootWhitelistFor(rtId) {
6648
+ return this.perspectiveRootRtIds.has(rtId)
6649
+ ? this.activePerspectiveWhitelist()
6650
+ : null;
6651
+ }
6652
+ /**
6653
+ * Resolves the active perspective from the configured list, falling back to a
6654
+ * configured/built-in spatial one when the selected key is unknown.
6655
+ */
6656
+ async resolveActivePerspective() {
6657
+ const perspectives = await this.getPerspectives();
6658
+ return (perspectives.find((p) => p.key === this.activePerspectiveKey) ??
6659
+ perspectives.find((p) => p.key === SPATIAL_PERSPECTIVE_KEY) ??
6660
+ BUILT_IN_SPATIAL_PERSPECTIVE);
6661
+ }
6002
6662
  isCkModelsRoot(item) {
6003
6663
  return !!item && 'isCkModelsRoot' in item;
6004
6664
  }
@@ -6008,20 +6668,16 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6008
6668
  isCkType(item) {
6009
6669
  return !!item && 'ckTypeId' in item && !('rtId' in item) && !('id' in item);
6010
6670
  }
6011
- // Define metadata for different entity types and their hierarchical relationships
6012
- static levelMetaData = [
6013
- new LevelMetaData('Basic/Tree', [new RtAssociationMetaData('System/ParentChild', 'Basic/TreeNode')], [
6014
- new RtAssociationMetaData('System/ParentChild', 'Basic/TreeNode'),
6015
- new RtAssociationMetaData('Basic/RelatedClassification', 'Basic/Asset'),
6016
- ]),
6017
- new LevelMetaData('*', [
6018
- new RtAssociationMetaData('System/ParentChild', 'Basic/TreeNode'),
6019
- new RtAssociationMetaData('Basic/RelatedClassification', 'Basic/Asset'),
6020
- ], [
6021
- new RtAssociationMetaData('System/ParentChild', 'Basic/TreeNode'),
6022
- new RtAssociationMetaData('Basic/RelatedClassification', 'Basic/Asset'),
6023
- ]),
6024
- ];
6671
+ isAssociationGroup(item) {
6672
+ return !!item && 'isAssociationGroup' in item;
6673
+ }
6674
+ /**
6675
+ * Cache of inbound association roles per CK type id. Discovered lazily from
6676
+ * the CK schema and reused across tree expansions (and to decide whether a
6677
+ * child entity node is expandable). Lives on the instance because the tree is
6678
+ * created fresh per browser/picker mount.
6679
+ */
6680
+ inboundRolesCache = new Map();
6025
6681
  // Define visual metadata for different entity types
6026
6682
  static ckTypeMetaData = [
6027
6683
  new CkTypeMetaData('Basic/Tree', 'Tree', 'Tree Structure', folderMoreIcon),
@@ -6043,92 +6699,300 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6043
6699
  if (this.isCkType(item.item)) {
6044
6700
  return [];
6045
6701
  }
6702
+ // Handle association group node - lazily load its target entities
6703
+ if (this.isAssociationGroup(item.item)) {
6704
+ const group = item.item;
6705
+ const targets = await this.fetchAssociationTargets(group.parentRtId, group.parentCkTypeId, group.roleId, group.targetCkTypeId, group.direction);
6706
+ return this.buildEntityTreeItems(targets);
6707
+ }
6046
6708
  // Handle regular runtime entity
6047
6709
  const rtEntity = item.item;
6048
6710
  if (!rtEntity.rtId || !rtEntity.ckTypeId) {
6049
6711
  return [];
6050
6712
  }
6051
- // Find metadata for the current item type
6052
- let metaData = RuntimeBrowserDataSource.levelMetaData.find((x) => x.ckTypeId === rtEntity.ckTypeId);
6053
- if (!metaData) {
6054
- metaData = RuntimeBrowserDataSource.levelMetaData.find((x) => x.ckTypeId === '*');
6713
+ // Discover the inbound association roles from the entity's ACTUAL inbound
6714
+ // edges (associations.definitions), not the CK type schema. This mirrors the
6715
+ // entity detail "Associations" tab and also surfaces roles that exist as
6716
+ // runtime edges but are no longer declared on the type in the installed CK
6717
+ // model (orphan roles after model evolution). Each group carries its concrete
6718
+ // origin CK type (the ckId used to load its targets) and an exact count.
6719
+ const discoveredGroups = await this.discoverEntityInboundRoleGroups(rtEntity.rtId, rtEntity.ckTypeId);
6720
+ // AB#4263: when this entity is a root of the active `Type` perspective,
6721
+ // restrict its direct children to the perspective's primary + secondary
6722
+ // roles (whitelist-at-root-only). Deeper nodes are not in
6723
+ // perspectiveRootRtIds, so they keep full auto-discovery.
6724
+ const whitelist = this.rootWhitelistFor(rtEntity.rtId);
6725
+ const primaryRoleId = whitelist ? this.activePerspective.primaryRoleId : undefined;
6726
+ const roleGroups = whitelist
6727
+ ? discoveredGroups.filter((g) => whitelist.has(g.roleId))
6728
+ : discoveredGroups;
6729
+ // Apply the optional per-tenant TreeNavigationConfiguration overrides on top
6730
+ // of auto-discovery: hide (visible), relabel (displayName), reorder
6731
+ // (sortIndex), flatten vs group (grouped), icon. Defaults reproduce Phase 1:
6732
+ // System/ParentChild flat, every other role grouped. In a perspective root
6733
+ // the perspective's primary role is flattened at the top instead.
6734
+ const annotated = await Promise.all(roleGroups.map(async (group) => {
6735
+ const override = await this.treeNavConfig.resolve(rtEntity.ckTypeId, group.roleId);
6736
+ const defaultGrouped = primaryRoleId
6737
+ ? group.roleId !== primaryRoleId
6738
+ : group.roleId !== PARENT_CHILD_ROLE_ID;
6739
+ return {
6740
+ group,
6741
+ visible: override?.visible !== false,
6742
+ grouped: override?.grouped ?? defaultGrouped,
6743
+ displayName: override?.displayName,
6744
+ sortIndex: override?.sortIndex,
6745
+ icon: override?.icon,
6746
+ };
6747
+ }));
6748
+ const visibleRoles = annotated.filter((a) => a.visible);
6749
+ const result = [];
6750
+ // 1. Flattened roles (default: System/ParentChild) — their targets appear
6751
+ // directly under the node to preserve the familiar hierarchy navigation.
6752
+ const flattenedTargetLists = await Promise.all(visibleRoles
6753
+ .filter((a) => !a.grouped)
6754
+ .map((a) => this.fetchAssociationTargets(rtEntity.rtId, rtEntity.ckTypeId, a.group.roleId, a.group.targetCkTypeId, GraphDirectionDto.InboundDto)));
6755
+ result.push(...(await this.buildEntityTreeItems(flattenedTargetLists.flat())));
6756
+ // 2. Grouped roles become expandable group nodes (counts already known from
6757
+ // the discovered edges). Ordered by configured sortIndex.
6758
+ const groupNodes = visibleRoles
6759
+ .filter((a) => a.grouped && a.group.count > 0)
6760
+ .sort((x, y) => this.compareGroupOrder(x, y));
6761
+ for (const a of groupNodes) {
6762
+ const group = a.group;
6763
+ const groupNode = {
6764
+ isAssociationGroup: true,
6765
+ parentRtId: rtEntity.rtId,
6766
+ parentCkTypeId: rtEntity.ckTypeId,
6767
+ roleId: group.roleId,
6768
+ targetCkTypeId: group.targetCkTypeId,
6769
+ direction: GraphDirectionDto.InboundDto,
6770
+ };
6771
+ const label = `${a.displayName ?? group.navigationPropertyName} (${group.count})`;
6772
+ result.push(new TreeItemDataTyped(this.buildGroupNodeId(rtEntity.rtId, rtEntity.ckTypeId, group), label, `${group.roleId} → ${group.targetCkTypeId}`, groupNode, this.resolveGroupIcon(a.icon), true));
6055
6773
  }
6056
- if (!metaData) {
6774
+ return result;
6775
+ }
6776
+ /**
6777
+ * Discovers the navigable inbound role groups of an entity from its ACTUAL
6778
+ * inbound edges (associations.definitions), grouped by (roleId, origin CK
6779
+ * type) with exact counts. Labels are enriched from the CK type schema
6780
+ * (friendly inbound navigation property name) when the role is declared on the
6781
+ * type, and otherwise derived from the role id (so orphan roles still get a
6782
+ * readable label).
6783
+ */
6784
+ async discoverEntityInboundRoleGroups(rtId, ckTypeId) {
6785
+ let definitions = [];
6786
+ try {
6787
+ const response = await firstValueFrom(this.getRuntimeEntityAssociationsByIdDtoGQL.fetch({
6788
+ variables: {
6789
+ rtId,
6790
+ ckTypeId,
6791
+ direction: GraphDirectionDto.InboundDto,
6792
+ first: 2000,
6793
+ },
6794
+ fetchPolicy: 'network-only',
6795
+ }));
6796
+ definitions =
6797
+ response.data?.runtime?.runtimeEntities?.items?.[0]?.associations
6798
+ ?.definitions?.items ?? [];
6799
+ }
6800
+ catch (error) {
6801
+ console.error('Error discovering inbound association edges', { ckTypeId, rtId }, error);
6057
6802
  return [];
6058
6803
  }
6059
- const mergedResultsMap = {};
6060
- // Fetch children based on direct and indirect associations
6061
- for (const directRole of metaData.directRoles) {
6062
- const rtEntitiesOrigins = new Array();
6063
- for (const indirectRole of metaData.indirectRoles) {
6064
- try {
6065
- const result = await firstValueFrom(this.getTreeNodesDtoGQL
6066
- .fetch({
6067
- variables: {
6068
- rtId: rtEntity.rtId,
6069
- ckTypeId: rtEntity.ckTypeId,
6070
- directRoleId: directRole.roleId,
6071
- directTargetCkTypeId: directRole.ckTypeId,
6072
- indirectRoleId: indirectRole.roleId,
6073
- indirectTargetCkTypeId: indirectRole.ckTypeId,
6074
- },
6075
- })
6076
- .pipe(map((r) => r.data?.runtime?.runtimeEntities?.items?.at(0))));
6077
- if (result) {
6078
- rtEntitiesOrigins.push(result);
6079
- }
6080
- }
6081
- catch (error) {
6082
- console.error('Error fetching tree nodes:', error);
6083
- }
6804
+ // Count edges per role and remember a representative concrete origin type
6805
+ // (used as a fallback ckId for orphan roles not declared on the type).
6806
+ const edgeCount = new Map();
6807
+ const edgeOrigin = new Map();
6808
+ for (const def of definitions) {
6809
+ const roleId = String(def?.ckAssociationRoleId ?? '');
6810
+ const origin = String(def?.originCkTypeId ?? '');
6811
+ if (!roleId || !origin || NON_NAVIGABLE_TARGET_CK_TYPES.has(origin)) {
6812
+ continue;
6813
+ }
6814
+ edgeCount.set(roleId, (edgeCount.get(roleId) ?? 0) + 1);
6815
+ if (!edgeOrigin.has(roleId)) {
6816
+ edgeOrigin.set(roleId, origin);
6817
+ }
6818
+ }
6819
+ // Declared roles from the CK type schema give the friendly inbound name and,
6820
+ // crucially, the origin BASE type — using it as the ckId aggregates all
6821
+ // concrete subtypes into a single group (e.g. one "Sensoren (5)" instead of
6822
+ // one group per concrete sensor type).
6823
+ const schemaRoles = await this.getInboundRoles(ckTypeId);
6824
+ const schemaByRole = new Map();
6825
+ for (const r of schemaRoles) {
6826
+ if (!schemaByRole.has(r.roleId)) {
6827
+ schemaByRole.set(r.roleId, {
6828
+ origin: r.targetCkTypeId,
6829
+ nav: r.navigationPropertyName,
6830
+ });
6084
6831
  }
6085
- // Process children and build tree items
6086
- for (const root of rtEntitiesOrigins) {
6087
- if (!root?.associations?.targets?.items) {
6832
+ }
6833
+ // One group per role actually present on the entity. Declared roles use the
6834
+ // schema base type as ckId; orphan roles (edge exists but role not declared
6835
+ // on the type in the installed model, e.g. EnergyIQ/StoreyElements) fall back
6836
+ // to the concrete edge origin type and a derived label.
6837
+ const groups = [];
6838
+ for (const [roleId, count] of edgeCount) {
6839
+ const schema = schemaByRole.get(roleId);
6840
+ groups.push({
6841
+ roleId,
6842
+ targetCkTypeId: schema?.origin ?? edgeOrigin.get(roleId) ?? '',
6843
+ navigationPropertyName: schema?.nav ?? this.deriveRoleLabel(roleId),
6844
+ count,
6845
+ });
6846
+ }
6847
+ return groups;
6848
+ }
6849
+ /** Readable fallback label for a role id without a schema navigation name. */
6850
+ deriveRoleLabel(roleId) {
6851
+ const slash = roleId.lastIndexOf('/');
6852
+ return slash >= 0 ? roleId.slice(slash + 1) : roleId;
6853
+ }
6854
+ /**
6855
+ * Orders group nodes by configured sortIndex (ascending, unconfigured last),
6856
+ * then by navigation property name for a stable, readable order.
6857
+ */
6858
+ compareGroupOrder(x, y) {
6859
+ const xi = x.sortIndex ?? Number.MAX_SAFE_INTEGER;
6860
+ const yi = y.sortIndex ?? Number.MAX_SAFE_INTEGER;
6861
+ if (xi !== yi) {
6862
+ return xi - yi;
6863
+ }
6864
+ return x.group.navigationPropertyName.localeCompare(y.group.navigationPropertyName);
6865
+ }
6866
+ /** Resolves a configured group icon name to an SVG icon (folder by default). */
6867
+ resolveGroupIcon(name) {
6868
+ switch (name) {
6869
+ case 'file':
6870
+ return fileIcon;
6871
+ case 'gear':
6872
+ return gearIcon;
6873
+ case 'database':
6874
+ return storage;
6875
+ case 'code':
6876
+ return code;
6877
+ case 'folder':
6878
+ default:
6879
+ return folderMoreIcon;
6880
+ }
6881
+ }
6882
+ /**
6883
+ * Returns the inbound association roles of a CK type, discovered from the CK
6884
+ * schema and cached per type id for the lifetime of this data source.
6885
+ */
6886
+ getInboundRoles(ckTypeId) {
6887
+ const cached = this.inboundRolesCache.get(ckTypeId);
6888
+ if (cached) {
6889
+ return cached;
6890
+ }
6891
+ const promise = firstValueFrom(this.getCkTypeAssociationRolesDtoGQL.fetch({ variables: { ckTypeId } }))
6892
+ .then((response) => {
6893
+ const all = response.data?.constructionKit?.types?.items?.[0]?.associations?.in
6894
+ ?.all ?? [];
6895
+ const roles = [];
6896
+ for (const role of all) {
6897
+ if (!role) {
6088
6898
  continue;
6089
6899
  }
6090
- for (const child of root.associations.targets.items) {
6091
- if (!child?.rtId || !child?.ckTypeId) {
6092
- continue;
6093
- }
6094
- const rtId = child?.rtId;
6095
- if (!mergedResultsMap[rtId]) {
6096
- // Find or create metadata for this child type
6097
- let childMetaData = RuntimeBrowserDataSource.ckTypeMetaData.find((x) => x.ckTypeId === child?.ckTypeId);
6098
- if (!childMetaData) {
6099
- childMetaData = new CkTypeMetaData(child?.ckTypeId || 'Unknown', child?.ckTypeId || 'Unknown', child?.ckTypeId || 'Unknown', code);
6100
- }
6101
- // Extract display name and description from attributes
6102
- const nameValue = child.attributes?.items?.find((x) => x?.attributeName === 'name')?.value;
6103
- const displayNameValue = child.attributes?.items?.find((x) => x?.attributeName === 'displayName')?.value;
6104
- // Debug logging
6105
- if (typeof nameValue === 'object') {
6106
- console.warn('Child name value is an object:', nameValue);
6107
- }
6108
- const text = (typeof nameValue === 'string'
6109
- ? nameValue
6110
- : typeof nameValue === 'object' && nameValue !== null
6111
- ? JSON.stringify(nameValue)
6112
- : null) ||
6113
- (typeof displayNameValue === 'string'
6114
- ? displayNameValue
6115
- : null) ||
6116
- child.ckTypeId ||
6117
- 'Unknown';
6118
- const descValue = child.attributes?.items?.find((x) => x?.attributeName === 'description')?.value;
6119
- const tooltip = (typeof descValue === 'string' ? descValue : null) ||
6120
- `${child.ckTypeId} - ${child.rtId}`;
6121
- mergedResultsMap[rtId] = new TreeItemDataTyped(rtId, text, tooltip, child, childMetaData.svgIcon, (child?.associations?.targets?.totalCount ?? 0) > 0);
6122
- }
6123
- else {
6124
- // Update expandable status if object already exists
6125
- mergedResultsMap[rtId].expandable ||=
6126
- (child?.associations?.targets?.totalCount ?? 0) > 0;
6127
- }
6900
+ // Use the RUNTIME ids from the backend, never the versioned fullName.
6901
+ // For an INBOUND role this entity is the association's TARGET side, so the
6902
+ // related entities to navigate to are on the ORIGIN side: query
6903
+ // targets(direction: INBOUND, ckId: rtOriginCkTypeId). rtTargetCkTypeId
6904
+ // here is this entity's own (target) type and would return nothing.
6905
+ // The runtime form must come from the backend (rtOriginCkTypeId) — it
6906
+ // follows a resolution logic that simply stripping the version does not.
6907
+ const targetCkTypeId = String(role.rtOriginCkTypeId ?? '');
6908
+ if (!targetCkTypeId || NON_NAVIGABLE_TARGET_CK_TYPES.has(targetCkTypeId)) {
6909
+ continue;
6128
6910
  }
6911
+ roles.push({
6912
+ roleId: String(role.rtRoleId ?? ''),
6913
+ navigationPropertyName: role.navigationPropertyName,
6914
+ targetCkTypeId,
6915
+ multiplicity: role.multiplicity,
6916
+ });
6129
6917
  }
6918
+ return roles;
6919
+ })
6920
+ .catch((error) => {
6921
+ console.error('Error fetching association roles for type', ckTypeId, error);
6922
+ // Drop the failed promise from the cache so a later expand can retry.
6923
+ this.inboundRolesCache.delete(ckTypeId);
6924
+ return [];
6925
+ });
6926
+ this.inboundRolesCache.set(ckTypeId, promise);
6927
+ return promise;
6928
+ }
6929
+ /** Loads the target entities reachable from an entity through one role. */
6930
+ async fetchAssociationTargets(rtId, ckTypeId, roleId, targetCkTypeId, direction) {
6931
+ try {
6932
+ const response = await firstValueFrom(this.getTreeAssociationTargetsDtoGQL.fetch({
6933
+ variables: { rtId, ckTypeId, roleId, targetCkTypeId, direction },
6934
+ }));
6935
+ const items = response.data?.runtime?.runtimeEntities?.items?.[0]?.associations
6936
+ ?.targets?.items ?? [];
6937
+ return items.filter((i) => !!i);
6938
+ }
6939
+ catch (error) {
6940
+ console.error('Error fetching association targets', { ckTypeId, roleId, targetCkTypeId }, error);
6941
+ return [];
6942
+ }
6943
+ }
6944
+ /**
6945
+ * Builds tree items for runtime entities, de-duplicating by rtId and marking
6946
+ * an entity expandable when its CK type defines at least one inbound
6947
+ * association role.
6948
+ */
6949
+ async buildEntityTreeItems(entities) {
6950
+ const unique = new Map();
6951
+ for (const entity of entities) {
6952
+ if (entity?.rtId && entity?.ckTypeId && !unique.has(entity.rtId)) {
6953
+ unique.set(entity.rtId, entity);
6954
+ }
6955
+ }
6956
+ // Warm the role cache for every distinct child type in parallel so the
6957
+ // expandable flag can be resolved without a per-entity round trip.
6958
+ const distinctTypes = [
6959
+ ...new Set([...unique.values()].map((e) => e.ckTypeId)),
6960
+ ];
6961
+ await Promise.all(distinctTypes.map((t) => this.getInboundRoles(t)));
6962
+ const result = [];
6963
+ for (const entity of unique.values()) {
6964
+ const roles = await this.getInboundRoles(entity.ckTypeId);
6965
+ result.push(new TreeItemDataTyped(entity.rtId, this.extractDisplayName(entity), this.extractTooltip(entity), entity, this.resolveIcon(entity.ckTypeId), roles.length > 0));
6130
6966
  }
6131
- return Object.values(mergedResultsMap);
6967
+ return result;
6968
+ }
6969
+ /** Stable tree-node id for an association group node. */
6970
+ buildGroupNodeId(parentRtId, parentCkTypeId, role) {
6971
+ return `assoc:${parentCkTypeId}@${parentRtId}:${role.roleId}:${role.targetCkTypeId}`;
6972
+ }
6973
+ /** Resolves the display label of an entity from its name/displayName attributes. */
6974
+ extractDisplayName(entity) {
6975
+ const nameValue = entity.attributes?.items?.find((x) => x?.attributeName === 'name')?.value;
6976
+ const displayNameValue = entity.attributes?.items?.find((x) => x?.attributeName === 'displayName')?.value;
6977
+ return ((typeof nameValue === 'string'
6978
+ ? nameValue
6979
+ : typeof nameValue === 'object' && nameValue !== null
6980
+ ? JSON.stringify(nameValue)
6981
+ : null) ||
6982
+ (typeof displayNameValue === 'string' ? displayNameValue : null) ||
6983
+ entity.rtWellKnownName ||
6984
+ entity.ckTypeId ||
6985
+ 'Unknown');
6986
+ }
6987
+ /** Resolves the tooltip of an entity from its description attribute. */
6988
+ extractTooltip(entity) {
6989
+ const descValue = entity.attributes?.items?.find((x) => x?.attributeName === 'description')?.value;
6990
+ return ((typeof descValue === 'string' ? descValue : null) ||
6991
+ `${entity.ckTypeId} - ${entity.rtId}`);
6992
+ }
6993
+ /** Resolves the icon for a CK type, falling back to a generic entity icon. */
6994
+ resolveIcon(ckTypeId) {
6995
+ return (RuntimeBrowserDataSource.ckTypeMetaData.find((x) => x.ckTypeId === ckTypeId)?.svgIcon ?? code);
6132
6996
  }
6133
6997
  async fetchRootNodes() {
6134
6998
  try {
@@ -6136,6 +7000,17 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6136
7000
  // Add CK Models root node
6137
7001
  const ckModelsRoot = { isCkModelsRoot: true };
6138
7002
  result.push(new TreeItemDataTyped('ck-models-root', 'CK Models', 'Construction Kit Models', ckModelsRoot, folderOpenIcon, true));
7003
+ // AB#4263: resolve the active perspective and reset the root-id set that
7004
+ // scopes the whitelist to the direct children of these roots.
7005
+ this.perspectiveRootRtIds.clear();
7006
+ this.activePerspective = await this.resolveActivePerspective();
7007
+ // `Type` perspective: roots are all instances of the configured CK type.
7008
+ if (this.activePerspective.rootMode === 'Type' &&
7009
+ this.activePerspective.rootCkTypeId) {
7010
+ result.push(...(await this.fetchTypePerspectiveRoots(this.activePerspective.rootCkTypeId)));
7011
+ return result;
7012
+ }
7013
+ // Spatial perspective (default): all Basic/Tree entities, exactly as before.
6139
7014
  // Check if Basic construction kit is available before trying to fetch Tree entities
6140
7015
  const isBasicCkAvailable = await this.checkBasicConstructionKitAvailable();
6141
7016
  if (isBasicCkAvailable) {
@@ -6182,6 +7057,39 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6182
7057
  return [];
6183
7058
  }
6184
7059
  }
7060
+ /**
7061
+ * Loads all runtime instances of a CK type as the roots of a `Type`
7062
+ * perspective (AB#4263), registering their rtIds so the whitelist applies to
7063
+ * their direct children only. Expandability is schema-based (does the type
7064
+ * declare an inbound role, restricted to the whitelist when set).
7065
+ */
7066
+ async fetchTypePerspectiveRoots(ckTypeId) {
7067
+ try {
7068
+ const response = await firstValueFrom(this.apollo.query({
7069
+ query: GET_ROOT_ENTITIES_BY_CK_TYPE,
7070
+ variables: { ckTypeId, first: 2000 },
7071
+ fetchPolicy: 'network-only',
7072
+ }));
7073
+ const items = (response.data?.runtime?.runtimeEntities?.items ?? []).filter((i) => !!i && !!i.rtId && !!i.ckTypeId);
7074
+ // Expandable when the root type declares at least one inbound role that
7075
+ // survives the whitelist (avoids always-empty expand arrows).
7076
+ const whitelist = this.activePerspectiveWhitelist();
7077
+ const roles = items.length > 0 ? await this.getInboundRoles(items[0].ckTypeId) : [];
7078
+ const expandable = whitelist
7079
+ ? roles.some((r) => whitelist.has(r.roleId))
7080
+ : roles.length > 0;
7081
+ const result = [];
7082
+ for (const item of items) {
7083
+ this.perspectiveRootRtIds.add(item.rtId);
7084
+ result.push(new TreeItemDataTyped(`${item.ckTypeId}@${item.rtId}`, this.extractDisplayName(item), this.extractTooltip(item), item, this.resolveIcon(item.ckTypeId), expandable));
7085
+ }
7086
+ return result;
7087
+ }
7088
+ catch (error) {
7089
+ console.error('Error fetching type-perspective roots for', ckTypeId, error);
7090
+ return [];
7091
+ }
7092
+ }
6185
7093
  /**
6186
7094
  * Gets ParentChild association of given Runtime Entity.
6187
7095
  *
@@ -6475,11 +7383,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
6475
7383
  }]
6476
7384
  }] });
6477
7385
 
7386
+ /**
7387
+ * Small toolbar control that lets the user switch the active tree perspective
7388
+ * (AB#4263). Theme-neutral (Kendo CSS variables only). Renders nothing when
7389
+ * there is a single perspective, so a zero-config tenant sees no switcher.
7390
+ */
7391
+ class PerspectiveSwitcherComponent {
7392
+ /** All selectable perspectives (built-in spatial first). */
7393
+ perspectives = input([], /* @ts-ignore */
7394
+ ...(ngDevMode ? [{ debugName: "perspectives" }] : /* istanbul ignore next */ []));
7395
+ /** Currently active perspective key. */
7396
+ activeKey = input('Spatial', /* @ts-ignore */
7397
+ ...(ngDevMode ? [{ debugName: "activeKey" }] : /* istanbul ignore next */ []));
7398
+ /** Label shown before the dropdown. */
7399
+ label = input('Perspective', /* @ts-ignore */
7400
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
7401
+ /** Emits the selected perspective key. */
7402
+ perspectiveChange = output();
7403
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PerspectiveSwitcherComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7404
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: PerspectiveSwitcherComponent, isStandalone: true, selector: "mm-perspective-switcher", inputs: { perspectives: { classPropertyName: "perspectives", publicName: "perspectives", isSignal: true, isRequired: false, transformFunction: null }, activeKey: { classPropertyName: "activeKey", publicName: "activeKey", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { perspectiveChange: "perspectiveChange" }, ngImport: i0, template: `
7405
+ @if (perspectives().length > 1) {
7406
+ <div class="perspective-switcher">
7407
+ <span class="switcher-label">{{ label() }}</span>
7408
+ <kendo-dropdownlist
7409
+ [data]="perspectives()"
7410
+ [textField]="'displayName'"
7411
+ [valueField]="'key'"
7412
+ [valuePrimitive]="true"
7413
+ [value]="activeKey()"
7414
+ (valueChange)="perspectiveChange.emit($event)"
7415
+ ></kendo-dropdownlist>
7416
+ </div>
7417
+ }
7418
+ `, isInline: true, styles: [".perspective-switcher{display:flex;align-items:center;gap:8px;padding:4px 8px}.switcher-label{font-size:.85rem;font-weight:600;color:var(--kendo-color-subtle, #6c757d);white-space:nowrap}\n"], dependencies: [{ kind: "ngmodule", type: DropDownListModule }, { kind: "component", type: i3$1.DropDownListComponent, selector: "kendo-dropdownlist", inputs: ["customIconClass", "showStickyHeader", "icon", "svgIcon", "loading", "data", "value", "textField", "valueField", "adaptiveMode", "adaptiveTitle", "adaptiveSubtitle", "popupSettings", "listHeight", "defaultItem", "disabled", "itemDisabled", "readonly", "filterable", "virtual", "ignoreCase", "delay", "valuePrimitive", "tabindex", "tabIndex", "size", "rounded", "fillMode", "leftRightArrowsNavigation", "id"], outputs: ["valueChange", "filterChange", "selectionChange", "open", "opened", "close", "closed", "focus", "blur"], exportAs: ["kendoDropDownList"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
7419
+ }
7420
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: PerspectiveSwitcherComponent, decorators: [{
7421
+ type: Component,
7422
+ args: [{ selector: 'mm-perspective-switcher', standalone: true, imports: [DropDownListModule], template: `
7423
+ @if (perspectives().length > 1) {
7424
+ <div class="perspective-switcher">
7425
+ <span class="switcher-label">{{ label() }}</span>
7426
+ <kendo-dropdownlist
7427
+ [data]="perspectives()"
7428
+ [textField]="'displayName'"
7429
+ [valueField]="'key'"
7430
+ [valuePrimitive]="true"
7431
+ [value]="activeKey()"
7432
+ (valueChange)="perspectiveChange.emit($event)"
7433
+ ></kendo-dropdownlist>
7434
+ </div>
7435
+ }
7436
+ `, changeDetection: ChangeDetectionStrategy.OnPush, styles: [".perspective-switcher{display:flex;align-items:center;gap:8px;padding:4px 8px}.switcher-label{font-size:.85rem;font-weight:600;color:var(--kendo-color-subtle, #6c757d);white-space:nowrap}\n"] }]
7437
+ }], propDecorators: { perspectives: [{ type: i0.Input, args: [{ isSignal: true, alias: "perspectives", required: false }] }], activeKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeKey", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], perspectiveChange: [{ type: i0.Output, args: ["perspectiveChange"] }] } });
7438
+
6478
7439
  class EntitySelectorDialogComponent {
6479
7440
  windowRef = inject(WindowRef);
6480
7441
  treeDataSource = inject(RuntimeBrowserDataSource);
7442
+ tree;
7443
+ /** Selectable tree perspectives (AB#4263); switcher hides itself when <= 1. */
7444
+ perspectives = signal([], /* @ts-ignore */
7445
+ ...(ngDevMode ? [{ debugName: "perspectives" }] : /* istanbul ignore next */ []));
7446
+ activePerspectiveKey = signal(this.treeDataSource.getActivePerspectiveKey(), /* @ts-ignore */
7447
+ ...(ngDevMode ? [{ debugName: "activePerspectiveKey" }] : /* istanbul ignore next */ []));
6481
7448
  data = {};
6482
7449
  selectedEntity = null;
7450
+ async ngOnInit() {
7451
+ try {
7452
+ this.perspectives.set(await this.treeDataSource.getPerspectives());
7453
+ this.activePerspectiveKey.set(this.treeDataSource.getActivePerspectiveKey());
7454
+ }
7455
+ catch (error) {
7456
+ console.error('Error loading perspectives', error);
7457
+ }
7458
+ }
7459
+ /** Switches the active perspective and reloads the picker tree. */
7460
+ async onPerspectiveChange(key) {
7461
+ this.treeDataSource.setActivePerspective(key);
7462
+ this.activePerspectiveKey.set(key);
7463
+ this.selectedEntity = null;
7464
+ await this.tree?.refreshTree();
7465
+ }
6483
7466
  onNodeSelected(node) {
6484
7467
  const item = node.item;
6485
7468
  if (item && typeof item === 'object' && 'rtId' in item && 'ckTypeId' in item) {
@@ -6509,11 +7492,17 @@ class EntitySelectorDialogComponent {
6509
7492
  this.windowRef.close();
6510
7493
  }
6511
7494
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntitySelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
6512
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntitySelectorDialogComponent, isStandalone: true, selector: "mm-entity-selector-dialog", providers: [RuntimeBrowserDataSource], ngImport: i0, template: `
7495
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: EntitySelectorDialogComponent, isStandalone: true, selector: "mm-entity-selector-dialog", providers: [RuntimeBrowserDataSource], viewQueries: [{ propertyName: "tree", first: true, predicate: ["tree"], descendants: true }], ngImport: i0, template: `
6513
7496
  <div class="entity-selector">
6514
7497
  <div class="entity-selector-body">
7498
+ <mm-perspective-switcher
7499
+ [perspectives]="perspectives()"
7500
+ [activeKey]="activePerspectiveKey()"
7501
+ (perspectiveChange)="onPerspectiveChange($event)"
7502
+ ></mm-perspective-switcher>
6515
7503
  <div class="tree-section">
6516
7504
  <mm-tree-view
7505
+ #tree
6517
7506
  [dataSource]="treeDataSource"
6518
7507
  (nodeSelected)="onNodeSelected($event)"
6519
7508
  ></mm-tree-view>
@@ -6548,7 +7537,7 @@ class EntitySelectorDialogComponent {
6548
7537
  </button>
6549
7538
  </div>
6550
7539
  </div>
6551
- `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}.entity-selector{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;box-sizing:border-box}.entity-selector-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}.tree-section{flex:1;min-height:0;overflow:auto;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #ffffff)}.selection-preview{padding:10px 12px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface-alt, #f8f9fa);display:flex;flex-direction:column;gap:4px;flex-shrink:0}.preview-row{display:flex;gap:8px}.preview-row .preview-label{font-weight:600;min-width:45px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem}.preview-row .preview-value{font-size:.85rem}.preview-row .monospace{font-family:monospace}.selection-hint{text-align:center;padding:8px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem;flex-shrink:0}.dialog-actions{flex:0 0 auto;display:flex;gap:8px;justify-content:flex-end;padding:10px 14px;border-top:1px solid var(--kendo-color-border, #dee2e6);background:var(--kendo-color-surface, transparent)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: TreeComponent, selector: "mm-tree-view", inputs: ["dataSource"], outputs: ["nodeSelected", "nodeClick", "nodeDoubleClick", "nodeDrop", "expand", "collapse"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
7540
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}.entity-selector{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;box-sizing:border-box}.entity-selector-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}.tree-section{flex:1;min-height:0;overflow:auto;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #ffffff)}.selection-preview{padding:10px 12px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface-alt, #f8f9fa);display:flex;flex-direction:column;gap:4px;flex-shrink:0}.preview-row{display:flex;gap:8px}.preview-row .preview-label{font-weight:600;min-width:45px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem}.preview-row .preview-value{font-size:.85rem}.preview-row .monospace{font-family:monospace}.selection-hint{text-align:center;padding:8px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem;flex-shrink:0}.dialog-actions{flex:0 0 auto;display:flex;gap:8px;justify-content:flex-end;padding:10px 14px;border-top:1px solid var(--kendo-color-border, #dee2e6);background:var(--kendo-color-surface, transparent)}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: ButtonModule }, { kind: "component", type: i1$1.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconPosition", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: TreeComponent, selector: "mm-tree-view", inputs: ["dataSource"], outputs: ["nodeSelected", "nodeClick", "nodeDoubleClick", "nodeDrop", "expand", "collapse"] }, { kind: "component", type: PerspectiveSwitcherComponent, selector: "mm-perspective-switcher", inputs: ["perspectives", "activeKey", "label"], outputs: ["perspectiveChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
6552
7541
  }
6553
7542
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntitySelectorDialogComponent, decorators: [{
6554
7543
  type: Component,
@@ -6556,11 +7545,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
6556
7545
  CommonModule,
6557
7546
  ButtonModule,
6558
7547
  TreeComponent,
7548
+ PerspectiveSwitcherComponent,
6559
7549
  ], providers: [RuntimeBrowserDataSource], template: `
6560
7550
  <div class="entity-selector">
6561
7551
  <div class="entity-selector-body">
7552
+ <mm-perspective-switcher
7553
+ [perspectives]="perspectives()"
7554
+ [activeKey]="activePerspectiveKey()"
7555
+ (perspectiveChange)="onPerspectiveChange($event)"
7556
+ ></mm-perspective-switcher>
6562
7557
  <div class="tree-section">
6563
7558
  <mm-tree-view
7559
+ #tree
6564
7560
  [dataSource]="treeDataSource"
6565
7561
  (nodeSelected)="onNodeSelected($event)"
6566
7562
  ></mm-tree-view>
@@ -6596,7 +7592,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
6596
7592
  </div>
6597
7593
  </div>
6598
7594
  `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:flex;flex-direction:column;height:100%;min-height:0}.entity-selector{display:flex;flex-direction:column;flex:1 1 auto;min-height:0;box-sizing:border-box}.entity-selector-body{flex:1 1 auto;min-height:0;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:12px}.tree-section{flex:1;min-height:0;overflow:auto;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface, #ffffff)}.selection-preview{padding:10px 12px;border:1px solid var(--kendo-color-border, #dee2e6);border-radius:4px;background:var(--kendo-color-surface-alt, #f8f9fa);display:flex;flex-direction:column;gap:4px;flex-shrink:0}.preview-row{display:flex;gap:8px}.preview-row .preview-label{font-weight:600;min-width:45px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem}.preview-row .preview-value{font-size:.85rem}.preview-row .monospace{font-family:monospace}.selection-hint{text-align:center;padding:8px;color:var(--kendo-color-subtle, #6c757d);font-size:.85rem;flex-shrink:0}.dialog-actions{flex:0 0 auto;display:flex;gap:8px;justify-content:flex-end;padding:10px 14px;border-top:1px solid var(--kendo-color-border, #dee2e6);background:var(--kendo-color-surface, transparent)}\n"] }]
6599
- }] });
7595
+ }], propDecorators: { tree: [{
7596
+ type: ViewChild,
7597
+ args: ['tree']
7598
+ }] } });
6600
7599
 
6601
7600
  const DIALOG_KEY$1 = 'entity-selector';
6602
7601
  const DEFAULT_SIZE$1 = { width: 600, height: 680 };
@@ -8256,6 +9255,7 @@ const DEFAULT_RUNTIME_BROWSER_MESSAGES = {
8256
9255
  mappingNotSet: '(not set)',
8257
9256
  mappingSelect: 'Select...',
8258
9257
  mappingNoneConfigured: 'No data point mappings configured yet.',
9258
+ perspective: 'Perspective',
8259
9259
  };
8260
9260
 
8261
9261
  class DataMappingListComponent {
@@ -9907,64 +10907,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
9907
10907
  type: Input
9908
10908
  }] } });
9909
10909
 
9910
- const GetCkTypeAssociationRolesDocumentDto = gql `
9911
- query getCkTypeAssociationRoles($ckTypeId: String!) {
9912
- constructionKit {
9913
- types(rtCkId: $ckTypeId, first: 1) {
9914
- items {
9915
- rtCkTypeId
9916
- associations {
9917
- in {
9918
- all {
9919
- roleId {
9920
- fullName
9921
- semanticVersionedFullName
9922
- }
9923
- rtRoleId
9924
- navigationPropertyName
9925
- multiplicity
9926
- targetCkTypeId {
9927
- fullName
9928
- }
9929
- rtTargetCkTypeId
9930
- }
9931
- }
9932
- out {
9933
- all {
9934
- roleId {
9935
- fullName
9936
- semanticVersionedFullName
9937
- }
9938
- rtRoleId
9939
- navigationPropertyName
9940
- multiplicity
9941
- targetCkTypeId {
9942
- fullName
9943
- }
9944
- rtTargetCkTypeId
9945
- }
9946
- }
9947
- }
9948
- }
9949
- }
9950
- }
9951
- }
9952
- `;
9953
- class GetCkTypeAssociationRolesDtoGQL extends i1$3.Query {
9954
- document = GetCkTypeAssociationRolesDocumentDto;
9955
- constructor(apollo) {
9956
- super(apollo);
9957
- }
9958
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, deps: [{ token: i1$3.Apollo }], target: i0.ɵɵFactoryTarget.Injectable });
9959
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, providedIn: 'root' });
9960
- }
9961
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: GetCkTypeAssociationRolesDtoGQL, decorators: [{
9962
- type: Injectable,
9963
- args: [{
9964
- providedIn: 'root'
9965
- }]
9966
- }], ctorParameters: () => [{ type: i1$3.Apollo }] });
9967
-
9968
10910
  /**
9969
10911
  * Validates whether a drag-and-drop move (reparenting) is allowed by the CK model.
9970
10912
  *
@@ -14251,6 +15193,26 @@ class RuntimeBrowserComponent {
14251
15193
  ...this.messages(),
14252
15194
  }), /* @ts-ignore */
14253
15195
  ...(ngDevMode ? [{ debugName: "resolvedMessages" }] : /* istanbul ignore next */ []));
15196
+ /** Selectable tree perspectives (AB#4263); switcher hides itself when <= 1. */
15197
+ perspectives = signal([], /* @ts-ignore */
15198
+ ...(ngDevMode ? [{ debugName: "perspectives" }] : /* istanbul ignore next */ []));
15199
+ activePerspectiveKey = signal(this.dataSource.getActivePerspectiveKey(), /* @ts-ignore */
15200
+ ...(ngDevMode ? [{ debugName: "activePerspectiveKey" }] : /* istanbul ignore next */ []));
15201
+ /** Switches the active perspective and reloads the tree from its new roots. */
15202
+ async onPerspectiveChange(key) {
15203
+ this.dataSource.setActivePerspective(key);
15204
+ this.activePerspectiveKey.set(key);
15205
+ await this.treeDetail?.refreshTree();
15206
+ }
15207
+ async loadPerspectives() {
15208
+ try {
15209
+ this.perspectives.set(await this.dataSource.getPerspectives());
15210
+ this.activePerspectiveKey.set(this.dataSource.getActivePerspectiveKey());
15211
+ }
15212
+ catch (error) {
15213
+ console.error('Error loading perspectives', error);
15214
+ }
15215
+ }
14254
15216
  treeDetail;
14255
15217
  detailsPanel;
14256
15218
  // Define toolbar actions
@@ -14330,6 +15292,8 @@ class RuntimeBrowserComponent {
14330
15292
  return !this.isLoading && this.isSelectedItemAnRtEntity && !this.isEditing;
14331
15293
  }
14332
15294
  ngAfterViewInit() {
15295
+ // Load the selectable perspectives for the switcher (AB#4263).
15296
+ void this.loadPerspectives();
14333
15297
  // Restore tree state after the tree has been initialized
14334
15298
  this.waitForTreeComponent().then((isReady) => {
14335
15299
  if (isReady) {
@@ -14835,6 +15799,12 @@ class RuntimeBrowserComponent {
14835
15799
  <!-- Main Content -->
14836
15800
  <div class="lcars-content-panel">
14837
15801
  <div class="panel-accent-top"></div>
15802
+ <mm-perspective-switcher
15803
+ [perspectives]="perspectives()"
15804
+ [activeKey]="activePerspectiveKey()"
15805
+ [label]="resolvedMessages().perspective ?? 'Perspective'"
15806
+ (perspectiveChange)="onPerspectiveChange($event)"
15807
+ ></mm-perspective-switcher>
14838
15808
  <mm-base-tree-detail
14839
15809
  #treeDetail
14840
15810
  [treeDataSource]="dataSource"
@@ -14870,11 +15840,15 @@ class RuntimeBrowserComponent {
14870
15840
  </div>
14871
15841
  </div>
14872
15842
  </div>
14873
- `, isInline: true, styles: [".runtime-browser-container{display:flex;flex-direction:column;height:100%;padding:16px;gap:16px}::ng-deep mm-base-tree-detail .k-splitter{background:transparent;border:none}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar{background:linear-gradient(180deg,var(--octo-mint-30),transparent);width:4px!important}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar:hover{background:linear-gradient(180deg,var(--octo-mint-50),var(--octo-mint-20))}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf{font-family:Roboto,sans-serif;transition:all .2s ease;border-radius:4px}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf:hover{background:var(--octo-mint-10);color:var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected{background:linear-gradient(90deg,var(--octo-mint-20),transparent);color:var(--octo-mint);border-left:3px solid var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected:hover{background:linear-gradient(90deg,var(--octo-mint-25),transparent)}::ng-deep mm-base-tree-detail .toolbar{background:linear-gradient(90deg,var(--octo-mint-05),transparent);border-bottom:1px solid var(--octo-mint-20)}::ng-deep mm-base-tree-detail .base-tree-detail-container{height:100%;display:flex;flex-direction:column}::ng-deep mm-base-tree-detail .base-tree-detail-container .k-splitter{flex:1;min-height:0}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane{display:flex!important;flex-direction:column!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details{display:flex!important;flex:1!important;min-height:0!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details>.runtime-browser-details{flex:1;display:flex;flex-direction:column;min-height:0}@media(max-width:1024px){.runtime-browser-container{padding:12px;gap:12px}}@media(max-width:768px){.runtime-browser-container{padding:8px;gap:10px}}\n"], dependencies: [{ kind: "component", type: BaseTreeDetailComponent, selector: "mm-base-tree-detail", inputs: ["treeDataSource", "leftPaneSize", "leftToolbarActions", "rightToolbarActions"], outputs: ["nodeSelected", "nodeDropped"] }, { kind: "component", type: RuntimeBrowserDetailsComponent, selector: "mm-runtime-browser-details", inputs: ["selectedItem", "showDataMapping", "messages", "expressionValidator"], outputs: ["entitySaved"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
15843
+ `, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}.runtime-browser-container{display:flex;flex-direction:column;height:100%;padding:16px;gap:16px}::ng-deep mm-base-tree-detail .k-splitter{background:transparent;border:none}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar{background:linear-gradient(180deg,var(--octo-mint-30),transparent);width:4px!important}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar:hover{background:linear-gradient(180deg,var(--octo-mint-50),var(--octo-mint-20))}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf{font-family:Roboto,sans-serif;transition:all .2s ease;border-radius:4px}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf:hover{background:var(--octo-mint-10);color:var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected{background:linear-gradient(90deg,var(--octo-mint-20),transparent);color:var(--octo-mint);border-left:3px solid var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected:hover{background:linear-gradient(90deg,var(--octo-mint-25),transparent)}::ng-deep mm-base-tree-detail .toolbar{background:linear-gradient(90deg,var(--octo-mint-05),transparent);border-bottom:1px solid var(--octo-mint-20)}::ng-deep mm-base-tree-detail .base-tree-detail-container{height:100%;display:flex;flex-direction:column}::ng-deep mm-base-tree-detail .base-tree-detail-container .k-splitter{flex:1;min-height:0}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane{display:flex!important;flex-direction:column!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details{display:flex!important;flex:1!important;min-height:0!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details>.runtime-browser-details{flex:1;display:flex;flex-direction:column;min-height:0}@media(max-width:1024px){.runtime-browser-container{padding:12px;gap:12px}}@media(max-width:768px){.runtime-browser-container{padding:8px;gap:10px}}\n"], dependencies: [{ kind: "component", type: BaseTreeDetailComponent, selector: "mm-base-tree-detail", inputs: ["treeDataSource", "leftPaneSize", "leftToolbarActions", "rightToolbarActions"], outputs: ["nodeSelected", "nodeDropped"] }, { kind: "component", type: RuntimeBrowserDetailsComponent, selector: "mm-runtime-browser-details", inputs: ["selectedItem", "showDataMapping", "messages", "expressionValidator"], outputs: ["entitySaved"] }, { kind: "component", type: PerspectiveSwitcherComponent, selector: "mm-perspective-switcher", inputs: ["perspectives", "activeKey", "label"], outputs: ["perspectiveChange"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14874
15844
  }
14875
15845
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserComponent, decorators: [{
14876
15846
  type: Component,
14877
- args: [{ selector: 'mm-runtime-browser', imports: [BaseTreeDetailComponent, RuntimeBrowserDetailsComponent], template: `
15847
+ args: [{ selector: 'mm-runtime-browser', imports: [
15848
+ BaseTreeDetailComponent,
15849
+ RuntimeBrowserDetailsComponent,
15850
+ PerspectiveSwitcherComponent,
15851
+ ], template: `
14878
15852
  <div class="runtime-browser-container kendo-theme-provider">
14879
15853
  <!-- LCARS Header -->
14880
15854
  <div class="lcars-page-header">
@@ -14901,6 +15875,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14901
15875
  <!-- Main Content -->
14902
15876
  <div class="lcars-content-panel">
14903
15877
  <div class="panel-accent-top"></div>
15878
+ <mm-perspective-switcher
15879
+ [perspectives]="perspectives()"
15880
+ [activeKey]="activePerspectiveKey()"
15881
+ [label]="resolvedMessages().perspective ?? 'Perspective'"
15882
+ (perspectiveChange)="onPerspectiveChange($event)"
15883
+ ></mm-perspective-switcher>
14904
15884
  <mm-base-tree-detail
14905
15885
  #treeDetail
14906
15886
  [treeDataSource]="dataSource"
@@ -14936,7 +15916,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14936
15916
  </div>
14937
15917
  </div>
14938
15918
  </div>
14939
- `, changeDetection: ChangeDetectionStrategy.Eager, styles: [".runtime-browser-container{display:flex;flex-direction:column;height:100%;padding:16px;gap:16px}::ng-deep mm-base-tree-detail .k-splitter{background:transparent;border:none}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar{background:linear-gradient(180deg,var(--octo-mint-30),transparent);width:4px!important}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar:hover{background:linear-gradient(180deg,var(--octo-mint-50),var(--octo-mint-20))}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf{font-family:Roboto,sans-serif;transition:all .2s ease;border-radius:4px}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf:hover{background:var(--octo-mint-10);color:var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected{background:linear-gradient(90deg,var(--octo-mint-20),transparent);color:var(--octo-mint);border-left:3px solid var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected:hover{background:linear-gradient(90deg,var(--octo-mint-25),transparent)}::ng-deep mm-base-tree-detail .toolbar{background:linear-gradient(90deg,var(--octo-mint-05),transparent);border-bottom:1px solid var(--octo-mint-20)}::ng-deep mm-base-tree-detail .base-tree-detail-container{height:100%;display:flex;flex-direction:column}::ng-deep mm-base-tree-detail .base-tree-detail-container .k-splitter{flex:1;min-height:0}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane{display:flex!important;flex-direction:column!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details{display:flex!important;flex:1!important;min-height:0!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details>.runtime-browser-details{flex:1;display:flex;flex-direction:column;min-height:0}@media(max-width:1024px){.runtime-browser-container{padding:12px;gap:12px}}@media(max-width:768px){.runtime-browser-container{padding:8px;gap:10px}}\n"] }]
15919
+ `, changeDetection: ChangeDetectionStrategy.Eager, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}.runtime-browser-container{display:flex;flex-direction:column;height:100%;padding:16px;gap:16px}::ng-deep mm-base-tree-detail .k-splitter{background:transparent;border:none}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar{background:linear-gradient(180deg,var(--octo-mint-30),transparent);width:4px!important}::ng-deep mm-base-tree-detail .k-splitter .k-splitbar:hover{background:linear-gradient(180deg,var(--octo-mint-50),var(--octo-mint-20))}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf{font-family:Roboto,sans-serif;transition:all .2s ease;border-radius:4px}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf:hover{background:var(--octo-mint-10);color:var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected{background:linear-gradient(90deg,var(--octo-mint-20),transparent);color:var(--octo-mint);border-left:3px solid var(--octo-mint)}::ng-deep mm-base-tree-detail kendo-treeview .k-treeview-item .k-treeview-leaf.k-selected:hover{background:linear-gradient(90deg,var(--octo-mint-25),transparent)}::ng-deep mm-base-tree-detail .toolbar{background:linear-gradient(90deg,var(--octo-mint-05),transparent);border-bottom:1px solid var(--octo-mint-20)}::ng-deep mm-base-tree-detail .base-tree-detail-container{height:100%;display:flex;flex-direction:column}::ng-deep mm-base-tree-detail .base-tree-detail-container .k-splitter{flex:1;min-height:0}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane{display:flex!important;flex-direction:column!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details{display:flex!important;flex:1!important;min-height:0!important;height:100%!important}::ng-deep mm-base-tree-detail kendo-splitter-pane.detail-pane>mm-runtime-browser-details>.runtime-browser-details{flex:1;display:flex;flex-direction:column;min-height:0}@media(max-width:1024px){.runtime-browser-container{padding:12px;gap:12px}}@media(max-width:768px){.runtime-browser-container{padding:8px;gap:10px}}\n"] }]
14940
15920
  }], propDecorators: { messages: [{ type: i0.Input, args: [{ isSignal: true, alias: "messages", required: false }] }], showDataMapping: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDataMapping", required: false }] }], expressionValidator: [{ type: i0.Input, args: [{ isSignal: true, alias: "expressionValidator", required: false }] }], treeDetail: [{
14941
15921
  type: ViewChild,
14942
15922
  args: ['treeDetail', { static: false }]
@@ -14951,17 +15931,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
14951
15931
  */
14952
15932
  class RuntimeBrowserOutletComponent {
14953
15933
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserOutletComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14954
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: RuntimeBrowserOutletComponent, isStandalone: true, selector: "mm-runtime-browser-outlet", ngImport: i0, template: `<router-outlet />`, isInline: true, dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
15934
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: RuntimeBrowserOutletComponent, isStandalone: true, selector: "mm-runtime-browser-outlet", ngImport: i0, template: `<router-outlet />`, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}\n"], dependencies: [{ kind: "directive", type: RouterOutlet, selector: "router-outlet", inputs: ["name", "routerOutletData"], outputs: ["activate", "deactivate", "attach", "detach"], exportAs: ["outlet"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14955
15935
  }
14956
15936
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserOutletComponent, decorators: [{
14957
15937
  type: Component,
14958
- args: [{
14959
- selector: 'mm-runtime-browser-outlet',
14960
- standalone: true,
14961
- imports: [RouterOutlet],
14962
- changeDetection: ChangeDetectionStrategy.Eager,
14963
- template: `<router-outlet />`,
14964
- }]
15938
+ args: [{ selector: 'mm-runtime-browser-outlet', standalone: true, imports: [RouterOutlet], changeDetection: ChangeDetectionStrategy.Eager, template: `<router-outlet />`, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}\n"] }]
14965
15939
  }] });
14966
15940
 
14967
15941
  /**
@@ -14982,17 +15956,11 @@ class RuntimeBrowserPageComponent {
14982
15956
  ? this.injectedMessages
14983
15957
  : DEFAULT_RUNTIME_BROWSER_MESSAGES;
14984
15958
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserPageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
14985
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: RuntimeBrowserPageComponent, isStandalone: true, selector: "mm-runtime-browser-page", ngImport: i0, template: `<mm-runtime-browser [messages]="messages" />`, isInline: true, dependencies: [{ kind: "component", type: RuntimeBrowserComponent, selector: "mm-runtime-browser", inputs: ["messages", "showDataMapping", "expressionValidator"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
15959
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.2", type: RuntimeBrowserPageComponent, isStandalone: true, selector: "mm-runtime-browser-page", ngImport: i0, template: `<mm-runtime-browser [messages]="messages" />`, isInline: true, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}\n"], dependencies: [{ kind: "component", type: RuntimeBrowserComponent, selector: "mm-runtime-browser", inputs: ["messages", "showDataMapping", "expressionValidator"] }], changeDetection: i0.ChangeDetectionStrategy.Eager });
14986
15960
  }
14987
15961
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserPageComponent, decorators: [{
14988
15962
  type: Component,
14989
- args: [{
14990
- selector: 'mm-runtime-browser-page',
14991
- standalone: true,
14992
- imports: [RuntimeBrowserComponent],
14993
- changeDetection: ChangeDetectionStrategy.Eager,
14994
- template: `<mm-runtime-browser [messages]="messages" />`,
14995
- }]
15963
+ args: [{ selector: 'mm-runtime-browser-page', standalone: true, imports: [RuntimeBrowserComponent], changeDetection: ChangeDetectionStrategy.Eager, template: `<mm-runtime-browser [messages]="messages" />`, styles: [":host{display:flex;flex-direction:column;height:100%;width:100%}\n"] }]
14996
15964
  }] });
14997
15965
 
14998
15966
  /**
@@ -18018,5 +18986,5 @@ function provideOctoUi() {
18018
18986
  * Generated bundle index. Do not edit.
18019
18987
  */
18020
18988
 
18021
- export { AssociationValidationService, AttributeSelectorDialogComponent, AttributeSelectorDialogService, AttributeSortSelectorDialogComponent, AttributeSortSelectorDialogService, AttributeValueTypeDto, CkTypeSelectorDialogComponent, CkTypeSelectorDialogService, CkTypeSelectorInputComponent, DEFAULT_CK_TYPE_SELECTOR_DIALOG_MESSAGES, DEFAULT_CK_TYPE_SELECTOR_INPUT_MESSAGES, DEFAULT_DATA_POINT, DEFAULT_MAPPING_COVERAGE_TREE_CONFIG, DEFAULT_PROPERTY_GRID_MESSAGES, DEFAULT_RUNTIME_BROWSER_MESSAGES, DataMappingOverviewComponent, DataPointPickerComponent, DataPointResolverService, DefaultPropertyCategory, EntityDetailComponent, EntityIdInfoComponent, EntitySelectorDialogComponent, EntitySelectorDialogService, FieldFilterEditorComponent, MappingCoverageTreeComponent, MappingCoverageTreeDataSource, MappingEditDialogComponent, MappingEditDialogService, OctoGraphQlDataSource, OctoGraphQlHierarchyDataSource, OctoLoaderComponent, PropertyConverterService, PropertyDisplayMode, PropertyGridComponent, PropertyValueDisplayComponent, RUNTIME_BROWSER_MESSAGES, RecordDetailDialogComponent, RtEntityIdHelper, RuntimeBrowserComponent, RuntimeBrowserOutletComponent, RuntimeBrowserPageComponent, RuntimeBrowserStateService, RuntimeEntityVariableDialogComponent, RuntimeEntityVariableDialogService, TenantSwitcherComponent, account_tree, add, analytics, app_registration, article, botService, category, chat, checklist, code, component_exchange, computer, createRuntimeBrowserRoutes, customer, dashboard, event_list, extractDataPointNames, graphic_eq, group, identityService, insert_link, manage_accounts, more_time, notifications, page_info, pages, person_search, playlist_add_check, pool, power, provideOctoUi, publicIcon, query_builder, schedule_send, settings, sort, storage, swagger, swagger_asset, swagger_bot, swagger_communication, swagger_identity, team_dashboard, tenancy, text_snippet, travel_explore, user_diagnostics, webhook, work };
18989
+ export { AssociationValidationService, AttributeSelectorDialogComponent, AttributeSelectorDialogService, AttributeSortSelectorDialogComponent, AttributeSortSelectorDialogService, AttributeValueTypeDto, CkTypeSelectorDialogComponent, CkTypeSelectorDialogService, CkTypeSelectorInputComponent, DEFAULT_CK_TYPE_SELECTOR_DIALOG_MESSAGES, DEFAULT_CK_TYPE_SELECTOR_INPUT_MESSAGES, DEFAULT_DATA_POINT, DEFAULT_MAPPING_COVERAGE_TREE_CONFIG, DEFAULT_PROPERTY_GRID_MESSAGES, DEFAULT_RUNTIME_BROWSER_MESSAGES, DataMappingOverviewComponent, DataPointPickerComponent, DataPointResolverService, DefaultPropertyCategory, EntityDetailComponent, EntityIdInfoComponent, EntitySelectorDialogComponent, EntitySelectorDialogService, FieldFilterEditorComponent, MappingCoverageTreeComponent, MappingCoverageTreeDataSource, MappingEditDialogComponent, MappingEditDialogService, OctoGraphQlDataSource, OctoGraphQlHierarchyDataSource, OctoLoaderComponent, PropertyConverterService, PropertyDisplayMode, PropertyGridComponent, PropertyValueDisplayComponent, RUNTIME_BROWSER_MESSAGES, RecordDetailDialogComponent, RtEntityIdHelper, RuntimeBrowserComponent, RuntimeBrowserOutletComponent, RuntimeBrowserPageComponent, RuntimeBrowserStateService, RuntimeEntityVariableDialogComponent, RuntimeEntityVariableDialogService, TREE_NAVIGATION_CONFIG_CONSTANTS, TenantSwitcherComponent, TreeNavigationConfigService, account_tree, add, analytics, app_registration, article, botService, category, chat, checklist, code, component_exchange, computer, createRuntimeBrowserRoutes, customer, dashboard, event_list, extractDataPointNames, graphic_eq, group, identityService, insert_link, manage_accounts, more_time, notifications, page_info, pages, person_search, playlist_add_check, pool, power, provideOctoUi, publicIcon, query_builder, schedule_send, settings, sort, storage, swagger, swagger_asset, swagger_bot, swagger_communication, swagger_identity, team_dashboard, tenancy, text_snippet, travel_explore, user_diagnostics, webhook, work };
18022
18990
  //# sourceMappingURL=meshmakers-octo-ui.mjs.map