[PM-11201] Add the ability to sort by Name, Group, and Permission within the collection and item tables (#11370)

* Added sorting to vault, name, permission and group

Added default sorting

* Fixed import

* reverted test on template

* Only add sorting functionality to admin console

* changed code order
This commit is contained in:
SmithThe4th 2024-10-07 11:10:48 +02:00 committed by GitHub
parent e1d46045e0
commit 37faccb7e9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 168 additions and 6 deletions

View File

@ -16,13 +16,38 @@
"all" | i18n "all" | i18n
}}</label> }}</label>
</th> </th>
<th bitCell [class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'">{{ "name" | i18n }}</th> <!-- Organization vault -->
<th
*ngIf="showAdminActions"
bitCell
bitSortable="name"
[fn]="sortByName"
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<!-- Individual vault -->
<th
*ngIf="!showAdminActions"
bitCell
[class]="showExtraColumn ? 'lg:tw-w-3/5' : 'tw-w-full'"
>
{{ "name" | i18n }}
</th>
<th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell"> <th bitCell *ngIf="showOwner" class="tw-hidden tw-w-2/5 lg:tw-table-cell">
{{ "owner" | i18n }} {{ "owner" | i18n }}
</th> </th>
<th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th> <th bitCell class="tw-w-2/5" *ngIf="showCollections">{{ "collections" | i18n }}</th>
<th bitCell class="tw-w-2/5" *ngIf="showGroups">{{ "groups" | i18n }}</th> <th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
<th bitCell class="tw-w-2/5" *ngIf="showPermissionsColumn"> {{ "groups" | i18n }}
</th>
<th
bitCell
bitSortable="permissions"
[fn]="sortByPermissions"
class="tw-w-2/5"
*ngIf="showPermissionsColumn"
>
{{ "permission" | i18n }} {{ "permission" | i18n }}
</th> </th>
<th bitCell class="tw-w-12 tw-text-right"> <th bitCell class="tw-w-12 tw-text-right">

View File

@ -1,14 +1,19 @@
import { SelectionModel } from "@angular/cdk/collections"; 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 { 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { TableDataSource } from "@bitwarden/components"; import { TableDataSource } from "@bitwarden/components";
import { GroupView } from "../../../admin-console/organizations/core"; 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 { VaultItem } from "./vault-item";
import { VaultItemEvent } from "./vault-item-event"; import { VaultItemEvent } from "./vault-item-event";
@ -25,6 +30,7 @@ const MaxSelectionCount = 500;
// changeDetection: ChangeDetectionStrategy.OnPush, // changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class VaultItemsComponent { export class VaultItemsComponent {
protected i18nService = inject(I18nService);
protected RowHeight = RowHeight; protected RowHeight = RowHeight;
@Input() disabled: boolean; @Input() disabled: boolean;
@ -197,7 +203,7 @@ export class VaultItemsComponent {
private refreshItems() { private refreshItems() {
const collections: VaultItem[] = this.collections.map((collection) => ({ collection })); const collections: VaultItem[] = this.collections.map((collection) => ({ collection }));
const ciphers: VaultItem[] = this.ciphers.map((cipher) => ({ cipher })); 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(); this.selection.clear();
@ -208,6 +214,11 @@ export class VaultItemsComponent {
(item.collection !== undefined && item.collection.id !== Unassigned), (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; this.dataSource.data = items;
} }
@ -293,6 +304,112 @@ export class VaultItemsComponent {
return false; 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 { private hasPersonalItems(): boolean {
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null); return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
} }
@ -306,4 +423,24 @@ export class VaultItemsComponent {
private getUniqueOrganizationIds(): Set<string> { private getUniqueOrganizationIds(): Set<string> {
return new Set(this.selection.selected.flatMap((i) => i.cipher?.organizationId ?? [])); 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";
}
} }