mirror of
https://github.com/bitwarden/browser
synced 2025-12-27 13:43:41 +00:00
[PM-7162] Cipher Form - Item Details (#9758)
* [PM-7162] Fix weird angular error regarding disabled component bit-select * [PM-7162] Introduce CipherFormConfigService and related types * [PM-7162] Introduce CipherFormService * [PM-7162] Introduce the Item Details section component and the CipherFormContainer interface * [PM-7162] Introduce the CipherForm component * [PM-7162] Add strongly typed QueryParams to the add-edit-v2.component * [PM-7162] Export CipherForm from Vault Lib * [PM-7162] Use the CipherForm in Browser AddEditV2 * [PM-7162] Introduce CipherForm storybook * [PM-7162] Remove VaultPopupListFilterService dependency from NewItemDropDownV2 component * [PM-7162] Add support for content projection of attachment button * [PM-7162] Fix typo * [PM-7162] Cipher form service cleanup * [PM-7162] Move readonly collection notice to bit-hint * [PM-7162] Refactor CipherFormConfig type to enforce required properties with Typescript * [PM-7162] Fix storybook after config changes * [PM-7162] Use new add-edit component for clone route
This commit is contained in:
@@ -3492,9 +3492,31 @@
|
||||
"itemsWithNoFolder": {
|
||||
"message": "Items with no folder"
|
||||
},
|
||||
"itemDetails": {
|
||||
"message": "Item details"
|
||||
},
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"organizationIsDeactivated": {
|
||||
"message": "Organization is deactivated"
|
||||
},
|
||||
"owner": {
|
||||
"message": "Owner"
|
||||
},
|
||||
"selfOwnershipLabel": {
|
||||
"message": "You",
|
||||
"description": "Used as a label to indicate that the user is the owner of an item."
|
||||
},
|
||||
"contactYourOrgAdmin": {
|
||||
"message": "Items in deactivated organizations cannot be accessed. Contact your organization owner for assistance."
|
||||
},
|
||||
|
||||
@@ -323,12 +323,11 @@ const routes: Routes = [
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "appearance" },
|
||||
},
|
||||
{
|
||||
...extensionRefreshSwap(AddEditComponent, AddEditV2Component, {
|
||||
path: "clone-cipher",
|
||||
component: AddEditComponent,
|
||||
canActivate: [AuthGuard],
|
||||
data: { state: "clone-cipher" },
|
||||
},
|
||||
}),
|
||||
{
|
||||
path: "send-type",
|
||||
component: SendTypeComponent,
|
||||
|
||||
@@ -1,10 +1,21 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="headerText" showBackButton> </popup-header>
|
||||
|
||||
<app-open-attachments *ngIf="isEdit" [cipherId]="cipherId"></app-open-attachments>
|
||||
<vault-cipher-form
|
||||
*ngIf="!loading"
|
||||
formId="cipherForm"
|
||||
[config]="config"
|
||||
(cipherSaved)="onCipherSaved($event)"
|
||||
[submitBtn]="submitBtn"
|
||||
>
|
||||
<app-open-attachments
|
||||
slot="attachment-button"
|
||||
[cipherId]="originalCipherId"
|
||||
></app-open-attachments>
|
||||
</vault-cipher-form>
|
||||
|
||||
<popup-footer slot="footer">
|
||||
<button bitButton type="button" buttonType="primary">
|
||||
<button bitButton type="submit" form="cipherForm" buttonType="primary" #submitBtn>
|
||||
{{ "save" | i18n }}
|
||||
</button>
|
||||
</popup-footer>
|
||||
|
||||
@@ -1,24 +1,86 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { CommonModule, Location } from "@angular/common";
|
||||
import { Component } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormsModule } from "@angular/forms";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { ActivatedRoute, Params } from "@angular/router";
|
||||
import { map, switchMap } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { CipherId } from "@bitwarden/common/types/guid";
|
||||
import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { SearchModule, ButtonModule } from "@bitwarden/components";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
import { AsyncActionsModule, ButtonModule, SearchModule } from "@bitwarden/components";
|
||||
import {
|
||||
CipherFormConfig,
|
||||
CipherFormConfigService,
|
||||
CipherFormMode,
|
||||
CipherFormModule,
|
||||
DefaultCipherFormConfigService,
|
||||
} from "@bitwarden/vault";
|
||||
|
||||
import { PopupFooterComponent } from "../../../../../platform/popup/layout/popup-footer.component";
|
||||
import { PopupHeaderComponent } from "../../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../../platform/popup/layout/popup-page.component";
|
||||
import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-attachments.component";
|
||||
|
||||
/**
|
||||
* Helper class to parse query parameters for the AddEdit route.
|
||||
*/
|
||||
class QueryParams {
|
||||
constructor(params: Params) {
|
||||
this.cipherId = params.cipherId;
|
||||
this.type = parseInt(params.type, null);
|
||||
this.clone = params.clone === "true";
|
||||
this.folderId = params.folderId;
|
||||
this.organizationId = params.organizationId;
|
||||
this.collectionId = params.collectionId;
|
||||
this.uri = params.uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* The ID of the cipher to edit or clone.
|
||||
*/
|
||||
cipherId?: CipherId;
|
||||
|
||||
/**
|
||||
* The type of cipher to create.
|
||||
*/
|
||||
type: CipherType;
|
||||
|
||||
/**
|
||||
* Whether to clone the cipher.
|
||||
*/
|
||||
clone?: boolean;
|
||||
|
||||
/**
|
||||
* Optional folderId to pre-select.
|
||||
*/
|
||||
folderId?: string;
|
||||
|
||||
/**
|
||||
* Optional organizationId to pre-select.
|
||||
*/
|
||||
organizationId?: OrganizationId;
|
||||
|
||||
/**
|
||||
* Optional collectionId to pre-select.
|
||||
*/
|
||||
collectionId?: CollectionId;
|
||||
|
||||
/**
|
||||
* Optional URI to pre-fill for login ciphers.
|
||||
*/
|
||||
uri?: string;
|
||||
}
|
||||
|
||||
export type AddEditQueryParams = Partial<Record<keyof QueryParams, string>>;
|
||||
|
||||
@Component({
|
||||
selector: "app-add-edit-v2",
|
||||
templateUrl: "add-edit-v2.component.html",
|
||||
standalone: true,
|
||||
providers: [{ provide: CipherFormConfigService, useClass: DefaultCipherFormConfigService }],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SearchModule,
|
||||
@@ -29,33 +91,86 @@ import { OpenAttachmentsComponent } from "../attachments/open-attachments/open-a
|
||||
PopupPageComponent,
|
||||
PopupHeaderComponent,
|
||||
PopupFooterComponent,
|
||||
CipherFormModule,
|
||||
AsyncActionsModule,
|
||||
],
|
||||
})
|
||||
export class AddEditV2Component {
|
||||
headerText: string;
|
||||
cipherId: CipherId;
|
||||
isEdit: boolean = false;
|
||||
config: CipherFormConfig;
|
||||
|
||||
get loading() {
|
||||
return this.config == null;
|
||||
}
|
||||
|
||||
get originalCipherId(): CipherId | null {
|
||||
return this.config?.originalCipher.id as CipherId;
|
||||
}
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private location: Location,
|
||||
private i18nService: I18nService,
|
||||
private addEditFormConfigService: CipherFormConfigService,
|
||||
) {
|
||||
this.subscribeToParams();
|
||||
}
|
||||
|
||||
subscribeToParams(): void {
|
||||
this.route.queryParams.pipe(takeUntilDestroyed()).subscribe((params) => {
|
||||
const isNew = params.isNew?.toLowerCase() === "true";
|
||||
const cipherType = parseInt(params.type);
|
||||
|
||||
this.isEdit = !isNew;
|
||||
this.cipherId = params.cipherId;
|
||||
this.headerText = this.setHeader(isNew, cipherType);
|
||||
});
|
||||
onCipherSaved(savedCipher: CipherView) {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
setHeader(isNew: boolean, type: CipherType) {
|
||||
const partOne = isNew ? "newItemHeader" : "editItemHeader";
|
||||
subscribeToParams(): void {
|
||||
this.route.queryParams
|
||||
.pipe(
|
||||
takeUntilDestroyed(),
|
||||
map((params) => new QueryParams(params)),
|
||||
switchMap(async (params) => {
|
||||
let mode: CipherFormMode;
|
||||
if (params.cipherId == null) {
|
||||
mode = "add";
|
||||
} else {
|
||||
mode = params.clone ? "clone" : "edit";
|
||||
}
|
||||
const config = await this.addEditFormConfigService.buildConfig(
|
||||
mode,
|
||||
params.cipherId,
|
||||
params.type,
|
||||
);
|
||||
|
||||
if (config.mode === "edit" && !config.originalCipher.edit) {
|
||||
config.mode = "partial-edit";
|
||||
}
|
||||
|
||||
this.setInitialValuesFromParams(params, config);
|
||||
|
||||
return config;
|
||||
}),
|
||||
)
|
||||
.subscribe((config) => {
|
||||
this.config = config;
|
||||
this.headerText = this.setHeader(config.mode, config.cipherType);
|
||||
});
|
||||
}
|
||||
|
||||
setInitialValuesFromParams(params: QueryParams, config: CipherFormConfig) {
|
||||
config.initialValues = {};
|
||||
if (params.folderId) {
|
||||
config.initialValues.folderId = params.folderId;
|
||||
}
|
||||
if (params.organizationId) {
|
||||
config.initialValues.organizationId = params.organizationId;
|
||||
}
|
||||
if (params.collectionId) {
|
||||
config.initialValues.collectionIds = [params.collectionId];
|
||||
}
|
||||
if (params.uri) {
|
||||
config.initialValues.loginUri = params.uri;
|
||||
}
|
||||
}
|
||||
|
||||
setHeader(mode: CipherFormMode, type: CipherType) {
|
||||
const partOne = mode === "edit" || mode === "partial-edit" ? "editItemHeader" : "newItemHeader";
|
||||
|
||||
switch (type) {
|
||||
case CipherType.Login:
|
||||
|
||||
@@ -19,6 +19,7 @@ import { PasswordRepromptService } from "@bitwarden/vault";
|
||||
import { BrowserApi } from "../../../../../platform/browser/browser-api";
|
||||
import BrowserPopupUtils from "../../../../../platform/popup/browser-popup-utils";
|
||||
import { VaultPopupAutofillService } from "../../../services/vault-popup-autofill.service";
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -145,9 +146,10 @@ export class ItemMoreOptionsComponent {
|
||||
|
||||
await this.router.navigate(["/clone-cipher"], {
|
||||
queryParams: {
|
||||
cloneMode: true,
|
||||
clone: true.toString(),
|
||||
cipherId: this.cipher.id,
|
||||
},
|
||||
type: this.cipher.type.toString(),
|
||||
} as AddEditQueryParams,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { Component, Input } from "@angular/core";
|
||||
import { Router, RouterLink } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components";
|
||||
import { ButtonModule, MenuModule, NoItemsModule } from "@bitwarden/components";
|
||||
|
||||
import { AddEditQueryParams } from "../add-edit/add-edit-v2.component";
|
||||
|
||||
export interface NewItemInitialValues {
|
||||
folderId?: string;
|
||||
organizationId?: OrganizationId;
|
||||
collectionId?: CollectionId;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "app-new-item-dropdown",
|
||||
@@ -12,17 +21,27 @@ import { ButtonModule, NoItemsModule, MenuModule } from "@bitwarden/components";
|
||||
standalone: true,
|
||||
imports: [NoItemsModule, JslibModule, CommonModule, ButtonModule, RouterLink, MenuModule],
|
||||
})
|
||||
export class NewItemDropdownV2Component implements OnInit, OnDestroy {
|
||||
export class NewItemDropdownV2Component {
|
||||
cipherType = CipherType;
|
||||
|
||||
/**
|
||||
* Optional initial values to pass to the add cipher form
|
||||
*/
|
||||
@Input()
|
||||
initialValues: NewItemInitialValues;
|
||||
|
||||
constructor(private router: Router) {}
|
||||
|
||||
ngOnInit(): void {}
|
||||
private buildQueryParams(type: CipherType): AddEditQueryParams {
|
||||
return {
|
||||
type: type.toString(),
|
||||
collectionId: this.initialValues?.collectionId,
|
||||
organizationId: this.initialValues?.organizationId,
|
||||
folderId: this.initialValues?.folderId,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {}
|
||||
|
||||
// TODO PM-6826: add selectedVault query param
|
||||
newItemNavigate(type: CipherType) {
|
||||
void this.router.navigate(["/add-cipher"], { queryParams: { type: type, isNew: true } });
|
||||
void this.router.navigate(["/add-cipher"], { queryParams: this.buildQueryParams(type) });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<popup-page>
|
||||
<popup-header slot="header" [pageTitle]="'vault' | i18n">
|
||||
<ng-container slot="end">
|
||||
<app-new-item-dropdown></app-new-item-dropdown>
|
||||
<app-new-item-dropdown [initialValues]="newItemItemValues$ | async"></app-new-item-dropdown>
|
||||
|
||||
<app-pop-out></app-pop-out>
|
||||
<app-current-account></app-current-account>
|
||||
@@ -15,7 +15,10 @@
|
||||
<bit-no-items [icon]="vaultIcon">
|
||||
<ng-container slot="title">{{ "yourVaultIsEmpty" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "autofillSuggestionsTip" | i18n }}</ng-container>
|
||||
<app-new-item-dropdown slot="button"></app-new-item-dropdown>
|
||||
<app-new-item-dropdown
|
||||
slot="button"
|
||||
[initialValues]="newItemItemValues$ | async"
|
||||
></app-new-item-dropdown>
|
||||
</bit-no-items>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,9 +2,10 @@ import { CommonModule } from "@angular/common";
|
||||
import { Component, OnDestroy, OnInit } from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { RouterLink } from "@angular/router";
|
||||
import { combineLatest } from "rxjs";
|
||||
import { combineLatest, map, Observable, shareReplay } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherType } from "@bitwarden/common/vault/enums";
|
||||
import { ButtonModule, Icons, NoItemsModule } from "@bitwarden/components";
|
||||
|
||||
@@ -13,8 +14,12 @@ import { PopOutComponent } from "../../../../platform/popup/components/pop-out.c
|
||||
import { PopupHeaderComponent } from "../../../../platform/popup/layout/popup-header.component";
|
||||
import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page.component";
|
||||
import { VaultPopupItemsService } from "../../services/vault-popup-items.service";
|
||||
import { VaultPopupListFiltersService } from "../../services/vault-popup-list-filters.service";
|
||||
import { AutofillVaultListItemsComponent, VaultListItemsContainerComponent } from "../vault-v2";
|
||||
import { NewItemDropdownV2Component } from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import {
|
||||
NewItemDropdownV2Component,
|
||||
NewItemInitialValues,
|
||||
} from "../vault-v2/new-item-dropdown/new-item-dropdown-v2.component";
|
||||
import { VaultListFiltersComponent } from "../vault-v2/vault-list-filters/vault-list-filters.component";
|
||||
import { VaultV2SearchComponent } from "../vault-v2/vault-search/vault-v2-search.component";
|
||||
|
||||
@@ -50,6 +55,17 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
protected favoriteCiphers$ = this.vaultPopupItemsService.favoriteCiphers$;
|
||||
protected remainingCiphers$ = this.vaultPopupItemsService.remainingCiphers$;
|
||||
|
||||
protected newItemItemValues$: Observable<NewItemInitialValues> =
|
||||
this.vaultPopupListFiltersService.filters$.pipe(
|
||||
map((filter) => ({
|
||||
organizationId: (filter.organization?.id ||
|
||||
filter.collection?.organizationId) as OrganizationId,
|
||||
collectionId: filter.collection?.id as CollectionId,
|
||||
folderId: filter.folder?.id,
|
||||
})),
|
||||
shareReplay({ refCount: true, bufferSize: 1 }),
|
||||
);
|
||||
|
||||
/** Visual state of the vault */
|
||||
protected vaultState: VaultState | null = null;
|
||||
|
||||
@@ -59,7 +75,10 @@ export class VaultV2Component implements OnInit, OnDestroy {
|
||||
|
||||
protected VaultStateEnum = VaultState;
|
||||
|
||||
constructor(private vaultPopupItemsService: VaultPopupItemsService) {
|
||||
constructor(
|
||||
private vaultPopupItemsService: VaultPopupItemsService,
|
||||
private vaultPopupListFiltersService: VaultPopupListFiltersService,
|
||||
) {
|
||||
combineLatest([
|
||||
this.vaultPopupItemsService.emptyVault$,
|
||||
this.vaultPopupItemsService.noFilteredResults$,
|
||||
|
||||
@@ -173,6 +173,10 @@
|
||||
"message": "No folder",
|
||||
"description": "This is the folder for uncategorized items"
|
||||
},
|
||||
"selfOwnershipLabel": {
|
||||
"message": "You",
|
||||
"description": "Used as a label to indicate that the user is the owner of an item."
|
||||
},
|
||||
"addFolder": {
|
||||
"message": "Add folder"
|
||||
},
|
||||
@@ -401,6 +405,21 @@
|
||||
"item": {
|
||||
"message": "Item"
|
||||
},
|
||||
"itemDetails": {
|
||||
"message": "Item details"
|
||||
},
|
||||
"itemName": {
|
||||
"message": "Item name"
|
||||
},
|
||||
"cannotRemoveViewOnlyCollections": {
|
||||
"message": "You cannot remove collections with View only permissions: $COLLECTIONS$",
|
||||
"placeholders": {
|
||||
"collections": {
|
||||
"content": "$1",
|
||||
"example": "Work, Personal"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ex": {
|
||||
"message": "ex.",
|
||||
"description": "Short abbreviation for 'example'."
|
||||
@@ -4159,7 +4178,7 @@
|
||||
},
|
||||
"ssoHandOff": {
|
||||
"message": "You may now close this tab and continue in the extension."
|
||||
},
|
||||
},
|
||||
"youSuccessfullyLoggedIn": {
|
||||
"message": "You successfully logged in"
|
||||
},
|
||||
@@ -8494,7 +8513,7 @@
|
||||
},
|
||||
"billingHistoryDescription": {
|
||||
"message": "Download a CSV to obtain client details for each billing date. Prorated charges are not included in the CSV and may vary from the linked invoice. For the most accurate billing details, refer to your monthly invoices.",
|
||||
"description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations."
|
||||
"description": "A paragraph on the Billing History page of the Provider Portal letting users know they can download a CSV report for their invoices that does not include prorations."
|
||||
},
|
||||
"monthlySubscriptionUserSeatsMessage": {
|
||||
"message": "Adjustments to your subscription will result in prorated charges to your billing totals on your next billing period. "
|
||||
|
||||
Reference in New Issue
Block a user