1
0
mirror of https://github.com/bitwarden/web synced 2025-12-31 23:53:13 +00:00

[SG-32] Add Ownership badge to vault items (#1623)

* [feature] Base implementation of EUVR filter changes

* [refactor] Relocated vault-filters to app/modules

* [refactor] Reuse vault-filters component for organizations

* [refactor] Remove unused org filter component

* [bug] .gitmodules branch change

* [bug] Load organization filters after sync during login

* [refactor] Introduce a SharedModule

* [refactor] Created a home for loose components

* [refactor] Convert VaultComponent and OrgVaultComponent into a pair of modules

* [refactor] Implement <bit-menu> for organization filter actions

* [feature] Improve a11y standards of the vault filters module

* [bug] Recreate package-lock.json

* Fix build issue

* [bug] Remove duplicate this.go() call

* Add organization owner badge to vault items

* Fix capitalization

* Re-organize new components into modules

* Use tailwind css class

Co-authored-by: addison <addisonbeck1@gmail.com>
Co-authored-by: Hinton <oscar@oscarhinton.com>
This commit is contained in:
Robyn MacCallum
2022-05-03 14:05:16 -04:00
committed by GitHub
parent da683e0df4
commit d6bbb656f2
13 changed files with 160 additions and 13 deletions

View File

@@ -156,13 +156,15 @@ import { CollectionsComponent } from "../vault/collections.component";
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
import { ShareComponent } from "../vault/share.component";
import { PipesModule } from "./pipes/pipes.module";
import { SharedModule } from "./shared.module";
import { VaultFilterModule } from "./vault-filter/vault-filter.module";
import { OrganizationBadgeModule } from "./vault/modules/organization-badge/organization-badge.module";
// Please do not add to this list of declarations - we should refactor these into modules when doing so makes sense until there are none left.
// If you are building new functionality, please create or extend a feature module instead.
@NgModule({
imports: [SharedModule, VaultFilterModule],
imports: [SharedModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
declarations: [
PremiumBadgeComponent,
AcceptEmergencyComponent,

View File

@@ -0,0 +1,14 @@
import { Pipe, PipeTransform } from "@angular/core";
import { Organization } from "jslib-common/models/domain/organization";
@Pipe({
name: "orgNameFromId",
pure: true,
})
export class GetOrgNameFromIdPipe implements PipeTransform {
transform(value: string, organizations: Organization[]) {
const orgName = organizations.find((o) => o.id === value)?.name;
return orgName;
}
}

View File

@@ -0,0 +1,10 @@
import { NgModule } from "@angular/core";
import { GetOrgNameFromIdPipe } from "./get-organization-name.pipe";
@NgModule({
imports: [],
declarations: [GetOrgNameFromIdPipe],
exports: [GetOrgNameFromIdPipe],
})
export class PipesModule {}

View File

@@ -54,6 +54,7 @@
(onShareClicked)="shareCipher($event)"
(onCollectionsClicked)="editCipherCollections($event)"
(onCloneClicked)="cloneCipher($event)"
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
>
</app-vault-ciphers>
</div>

View File

@@ -159,6 +159,16 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
this.go();
}
async applyOrganizationFilter(orgId: string) {
if (orgId == null) {
this.activeFilter.resetOrganization();
this.activeFilter.myVaultOnly = true;
} else {
this.activeFilter.selectedOrganizationId = orgId;
}
await this.applyVaultFilter(this.activeFilter);
}
filterSearchText(searchText: string) {
this.ciphersComponent.searchText = searchText;
this.ciphersComponent.search(200);

View File

@@ -0,0 +1,12 @@
import { NgModule } from "@angular/core";
import { SharedModule } from "../../../shared.module";
import { OrganizationNameBadgeComponent } from "./organization-name-badge.component";
@NgModule({
imports: [SharedModule],
declarations: [OrganizationNameBadgeComponent],
exports: [OrganizationNameBadgeComponent],
})
export class OrganizationBadgeModule {}

View File

@@ -0,0 +1,9 @@
<button
bit-badge
[style.color]="textColor"
[style.background-color]="color"
appA11yTitle="{{ organizationName }}"
(click)="emitOnOrganizationClicked()"
>
{{ organizationName | ellipsis: 13 }}
</button>

View File

@@ -0,0 +1,69 @@
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
import { I18nService } from "jslib-common/abstractions/i18n.service";
@Component({
selector: "app-org-badge",
templateUrl: "organization-name-badge.component.html",
})
export class OrganizationNameBadgeComponent implements OnInit {
@Input() organizationName: string;
@Input() color: string;
@Output() onOrganizationClicked = new EventEmitter<string>();
textColor: string;
constructor(private i18nService: I18nService) {}
ngOnInit(): void {
if (this.organizationName == null || this.organizationName === "") {
this.organizationName = this.i18nService.t("me");
}
const upperData = this.organizationName.toUpperCase();
if (this.color == null) {
this.color = this.stringToColor(upperData);
}
this.textColor = this.pickTextColorBasedOnBgColor();
}
// This value currently isn't stored anywhere, only calculated in the app-avatar component
// Once we are allowing org colors to be changed and saved, change this out
private stringToColor(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = "#";
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xff;
color += ("00" + value.toString(16)).substr(-2);
}
return color;
}
// There are a few ways to calculate text color for contrast, this one seems to fit accessibility guidelines best.
// https://stackoverflow.com/a/3943023/6869691
private pickTextColorBasedOnBgColor() {
const color = this.color.charAt(0) === "#" ? this.color.substring(1, 7) : this.color;
const r = parseInt(color.substring(0, 2), 16); // hexToR
const g = parseInt(color.substring(2, 4), 16); // hexToG
const b = parseInt(color.substring(4, 6), 16); // hexToB
const uicolors = [r / 255, g / 255, b / 255];
const c = uicolors.map((c) => {
if (c <= 0.03928) {
return c / 12.92;
} else {
return Math.pow((c + 0.055) / 1.055, 2.4);
}
});
const L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
return L > 0.179 ? "black !important" : "white !important";
}
emitOnOrganizationClicked() {
this.onOrganizationClicked.emit();
}
}

View File

@@ -5,6 +5,7 @@ import { CipherService } from "jslib-common/abstractions/cipher.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from "jslib-common/abstractions/search.service";
@@ -37,7 +38,8 @@ export class CiphersComponent extends BaseCiphersComponent {
totpService: TotpService,
passwordRepromptService: PasswordRepromptService,
logService: LogService,
stateService: StateService
stateService: StateService,
organizationService: OrganizationService
) {
super(
searchService,
@@ -48,7 +50,8 @@ export class CiphersComponent extends BaseCiphersComponent {
totpService,
stateService,
passwordRepromptService,
logService
logService,
organizationService
);
}

View File

@@ -1,9 +1,11 @@
import { NgModule } from "@angular/core";
import { LooseComponentsModule } from "./modules/loose-components.module";
import { PipesModule } from "./modules/pipes/pipes.module";
import { SharedModule } from "./modules/shared.module";
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
import { IndividualVaultModule } from "./modules/vault/modules/individual-vault/individual-vault.module";
import { OrganizationBadgeModule } from "./modules/vault/modules/organization-badge/organization-badge.module";
import { OrganizationVaultModule } from "./modules/vault/modules/organization-vault/organization-vault.module";
@NgModule({
@@ -13,12 +15,16 @@ import { OrganizationVaultModule } from "./modules/vault/modules/organization-va
IndividualVaultModule,
OrganizationVaultModule,
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
],
exports: [
LooseComponentsModule,
IndividualVaultModule,
OrganizationVaultModule,
VaultFilterModule,
OrganizationBadgeModule,
PipesModule,
],
bootstrap: [],
})

View File

@@ -24,15 +24,6 @@
title="{{ 'editItem' | i18n }}"
>{{ c.name }}</a
>
<ng-container *ngIf="!organization && c.organizationId">
<i
class="bwi bwi-collection"
appStopProp
title="{{ 'shared' | i18n }}"
aria-hidden="true"
></i>
<span class="sr-only">{{ "shared" | i18n }}</span>
</ng-container>
<ng-container *ngIf="c.hasAttachments">
<i
class="bwi bwi-paperclip"
@@ -54,6 +45,13 @@
<br />
<small appStopProp>{{ c.subTitle }}</small>
</td>
<td *ngIf="organizations.length > 0 && !organization" class="tw-w-28">
<app-org-badge
organizationName="{{ c.organizationId | orgNameFromId: organizations }}"
[color]="!c.organizationId ? '#175ddc' : null"
(onOrganizationClicked)="onOrganizationClicked(c.organizationId)"
></app-org-badge>
</td>
<td class="table-list-options">
<div class="dropdown" appListDropdown>
<button

View File

@@ -5,6 +5,7 @@ import { CipherService } from "jslib-common/abstractions/cipher.service";
import { EventService } from "jslib-common/abstractions/event.service";
import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service";
import { OrganizationService } from "jslib-common/abstractions/organization.service";
import { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
import { SearchService } from "jslib-common/abstractions/search.service";
@@ -13,6 +14,7 @@ import { TotpService } from "jslib-common/abstractions/totp.service";
import { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
import { CipherType } from "jslib-common/enums/cipherType";
import { EventType } from "jslib-common/enums/eventType";
import { Organization } from "jslib-common/models/domain/organization";
import { CipherView } from "jslib-common/models/view/cipherView";
const MaxCheckedCount = 500;
@@ -27,12 +29,14 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
@Output() onShareClicked = new EventEmitter<CipherView>();
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
@Output() onCloneClicked = new EventEmitter<CipherView>();
@Output() onOrganzationBadgeClicked = new EventEmitter<string>();
pagedCiphers: CipherView[] = [];
pageSize = 200;
cipherType = CipherType;
actionPromise: Promise<any>;
userHasPremiumAccess = false;
organizations: Organization[] = [];
private didScroll = false;
private pagedCiphersCount = 0;
@@ -47,7 +51,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
protected totpService: TotpService,
protected stateService: StateService,
protected passwordRepromptService: PasswordRepromptService,
private logService: LogService
private logService: LogService,
private organizationService: OrganizationService
) {
super(searchService);
}
@@ -60,6 +65,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
// Do not use ngOnInit() for anything that requires sync data.
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
await super.load(filter, deleted);
this.organizations = await this.organizationService.getAll();
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
}
@@ -273,6 +279,10 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
}
}
onOrganizationClicked(organizationId: string) {
this.onOrganzationBadgeClicked.emit(organizationId);
}
protected deleteCipher(id: string, permanent: boolean) {
return permanent
? this.cipherService.deleteWithServer(id)

View File

@@ -421,6 +421,9 @@
"message": "Copy URI",
"description": "Copy URI to clipboard"
},
"me": {
"message": "Me"
},
"myVault": {
"message": "My Vault"
},