mirror of
https://github.com/bitwarden/web
synced 2026-01-06 10:33:17 +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:
@@ -156,13 +156,15 @@ import { CollectionsComponent } from "../vault/collections.component";
|
|||||||
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
import { FolderAddEditComponent } from "../vault/folder-add-edit.component";
|
||||||
import { ShareComponent } from "../vault/share.component";
|
import { ShareComponent } from "../vault/share.component";
|
||||||
|
|
||||||
|
import { PipesModule } from "./pipes/pipes.module";
|
||||||
import { SharedModule } from "./shared.module";
|
import { SharedModule } from "./shared.module";
|
||||||
import { VaultFilterModule } from "./vault-filter/vault-filter.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.
|
// 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.
|
// If you are building new functionality, please create or extend a feature module instead.
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, VaultFilterModule],
|
imports: [SharedModule, VaultFilterModule, OrganizationBadgeModule, PipesModule],
|
||||||
declarations: [
|
declarations: [
|
||||||
PremiumBadgeComponent,
|
PremiumBadgeComponent,
|
||||||
AcceptEmergencyComponent,
|
AcceptEmergencyComponent,
|
||||||
|
|||||||
14
src/app/modules/pipes/get-organization-name.pipe.ts
Normal file
14
src/app/modules/pipes/get-organization-name.pipe.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/modules/pipes/pipes.module.ts
Normal file
10
src/app/modules/pipes/pipes.module.ts
Normal 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 {}
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
(onShareClicked)="shareCipher($event)"
|
(onShareClicked)="shareCipher($event)"
|
||||||
(onCollectionsClicked)="editCipherCollections($event)"
|
(onCollectionsClicked)="editCipherCollections($event)"
|
||||||
(onCloneClicked)="cloneCipher($event)"
|
(onCloneClicked)="cloneCipher($event)"
|
||||||
|
(onOrganzationBadgeClicked)="applyOrganizationFilter($event)"
|
||||||
>
|
>
|
||||||
</app-vault-ciphers>
|
</app-vault-ciphers>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -159,6 +159,16 @@ export class IndividualVaultComponent implements OnInit, OnDestroy {
|
|||||||
this.go();
|
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) {
|
filterSearchText(searchText: string) {
|
||||||
this.ciphersComponent.searchText = searchText;
|
this.ciphersComponent.searchText = searchText;
|
||||||
this.ciphersComponent.search(200);
|
this.ciphersComponent.search(200);
|
||||||
|
|||||||
@@ -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 {}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<button
|
||||||
|
bit-badge
|
||||||
|
[style.color]="textColor"
|
||||||
|
[style.background-color]="color"
|
||||||
|
appA11yTitle="{{ organizationName }}"
|
||||||
|
(click)="emitOnOrganizationClicked()"
|
||||||
|
>
|
||||||
|
{{ organizationName | ellipsis: 13 }}
|
||||||
|
</button>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { CipherService } from "jslib-common/abstractions/cipher.service";
|
|||||||
import { EventService } from "jslib-common/abstractions/event.service";
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.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 { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { SearchService } from "jslib-common/abstractions/search.service";
|
import { SearchService } from "jslib-common/abstractions/search.service";
|
||||||
@@ -37,7 +38,8 @@ export class CiphersComponent extends BaseCiphersComponent {
|
|||||||
totpService: TotpService,
|
totpService: TotpService,
|
||||||
passwordRepromptService: PasswordRepromptService,
|
passwordRepromptService: PasswordRepromptService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
stateService: StateService
|
stateService: StateService,
|
||||||
|
organizationService: OrganizationService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
searchService,
|
searchService,
|
||||||
@@ -48,7 +50,8 @@ export class CiphersComponent extends BaseCiphersComponent {
|
|||||||
totpService,
|
totpService,
|
||||||
stateService,
|
stateService,
|
||||||
passwordRepromptService,
|
passwordRepromptService,
|
||||||
logService
|
logService,
|
||||||
|
organizationService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { LooseComponentsModule } from "./modules/loose-components.module";
|
import { LooseComponentsModule } from "./modules/loose-components.module";
|
||||||
|
import { PipesModule } from "./modules/pipes/pipes.module";
|
||||||
import { SharedModule } from "./modules/shared.module";
|
import { SharedModule } from "./modules/shared.module";
|
||||||
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
|
import { VaultFilterModule } from "./modules/vault-filter/vault-filter.module";
|
||||||
import { IndividualVaultModule } from "./modules/vault/modules/individual-vault/individual-vault.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";
|
import { OrganizationVaultModule } from "./modules/vault/modules/organization-vault/organization-vault.module";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -13,12 +15,16 @@ import { OrganizationVaultModule } from "./modules/vault/modules/organization-va
|
|||||||
IndividualVaultModule,
|
IndividualVaultModule,
|
||||||
OrganizationVaultModule,
|
OrganizationVaultModule,
|
||||||
VaultFilterModule,
|
VaultFilterModule,
|
||||||
|
OrganizationBadgeModule,
|
||||||
|
PipesModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
LooseComponentsModule,
|
LooseComponentsModule,
|
||||||
IndividualVaultModule,
|
IndividualVaultModule,
|
||||||
OrganizationVaultModule,
|
OrganizationVaultModule,
|
||||||
VaultFilterModule,
|
VaultFilterModule,
|
||||||
|
OrganizationBadgeModule,
|
||||||
|
PipesModule,
|
||||||
],
|
],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,15 +24,6 @@
|
|||||||
title="{{ 'editItem' | i18n }}"
|
title="{{ 'editItem' | i18n }}"
|
||||||
>{{ c.name }}</a
|
>{{ 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">
|
<ng-container *ngIf="c.hasAttachments">
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-paperclip"
|
class="bwi bwi-paperclip"
|
||||||
@@ -54,6 +45,13 @@
|
|||||||
<br />
|
<br />
|
||||||
<small appStopProp>{{ c.subTitle }}</small>
|
<small appStopProp>{{ c.subTitle }}</small>
|
||||||
</td>
|
</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">
|
<td class="table-list-options">
|
||||||
<div class="dropdown" appListDropdown>
|
<div class="dropdown" appListDropdown>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { CipherService } from "jslib-common/abstractions/cipher.service";
|
|||||||
import { EventService } from "jslib-common/abstractions/event.service";
|
import { EventService } from "jslib-common/abstractions/event.service";
|
||||||
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
import { I18nService } from "jslib-common/abstractions/i18n.service";
|
||||||
import { LogService } from "jslib-common/abstractions/log.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 { PasswordRepromptService } from "jslib-common/abstractions/passwordReprompt.service";
|
||||||
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "jslib-common/abstractions/platformUtils.service";
|
||||||
import { SearchService } from "jslib-common/abstractions/search.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 { CipherRepromptType } from "jslib-common/enums/cipherRepromptType";
|
||||||
import { CipherType } from "jslib-common/enums/cipherType";
|
import { CipherType } from "jslib-common/enums/cipherType";
|
||||||
import { EventType } from "jslib-common/enums/eventType";
|
import { EventType } from "jslib-common/enums/eventType";
|
||||||
|
import { Organization } from "jslib-common/models/domain/organization";
|
||||||
import { CipherView } from "jslib-common/models/view/cipherView";
|
import { CipherView } from "jslib-common/models/view/cipherView";
|
||||||
|
|
||||||
const MaxCheckedCount = 500;
|
const MaxCheckedCount = 500;
|
||||||
@@ -27,12 +29,14 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
|||||||
@Output() onShareClicked = new EventEmitter<CipherView>();
|
@Output() onShareClicked = new EventEmitter<CipherView>();
|
||||||
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
|
@Output() onCollectionsClicked = new EventEmitter<CipherView>();
|
||||||
@Output() onCloneClicked = new EventEmitter<CipherView>();
|
@Output() onCloneClicked = new EventEmitter<CipherView>();
|
||||||
|
@Output() onOrganzationBadgeClicked = new EventEmitter<string>();
|
||||||
|
|
||||||
pagedCiphers: CipherView[] = [];
|
pagedCiphers: CipherView[] = [];
|
||||||
pageSize = 200;
|
pageSize = 200;
|
||||||
cipherType = CipherType;
|
cipherType = CipherType;
|
||||||
actionPromise: Promise<any>;
|
actionPromise: Promise<any>;
|
||||||
userHasPremiumAccess = false;
|
userHasPremiumAccess = false;
|
||||||
|
organizations: Organization[] = [];
|
||||||
|
|
||||||
private didScroll = false;
|
private didScroll = false;
|
||||||
private pagedCiphersCount = 0;
|
private pagedCiphersCount = 0;
|
||||||
@@ -47,7 +51,8 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
|||||||
protected totpService: TotpService,
|
protected totpService: TotpService,
|
||||||
protected stateService: StateService,
|
protected stateService: StateService,
|
||||||
protected passwordRepromptService: PasswordRepromptService,
|
protected passwordRepromptService: PasswordRepromptService,
|
||||||
private logService: LogService
|
private logService: LogService,
|
||||||
|
private organizationService: OrganizationService
|
||||||
) {
|
) {
|
||||||
super(searchService);
|
super(searchService);
|
||||||
}
|
}
|
||||||
@@ -60,6 +65,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnDestroy
|
|||||||
// Do not use ngOnInit() for anything that requires sync data.
|
// Do not use ngOnInit() for anything that requires sync data.
|
||||||
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
async load(filter: (cipher: CipherView) => boolean = null, deleted = false) {
|
||||||
await super.load(filter, deleted);
|
await super.load(filter, deleted);
|
||||||
|
this.organizations = await this.organizationService.getAll();
|
||||||
this.userHasPremiumAccess = await this.stateService.getCanAccessPremium();
|
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) {
|
protected deleteCipher(id: string, permanent: boolean) {
|
||||||
return permanent
|
return permanent
|
||||||
? this.cipherService.deleteWithServer(id)
|
? this.cipherService.deleteWithServer(id)
|
||||||
|
|||||||
@@ -421,6 +421,9 @@
|
|||||||
"message": "Copy URI",
|
"message": "Copy URI",
|
||||||
"description": "Copy URI to clipboard"
|
"description": "Copy URI to clipboard"
|
||||||
},
|
},
|
||||||
|
"me": {
|
||||||
|
"message": "Me"
|
||||||
|
},
|
||||||
"myVault": {
|
"myVault": {
|
||||||
"message": "My Vault"
|
"message": "My Vault"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user