mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-22 11:45:59 +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
|
"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"
|
||||||
|
default="desc"
|
||||||
|
[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,13 +1,17 @@
|
|||||||
import { SelectionModel } from "@angular/cdk/collections";
|
import { SelectionModel } from "@angular/cdk/collections";
|
||||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
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 { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
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 { 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";
|
||||||
|
|
||||||
@ -17,6 +21,8 @@ export const RowHeightClass = `tw-h-[65px]`;
|
|||||||
|
|
||||||
const MaxSelectionCount = 500;
|
const MaxSelectionCount = 500;
|
||||||
|
|
||||||
|
type ItemPermission = CollectionPermission | "NoAccess";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-vault-items",
|
selector: "app-vault-items",
|
||||||
templateUrl: "vault-items.component.html",
|
templateUrl: "vault-items.component.html",
|
||||||
@ -333,6 +339,119 @@ export class VaultItemsComponent {
|
|||||||
return (canEditOrManageAllCiphers || this.allCiphersHaveEditAccess()) && collectionNotSelected;
|
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 {
|
private hasPersonalItems(): boolean {
|
||||||
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
return this.selection.selected.some(({ cipher }) => cipher?.organizationId === null);
|
||||||
}
|
}
|
||||||
@ -346,4 +465,58 @@ 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: 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 { coerceBooleanProperty } from "@angular/cdk/coercion";
|
||||||
import { Component, HostBinding, Input, OnInit } from "@angular/core";
|
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";
|
import { TableComponent } from "./table.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@ -19,12 +19,16 @@ export class SortableComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() bitSortable: string;
|
@Input() bitSortable: string;
|
||||||
|
|
||||||
private _default: boolean;
|
private _default: SortDirection | boolean = false;
|
||||||
/**
|
/**
|
||||||
* Mark the column as the default sort column
|
* Mark the column as the default sort column
|
||||||
*/
|
*/
|
||||||
@Input() set default(value: boolean | "") {
|
@Input() set default(value: SortDirection | boolean | "") {
|
||||||
this._default = coerceBooleanProperty(value);
|
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
|
* @example
|
||||||
* fn = (a, b) => a.name.localeCompare(b.name)
|
* 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;
|
@Input() fn: SortFn;
|
||||||
|
|
||||||
@ -52,8 +61,18 @@ export class SortableComponent implements OnInit {
|
|||||||
|
|
||||||
protected setActive() {
|
protected setActive() {
|
||||||
if (this.table.dataSource) {
|
if (this.table.dataSource) {
|
||||||
const direction = this.isActive && this.direction === "asc" ? "desc" : "asc";
|
const defaultDirection = this._default === "desc" ? "desc" : "asc";
|
||||||
this.table.dataSource.sort = { column: this.bitSortable, direction: direction, fn: this.fn };
|
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";
|
import { BehaviorSubject, combineLatest, map, Observable, Subscription } from "rxjs";
|
||||||
|
|
||||||
export type SortDirection = "asc" | "desc";
|
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 = {
|
export type Sort = {
|
||||||
column?: string;
|
column?: string;
|
||||||
direction: SortDirection;
|
direction: SortDirection;
|
||||||
@ -166,7 +166,7 @@ export class TableDataSource<T> extends DataSource<T> {
|
|||||||
return data.sort((a, b) => {
|
return data.sort((a, b) => {
|
||||||
// If a custom sort function is provided, use it instead of the default.
|
// If a custom sort function is provided, use it instead of the default.
|
||||||
if (sort.fn) {
|
if (sort.fn) {
|
||||||
return sort.fn(a, b) * directionModifier;
|
return sort.fn(a, b, sort.direction) * directionModifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
let valueA = this.sortingDataAccessor(a, column);
|
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
|
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
|
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`.
|
`default`.
|
||||||
|
|
||||||
```html
|
```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>
|
<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.
|
It's also possible to define a custom sorting function by setting the `fn` input.
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
// Basic sort function
|
||||||
const sortFn = (a: T, b: T) => (a.id > b.id ? 1 : -1);
|
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
|
### Filtering
|
||||||
|
Loading…
Reference in New Issue
Block a user