[PM-8655] Update web app new item button (#10354)

* Add additional extension refresh menu behind feature flag.

* Open new cipher dialog with proper cipher type selected.

* Adjust onboarding copy and default to login cipher.

* Update "New item" button styles.

* Add test to ensure onboarding component always calls onAddCipher.emit with the login cipher type.

* Hide onboarding and new item changes behind feature flag

* Fix missing mock in test.

* Remove extensionRefreshEnabled$ and conditional styles from the "add new" button.

* Remove rounding class from menu "new" button.
This commit is contained in:
Alec Rippberger 2024-08-08 23:45:47 -05:00 committed by GitHub
parent 304bd662ec
commit 2b69ccda40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 135 additions and 36 deletions

View File

@ -69,6 +69,53 @@
<div *ngIf="filter.type !== 'trash'" class="tw-shrink-0">
<div appListDropdown>
<ng-container [ngSwitch]="extensionRefreshEnabled">
<ng-container *ngSwitchCase="true">
<button
bitButton
buttonType="primary"
type="button"
[bitMenuTriggerFor]="addOptions"
id="newItemDropdown"
appA11yTitle="{{ 'new' | i18n }}"
>
<i class="bwi bwi-plus-f" aria-hidden="true"></i>
{{ "new" | i18n }}<i class="bwi tw-ml-2" aria-hidden="true"></i>
</button>
<bit-menu #addOptions aria-labelledby="newItemDropdown">
<button type="button" bitMenuItem (click)="addCipher(CipherType.Login)">
<i class="bwi bwi-globe" slot="start" aria-hidden="true"></i>
{{ "typeLogin" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Card)">
<i class="bwi bwi-credit-card" slot="start" aria-hidden="true"></i>
{{ "typeCard" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.Identity)">
<i class="bwi bwi-id-card" slot="start" aria-hidden="true"></i>
{{ "typeIdentity" | i18n }}
</button>
<button type="button" bitMenuItem (click)="addCipher(CipherType.SecureNote)">
<i class="bwi bwi-sticky-note" slot="start" aria-hidden="true"></i>
{{ "note" | i18n }}
</button>
<bit-menu-divider />
<button type="button" bitMenuItem (click)="addFolder()">
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
<ng-container *ngSwitchCase="false">
<button
bitButton
buttonType="primary"
@ -88,11 +135,18 @@
<i class="bwi bwi-fw bwi-folder" aria-hidden="true"></i>
{{ "folder" | i18n }}
</button>
<button *ngIf="canCreateCollections" type="button" bitMenuItem (click)="addCollection()">
<button
*ngIf="canCreateCollections"
type="button"
bitMenuItem
(click)="addCollection()"
>
<i class="bwi bwi-fw bwi-collection" aria-hidden="true"></i>
{{ "collection" | i18n }}
</button>
</bit-menu>
</ng-container>
</ng-container>
</div>
</div>
</app-header>

View File

@ -14,6 +14,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { BreadcrumbsModule, MenuModule } from "@bitwarden/components";
@ -47,6 +48,8 @@ export class VaultHeaderComponent implements OnInit {
protected Unassigned = Unassigned;
protected All = All;
protected CollectionDialogTabType = CollectionDialogTabType;
protected CipherType = CipherType;
protected extensionRefreshEnabled = false;
/**
* Boolean to determine the loading state of the header.
@ -67,7 +70,7 @@ export class VaultHeaderComponent implements OnInit {
@Input() canCreateCollections: boolean;
/** Emits an event when the new item button is clicked in the header */
@Output() onAddCipher = new EventEmitter<void>();
@Output() onAddCipher = new EventEmitter<CipherType | undefined>();
/** Emits an event when the new collection button is clicked in the 'New' dropdown menu */
@Output() onAddCollection = new EventEmitter<null>();
@ -92,6 +95,9 @@ export class VaultHeaderComponent implements OnInit {
this.flexibleCollectionsV1Enabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.FlexibleCollectionsV1),
);
this.extensionRefreshEnabled = await firstValueFrom(
this.configService.getFeatureFlag$(FeatureFlag.ExtensionRefresh),
);
}
/**
@ -199,8 +205,8 @@ export class VaultHeaderComponent implements OnInit {
this.onDeleteCollection.emit();
}
protected addCipher() {
this.onAddCipher.emit();
protected addCipher(cipherType?: CipherType) {
this.onAddCipher.emit(cipherType);
}
async addFolder(): Promise<void> {

View File

@ -22,7 +22,12 @@
<p class="tw-pl-1">
{{ "onboardingImportDataDetailsPartOne" | i18n }}
<button type="button" bitLink (click)="emitToAddCipher()">
{{ "onboardingImportDataDetailsLink" | i18n }}
{{
(extensionRefreshEnabled
? "onboardingImportDataDetailsLoginLink"
: "onboardingImportDataDetailsLink"
) | i18n
}}
</button>
<span>
{{ "onboardingImportDataDetailsPartTwoNoOrgs" | i18n }}

View File

@ -5,9 +5,11 @@ import { Subject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateProvider } from "@bitwarden/common/platform/state";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { VaultOnboardingService as VaultOnboardingServiceAbstraction } from "./services/abstraction/vault-onboarding.service";
@ -24,6 +26,7 @@ describe("VaultOnboardingComponent", () => {
let mockStateProvider: Partial<StateProvider>;
let setInstallExtLinkSpy: any;
let individualVaultPolicyCheckSpy: any;
let mockConfigService: MockProxy<ConfigService>;
beforeEach(() => {
mockPolicyService = mock<PolicyService>();
@ -42,6 +45,7 @@ describe("VaultOnboardingComponent", () => {
}),
),
};
mockConfigService = mock<ConfigService>();
// eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({
@ -54,6 +58,7 @@ describe("VaultOnboardingComponent", () => {
{ provide: I18nService, useValue: mockI18nService },
{ provide: ApiService, useValue: mockApiService },
{ provide: StateProvider, useValue: mockStateProvider },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compileComponents();
fixture = TestBed.createComponent(VaultOnboardingComponent);
@ -178,4 +183,14 @@ describe("VaultOnboardingComponent", () => {
expect(saveCompletedTasksSpy).toHaveBeenCalled();
});
});
describe("emitToAddCipher", () => {
it("always emits the `CipherType.Login` type when called", () => {
const emitSpy = jest.spyOn(component.onAddCipher, "emit");
component.emitToAddCipher();
expect(emitSpy).toHaveBeenCalledWith(CipherType.Login);
});
});
});

View File

@ -16,7 +16,10 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { VaultOnboardingMessages } from "@bitwarden/common/vault/enums/vault-onboarding.enum";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { LinkModule } from "@bitwarden/components";
@ -41,7 +44,7 @@ import { VaultOnboardingService, VaultOnboardingTasks } from "./services/vault-o
export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
@Input() ciphers: CipherView[];
@Input() orgs: Organization[];
@Output() onAddCipher = new EventEmitter<void>();
@Output() onAddCipher = new EventEmitter<CipherType>();
extensionUrl: string;
isIndividualPolicyVault: boolean;
@ -53,12 +56,14 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
protected onboardingTasks$: Observable<VaultOnboardingTasks>;
protected showOnboarding = false;
protected extensionRefreshEnabled = false;
constructor(
protected platformUtilsService: PlatformUtilsService,
protected policyService: PolicyService,
private apiService: ApiService,
private vaultOnboardingService: VaultOnboardingServiceAbstraction,
private configService: ConfigService,
) {}
async ngOnInit() {
@ -67,6 +72,9 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
this.setInstallExtLink();
this.individualVaultPolicyCheck();
this.checkForBrowserExtension();
this.extensionRefreshEnabled = await this.configService.getFeatureFlag(
FeatureFlag.ExtensionRefresh,
);
}
async ngOnChanges(changes: SimpleChanges) {
@ -162,7 +170,7 @@ export class VaultOnboardingComponent implements OnInit, OnChanges, OnDestroy {
}
emitToAddCipher() {
this.onAddCipher.emit();
this.onAddCipher.emit(CipherType.Login);
}
setInstallExtLink() {

View File

@ -6,14 +6,18 @@
[organizations]="allOrganizations"
[canCreateCollections]="canCreateCollections"
[collection]="selectedCollection"
(onAddCipher)="addCipher()"
(onAddCipher)="addCipher($event)"
(onAddCollection)="addCollection()"
(onAddFolder)="addFolder()"
(onEditCollection)="editCollection(selectedCollection.node, $event.tab)"
(onDeleteCollection)="deleteCollection(selectedCollection.node)"
></app-vault-header>
<app-vault-onboarding [ciphers]="ciphers" [orgs]="allOrganizations" (onAddCipher)="addCipher()">
<app-vault-onboarding
[ciphers]="ciphers"
[orgs]="allOrganizations"
(onAddCipher)="addCipher($event)"
>
</app-vault-onboarding>
<div class="tw-flex tw-flex-row -tw-mx-2.5">
@ -80,7 +84,7 @@
(click)="addCipher()"
*ngIf="filter.type !== 'trash'"
>
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
<i class="bwi bwi-plus-f bwi-fw" aria-hidden="true"></i>
{{ "newItem" | i18n }}
</button>
</div>

View File

@ -50,6 +50,7 @@ import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CollectionData } from "@bitwarden/common/vault/models/data/collection.data";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@ -163,7 +164,6 @@ export class VaultComponent implements OnInit, OnDestroy {
protected vaultBulkManagementActionEnabled$ = this.configService.getFeatureFlag$(
FeatureFlag.VaultBulkManagementAction,
);
private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>();
@ -586,9 +586,9 @@ export class VaultComponent implements OnInit, OnDestroy {
}
}
async addCipher() {
async addCipher(cipherType?: CipherType) {
const component = await this.editCipher(null);
component.type = this.activeFilter.cipherType;
component.type = cipherType || this.activeFilter.cipherType;
if (this.activeFilter.organizationId !== "MyVault") {
component.organizationId = this.activeFilter.organizationId;
component.collections = (

View File

@ -36,6 +36,9 @@
"notes": {
"message": "Notes"
},
"note": {
"message": "Note"
},
"customFields": {
"message": "Custom fields"
},
@ -1505,6 +1508,10 @@
"message": "new item",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)"
},
"onboardingImportDataDetailsLoginLink": {
"message": "new login",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new login instead. (Optional second half: You may need to wait until your administrator confirms your organization membership.)"
},
"onboardingImportDataDetailsPartTwoNoOrgs": {
"message": " instead.",
"description": "This will be part of a larger sentence, that will read like this: If you don't have any data to import, you can create a new item instead."