@meshmakers/octo-ui 3.4.290 → 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.
@@ -6062,6 +6062,46 @@ const CONFIG_QUERY = gql `
6062
6062
  }
6063
6063
  }
6064
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
+ `;
6065
6105
  const ROLE_SUGGESTIONS_QUERY = gql `
6066
6106
  query treeNavigationRoleSuggestions($ckTypeId: String!) {
6067
6107
  constructionKit {
@@ -6126,6 +6166,8 @@ class TreeNavigationConfigService {
6126
6166
  apollo = inject(Apollo);
6127
6167
  /** key `${sourceCkTypeId}::${roleId}` -> override; cached for the session. */
6128
6168
  overridesPromise;
6169
+ /** Configured extra perspectives; cached for the session. */
6170
+ perspectivesPromise;
6129
6171
  /**
6130
6172
  * Resolves the override for one (source type, role) pair, preferring an exact
6131
6173
  * source-type match over a wildcard (`*`) rule. Returns undefined when nothing
@@ -6139,6 +6181,58 @@ class TreeNavigationConfigService {
6139
6181
  /** Forces a reload on next access (e.g. after a tenant switch). */
6140
6182
  reset() {
6141
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
+ };
6142
6236
  }
6143
6237
  /**
6144
6238
  * Returns the inbound association roles declared on a CK type, for the role
@@ -6209,6 +6303,7 @@ class TreeNavigationConfigService {
6209
6303
  */
6210
6304
  async loadConfig() {
6211
6305
  const { typePresent, rtId, rawRoles } = await this.fetchSingleton();
6306
+ const rawPerspectives = typePresent ? await this.fetchPerspectivesRaw() : [];
6212
6307
  const roles = [];
6213
6308
  for (const raw of rawRoles) {
6214
6309
  if (!raw?.sourceCkTypeId || !raw?.roleId) {
@@ -6224,22 +6319,38 @@ class TreeNavigationConfigService {
6224
6319
  icon: raw.icon ?? undefined,
6225
6320
  });
6226
6321
  }
6227
- return { typePresent, rtId, roles };
6322
+ const perspectives = rawPerspectives
6323
+ .map((raw) => this.toPerspectiveDefinition(raw))
6324
+ .filter((p) => p !== null);
6325
+ return { typePresent, rtId, roles, perspectives };
6228
6326
  }
6229
6327
  /**
6230
- * Creates or updates the singleton with the given rules, then invalidates the
6231
- * resolve cache so the trees pick up the change on the next expand. Returns the
6232
- * singleton rtId.
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.
6233
6331
  */
6234
- async saveConfig(rtId, roles) {
6332
+ async saveConfig(rtId, roles, perspectives = []) {
6235
6333
  const cleanRoles = roles
6236
6334
  .filter((r) => r.sourceCkTypeId && r.roleId)
6237
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).
6238
6343
  let savedRtId;
6239
6344
  if (rtId) {
6345
+ const item = { roles: cleanRoles };
6346
+ if (cleanPerspectives.length > 0) {
6347
+ item['perspectives'] = cleanPerspectives;
6348
+ }
6240
6349
  const result = await firstValueFrom(this.apollo.mutate({
6241
6350
  mutation: UPDATE_CONFIG_MUTATION,
6242
- variables: { entities: [{ rtId, item: { roles: cleanRoles } }] },
6351
+ variables: {
6352
+ entities: [{ rtId, item }],
6353
+ },
6243
6354
  fetchPolicy: 'no-cache',
6244
6355
  }));
6245
6356
  savedRtId =
@@ -6255,6 +6366,9 @@ class TreeNavigationConfigService {
6255
6366
  rtWellKnownName: CONFIG_WELL_KNOWN_NAME,
6256
6367
  name: 'Tree Navigation',
6257
6368
  roles: cleanRoles,
6369
+ ...(cleanPerspectives.length > 0
6370
+ ? { perspectives: cleanPerspectives }
6371
+ : {}),
6258
6372
  },
6259
6373
  ],
6260
6374
  },
@@ -6290,11 +6404,45 @@ class TreeNavigationConfigService {
6290
6404
  input['icon'] = role.icon;
6291
6405
  return input;
6292
6406
  }
6293
- /** Probes the CK schema and (when present) loads the singleton's raw roles. */
6294
- async fetchSingleton() {
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() {
6295
6436
  const exists = await firstValueFrom(this.apollo.query({ query: CONFIG_TYPE_EXISTS_QUERY, fetchPolicy: 'network-only' }));
6296
- const typePresent = (exists.data?.constructionKit?.types?.items?.length ?? 0) > 0;
6297
- if (!typePresent) {
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())) {
6298
6446
  return { typePresent: false, rtId: null, rawRoles: [] };
6299
6447
  }
6300
6448
  const result = await firstValueFrom(this.apollo.query({ query: CONFIG_QUERY, fetchPolicy: 'network-only' }));
@@ -6305,6 +6453,23 @@ class TreeNavigationConfigService {
6305
6453
  rawRoles: item?.roles ?? [],
6306
6454
  };
6307
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
+ }
6308
6473
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: TreeNavigationConfigService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
6309
6474
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: TreeNavigationConfigService, providedIn: 'root' });
6310
6475
  }
@@ -6356,6 +6521,44 @@ const PARENT_CHILD_ROLE_ID = 'System/ParentChild';
6356
6521
  * tree from firing a guaranteed-failing query for every entity.
6357
6522
  */
6358
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
+ `;
6359
6562
  class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6360
