diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.html b/apps/web/src/app/vault/components/vault-items/vault-items.component.html
index 99516907bb..55fa4bcb4e 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.component.html
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.html
@@ -16,13 +16,38 @@
"all" | i18n
}}
-
{{ "name" | i18n }} |
+
+
+ {{ "name" | i18n }}
+ |
+
+
+ {{ "name" | i18n }}
+ |
{{ "owner" | i18n }}
|
{{ "collections" | i18n }} |
- {{ "groups" | i18n }} |
-
+ |
+ {{ "groups" | i18n }}
+ |
+
{{ "permission" | i18n }}
|
diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
index 404a1affbd..44e38073c9 100644
--- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
+++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts
@@ -1,14 +1,19 @@
import { SelectionModel } from "@angular/cdk/collections";
-import { Component, EventEmitter, Input, Output } from "@angular/core";
+import { Component, EventEmitter, inject, Input, Output } from "@angular/core";
-import { Unassigned } from "@bitwarden/admin-console/common";
+import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
+import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core";
+import {
+ CollectionPermission,
+ convertToPermission,
+} from "./../../../admin-console/organizations/shared/components/access-selector/access-selector.models";
import { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event";
@@ -25,6 +30,7 @@ const MaxSelectionCount = 500;
// changeDetection: ChangeDetectionStrategy.OnPush,
})
export class VaultItemsComponent {
+ protected i18nService = inject(I18nService);
protected RowHeight = RowHeight;
@Input() disabled: boolean;
@@ -197,7 +203,7 @@ export class VaultItemsComponent {
private refreshItems() {
const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher }));
- const items: VaultItem[] = [].concat(collections).concat(ciphers);
+ let items: VaultItem[] = [].concat(collections).concat(ciphers);
this.selection.clear();
@@ -208,6 +214,11 @@ export class VaultItemsComponent {
(item.collection !== undefined && item.collection.id !== Unassigned),
);
+ // Apply sorting only for organization vault
+ if (this.showAdminActions) {
+ items = items.sort(this.sortByGroups);
+ }
+
this.dataSource.data = items;
}
@@ -293,6 +304,112 @@ export class VaultItemsComponent {
return false;
}
+ /**
+ * Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
+ */
+ protected sortByName = (a: VaultItem, b: VaultItem) => {
+ const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
+
+ // First, sort collections before ciphers
+ if (a.collection && !b.collection) {
+ return -1;
+ }
+ if (!a.collection && b.collection) {
+ return 1;
+ }
+
+ return getName(a).localeCompare(getName(b));
+ };
+
+ /**
+ * Sorts VaultItems based on group names
+ */
+ protected sortByGroups = (a: VaultItem, b: VaultItem): number => {
+ const getGroupNames = (item: VaultItem): string => {
+ if (item.collection instanceof CollectionAdminView) {
+ return item.collection.groups
+ .map((group) => this.getGroupName(group.id))
+ .filter(Boolean)
+ .join(",");
+ }
+
+ return "";
+ };
+
+ const aGroupNames = getGroupNames(a);
+ const bGroupNames = getGroupNames(b);
+
+ if (aGroupNames.length !== bGroupNames.length) {
+ return bGroupNames.length - aGroupNames.length;
+ }
+
+ return aGroupNames.localeCompare(bGroupNames);
+ };
+
+ /**
+ * Sorts VaultItems based on their permissions, with higher permissions taking precedence.
+ * If permissions are equal, it falls back to sorting by name.
+ */
+ protected sortByPermissions = (a: VaultItem, b: VaultItem): number => {
+ const getPermissionPriority = (item: VaultItem): number => {
+ if (item.collection instanceof CollectionAdminView) {
+ const permission = this.getCollectionPermission(item.collection);
+
+ switch (permission) {
+ case CollectionPermission.Manage:
+ return 5;
+ case CollectionPermission.Edit:
+ return 4;
+ case CollectionPermission.EditExceptPass:
+ return 3;
+ case CollectionPermission.View:
+ return 2;
+ case CollectionPermission.ViewExceptPass:
+ return 1;
+ case "NoAccess":
+ return 0;
+ }
+ }
+
+ return -1;
+ };
+
+ const priorityA = getPermissionPriority(a);
+ const priorityB = getPermissionPriority(b);
+
+ // Higher priority first
+ if (priorityA !== priorityB) {
+ return priorityB - priorityA;
+ }
+
+ return this.sortByName(a, b);
+ };
+
+ /**
+ * Default sorting function for vault items.
+ * Sorts by: 1. Collections before ciphers
+ * 2. Highest permission first
+ * 3. Alphabetical order of collections and ciphers
+ */
+ private defaultSort = (a: VaultItem, b: VaultItem) => {
+ // First, sort collections before ciphers
+ if (a.collection && !b.collection) {
+ return -1;
+ }
+ if (!a.collection && b.collection) {
+ return 1;
+ }
+
+ // Next, sort by permissions
+ const permissionSort = this.sortByPermissions(a, b);
+ if (permissionSort !== 0) {
+ return permissionSort;
+ }
+
+ // Finally, sort by name
+ return this.sortByName(a, b);
+ };
+
private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
}
@@ -306,4 +423,24 @@ export class VaultItemsComponent {
private getUniqueOrganizationIds(): Set {
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? []));
}
+
+ private getGroupName(groupId: string): string | undefined {
+ return this.allGroups.find((g) => g.id === groupId)?.name;
+ }
+
+ private getCollectionPermission(
+ collection: CollectionAdminView,
+ ): CollectionPermission | "NoAccess" {
+ const organization = this.allOrganizations.find((o) => o.id === collection.organizationId);
+
+ if (collection.id == Unassigned && organization?.canEditUnassignedCiphers) {
+ return CollectionPermission.Edit;
+ }
+
+ if (collection.assigned) {
+ return convertToPermission(collection);
+ }
+
+ return "NoAccess";
+ }
}
|