diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 59d565e4fb05..a9187194d901 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -1496,6 +1496,12 @@ "myFolders": { "message": "My folders" }, + "addSharedFolder": { + "message": "Add shared folder" + }, + "newSharedFolder": { + "message": "New shared folder" + }, "bitwardenForBusinessPageDesc": { "message": "Bitwarden for Business allows you to share your vault items with others by using an organization. Learn more on the bitwarden.com website." }, diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 40f3b9edb39c..0adaea6c5a80 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -149,6 +149,7 @@ import { RoutedVaultFilterService, RoutedVaultFilterBridgeService, VAULT_FILTER_BASE_ROUTE, + Vfo1TerminologyService, } from "@bitwarden/vault"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; @@ -615,6 +616,7 @@ const safeProviders: SafeProvider[] = [ CollectionService, AccountServiceAbstraction, ConfigService, + Vfo1TerminologyService, ], }), safeProvider({ diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 08d24f9c02d9..262cc2444f1b 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -116,6 +116,12 @@ "myFolders": { "message": "My folders" }, + "addSharedFolder": { + "message": "Add shared folder" + }, + "newSharedFolder": { + "message": "New shared folder" + }, "share": { "message": "Share" }, diff --git a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts index 39e272f11cb5..5d2068b214e8 100644 --- a/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/admin-console/organizations/collections/vault-filter/vault-filter.service.ts @@ -12,7 +12,11 @@ import { StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { VaultFilterService as BaseVaultFilterService, CollectionFilter } from "@bitwarden/vault"; +import { + VaultFilterService as BaseVaultFilterService, + CollectionFilter, + Vfo1TerminologyService, +} from "@bitwarden/vault"; @Injectable() export class VaultFilterService extends BaseVaultFilterService implements OnDestroy { @@ -35,6 +39,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest collectionService: CollectionService, accountService: AccountService, configService: ConfigService, + vfo1Terminology: Vfo1TerminologyService, ) { super( organizationService, @@ -46,6 +51,7 @@ export class VaultFilterService extends BaseVaultFilterService implements OnDest collectionService, accountService, configService, + vfo1Terminology, ); } diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 488c8a2b1a16..baa32420e0ad 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1206,6 +1206,12 @@ "myFolders": { "message": "My folders" }, + "addSharedFolder": { + "message": "Add shared folder" + }, + "newSharedFolder": { + "message": "New shared folder" + }, "attachments": { "message": "Attachments" }, diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html index 381d86cc25b4..b7cafe073bf0 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.html @@ -39,7 +39,7 @@

{{ "itemDetails" | i18n }}

@if (showCollectionsControl) { - {{ "collections" | i18n }} + {{ "collections" | vfo1I18n: "sharedFolders" }} {{ "itemDetails" | i18n }} } @if (!config.hideIndividualVaultFields) { - {{ "folder" | i18n }} + {{ "folder" | vfo1I18n: "myFolder" }} @for (folder of config.folders; track folder.id) { diff --git a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts index 6ca062321a74..84874718f373 100644 --- a/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts +++ b/libs/vault/src/cipher-form/components/item-details/item-details-section.component.ts @@ -33,6 +33,7 @@ import { TypographyModule, } from "@bitwarden/components"; +import { Vfo1I18nPipe } from "../../../pipes/vfo1-i18n.pipe"; import { CipherFormConfig, OptionalInitialValues, @@ -55,6 +56,7 @@ import { CipherFormContainer } from "../../cipher-form-container"; IconButtonModule, JslibModule, CommonModule, + Vfo1I18nPipe, ], }) export class ItemDetailsSectionComponent implements OnInit { diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts index 30362678fbb3..051600b18c45 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.spec.ts @@ -9,6 +9,7 @@ import { DomainSettingsService } from "@bitwarden/common/autofill/services/domai 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 { CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -88,6 +89,27 @@ describe("ItemDetailsV2Component", () => { expect(itemDetailsList[3].nativeElement.textContent.trim()).toContain(folder.name); }); + describe("getAriaLabel", () => { + it("separates the label and name with a space for organizations", () => { + const org = Object.assign(new Organization(), { name: "Organization 1" }); + expect(component.getAriaLabel(org)).toBe("owner Organization 1"); + }); + + it("separates the label and name with a space for collections", () => { + const col = new CollectionView({ + id: "col1" as CollectionId, + organizationId: "org1" as OrganizationId, + name: "Collection 1", + }); + expect(component.getAriaLabel(col)).toBe("collection Collection 1"); + }); + + it("separates the label and name with a space for folders", () => { + const folderView = Object.assign(new FolderView(), { name: "Folder 1" }); + expect(component.getAriaLabel(folderView)).toBe("folder Folder 1"); + }); + }); + it("does not render owner when `hideOwner` is true", () => { componentRef.setInput("hideOwner", true); fixture.detectChanges(); diff --git a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts index 04c5137eacc4..8528d4ccba8e 100644 --- a/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts +++ b/libs/vault/src/cipher-view/item-details/item-details-v2.component.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { CommonModule } from "@angular/common"; -import { Component, computed, input, signal } from "@angular/core"; +import { Component, computed, inject, input, signal } from "@angular/core"; // 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"; @@ -23,6 +23,7 @@ import { } from "@bitwarden/components"; import { OrgIconDirective } from "../../components/org-icon.directive"; +import { Vfo1TerminologyService } from "../../services/vfo1-terminology.service"; // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @@ -47,6 +48,8 @@ export class ItemDetailsV2Component { readonly collections = input(); readonly showAllDetails = signal(false); + private readonly terminology = inject(Vfo1TerminologyService); + readonly showOwnership = computed(() => { return this.cipher().organizationId && this.organization() && !this.hideOwner(); }); @@ -95,11 +98,13 @@ export class ItemDetailsV2Component { getAriaLabel(item: Organization | CollectionView | FolderView): string { if (item instanceof Organization) { - return this.i18nService.t("owner") + item.name; + return `${this.i18nService.t("owner")} ${item.name}`; } else if (item instanceof CollectionView) { - return this.i18nService.t("collection") + item.name; + const label = this.i18nService.t(this.terminology.enabled() ? "sharedFolder" : "collection"); + return `${label} ${item.name}`; } else if (item instanceof FolderView) { - return this.i18nService.t("folder") + item.name; + const label = this.i18nService.t(this.terminology.enabled() ? "myFolder" : "folder"); + return `${label} ${item.name}`; } return ""; } @@ -117,9 +122,9 @@ export class ItemDetailsV2Component { getItemTitle(item: Organization | CollectionView | FolderView): string { if (item instanceof CollectionView) { - return this.i18nService.t("collection"); + return this.i18nService.t(this.terminology.enabled() ? "sharedFolder" : "collection"); } else if (item instanceof FolderView) { - return this.i18nService.t("folder"); + return this.i18nService.t(this.terminology.enabled() ? "myFolder" : "folder"); } return ""; } diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html index 8ace92850bbb..e1a11733987c 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.html @@ -56,7 +56,7 @@ @if (canCreateCollection()) { } diff --git a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts index bf379d9bd9de..1c0b25c75471 100644 --- a/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts +++ b/libs/vault/src/components/new-cipher-menu/new-cipher-menu.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from "@angular/common"; -import { Component, input, output } from "@angular/core"; +import { Component, inject, input, output } from "@angular/core"; import { toObservable, toSignal } from "@angular/core/rxjs-interop"; import { combineLatest, map, shareReplay } from "rxjs"; @@ -21,6 +21,9 @@ import { } from "@bitwarden/components"; import { I18nPipe } from "@bitwarden/ui-common"; +import { Vfo1I18nPipe } from "../../pipes/vfo1-i18n.pipe"; +import { Vfo1TerminologyService } from "../../services/vfo1-terminology.service"; + // FIXME(https://bitwarden.atlassian.net/browse/CL-764): Migrate to OnPush // eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection @Component({ @@ -33,6 +36,7 @@ import { I18nPipe } from "@bitwarden/ui-common"; PopoverModule, I18nPipe, JslibModule, + Vfo1I18nPipe, TooltipDirective, ], }) @@ -62,6 +66,8 @@ export class NewCipherMenuComponent { cipherAdded = output(); onAddItemDialog = output(); + private readonly terminology = inject(Vfo1TerminologyService); + private readonly btnTextAddCreateFeatureFlag = toSignal( this.configService.getFeatureFlag$(FeatureFlag.PM32380_BtnTextAddCreate), { initialValue: false }, @@ -113,10 +119,11 @@ export class NewCipherMenuComponent { // If only collections can be created, be specific if (!canCreateCipher && !canCreateFolder && canCreateCollection) { + const sharedFolderTerminology = this.terminology.enabled(); if (btnTextAddCreateFeatureFlag) { - return "addCollection"; + return sharedFolderTerminology ? "addSharedFolder" : "addCollection"; } else { - return "newCollection"; + return sharedFolderTerminology ? "newSharedFolder" : "newCollection"; } } diff --git a/libs/vault/src/services/vault-filter.service.spec.ts b/libs/vault/src/services/vault-filter.service.spec.ts index 015cb6592ed1..419dde29f8bb 100644 --- a/libs/vault/src/services/vault-filter.service.spec.ts +++ b/libs/vault/src/services/vault-filter.service.spec.ts @@ -31,6 +31,7 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; import { VaultFilterService } from "./vault-filter.service"; +import { Vfo1TerminologyService } from "./vfo1-terminology.service"; jest.mock("@bitwarden/angular/vault/vault-filter/services/vault-filter.service", () => ({ sortDefaultCollections: jest.fn((): CollectionView[] => []), @@ -54,6 +55,7 @@ describe("vault filter service", () => { let autoConfirmPolicy: ReplaySubject; let stateProvider: FakeStateProvider; let configService: MockProxy; + let vfo1Terminology: MockProxy; const mockUserId = Utils.newGuid() as UserId; let accountService: FakeAccountService; @@ -70,6 +72,8 @@ describe("vault filter service", () => { i18nService.collator = new Intl.Collator("en-US"); collectionService = mock(); configService = mock(); + vfo1Terminology = mock(); + vfo1Terminology.enabled.mockReturnValue(false); organizations = new ReplaySubject(1); folderViews = new ReplaySubject(1); @@ -104,11 +108,28 @@ describe("vault filter service", () => { collectionService, accountService, configService, + vfo1Terminology, ); collapsedGroupingsState = stateProvider.singleUser.getFake(mockUserId, COLLAPSED_GROUPINGS); organizations.next([]); }); + describe("shared-folder terminology filter heads", () => { + it("uses collection/folder head names when the flag is off", () => { + vfo1Terminology.enabled.mockReturnValue(false); + + expect(vaultFilterService["getCollectionFilterHead"]().node.name).toBe("collections"); + expect(vaultFilterService["getFolderFilterHead"]().node.name).toBe("folders"); + }); + + it("uses shared-folder/my-folder head names when the flag is on", () => { + vfo1Terminology.enabled.mockReturnValue(true); + + expect(vaultFilterService["getCollectionFilterHead"]().node.name).toBe("sharedFolders"); + expect(vaultFilterService["getFolderFilterHead"]().node.name).toBe("myFolders"); + }); + }); + describe("collapsed filter nodes", () => { const nodes = new Set(["1", "2"]); @@ -382,6 +403,7 @@ describe("vault filter service", () => { collectionService, accountService, configService, + vfo1Terminology, ); }); diff --git a/libs/vault/src/services/vault-filter.service.ts b/libs/vault/src/services/vault-filter.service.ts index ffed37f5e361..845ef991bc59 100644 --- a/libs/vault/src/services/vault-filter.service.ts +++ b/libs/vault/src/services/vault-filter.service.ts @@ -53,6 +53,8 @@ import { OrganizationFilter, } from ".."; +import { Vfo1TerminologyService } from "./vfo1-terminology.service"; + const NestingDelimiter = "/"; @Injectable() @@ -211,6 +213,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { protected collectionService: CollectionService, protected accountService: AccountService, protected configService: ConfigService, + protected vfo1Terminology: Vfo1TerminologyService, ) {} async getCollectionNodeFromTree(id: string) { @@ -358,7 +361,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { protected getCollectionFilterHead(): TreeNode { const head = CollectionView.vaultFilterHead() as CollectionFilter; - return new TreeNode(head, null, "collections", "AllCollections"); + const name = this.vfo1Terminology.enabled() ? "sharedFolders" : "collections"; + return new TreeNode(head, null, name, "AllCollections"); } protected async filterFolders( @@ -401,7 +405,8 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { protected getFolderFilterHead(): TreeNode { const head = new FolderView() as FolderFilter; - return new TreeNode(head, null, "folders", "AllFolders"); + const name = this.vfo1Terminology.enabled() ? "myFolders" : "folders"; + return new TreeNode(head, null, name, "AllFolders"); } protected buildCipherTypeTree(): Observable> {