6563
  getTreesDtoGQL = inject(GetTreesDtoGQL);
6361
6564
  getCkTypeAssociationRolesDtoGQL = inject(GetCkTypeAssociationRolesDtoGQL);
@@ -6369,6 +6572,93 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6369
6572
  updateTreeNodesDtoGQL = inject(UpdateTreeNodesDtoGQL);
6370
6573
  typeHelperService = inject(TypeHelperService);
6371
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
+ }
6372
6662
  isCkModelsRoot(item) {
6373
6663
  return !!item && 'isCkModelsRoot' in item;
6374
6664
  }
@@ -6426,17 +6716,30 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6426
6716
  // runtime edges but are no longer declared on the type in the installed CK
6427
6717
  // model (orphan roles after model evolution). Each group carries its concrete
6428
6718
  // origin CK type (the ckId used to load its targets) and an exact count.
6429
- const roleGroups = await this.discoverEntityInboundRoleGroups(rtEntity.rtId, rtEntity.ckTypeId);
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;
6430
6729
  // Apply the optional per-tenant TreeNavigationConfiguration overrides on top
6431
6730
  // of auto-discovery: hide (visible), relabel (displayName), reorder
6432
6731
  // (sortIndex), flatten vs group (grouped), icon. Defaults reproduce Phase 1:
6433
- // System/ParentChild flat, every other role grouped.
6732
+ // System/ParentChild flat, every other role grouped. In a perspective root
6733
+ // the perspective's primary role is flattened at the top instead.
6434
6734
  const annotated = await Promise.all(roleGroups.map(async (group) => {
6435
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;
6436
6739
  return {
6437
6740
  group,
6438
6741
  visible: override?.visible !== false,
6439
- grouped: override?.grouped ?? group.roleId !== PARENT_CHILD_ROLE_ID,
6742
+ grouped: override?.grouped ?? defaultGrouped,
6440
6743
  displayName: override?.displayName,
6441
6744
  sortIndex: override?.sortIndex,
6442
6745
  icon: override?.icon,
@@ -6697,6 +7000,17 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6697
7000
  // Add CK Models root node
6698
7001
  const ckModelsRoot = { isCkModelsRoot: true };
6699
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.
6700
7014
  // Check if Basic construction kit is available before trying to fetch Tree entities
6701
7015
  const isBasicCkAvailable = await this.checkBasicConstructionKitAvailable();
6702
7016
  if (isBasicCkAvailable) {
@@ -6743,6 +7057,39 @@ class RuntimeBrowserDataSource extends OctoGraphQlHierarchyDataSource {
6743
7057
  return [];
6744
7058
  }
6745
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
+ }
6746
7093
  /**
6747
7094
  * Gets ParentChild association of given Runtime Entity.
6748
7095
  *
@@ -7036,11 +7383,86 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
7036
7383
  }]
7037
7384
  }] });
7038
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
+
7039
7439
  class EntitySelectorDialogComponent {
7040
7440
  windowRef = inject(WindowRef);
7041
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 */ []));
7042
7448
  data = {};
7043
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
+ }
7044
7466
  onNodeSelected(node) {
7045
7467
  const item = node.item;
7046
7468
  if (item && typeof item === 'object' && 'rtId' in item && 'ckTypeId' in item) {
@@ -7070,11 +7492,17 @@ class EntitySelectorDialogComponent {
7070
7492
  this.windowRef.close();
7071
7493
  }
7072
7494
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntitySelectorDialogComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
7073
- 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: `
7074
7496
  <div class="entity-selector">
7075
7497
  <div class="entity-selector-body">
7498
+ <mm-perspective-switcher
7499
+ [perspectives]="perspectives()"
7500
+ [activeKey]="activePerspectiveKey()"
7501
+ (perspectiveChange)="onPerspectiveChange($event)"
7502
+ ></mm-perspective-switcher>
7076
7503
  <div class="tree-section">
7077
7504
  <mm-tree-view
7505
+ #tree
7078
7506
  [dataSource]="treeDataSource"
7079
7507
  (nodeSelected)="onNodeSelected($event)"
7080
7508
  ></mm-tree-view>
@@ -7109,7 +7537,7 @@ class EntitySelectorDialogComponent {
7109
7537
  </button>
7110
7538
  </div>
7111
7539
  </div>
7112
- `, 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 });
7113
7541
  }
