Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
SignalListComponent,
SignalListConfig,
SignalListSort,
naturalCompare,
} from '@stratosui/core';
import type { EndpointModel } from '@stratosui/store';

Expand Down Expand Up @@ -80,7 +81,7 @@ export class CloudFoundryComponent {
if (av == null && bv == null) return 0;
if (av == null) return 1;
if (bv == null) return -1;
return String(av).localeCompare(String(bv), undefined, { sensitivity: 'base', numeric: true }) * dir;
return naturalCompare(String(av), String(bv)) * dir;
});
});
const pageSize: WritableSignal<number> = signal(24);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,11 @@ export class CloudFoundryServicesSignalComponent implements OnInit {
{
header: 'Name', key: 'name', sortField: 'name',
kind: 'link',
link: (si: StServiceInstance) =>
['/services', si.type === 'user-provided' ? 'user-provided' : 'managed', si.cnsiGuid, si.guid],
link: (si: StServiceInstance) => {
const offeringGuid = si.servicePlan?.serviceOffering?.guid;
if (!offeringGuid) return null;
return ['/marketplace', si.cnsiGuid, offeringGuid, 'summary'];
},
render: (si: StServiceInstance) => si.name,
widthHint: '14rem',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<app-page-sub-nav>
<button class="btn btn-secondary btn-icon" name="cli" routerLink="/cloud-foundry/{{cfEndpointService.cfGuid}}/cli" title="CLI Info">
<span class="material-icons">terminal</span>
<span class="material-icons stratos-icons">terminal</span>
</button>
<app-polling-indicator class="ml-auto"
[manualPoll]="(cfEndpointService.appsLoading$ | async) === false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { take,
switchMap,
} from 'rxjs/operators';

import { naturalCompare } from '@stratosui/core';

import { CurrentUserPermissionsService } from '../../../../../../core/src/core/permissions/current-user-permissions.service';
import { APIResource, EntityInfo } from '../../../../../../store/src/types/api.types';
import { IOrganization, ISpace } from '../../../../cf-api.types';
Expand Down Expand Up @@ -285,7 +287,7 @@ export class CfRolesService {
catchError(() => of([] as APIResource<IOrganization>[])),
);
this.cfOrgs[cfGuid] = CfRolesService.filterEditableOrgOrSpace<IOrganization>(this.userPerms, true, orgs$).pipe(
map(orgs => orgs.sort((a, b) => a.entity.name.localeCompare(b.entity.name))),
map(orgs => orgs.sort((a, b) => naturalCompare(a.entity.name, b.entity.name))),
publishReplay(1),
refCount()
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
AppActionMonitorComponent,
AppMonitorComponentTypes,
ITableColumn,
ITableCellRequestMonitorIconConfig } from '@stratosui/core';
ITableCellRequestMonitorIconConfig,
naturalCompare } from '@stratosui/core';
import { entityCatalog, APIResource } from '@stratosui/store';
import { UsersRolesClearUpdateState } from '../../../../../actions/users-roles.actions';
import { ChangeCfUserRole } from '../../../../../actions/users.actions';
Expand Down Expand Up @@ -169,7 +170,7 @@ export class UsersRolesConfirmComponent implements OnInit, AfterContentInit {
username: ManageUsersSetUsernamesHelper.usernameFromGuid(change.userGuid),
roleName: this.fetchRoleName(change.role, !change.spaceGuid)
}))
.sort((a, b) => a.username.localeCompare(b.username)),
.sort((a, b) => naturalCompare(a.username, b.username)),
)
);
const changesViaUserGuid = this.updateChanges.pipe(
Expand All @@ -183,7 +184,7 @@ export class UsersRolesConfirmComponent implements OnInit, AfterContentInit {
username: this.fetchUsername(change.userGuid, users),
roleName: this.fetchRoleName(change.role, !change.spaceGuid)
}))
.sort((a, b) => a.username.localeCompare(b.username))
.sort((a, b) => naturalCompare(a.username, b.username))
)
);
this.changes$ = this.setUsernames ? changesViaUsername : changesViaUserGuid;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,10 +203,14 @@ export class ServicesWallComponent implements OnInit {
{
header: 'Name', key: 'name', sortField: 'name',
kind: 'link',
// Use the legacy detail-page route shape: /services/:type/:cnsi/:siGuid
// (the legacy detail page itself stays untouched in this migration).
link: (si: StServiceInstance) =>
['/services', si.type === 'user-provided' ? 'user-provided' : 'managed', si.cnsiGuid, si.guid],
// Land on the marketplace offering summary for managed instances;
// user-provided instances have no offering so the name stays
// plain text (signal-list renders `null` as non-link).
link: (si: StServiceInstance) => {
const offeringGuid = si.servicePlan?.serviceOffering?.guid;
if (!offeringGuid) return null;
return ['/marketplace', si.cnsiGuid, offeringGuid, 'summary'];
},
render: (si: StServiceInstance) => si.name,
widthHint: '14rem',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
import { Signal, computed } from '@angular/core';

import { detectSortContext, naturalCompare } from '@stratosui/core';

export interface SortSpec<T = unknown> {
// Property name on T to sort by. Declared as string (not keyof T) so
// the shape is compatible with SignalListComponent's SignalListSort
// without an explicit cast at the boundary.
field: string;
direction: 'asc' | 'desc';
// Optional per-list match-case toggle. Default (undefined === false) is
// case-insensitive natural sort.
caseSensitive?: boolean;
// Keep the generic so existing callers that parameterize SortSpec<StApp>
// continue to type-check.
_phantom?: T;
Expand Down Expand Up @@ -45,7 +50,18 @@ export class ViewPipeline<T> {
const getValue: (row: T) => unknown = extractor
? extractor
: (row: T) => (row as Record<string, unknown>)[spec.field];
return [...this.filteredItems()].sort((a, b) => {
const items = [...this.filteredItems()];
// Collection-aware pre-pass: scan all extracted string values for
// the xxx-sep?-nnn pattern. When most entries match, naturalCompare
// strips separator chars before tokenizing so `Org 4`, `Org_5`, and
// `Org6` sequence by their numeric token.
const strings: string[] = [];
for (const it of items) {
const v = getValue(it);
if (typeof v === 'string') strings.push(v);
}
const ctx = strings.length > 0 ? detectSortContext(strings) : {};
return items.sort((a, b) => {
const av = getValue(a);
const bv = getValue(b);
if (av == null && bv == null) return 0;
Expand All @@ -57,14 +73,8 @@ export class ViewPipeline<T> {
if (typeof av === 'number' && typeof bv === 'number') {
return (av - bv) * sign;
}
// Natural string sort for string/string comparisons:
// - case-insensitive (orgs starting with capital letters don't
// jump to the top of the list)
// - numeric-aware (org_2 sorts before org_10, not after)
// Falls back to `<` / `>` for non-string / mixed types (dates as
// ISO strings still compare correctly under localeCompare).
if (typeof av === 'string' && typeof bv === 'string') {
return av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' }) * sign;
return naturalCompare(av, bv, spec.caseSensitive, spec.direction, ctx);
}
return av < bv ? -1 * sign : av > bv ? 1 * sign : 0;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, Injector, OnDestroy, ViewChild, Vie
import { ReactiveFormsModule, FormControl, FormGroup, Validators } from '@angular/forms';
import { CustomFormFieldComponent, MatLabelComponent } from '@stratosui/core';
import { CustomSelectComponent, CustomOptionComponent } from '@stratosui/core';
import { naturalCompare } from '@stratosui/core';
import { toObservable } from '@angular/core/rxjs-interop';
import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import {
Expand Down Expand Up @@ -153,7 +154,7 @@ export class SelectPlanStepComponent implements OnDestroy {
this.clearNoPlans();
}
}),
map(plans => [...plans].sort((a, b) => this.getDisplayName(a).localeCompare(this.getDisplayName(b)))),
map(plans => [...plans].sort((a, b) => naturalCompare(this.getDisplayName(a), this.getDisplayName(b)))),
publishReplay(1),
refCount(),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<app-metadata-item class="mt-0" icon="title" label="Description">{{ description() }}</app-metadata-item>
<app-metadata-item icon="link" label="Instance Address" [clipboardValue]="apiUrl()">
<div class="flex min-h-6">
<div class="items-center flex mr-7 break-all">{{ apiUrl() }}</div>
<div class="items-center flex break-all">{{ apiUrl() }}</div>
</div>
</app-metadata-item>
<app-metadata-item icon="info_outline" label="CF API Version">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { CloudFoundryService } from '../../../../data-services/cloud-foundry.ser
import { writeWithJob } from '../../../../../services/async-jobs/write-with-job';
import type { StratosJob } from '../../../../../services/async-jobs/async-job.types';
import type { SignalListDropdownOption } from '@stratosui/core';
import { ListStateStore } from '@stratosui/core';
import { ListStateStore, naturalCompare } from '@stratosui/core';

@Injectable({ providedIn: 'root' })
export class CfAppsSignalConfigService {
Expand Down Expand Up @@ -226,7 +226,7 @@ export class CfAppsSignalConfigService {
}
}
const opts: SignalListDropdownOption[] = [{ label: 'All', value: null }];
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => a.localeCompare(b));
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => naturalCompare(a, b));
for (const [guid, label] of sorted) opts.push({ label, value: guid });
return opts;
});
Expand All @@ -247,7 +247,7 @@ export class CfAppsSignalConfigService {
}
}
const opts: SignalListDropdownOption[] = [{ label: 'All', value: null }];
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => a.localeCompare(b));
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => naturalCompare(a, b));
for (const [guid, label] of sorted) opts.push({ label, value: guid });
return opts;
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';

import type { SignalListDropdownOption } from '@stratosui/core';
import { ListStateStore } from '@stratosui/core';
import { ListStateStore, naturalCompare } from '@stratosui/core';

import { CnsiAuditEventsSource } from '../../../../../services/data-sources/cnsi-audit-events-source';
import { ViewPipeline, SortSpec } from '../../../../../services/data-sources/view-pipeline';
Expand Down Expand Up @@ -59,7 +59,7 @@ export class CfAuditEventsSignalConfigService {
readonly orgOptions: Signal<SignalListDropdownOption[]> = computed(() => {
const opts: SignalListDropdownOption[] = [{ label: 'All', value: null }];
const orgs = this.endpointDataService?.orgs() ?? [];
const sorted = [...orgs].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
const sorted = [...orgs].sort((a, b) => naturalCompare(a.name, b.name));
for (const o of sorted) opts.push({ label: o.name, value: o.guid });
return opts;
});
Expand All @@ -75,7 +75,7 @@ export class CfAuditEventsSignalConfigService {
if (org) {
const sorted = spaces
.filter(s => s.orgGuid === org)
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
.sort((a, b) => naturalCompare(a.name, b.name));
for (const s of sorted) opts.push({ label: s.name, value: s.guid });
return opts;
}
Expand All @@ -86,9 +86,9 @@ export class CfAuditEventsSignalConfigService {
orgName: orgNameByGuid.get(s.orgGuid) ?? '',
}));
augmented.sort((a, b) => {
const bySpace = a.spaceName.localeCompare(b.spaceName, undefined, { numeric: true });
const bySpace = naturalCompare(a.spaceName, b.spaceName);
if (bySpace !== 0) return bySpace;
return a.orgName.localeCompare(b.orgName, undefined, { numeric: true });
return naturalCompare(a.orgName, b.orgName);
});
for (const s of augmented) opts.push({ label: `${s.spaceName} - ${s.orgName}`, value: s.guid });
return opts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DestroyRef, Injectable, Injector, Signal, WritableSignal, computed, eff
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { SignalListDropdownOption } from '@stratosui/core';
import { ListStateStore } from '@stratosui/core';
import { ListStateStore, naturalCompare } from '@stratosui/core';
import { EndpointDataRegistry } from '../../../../../services/endpoint-data/endpoint-data.registry';
import type { EndpointDataService } from '../../../../../services/endpoint-data/endpoint-data.service';
import { ViewPipeline, SortSpec } from '../../../../../services/data-sources/view-pipeline';
Expand Down Expand Up @@ -179,7 +179,7 @@ export class CfRoutesSignalConfigService {
readonly orgOptions: Signal<SignalListDropdownOption[]> = computed(() => {
const orgs = this.endpointDataService?.orgs() ?? [];
const opts: SignalListDropdownOption[] = [{ label: 'All', value: null }];
const sorted = [...orgs].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
const sorted = [...orgs].sort((a, b) => naturalCompare(a.name, b.name));
for (const o of sorted) {
opts.push({ label: o.name, value: o.guid });
}
Expand All @@ -198,7 +198,7 @@ export class CfRoutesSignalConfigService {
if (org) {
const sorted = spaces
.filter(s => s.orgGuid === org)
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
.sort((a, b) => naturalCompare(a.name, b.name));
for (const s of sorted) opts.push({ label: s.name, value: s.guid });
return opts;
}
Expand All @@ -209,9 +209,9 @@ export class CfRoutesSignalConfigService {
orgName: orgNameByGuid.get(s.orgGuid) ?? '',
}));
augmented.sort((a, b) => {
const bySpace = a.spaceName.localeCompare(b.spaceName, undefined, { numeric: true });
const bySpace = naturalCompare(a.spaceName, b.spaceName);
if (bySpace !== 0) return bySpace;
return a.orgName.localeCompare(b.orgName, undefined, { numeric: true });
return naturalCompare(a.orgName, b.orgName);
});
for (const s of augmented) opts.push({ label: `${s.spaceName} - ${s.orgName}`, value: s.guid });
return opts;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type { EndpointDataService } from '../../../../../services/endpoint-data/
import type { StServiceInstance } from '../../../../../services/endpoint-data/stratos-types';
import { CloudFoundryService } from '../../../../data-services/cloud-foundry.service';
import type { SignalListDropdownOption } from '@stratosui/core';
import { ListStateStore } from '@stratosui/core';
import { ListStateStore, naturalCompare } from '@stratosui/core';

// Service instances list config — multi-CNSI by default (services-wall),
// with optional space + type narrowing for the per-space tabs.
Expand Down Expand Up @@ -166,7 +166,7 @@ export class CfServiceInstancesSignalConfigService {
if (!seen.has(o.guid)) seen.set(o.guid, o.name);
}
}
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => a.localeCompare(b, undefined, { numeric: true }));
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => naturalCompare(a, b));
for (const [guid, label] of sorted) opts.push({ label, value: guid });
return opts;
});
Expand All @@ -193,7 +193,7 @@ export class CfServiceInstancesSignalConfigService {
if (!seen.has(s.guid)) seen.set(s.guid, s.name);
}
}
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => a.localeCompare(b, undefined, { numeric: true }));
const sorted = Array.from(seen.entries()).sort(([, a], [, b]) => naturalCompare(a, b));
for (const [guid, label] of sorted) opts.push({ label, value: guid });
return opts;
}
Expand All @@ -210,9 +210,9 @@ export class CfServiceInstancesSignalConfigService {
}
const entries = Array.from(augmented.entries());
entries.sort(([, a], [, b]) => {
const bySpace = a.spaceName.localeCompare(b.spaceName, undefined, { numeric: true });
const bySpace = naturalCompare(a.spaceName, b.spaceName);
if (bySpace !== 0) return bySpace;
return a.orgName.localeCompare(b.orgName, undefined, { numeric: true });
return naturalCompare(a.orgName, b.orgName);
});
for (const [guid, { spaceName, orgName }] of entries) {
opts.push({ label: `${spaceName} - ${orgName}`, value: guid });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DestroyRef, Injectable, Injector, Signal, WritableSignal, computed, eff
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
import type { SignalListDropdownOption } from '@stratosui/core';
import { ListStateStore } from '@stratosui/core';
import { ListStateStore, naturalCompare } from '@stratosui/core';
import { CfUserListDiagnosticsService } from '../../../../../services/diagnostics/cf-user-list-diagnostics.service';
import { EndpointDataRegistry } from '../../../../../services/endpoint-data/endpoint-data.registry';
import type { EndpointDataService } from '../../../../../services/endpoint-data/endpoint-data.service';
Expand Down Expand Up @@ -132,7 +132,7 @@ export class CfUsersSignalConfigService {
readonly orgOptions: Signal<SignalListDropdownOption[]> = computed(() => {
const opts: SignalListDropdownOption[] = [{ label: 'All', value: null }];
const orgs = this.endpointDataService?.orgs() ?? [];
const sorted = [...orgs].sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
const sorted = [...orgs].sort((a, b) => naturalCompare(a.name, b.name));
for (const o of sorted) opts.push({ label: o.name, value: o.guid });
return opts;
});
Expand All @@ -148,7 +148,7 @@ export class CfUsersSignalConfigService {
if (org) {
const sorted = spaces
.filter(s => s.orgGuid === org)
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true }));
.sort((a, b) => naturalCompare(a.name, b.name));
for (const s of sorted) opts.push({ label: s.name, value: s.guid });
return opts;
}
Expand All @@ -159,9 +159,9 @@ export class CfUsersSignalConfigService {
orgName: orgNameByGuid.get(s.orgGuid) ?? '',
}));
augmented.sort((a, b) => {
const bySpace = a.spaceName.localeCompare(b.spaceName, undefined, { numeric: true });
const bySpace = naturalCompare(a.spaceName, b.spaceName);
if (bySpace !== 0) return bySpace;
return a.orgName.localeCompare(b.orgName, undefined, { numeric: true });
return naturalCompare(a.orgName, b.orgName);
});
for (const s of augmented) opts.push({ label: `${s.spaceName} - ${s.orgName}`, value: s.guid });
return opts;
Expand Down
Loading
Loading