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:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user