7114
7542
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: EntitySelectorDialogComponent, decorators: [{
7115
7543
  type: Component,
@@ -7117,11 +7545,18 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
7117
7545
  CommonModule,
7118
7546
  ButtonModule,
7119
7547
  TreeComponent,
7548
+ PerspectiveSwitcherComponent,
7120
7549
  ], providers: [RuntimeBrowserDataSource], template: `
7121
7550
  <div class="entity-selector">
7122
7551
  <div class="entity-selector-body">
7552
+ <mm-perspective-switcher
7553
+ [perspectives]="perspectives()"
7554
+ [activeKey]="activePerspectiveKey()"
7555
+ (perspectiveChange)="onPerspectiveChange($event)"
7556
+ ></mm-perspective-switcher>
7123
7557
  <div class="tree-section">
7124
7558
  <mm-tree-view
7559
+ #tree
7125
7560
  [dataSource]="treeDataSource"
7126
7561
  (nodeSelected)="onNodeSelected($event)"
7127
7562
  ></mm-tree-view>
@@ -7157,7 +7592,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
7157
7592
  </div>
7158
7593
  </div>
7159
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"] }]
7160
- }] });
7595
+ }], propDecorators: { tree: [{
7596
+ type: ViewChild,
7597
+ args: ['tree']
7598
+ }] } });
7161
7599
 
7162
7600
  const DIALOG_KEY$1 = 'entity-selector';
7163
7601
  const DEFAULT_SIZE$1 = { width: 600, height: 680 };
@@ -8817,6 +9255,7 @@ const DEFAULT_RUNTIME_BROWSER_MESSAGES = {
8817
9255
  mappingNotSet: '(not set)',
8818
9256
  mappingSelect: 'Select...',
8819
9257
  mappingNoneConfigured: 'No data point mappings configured yet.',
9258
+ perspective: 'Perspective',
8820
9259
  };
8821
9260
 
8822
9261
  class DataMappingListComponent {
@@ -14754,6 +15193,26 @@ class RuntimeBrowserComponent {
14754
15193
  ...this.messages(),
14755
15194
  }), /* @ts-ignore */
14756
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
+ }
14757
15216
  treeDetail;
14758
15217
  detailsPanel;
14759
15218
  // Define toolbar actions
@@ -14833,6 +15292,8 @@ class RuntimeBrowserComponent {
14833
15292
  return !this.isLoading && this.isSelectedItemAnRtEntity && !this.isEditing;
14834
15293
  }
14835
15294
  ngAfterViewInit() {
15295
+ // Load the selectable perspectives for the switcher (AB#4263).
15296
+ void this.loadPerspectives();
14836
15297
  // Restore tree state after the tree has been initialized
14837
15298
  this.waitForTreeComponent().then((isReady) => {
14838
15299
  if (isReady) {
@@ -15338,6 +15799,12 @@ class RuntimeBrowserComponent {
15338
15799
  <!-- Main Content -->
15339
15800
  <div class="lcars-content-panel">
15340
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>
15341
15808
  <mm-base-tree-detail
15342
15809
  #treeDetail
15343
15810
  [treeDataSource]="dataSource"
@@ -15373,11 +15840,15 @@ class RuntimeBrowserComponent {
15373
15840
  </div>
15374
15841
  </div>
15375
15842
  </div>
15376
- `, 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"] }], 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 });
15377
15844
  }
15378
15845
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: RuntimeBrowserComponent, decorators: [{
15379
15846
  type: Component,
15380
- args: [{ selector: 'mm-runtime-browser', imports: [BaseTreeDetailComponent, RuntimeBrowserDetailsComponent], template: `
15847
+ args: [{ selector: 'mm-runtime-browser', imports: [
15848
+ BaseTreeDetailComponent,
15849
+ RuntimeBrowserDetailsComponent,
15850
+ PerspectiveSwitcherComponent,
15851
+ ], template: `
15381
15852
  <div class="runtime-browser-container kendo-theme-provider">
15382
15853
  <!-- LCARS Header -->
15383
15854
  <div class="lcars-page-header">
@@ -15404,6 +15875,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
15404
15875
  <!-- Main Content -->
15405
15876
  <div class="lcars-content-panel">
15406
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>
15407
15884
  <mm-base-tree-detail
15408
15885
  #treeDetail
15409
15886
  [treeDataSource]="dataSource"