mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-21 11:35:34 +01:00
[PM-11201][CL-507] Add the ability to sort by Name, Group, and Permission within the collection and item tables (#11453)
* 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 * Fixed leftover test for sortingn * Fixed reference * sort permissions by ascending order * Fixed bug where a collection had multiple groups and sorting alphbatically didn't happen correctly all the time * Fixed bug whne creating a new cipher item * Introduced fnFactory to create a sort function with direction provided * Used new sort function to make collections always remain at the top and ciphers below * extracted logic to always sort collections at the top Added similar sorting to sortBygroup * removed org vault check * remove unused service * Sort only collections * Got rid of sortFn factory in favour of passing the direction as an optional parameter * Removed tenary * get cipher permissions * Use all collections to filter collection ids * Fixed ascending and descending issues * Added functionality to default sort in descending order * default sort permissions in descending order * Refactored setActive to not pass direction as a paramater
This commit is contained in:
parent
872f36752f
commit
faf7e3d315
@ -16,13 +16,39 @@
|
||||
"all" | i18n
|
||||
}}</label>
|
||||
</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">
|
||||
{{ "owner" | 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 class="tw-w-2/5" *ngIf="showPermissionsColumn">
|
||||
<th bitCell bitSortable="groups" [fn]="sortByGroups" class="tw-w-2/5" *ngIf="showGroups">
|
||||
{{ "groups" | i18n }}
|
||||
</th>
|
||||
<th
|
||||
bitCell
|
||||
bitSortable="permissions"
|
||||
default="desc"
|
||||
[fn]="sortByPermissions"
|
||||
class="tw-w-2/5"
|
||||
*ngIf="showPermissionsColumn"
|
||||
>
|
||||
{{ "permission" | i18n }}
|
||||
</th>
|
||||
<th bitCell class="tw-w-12 tw-text-right">
|
||||
|
@ -1,13 +1,17 @@
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
|
||||
import { CollectionView, Unassigned } from "@bitwarden/admin-console/common";
|
||||
import { CollectionView, Unassigned, CollectionAdminView } from "@bitwarden/admin-console/common";
|
||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { TableDataSource } from "@bitwarden/components";
|
||||
import { SortDirection, 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";
|
||||
|
||||
@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`;
|
||||
|
||||
const MaxSelectionCount = 500;
|
||||
|
||||
type ItemPermission = CollectionPermission | "NoAccess";
|
||||
|
||||
@Component({
|
||||
selector: "app-vault-items",
|
||||
templateUrl: "vault-items.component.html",
|
||||
@ -333,6 +339,119 @@ export class VaultItemsComponent {
|
||||
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems, grouping collections before ciphers, and sorting each group alphabetically by name.
|
||||
*/
|
||||
protected sortByName = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
/**
|
||||
* Sorts VaultItems based on group names
|
||||
*/
|
||||
protected sortByGroups = (a: VaultItem, b: VaultItem, direction: SortDirection) => {
|
||||
if (
|
||||
!(a.collection instanceof CollectionAdminView) &&
|
||||
!(b.collection instanceof CollectionAdminView)
|
||||
) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const getFirstGroupName = (collection: CollectionAdminView): string => {
|
||||
if (collection.groups.length > 0) {
|
||||
return collection.groups.map((group) => this.getGroupName(group.id) || "").sort()[0];
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
const aGroupName = getFirstGroupName(a.collection as CollectionAdminView);
|
||||
const bGroupName = getFirstGroupName(b.collection as CollectionAdminView);
|
||||
|
||||
// Collections with groups come before collections without groups.
|
||||
// If a collection has no groups, getFirstGroupName returns null.
|
||||
if (aGroupName === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (bGroupName === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return aGroupName.localeCompare(bGroupName);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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, direction: SortDirection) => {
|
||||
const getPermissionPriority = (item: VaultItem): number => {
|
||||
const permission = item.collection
|
||||
? this.getCollectionPermission(item.collection)
|
||||
: this.getCipherPermission(item.cipher);
|
||||
|
||||
const priorityMap = {
|
||||
[CollectionPermission.Manage]: 5,
|
||||
[CollectionPermission.Edit]: 4,
|
||||
[CollectionPermission.EditExceptPass]: 3,
|
||||
[CollectionPermission.View]: 2,
|
||||
[CollectionPermission.ViewExceptPass]: 1,
|
||||
NoAccess: 0,
|
||||
};
|
||||
|
||||
return priorityMap[permission] ?? -1;
|
||||
};
|
||||
|
||||
// Collections before ciphers
|
||||
const collectionCompare = this.prioritizeCollections(a, b, direction);
|
||||
if (collectionCompare !== 0) {
|
||||
return collectionCompare;
|
||||
}
|
||||
|
||||
const priorityA = getPermissionPriority(a);
|
||||
const priorityB = getPermissionPriority(b);
|
||||
|
||||
// Higher priority first
|
||||
if (priorityA !== priorityB) {
|
||||
return priorityA - priorityB;
|
||||
}
|
||||
|
||||
return this.compareNames(a, b);
|
||||
};
|
||||
|
||||
private compareNames(a: VaultItem, b: VaultItem): number {
|
||||
const getName = (item: VaultItem) => item.collection?.name || item.cipher?.name;
|
||||
return getName(a).localeCompare(getName(b));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts VaultItems by prioritizing collections over ciphers.
|
||||
* Collections are always placed before ciphers, regardless of the sorting direction.
|
||||
*/
|
||||
private prioritizeCollections(a: VaultItem, b: VaultItem, direction: SortDirection): number {
|
||||
if (a.collection && !b.collection) {
|
||||
return direction === "asc" ? -1 : 1;
|
||||
}
|
||||
|
||||
if (!a.collection && b.collection) {
|
||||
return direction === "asc" ? 1 : -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private hasPersonalItems(): boolean {
|
||||
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
||||
}
|
||||
@ -346,4 +465,58 @@ export class VaultItemsComponent {
|
||||
private getUniqueOrganizationIds(): Set<string> {
|
||||
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: CollectionView): ItemPermission {
|
||||
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";
|
||||
}
|
||||
|
||||
private getCipherPermission(cipher: CipherView): ItemPermission {
|
||||
if (!cipher.organizationId || cipher.collectionIds.length === 0) {
|
||||
return CollectionPermission.Manage;
|
||||
}
|
||||
|
||||
const filteredCollections = this.allCollections?.filter((collection) => {
|
||||
if (collection.assigned) {
|
||||
return cipher.collectionIds.find((id) => {
|
||||
if (collection.id === id) {
|
||||
return collection;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (filteredCollections?.length === 1) {
|
||||
return convertToPermission(filteredCollections[0]);
|
||||
}
|
||||
|
||||
if (filteredCollections?.length > 0) {
|
||||
const permissions = filteredCollections.map((collection) => convertToPermission(collection));
|
||||
|
||||
const orderedPermissions = [
|
||||
CollectionPermission.Manage,
|
||||
CollectionPermission.Edit,
|
||||
CollectionPermission.EditExceptPass,
|
||||
CollectionPermission.View,
|
||||
CollectionPermission.ViewExceptPass,
|
||||
];
|
||||
|
||||
return orderedPermissions.find((perm) => permissions.includes(perm));
|
||||
}
|
||||
|
||||
return "NoAccess";
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||
import { Component, HostBinding, Input, OnInit } from "@angular/core";
|
||||
|
||||
import type { SortFn } from "./table-data-source";
|
||||
import type { SortDirection, SortFn } from "./table-data-source";
|
||||
import { TableComponent } from "./table.component";
|
||||
|
||||
@Component({
|
||||
@ -19,12 +19,16 @@ export class SortableComponent implements OnInit {
|
||||
*/
|
||||
@Input() bitSortable: string;
|
||||
|
||||
private _default: boolean;
|
||||
private _default: SortDirection | boolean = false;
|
||||
/**
|
||||
* Mark the column as the default sort column
|
||||
*/
|
||||
@Input() set default(value: boolean | "") {
|
||||
this._default = coerceBooleanProperty(value);
|
||||
@Input() set default(value: SortDirection | boolean | "") {
|
||||
if (value === "desc" || value === "asc") {
|
||||
this._default = value;
|
||||
} else {
|
||||
this._default = coerceBooleanProperty(value) ? "asc" : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,6 +36,11 @@ export class SortableComponent implements OnInit {
|
||||
*
|
||||
* @example
|
||||
* fn = (a, b) => a.name.localeCompare(b.name)
|
||||
*
|
||||
* fn = (a, b, direction) => {
|
||||
* const result = a.name.localeCompare(b.name)
|
||||
* return direction === 'asc' ? result : -result;
|
||||
* }
|
||||
*/
|
||||
@Input() fn: SortFn;
|
||||
|
||||
@ -52,8 +61,18 @@ export class SortableComponent implements OnInit {
|
||||
|
||||
protected setActive() {
|
||||
if (this.table.dataSource) {
|
||||
const direction = this.isActive && this.direction === "asc" ? "desc" : "asc";
|
||||
this.table.dataSource.sort = { column: this.bitSortable, direction: direction, fn: this.fn };
|
||||
const defaultDirection = this._default === "desc" ? "desc" : "asc";
|
||||
const direction = this.isActive
|
||||
? this.direction === "asc"
|
||||
? "desc"
|
||||
: "asc"
|
||||
: defaultDirection;
|
||||
|
||||
this.table.dataSource.sort = {
|
||||
column: this.bitSortable,
|
||||
direction: direction,
|
||||
fn: this.fn,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { DataSource } from "@angular/cdk/collections";
|
||||
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
|
||||
|
||||
export type SortDirection = "asc" | "desc";
|
||||
export type SortFn = (a: any, b: any) => number;
|
||||
export type SortFn = (a: any, b: any, direction?: SortDirection) => number;
|
||||
export type Sort = {
|
||||
column?: string;
|
||||
direction: SortDirection;
|
||||
@ -166,7 +166,7 @@ export class TableDataSource<T> extends DataSource<T> {
|
||||
return data.sort((a, b) => {
|
||||
// If a custom sort function is provided, use it instead of the default.
|
||||
if (sort.fn) {
|
||||
return sort.fn(a, b) * directionModifier;
|
||||
return sort.fn(a, b, sort.direction) * directionModifier;
|
||||
}
|
||||
|
||||
let valueA = this.sortingDataAccessor(a, column);
|
||||
|
@ -105,7 +105,7 @@ within the `ng-template`which provides access to the rows using `let-rows$`.
|
||||
|
||||
We provide a simple component for displaying sortable column headers. The `bitSortable` component
|
||||
wires up to the `TableDataSource` and will automatically sort the data when clicked and display an
|
||||
indicator for which column is currently sorted. The dafault sorting can be specified by setting the
|
||||
indicator for which column is currently sorted. The default sorting can be specified by setting the
|
||||
`default`.
|
||||
|
||||
```html
|
||||
@ -113,10 +113,23 @@ indicator for which column is currently sorted. The dafault sorting can be speci
|
||||
<th bitCell bitSortable="name" default>Name</th>
|
||||
```
|
||||
|
||||
For default sorting in descending order, set default="desc"
|
||||
|
||||
```html
|
||||
<th bitCell bitSortable="name" default="desc">Name</th>
|
||||
```
|
||||
|
||||
It's also possible to define a custom sorting function by setting the `fn` input.
|
||||
|
||||
```ts
|
||||
// Basic sort function
|
||||
const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1);
|
||||
|
||||
// Direction aware sort function
|
||||
const sortByName = (a: T, b: T, direction?: SortDirection) => {
|
||||
const result = a.name.localeCompare(b.name);
|
||||
return direction === "asc" ? result : -result;
|
||||
};
|
||||
```
|
||||
|
||||
### Filtering
|
||||
|
Loading…
Reference in New Issue
Block a user