[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:
parent
e1d46045e0
commit
37faccb7e9
|
@ -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">
|
||||||
|
|
|
@ -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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue