1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-19 17:53:39 +00:00

[AC-974] [Technical Dependency] Refactor Vault Tables (#4967)

* [EC-974] feat: scaffold new vault-items component

* [EC-974] feat: add basic mocked data to story

* [EC-974] feat: add initial table version

* [EC-974] chore: split rows into separate components

* [EC-974] chore: rename item row to cipher row

* [EC-974] feat: create common vault item interface

* [EC-974] feat: use cdk virtual scrolling

* [EC-974] fix: tweak `itemSize`

* [EC-974] chore: move vault-items component to app/vault folder

* [EC-974] feat: initial support for extra column

* [EC-974] feat: start adding org badge

Having issues with modules import

* [EC-974] feat: add working owner column on collections row

* [EC-974] feat: add owner to ciphers

* [EC-974] fix: org name badge bugs when reused

* [EC-974] feat: fix and translate columns

* [EC-974] feat: allow collections to be non-editable

* [EC-974] feat: use data source

* [EC-974] fix: remove profile name from vault items

* [EC-974] feat: add events

* [EC-974] feat: add support for copy event

* [EC-974] feat: add support for collections column

* [EC-974] feat: add support for group badges

* [EC-974] chore: rename for consistency

* [EC-974] feat: change story to use template

* [EC-974] feat: add support for launching

* [EC-974] feat: add support for attachements

* [EC-974] feat: add stories for all use-cases

* [EC-974] feat: add support for cloning

* [EC-974] feat: add support for moving to organization

* [EC-974] feat: add support for editing cipher collections

* [EC-974] feat: add support for event logs

* [EC-974] feat: add support for trash/delete/restore

* [EC-974] feat: add support for editing collections

* [EC-974] feat: add support for access and delete collections

* [EC-974] feat: don't show menu if it's empty

* [EC-974] feat: initial buggy implementation of selection

* [EC-974] feat: implement bulk move

* [EC-974] feat: add support for bulk moving to org

* [EC-974] feat: add support for bulk restore

* [EC-974] feat: add support for bulk delete

* [EC-974] feat: add ability to disable the table

* [EC-974] feat: create new filter function based on routed model

* [EC-974] wip: start replacing vault items component

* [EC-974] feat: add support for fetching ciphers

* [EC-974] feat: hide trash by default

* [EC-974] feat: add support for the rest of the data

* [EC-974] feat: implement organization filtering using org badge

* [EC-974] feat: fix navigation to "my vault"

* [EC-974] feat: don't show bulk move options when filtering on org items

* [EC-974] feat: prepare for disabling table

* [EC-974] fix: add missing router link to collections

* [EC-974] feat: connect all outputs

* [EC-974] fix: list not properly refreshing after delete

* [EC-974] feat: limit selection to top 500 items

* [EC-974] feat: implement refresh tracker

* [EC-974] feat: use refresh tracker to disable vault items

* [EC-974] feat: add empty list message

* [AC-974] feat: add initial load with spinner and fix empty -> show list bug

* [EC-974] feat: replace action promise with simple loading boolean

* [EC-974] feat: refactor individual vault header

* [EC-974] feat: cache and make observables long lived

* [EC-974] feat: implement searching

* [EC-974] feat: add support for showing collections

* [EC-974] feat: add ciphers to org vault list

* [EC-974] feat: show group column

* [EC-974] feat: tweak settings for org vault

* [EC-974] feat: implement search using query params

* [EC-974] feat: add support for events that are common with individual vault

* [EC-974] feat: add support for all events

* [EC-974] feat: add support for empty list message and no permission message

* [EC-974] feat: always show table

* [EC-974] feat: fix layout issues due to incorrect row height

* [EC-974] feat: disable list if empty

* [EC-974] feat: improve sync handling

* [EC-974] feat: improve initial loading sequence

* [EC-974] feat: improve initial load sequence in org vault

* [EC-974] refactor: simplify and optimize data fetching

* [EC-974] feat: use observables from org service

* [EC-974] feat: refactor org vault header

* [EC-974] fix: data not refreshing properly

* [EC-974] fix: avoid collection double fetching

* [EC-974] chore: clean up refresh tracker

* [EC-974] chore: clean up old vault-items components

* [EC-974] chore: clean up old code in vault component

* [EC-974] fix: reduce rows in story

The story ends up too big for chromatic.

* [EC-974] docs: tweak and typo fixes of asyncToObservable docs comment

* [EC-974] fix: `attachements` typo

* [EC-974] chore: remove review question comment

* [EC-974] chore: remove unused `securityCode` if statement

* [EC-974] fix: use `takeUntill` for legacy dialogs

* [EC-974] fix: use CollectionDialogTabType instead of custom strings

* [EC-974] fix: copy implementation

* [EC-974] fix: use `useTotp` to check for premium features

* [EC-974] fix: use `tw-sr-only`

* [EC-974] chore: remove unecessary eslint disable

* [EC-974] fix: clarify vault item event naming

* [EC-974] fix: remove `new` from `app-new-vault-items`

* [EC-974] fix: collection row not disabled during loading

* [EC-974] chore: simplify router links without path changes

* [EC-974] feat: invert filter function to get rid of `cipherPassesFilter`

* [EC-974] fix: move `NestingDelimiter` to collection view

Nesting is currently only a presentational construct, and the concept does not exist in our domain.

* [EC-974] fix: org vault header not updating when switching org

* [EC-974] fix: table sizing jumping around

* [EC-974] fix: list not refreshing after restoring item

* [EC-974] fix: re-add missing unassigned collection

* [EC-974] fix don't show new item button in unassigned collection

* [EC-974] fix: navigations always leading to individual vault

* [EC-974] fix: remove checkbox when collections are not editable

* [EC-974] fix: null reference blocking collections from refreshing after delete

* [EC-974] fix: don't show checbox for collections that user does not have permissions to delete

* [EC-974] fix: navigate away from deleted folder

* [EC-974] chore: clean up un-used output

* [EC-974] fix: org badge changing color randomly

* [EC-974] fix: lint issues after merge

* [EC-974] fix: lower amount of ciphers in story

chromatic doesn't like large snapshots

* [EC-974] fix: "all collections" not taking `organizationId` filter into account

* [EC-974] fix: make sure unassigned appears in table too

* [EC-974] feat: add unassigned to storybook

* [EC-974] fix: forced row height not being applied properly

* [EC-974] fix: hopefully fix table jumping once and for all

* [EC-974] fix: attachemnts getting hidden

* [EC-974] feat: extract collection editable logic to parent component

* [EC-974] feat: separately track editable items

* [EC-974] feat: optimize permission checks

* [EC-974] fix: bulk menu hidden on chrome

:lolcry:

* [EC-974] fix: don't show groups column if org doesnt use groups

* [EC-974] feat: make entire row clickable

* [EC-974] fix: typo resulting in non-editable collections
This commit is contained in:
Andreas Coroiu
2023-04-13 20:48:29 +02:00
committed by GitHub
parent 5f26e58538
commit 0bc6add5c3
45 changed files with 3011 additions and 1775 deletions

View File

@@ -1,14 +1,10 @@
import { Component, EventEmitter, Input, Output } from "@angular/core";
import { Router } from "@angular/router";
import { firstValueFrom, lastValueFrom } from "rxjs";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { CollectionView } from "@bitwarden/common/admin-console/models/view/collection.view";
import { ProductType } from "@bitwarden/common/enums";
import { TreeNode } from "@bitwarden/common/models/domain/tree-node";
import {
@@ -22,90 +18,95 @@ import {
CollectionAdminService,
CollectionAdminView,
} from "../../../admin-console/organizations/core";
import { CollectionDialogTabType } from "../../../admin-console/organizations/shared";
import {
CollectionDialogResult,
CollectionDialogTabType,
openCollectionDialog,
} from "../../../admin-console/organizations/shared/components/collection-dialog";
import { VaultFilterService } from "../../individual-vault/vault-filter/services/abstractions/vault-filter.service";
import { VaultFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.model";
import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type";
All,
RoutedVaultFilterModel,
Unassigned,
} from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model";
@Component({
selector: "app-org-vault-header",
templateUrl: "./vault-header.component.html",
})
export class VaultHeaderComponent {
protected All = All;
protected Unassigned = Unassigned;
/**
* The organization currently being viewed
* Boolean to determine the loading state of the header.
* Shows a loading spinner if set to true
*/
@Input() loading: boolean;
/** Current active fitler */
@Input() filter: RoutedVaultFilterModel;
/** The organization currently being viewed */
@Input() organization: Organization;
/**
* Promise that is used to determine the loading state of the header via the ApiAction directive.
* When the promise exists and is not resolved, the loading spinner will be shown.
*/
@Input() actionPromise: Promise<any>;
/** Currently selected collection */
@Input() collection?: TreeNode<CollectionAdminView>;
/**
* The filter being actively applied to the vault view
*/
@Input() activeFilter: VaultFilter;
/**
* Emits when the active filter has been modified by the header
*/
@Output() activeFilterChanged = new EventEmitter<VaultFilter>();
/**
* Emits an event when a collection is modified or deleted via the header collection dropdown menu
*/
@Output() onCollectionChanged = new EventEmitter<CollectionView | null>();
/**
* Emits an event when the new item button is clicked in the header
*/
/** Emits an event when the new item button is clicked in the header */
@Output() onAddCipher = new EventEmitter<void>();
/** Emits an event when the new collection button is clicked in the header */
@Output() onAddCollection = new EventEmitter<void>();
/** Emits an event when the edit collection button is clicked in the header */
@Output() onEditCollection = new EventEmitter<{ tab: CollectionDialogTabType }>();
/** Emits an event when the delete collection button is clicked in the header */
@Output() onDeleteCollection = new EventEmitter<void>();
protected CollectionDialogTabType = CollectionDialogTabType;
protected organizations$ = this.organizationService.organizations$;
constructor(
private organizationService: OrganizationService,
private i18nService: I18nService,
private dialogService: DialogService,
private vaultFilterService: VaultFilterService,
private platformUtilsService: PlatformUtilsService,
private apiService: ApiService,
private logService: LogService,
private collectionAdminService: CollectionAdminService,
private router: Router
) {}
/**
* The id of the organization that is currently being filtered on.
* This can come from a collection filter, organization filter, or the current organization when viewed
* in the organization admin console and no other filters are applied.
*/
get activeOrganizationId() {
if (this.activeFilter.selectedCollectionNode != null) {
return this.activeFilter.selectedCollectionNode.node.organizationId;
}
if (this.activeFilter.selectedOrganizationNode != null) {
return this.activeFilter.selectedOrganizationNode.node.id;
}
return this.organization.id;
}
get title() {
if (this.activeFilter.isCollectionSelected) {
return this.activeFilter.selectedCollectionNode.node.name;
if (this.collection !== undefined) {
return this.collection.node.name;
}
if (this.activeFilter.isUnassignedCollectionSelected) {
if (this.filter.collectionId === Unassigned) {
return this.i18nService.t("unassigned");
}
return `${this.organization.name} ${this.i18nService.t("vault").toLowerCase()}`;
}
protected get showBreadcrumbs() {
return this.filter.collectionId !== undefined && this.filter.collectionId !== All;
}
/**
* A list of collection filters that form a chain from the organization root to currently selected collection.
* Begins from the organization root and excludes the currently selected collection.
*/
protected get collections() {
if (this.collection == undefined) {
return [];
}
const collections = [this.collection];
while (collections[collections.length - 1].parent != undefined) {
collections.push(collections[collections.length - 1].parent);
}
return collections
.slice(1)
.reverse()
.map((treeNode) => treeNode.node);
}
private showFreeOrgUpgradeDialog(): void {
const orgUpgradeSimpleDialogOpts: SimpleDialogOptions = {
title: this.i18nService.t("upgradeOrganization"),
@@ -140,23 +141,16 @@ export class VaultHeaderComponent {
});
}
applyCollectionFilter(collection: TreeNode<CollectionFilter>) {
const filter = this.activeFilter;
filter.resetFilter();
filter.selectedCollectionNode = collection;
this.activeFilterChanged.emit(filter);
}
canEditCollection(c: CollectionAdminView): boolean {
// Only edit collections if we're in the org vault and not editing "Unassigned"
if (this.organization === undefined || c.id === null) {
get canEditCollection(): boolean {
// Only edit collections if not editing "Unassigned"
if (this.collection === undefined) {
return false;
}
// Otherwise, check if we can edit the specified collection
return (
this.organization.canEditAnyCollection ||
(this.organization.canEditAssignedCollections && c.assigned)
(this.organization.canEditAssignedCollections && this.collection?.node.assigned)
);
}
@@ -173,77 +167,27 @@ export class VaultHeaderComponent {
}
}
const dialog = openCollectionDialog(this.dialogService, {
data: {
organizationId: this.organization?.id,
parentCollectionId: this.activeFilter.collectionId,
},
});
const result = await lastValueFrom(dialog.closed);
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
this.onCollectionChanged.emit(null);
}
this.onAddCollection.emit();
}
async editCollection(c: CollectionView, tab: "info" | "access"): Promise<void> {
const tabType = tab == "info" ? CollectionDialogTabType.Info : CollectionDialogTabType.Access;
const dialog = openCollectionDialog(this.dialogService, {
data: { collectionId: c?.id, organizationId: this.organization?.id, initialTab: tabType },
});
const result = await lastValueFrom(dialog.closed);
if (result === CollectionDialogResult.Saved || result === CollectionDialogResult.Deleted) {
this.onCollectionChanged.emit(c);
}
async editCollection(tab: CollectionDialogTabType): Promise<void> {
this.onEditCollection.emit({ tab });
}
canDeleteCollection(c: CollectionAdminView): boolean {
// Only delete collections if we're in the org vault and not deleting "Unassigned"
if (this.organization === undefined || c.id === null) {
get canDeleteCollection(): boolean {
// Only delete collections if not deleting "Unassigned"
if (this.collection === undefined) {
return false;
}
// Otherwise, check if we can delete the specified collection
return (
this.organization?.canDeleteAnyCollection ||
(this.organization?.canDeleteAssignedCollections && c.assigned)
(this.organization?.canDeleteAssignedCollections && this.collection.node.assigned)
);
}
async deleteCollection(collection: CollectionView): Promise<void> {
if (
!this.organization.canDeleteAnyCollection &&
!this.organization.canDeleteAssignedCollections
) {
this.platformUtilsService.showToast(
"error",
this.i18nService.t("errorOccurred"),
this.i18nService.t("missingPermissions")
);
return;
}
const confirmed = await this.platformUtilsService.showDialog(
this.i18nService.t("deleteCollectionConfirmation"),
collection.name,
this.i18nService.t("yes"),
this.i18nService.t("no"),
"warning"
);
if (!confirmed) {
return;
}
try {
this.actionPromise = this.apiService.deleteCollection(this.organization?.id, collection.id);
await this.actionPromise;
this.platformUtilsService.showToast(
"success",
null,
this.i18nService.t("deletedCollectionId", collection.name)
);
this.onCollectionChanged.emit(collection);
} catch (e) {
this.logService.error(e);
}
deleteCollection() {
this.onDeleteCollection.emit();
}
}