mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 13:53:34 +00:00
[PM-18066] new item details view (#15311)
* update item details v2 in libs for new view design. targets web, browser, and desktop
This commit is contained in:
@@ -5573,5 +5573,11 @@
|
|||||||
"wasmNotSupported": {
|
"wasmNotSupported": {
|
||||||
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
"message": "WebAssembly is not supported on your browser or is not enabled. WebAssembly is required to use the Bitwarden app.",
|
||||||
"description": "'WebAssembly' is a technical term and should not be translated."
|
"description": "'WebAssembly' is a technical term and should not be translated."
|
||||||
|
},
|
||||||
|
"showMore": {
|
||||||
|
"message": "Show more"
|
||||||
|
},
|
||||||
|
"showLess": {
|
||||||
|
"message": "Show less"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3620,7 +3620,7 @@
|
|||||||
},
|
},
|
||||||
"uriMatchDefaultStrategyHint": {
|
"uriMatchDefaultStrategyHint": {
|
||||||
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
"message": "URI match detection is how Bitwarden identifies autofill suggestions.",
|
||||||
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
"description": "Explains to the user that URI match detection determines how Bitwarden suggests autofill options, and clarifies that this default strategy applies when no specific match detection is set for a login item."
|
||||||
},
|
},
|
||||||
"regExAdvancedOptionWarning": {
|
"regExAdvancedOptionWarning": {
|
||||||
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
"message": "\"Regular expression\" is an advanced option with increased risk of exposing credentials.",
|
||||||
@@ -4066,6 +4066,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"showMore": {
|
||||||
|
"message": "Show more"
|
||||||
|
},
|
||||||
|
"showLess": {
|
||||||
|
"message": "Show less"
|
||||||
|
},
|
||||||
"enableAutotype": {
|
"enableAutotype": {
|
||||||
"message": "Enable Autotype"
|
"message": "Enable Autotype"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
|
|||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
|
||||||
import { mock } from "jest-mock-extended";
|
import { mock } from "jest-mock-extended";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
@@ -60,6 +63,11 @@ describe("EmergencyViewDialogComponent", () => {
|
|||||||
{ provide: AccountService, useValue: accountService },
|
{ provide: AccountService, useValue: accountService },
|
||||||
{ provide: TaskService, useValue: mock<TaskService>() },
|
{ provide: TaskService, useValue: mock<TaskService>() },
|
||||||
{ provide: LogService, useValue: mock<LogService>() },
|
{ provide: LogService, useValue: mock<LogService>() },
|
||||||
|
{
|
||||||
|
provide: EnvironmentService,
|
||||||
|
useValue: { environment$: of({ getIconsUrl: () => "https://icons.example.com" }) },
|
||||||
|
},
|
||||||
|
{ provide: DomainSettingsService, useValue: { showFavicons$: of(true) } },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
.overrideComponent(EmergencyViewDialogComponent, {
|
.overrideComponent(EmergencyViewDialogComponent, {
|
||||||
|
|||||||
@@ -11002,5 +11002,11 @@
|
|||||||
},
|
},
|
||||||
"providersubCanceledmessage": {
|
"providersubCanceledmessage": {
|
||||||
"message" : "To resubscribe, contact Bitwarden Customer Support."
|
"message" : "To resubscribe, contact Bitwarden Customer Support."
|
||||||
|
},
|
||||||
|
"showMore": {
|
||||||
|
"message": "Show more"
|
||||||
|
},
|
||||||
|
"showLess": {
|
||||||
|
"message": "Show less"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,44 @@
|
|||||||
<div class="tw-flex tw-justify-center tw-items-center" aria-hidden="true">
|
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
|
||||||
|
<div
|
||||||
|
class="tw-flex tw-justify-center tw-items-center"
|
||||||
|
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<ng-container *ngIf="data$ | async as data">
|
<ng-container *ngIf="data$ | async as data">
|
||||||
<img
|
@if (data.imageEnabled && data.image) {
|
||||||
[src]="data.image"
|
<img
|
||||||
*ngIf="data.imageEnabled && data.image"
|
[src]="data.image"
|
||||||
class="tw-size-6 tw-rounded-md"
|
class="tw-rounded-md"
|
||||||
alt=""
|
alt=""
|
||||||
decoding="async"
|
decoding="async"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
[ngClass]="{ 'tw-invisible tw-absolute': !imageLoaded() }"
|
[ngClass]="{
|
||||||
(load)="imageLoaded.set(true)"
|
'tw-invisible tw-absolute': !imageLoaded(),
|
||||||
(error)="imageLoaded.set(false)"
|
'tw-size-6': !coloredIcon(),
|
||||||
/>
|
}"
|
||||||
<i
|
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||||
class="tw-w-6 tw-text-muted bwi bwi-lg {{ data.icon }}"
|
(load)="imageLoaded.set(true)"
|
||||||
*ngIf="!data.imageEnabled || !data.image || !imageLoaded()"
|
(error)="imageLoaded.set(false)"
|
||||||
></i>
|
/>
|
||||||
|
}
|
||||||
|
@if (!data.imageEnabled || !data.image || !imageLoaded()) {
|
||||||
|
<div
|
||||||
|
[ngClass]="{
|
||||||
|
'tw-flex tw-items-center tw-justify-center': coloredIcon(),
|
||||||
|
'tw-bg-illustration-bg-primary tw-rounded-full':
|
||||||
|
data.icon?.startsWith('bwi-') && coloredIcon(),
|
||||||
|
}"
|
||||||
|
[ngStyle]="coloredIcon() ? { width: '36px', height: '36px' } : {}"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
class="tw-text-muted bwi bwi-lg {{ data.icon }}"
|
||||||
|
[ngStyle]="{
|
||||||
|
color: coloredIcon() ? 'rgb(var(--color-illustration-outline))' : null,
|
||||||
|
width: data.icon?.startsWith('credit-card') && coloredIcon() ? '36px' : null,
|
||||||
|
height: data.icon?.startsWith('credit-card') && coloredIcon() ? '30px' : null,
|
||||||
|
}"
|
||||||
|
></i>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,6 +27,11 @@ export class IconComponent {
|
|||||||
*/
|
*/
|
||||||
cipher = input.required<CipherViewLike>();
|
cipher = input.required<CipherViewLike>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* coloredIcon will adjust the size of favicons and the colors of the text icon when user is in the item details view.
|
||||||
|
*/
|
||||||
|
coloredIcon = input<boolean>(false);
|
||||||
|
|
||||||
imageLoaded = signal(false);
|
imageLoaded = signal(false);
|
||||||
|
|
||||||
protected data$: Observable<CipherIconDetails>;
|
protected data$: Observable<CipherIconDetails>;
|
||||||
|
|||||||
@@ -1,83 +1,86 @@
|
|||||||
<section class="tw-mb-5 bit-compact:tw-mb-4">
|
<section class="tw-mb-5 bit-compact:tw-mb-4">
|
||||||
<bit-section-header>
|
|
||||||
<h2 bitTypography="h6">{{ "itemDetails" | i18n }}</h2>
|
|
||||||
</bit-section-header>
|
|
||||||
<bit-card>
|
<bit-card>
|
||||||
<bit-form-field
|
<div
|
||||||
[disableMargin]="!cipher.collectionIds?.length && !showOwnership && !cipher.folderId"
|
class="tw-flex tw-place-items-center"
|
||||||
[disableReadOnlyBorder]="!cipher.collectionIds?.length && !showOwnership && !cipher.folderId"
|
[ngClass]="{
|
||||||
|
'tw-mb-2': allItems.length > 0,
|
||||||
|
}"
|
||||||
>
|
>
|
||||||
<bit-label [appTextDrag]="cipher.name">
|
<!-- Applying width and height styles directly to synchronize icon sizing between web/browser/desktop -->
|
||||||
{{ "itemName" | i18n }}
|
<div class="tw-flex tw-items-center tw-justify-center" style="width: 40px; height: 40px">
|
||||||
</bit-label>
|
<app-vault-icon [cipher]="cipher()" [coloredIcon]="true"></app-vault-icon>
|
||||||
<input
|
</div>
|
||||||
readonly
|
<h2 bitTypography="h4" class="tw-ml-2 tw-mt-2" data-testid="item-name">
|
||||||
id="itemName"
|
{{ cipher().name }}
|
||||||
bitInput
|
</h2>
|
||||||
type="text"
|
</div>
|
||||||
[value]="cipher.name"
|
<ng-container>
|
||||||
aria-readonly="true"
|
<div class="tw-flex tw-flex-col tw-mt-2 md:tw-flex-row md:tw-flex-wrap">
|
||||||
data-testid="item-name"
|
@for (item of showItems(); track item.id; let last = $last) {
|
||||||
/>
|
<span
|
||||||
</bit-form-field>
|
class="tw-flex tw-items-center tw-mt-2 tw-mr-4"
|
||||||
|
|
||||||
<ul
|
|
||||||
[attr.aria-label]="'itemLocation' | i18n"
|
|
||||||
*ngIf="cipher.collectionIds?.length || showOwnership || cipher.folderId"
|
|
||||||
class="tw-mb-0 tw-pl-0"
|
|
||||||
>
|
|
||||||
<li
|
|
||||||
*ngIf="showOwnership && organization"
|
|
||||||
class="tw-flex tw-items-center tw-list-none"
|
|
||||||
[ngClass]="{ 'tw-mb-3': cipher.collectionIds }"
|
|
||||||
bitTypography="body2"
|
|
||||||
[attr.aria-label]="('owner' | i18n) + organization.name"
|
|
||||||
data-testid="owner"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
appOrgIcon
|
|
||||||
[tierType]="organization.productTierType"
|
|
||||||
[size]="'large'"
|
|
||||||
[title]="'owner' | i18n"
|
|
||||||
></i>
|
|
||||||
<span aria-hidden="true" class="tw-pl-1.5">
|
|
||||||
{{ organization.name }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li
|
|
||||||
class="tw-list-none"
|
|
||||||
*ngIf="cipher.collectionIds && collections"
|
|
||||||
[attr.aria-label]="'collection' | i18n"
|
|
||||||
>
|
|
||||||
<ul data-testid="collections" [ngClass]="{ 'tw-mb-0': !cipher.folderId }" class="tw-pl-0">
|
|
||||||
<li
|
|
||||||
*ngFor="let collection of collections; let last = last"
|
|
||||||
class="tw-flex tw-items-center tw-list-none"
|
|
||||||
bitTypography="body2"
|
bitTypography="body2"
|
||||||
[ngClass]="{ 'tw-mb-3': last && cipher.folderId }"
|
[attr.aria-label]="getAriaLabel(item)"
|
||||||
[attr.aria-label]="collection.name"
|
[ngClass]="{ 'tw-mb-2': last && hasSmallScreen() }"
|
||||||
|
data-testid="item-details-list"
|
||||||
>
|
>
|
||||||
<i
|
@if (isOrgIcon(item)) {
|
||||||
class="bwi bwi-collection-shared bwi-lg"
|
<i
|
||||||
aria-hidden="true"
|
appOrgIcon
|
||||||
[title]="'collection' | i18n"
|
[tierType]="organization().productTierType"
|
||||||
></i>
|
[size]="'large'"
|
||||||
|
[title]="'owner' | i18n"
|
||||||
|
></i>
|
||||||
|
} @else {
|
||||||
|
<i
|
||||||
|
class="bwi bwi-lg"
|
||||||
|
[ngClass]="getIconClass(item)"
|
||||||
|
aria-hidden="true"
|
||||||
|
[title]="getItemTitle(item)"
|
||||||
|
></i>
|
||||||
|
}
|
||||||
<span aria-hidden="true" class="tw-pl-1.5">
|
<span aria-hidden="true" class="tw-pl-1.5">
|
||||||
{{ collection.name }}
|
{{ item.name }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</span>
|
||||||
</ul>
|
}
|
||||||
</li>
|
@if (allItems().length === 0) {
|
||||||
<li
|
<span
|
||||||
*ngIf="cipher.folderId && folder"
|
class="tw-flex tw-items-center tw-mt-2 tw-mr-4"
|
||||||
bitTypography="body2"
|
bitTypography="body2"
|
||||||
class="tw-flex tw-items-center tw-list-none"
|
[attr.aria-label]="'noneFolder' | i18n"
|
||||||
[attr.aria-label]="('folder' | i18n) + folder.name"
|
>
|
||||||
data-testid="folder"
|
<i class="bwi bwi-folder bwi-lg" aria-hidden="true" [title]="'folder' | i18n"></i>
|
||||||
>
|
<span aria-hidden="true" class="tw-pl-1.5">
|
||||||
<i class="bwi bwi-folder bwi-lg" aria-hidden="true" [title]="'folder' | i18n"></i>
|
{{ "noneFolder" | i18n }}
|
||||||
<span aria-hidden="true" class="tw-pl-1.5">{{ folder.name }} </span>
|
</span>
|
||||||
</li>
|
</span>
|
||||||
</ul>
|
}
|
||||||
|
@if (hasSmallScreen() && allItems().length > 2 && cipher().collectionIds.length > 1) {
|
||||||
|
<button
|
||||||
|
bitTypography="body2"
|
||||||
|
type="button"
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
class="tw-mt-1.5"
|
||||||
|
(click)="toggleShowMore()"
|
||||||
|
*ngIf="!showAllDetails()"
|
||||||
|
>
|
||||||
|
{{ "showMore" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
bitTypography="body2"
|
||||||
|
type="button"
|
||||||
|
bitLink
|
||||||
|
linkType="primary"
|
||||||
|
class="tw-mt-1.5"
|
||||||
|
(click)="toggleShowMore()"
|
||||||
|
*ngIf="showAllDetails()"
|
||||||
|
>
|
||||||
|
{{ "showLess" | i18n }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
</bit-card>
|
</bit-card>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
|
import { ComponentRef } from "@angular/core";
|
||||||
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
import { ComponentFixture, TestBed } from "@angular/core/testing";
|
||||||
import { By } from "@angular/platform-browser";
|
import { By } from "@angular/platform-browser";
|
||||||
|
import { of } from "rxjs";
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||||
|
import { ClientType } from "@bitwarden/common/enums";
|
||||||
|
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
|
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
@@ -14,6 +20,7 @@ import { ItemDetailsV2Component } from "./item-details-v2.component";
|
|||||||
describe("ItemDetailsV2Component", () => {
|
describe("ItemDetailsV2Component", () => {
|
||||||
let component: ItemDetailsV2Component;
|
let component: ItemDetailsV2Component;
|
||||||
let fixture: ComponentFixture<ItemDetailsV2Component>;
|
let fixture: ComponentFixture<ItemDetailsV2Component>;
|
||||||
|
let componentRef: ComponentRef<ItemDetailsV2Component>;
|
||||||
|
|
||||||
const cipher = {
|
const cipher = {
|
||||||
id: "cipher1",
|
id: "cipher1",
|
||||||
@@ -46,37 +53,46 @@ describe("ItemDetailsV2Component", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await TestBed.configureTestingModule({
|
await TestBed.configureTestingModule({
|
||||||
imports: [ItemDetailsV2Component],
|
imports: [ItemDetailsV2Component],
|
||||||
providers: [{ provide: I18nService, useValue: { t: (key: string) => key } }],
|
providers: [
|
||||||
|
{ provide: I18nService, useValue: { t: (key: string) => key } },
|
||||||
|
{ provide: PlatformUtilsService, useValue: { getClientType: () => ClientType.Web } },
|
||||||
|
{
|
||||||
|
provide: EnvironmentService,
|
||||||
|
useValue: { environment$: of({ getIconsUrl: () => "https://icons.example.com" }) },
|
||||||
|
},
|
||||||
|
{ provide: DomainSettingsService, useValue: { showFavicons$: of(true) } },
|
||||||
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(ItemDetailsV2Component);
|
fixture = TestBed.createComponent(ItemDetailsV2Component);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
component.cipher = cipher;
|
componentRef = fixture.componentRef;
|
||||||
component.organization = organization;
|
componentRef.setInput("cipher", cipher);
|
||||||
component.collections = [collection, collection2];
|
componentRef.setInput("organization", organization);
|
||||||
component.folder = folder;
|
componentRef.setInput("collections", [collection, collection2]);
|
||||||
|
componentRef.setInput("folder", folder);
|
||||||
|
jest.spyOn(component, "hasSmallScreen").mockReturnValue(false); // Mocking small screen check
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("displays all available fields", () => {
|
it("displays all available fields", () => {
|
||||||
const itemName = fixture.debugElement.query(By.css('[data-testid="item-name"]'));
|
const itemName = fixture.debugElement.query(By.css('[data-testid="item-name"]'));
|
||||||
const owner = fixture.debugElement.query(By.css('[data-testid="owner"]'));
|
const itemDetailsList = fixture.debugElement.queryAll(
|
||||||
const collections = fixture.debugElement.queryAll(By.css('[data-testid="collections"] li'));
|
By.css('[data-testid="item-details-list"]'),
|
||||||
const folderElement = fixture.debugElement.query(By.css('[data-testid="folder"]'));
|
);
|
||||||
|
|
||||||
expect(itemName.nativeElement.value).toBe(cipher.name);
|
expect(itemName.nativeElement.textContent.trim()).toEqual(cipher.name);
|
||||||
expect(owner.nativeElement.textContent.trim()).toBe(organization.name);
|
expect(itemDetailsList.length).toBe(4); // Organization, Collection, Collection2, Folder
|
||||||
expect(collections.map((c) => c.nativeElement.textContent.trim())).toEqual([
|
expect(itemDetailsList[0].nativeElement.textContent.trim()).toContain(organization.name);
|
||||||
collection.name,
|
expect(itemDetailsList[1].nativeElement.textContent.trim()).toContain(collection.name);
|
||||||
collection2.name,
|
expect(itemDetailsList[2].nativeElement.textContent.trim()).toContain(collection2.name);
|
||||||
]);
|
expect(itemDetailsList[3].nativeElement.textContent.trim()).toContain(folder.name);
|
||||||
expect(folderElement.nativeElement.textContent.trim()).toBe(folder.name);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not render owner when `hideOwner` is true", () => {
|
it("does not render owner when `hideOwner` is true", () => {
|
||||||
component.hideOwner = true;
|
componentRef.setInput("hideOwner", true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
const owner = fixture.debugElement.query(By.css('[data-testid="owner"]'));
|
const owner = fixture.debugElement.query(By.css('[data-testid="owner"]'));
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @ts-strict-ignore
|
||||||
import { CommonModule } from "@angular/common";
|
import { CommonModule } from "@angular/common";
|
||||||
import { Component, Input } from "@angular/core";
|
import { Component, computed, input, signal } from "@angular/core";
|
||||||
|
|
||||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||||
|
import { toSignal } from "@angular/core/rxjs-interop";
|
||||||
|
import { fromEvent, map, startWith } from "rxjs";
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-imports
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
|
||||||
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
import {
|
import {
|
||||||
|
ButtonLinkDirective,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
SectionHeaderComponent,
|
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
|
|
||||||
@@ -26,20 +29,96 @@ import { OrgIconDirective } from "../../components/org-icon.directive";
|
|||||||
CommonModule,
|
CommonModule,
|
||||||
JslibModule,
|
JslibModule,
|
||||||
CardComponent,
|
CardComponent,
|
||||||
SectionHeaderComponent,
|
|
||||||
TypographyModule,
|
TypographyModule,
|
||||||
OrgIconDirective,
|
OrgIconDirective,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
|
ButtonLinkDirective,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ItemDetailsV2Component {
|
export class ItemDetailsV2Component {
|
||||||
@Input() cipher: CipherView;
|
hideOwner = input<boolean>(false);
|
||||||
@Input() organization?: Organization;
|
cipher = input.required<CipherView>();
|
||||||
@Input() collections?: CollectionView[];
|
organization = input<Organization | undefined>();
|
||||||
@Input() folder?: FolderView;
|
folder = input<FolderView | undefined>();
|
||||||
@Input() hideOwner?: boolean = false;
|
collections = input<CollectionView[] | undefined>();
|
||||||
|
showAllDetails = signal(false);
|
||||||
|
|
||||||
get showOwnership() {
|
showOwnership = computed(() => {
|
||||||
return this.cipher.organizationId && this.organization && !this.hideOwner;
|
return this.cipher().organizationId && this.organization() && !this.hideOwner();
|
||||||
|
});
|
||||||
|
|
||||||
|
hasSmallScreen = toSignal(
|
||||||
|
fromEvent(window, "resize").pipe(
|
||||||
|
map(() => window.innerWidth),
|
||||||
|
startWith(window.innerWidth),
|
||||||
|
map((width) => width < 681),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Array to hold all details of item. Organization, Collections, and Folder
|
||||||
|
allItems = computed(() => {
|
||||||
|
let items: any[] = [];
|
||||||
|
if (this.showOwnership() && this.organization()) {
|
||||||
|
items.push(this.organization());
|
||||||
|
}
|
||||||
|
if (this.cipher().collectionIds?.length > 0 && this.collections()) {
|
||||||
|
items = [...items, ...this.collections()];
|
||||||
|
}
|
||||||
|
if (this.cipher().folderId && this.folder()) {
|
||||||
|
items.push(this.folder());
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
});
|
||||||
|
|
||||||
|
showItems = computed(() => {
|
||||||
|
if (
|
||||||
|
this.hasSmallScreen() &&
|
||||||
|
this.allItems().length > 2 &&
|
||||||
|
!this.showAllDetails() &&
|
||||||
|
this.cipher().collectionIds?.length > 1
|
||||||
|
) {
|
||||||
|
return this.allItems().slice(0, 2);
|
||||||
|
} else {
|
||||||
|
return this.allItems();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
toggleShowMore() {
|
||||||
|
this.showAllDetails.update((value) => !value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAriaLabel(item: Organization | CollectionView | FolderView): string {
|
||||||
|
if (item instanceof Organization) {
|
||||||
|
return this.i18nService.t("owner") + item.name;
|
||||||
|
} else if (item instanceof CollectionView) {
|
||||||
|
return this.i18nService.t("collection") + item.name;
|
||||||
|
} else if (item instanceof FolderView) {
|
||||||
|
return this.i18nService.t("folder") + item.name;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getIconClass(item: Organization | CollectionView | FolderView): string {
|
||||||
|
if (item instanceof CollectionView) {
|
||||||
|
return "bwi-collection-shared";
|
||||||
|
} else if (item instanceof FolderView) {
|
||||||
|
return "bwi-folder";
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemTitle(item: Organization | CollectionView | FolderView): string {
|
||||||
|
if (item instanceof CollectionView) {
|
||||||
|
return this.i18nService.t("collection");
|
||||||
|
} else if (item instanceof FolderView) {
|
||||||
|
return this.i18nService.t("folder");
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
isOrgIcon(item: Organization | CollectionView | FolderView): boolean {
|
||||||
|
return item instanceof Organization;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user