diff --git a/.eslintrc.json b/.eslintrc.json index 3a1c197326d..fae64d869e8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,7 +4,7 @@ "browser": true, "webextensions": true }, - "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular"], + "plugins": ["@typescript-eslint", "rxjs", "rxjs-angular", "import"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": ["./tsconfig.eslint.json"], @@ -18,6 +18,16 @@ "prettier", "plugin:rxjs/recommended" ], + "settings": { + "import/parsers": { + "@typescript-eslint/parser": [".ts"] + }, + "import/resolver": { + "typescript": { + "alwaysTryTypes": true + } + } + }, "rules": { "@typescript-eslint/no-explicit-any": "off", // TODO: This should be re-enabled "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], @@ -65,6 +75,27 @@ "selector": "CallExpression[callee.name='svgIcon']" } ], - "curly": ["error", "all"] + "curly": ["error", "all"], + "import/namespace": ["off"], // This doesn't resolve namespace imports correctly, but TS will throw for this anyway + "import/no-restricted-paths": [ + "error", + { + "zones": [ + // Do not allow angular/node/electron code to be imported into common + { + "target": "./libs/common/**/*", + "from": "./libs/angular/**/*" + }, + { + "target": "./libs/common/**/*", + "from": "./libs/node/**/*" + }, + { + "target": "./libs/common/**/*", + "from": "./libs/electron/**/*" + } + ] + } + ] } } diff --git a/.github/workflows/release-qa-web.yml b/.github/workflows/release-qa-web.yml index 1e31c95cbeb..f02286be2f7 100644 --- a/.github/workflows/release-qa-web.yml +++ b/.github/workflows/release-qa-web.yml @@ -34,15 +34,11 @@ jobs: uses: bitwarden/gh-actions/download-artifacts@850faad0cf6c02a8c0dc46eddde2363fbd6c373a with: workflow: build-web.yml - path: apps/web + path: apps/web/build workflow_conclusion: success branch: ${{ github.ref_name }} artifacts: web-*-cloud-QA.zip - # This should result in a build directory in the current working directory - - name: Unzip build asset - working-directory: apps/web - run: unzip web-*-cloud-QA.zip - name: Checkout Repo uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # v3.0.2 diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index a23bfa56501..3749bb4ae00 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -2017,7 +2017,7 @@ } }, "lastSeenOn": { - "message": "last seen on $DATE$", + "message": "last seen on: $DATE$", "placeholders": { "date": { "content": "$1", diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 99114bc007f..cf83b6e78d1 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -16,7 +16,7 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; -import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; @@ -27,6 +27,7 @@ import { SendService as SendServiceAbstraction } from "@bitwarden/common/abstrac import { SettingsService as SettingsServiceAbstraction } from "@bitwarden/common/abstractions/settings.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; +import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction"; import { SystemService as SystemServiceAbstraction } from "@bitwarden/common/abstractions/system.service"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; @@ -58,7 +59,7 @@ import { FolderApiService } from "@bitwarden/common/services/folder/folder-api.s import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; -import { OrganizationService } from "@bitwarden/common/services/organization.service"; +import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; @@ -68,6 +69,7 @@ import { SendService } from "@bitwarden/common/services/send.service"; import { SettingsService } from "@bitwarden/common/services/settings.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service"; +import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service"; import { SystemService } from "@bitwarden/common/services/system.service"; import { TokenService } from "@bitwarden/common/services/token.service"; import { TotpService } from "@bitwarden/common/services/totp.service"; @@ -158,6 +160,7 @@ export default class MainBackground { folderApiService: FolderApiServiceAbstraction; policyApiService: PolicyApiServiceAbstraction; userVerificationApiService: UserVerificationApiServiceAbstraction; + syncNotifierService: SyncNotifierServiceAbstraction; // Passed to the popup for Safari to workaround issues with theming, downloading, etc. backgroundWindow = window; @@ -298,7 +301,8 @@ export default class MainBackground { this.cryptoFunctionService, this.stateService ); - this.organizationService = new OrganizationService(this.stateService); + this.syncNotifierService = new SyncNotifierService(); + this.organizationService = new OrganizationService(this.stateService, this.syncNotifierService); this.policyService = new PolicyService(this.stateService, this.organizationService); this.policyApiService = new PolicyApiService( this.policyService, @@ -388,9 +392,9 @@ export default class MainBackground { this.logService, this.keyConnectorService, this.stateService, - this.organizationService, this.providerService, this.folderApiService, + this.syncNotifierService, logoutCallback ); this.eventService = new EventService( diff --git a/apps/browser/src/background/service_factories/organization-service.factory.ts b/apps/browser/src/background/service_factories/organization-service.factory.ts index 87692e64391..2e7c31b596e 100644 --- a/apps/browser/src/background/service_factories/organization-service.factory.ts +++ b/apps/browser/src/background/service_factories/organization-service.factory.ts @@ -1,12 +1,17 @@ -import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/abstractions/organization.service"; -import { OrganizationService } from "@bitwarden/common/services/organization.service"; +import { OrganizationService as AbstractOrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; import { FactoryOptions, CachedServices, factory } from "./factory-options"; import { stateServiceFactory, StateServiceInitOptions } from "./state-service.factory"; +import { + syncNotifierServiceFactory, + SyncNotifierServiceInitOptions, +} from "./sync-notifier-service.factory"; type OrganizationServiceFactoryOptions = FactoryOptions; export type OrganizationServiceInitOptions = OrganizationServiceFactoryOptions & + SyncNotifierServiceInitOptions & StateServiceInitOptions; export function organizationServiceFactory( @@ -17,6 +22,10 @@ export function organizationServiceFactory( cache, "organizationService", opts, - async () => new OrganizationService(await stateServiceFactory(cache, opts)) + async () => + new OrganizationService( + await stateServiceFactory(cache, opts), + await syncNotifierServiceFactory(cache, opts) + ) ); } diff --git a/apps/browser/src/background/service_factories/sync-notifier-service.factory.ts b/apps/browser/src/background/service_factories/sync-notifier-service.factory.ts new file mode 100644 index 00000000000..58699bdff58 --- /dev/null +++ b/apps/browser/src/background/service_factories/sync-notifier-service.factory.ts @@ -0,0 +1,17 @@ +import { SyncNotifierService as AbstractSyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction"; +import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service"; + +import { FactoryOptions, CachedServices, factory } from "./factory-options"; + +type SyncNotifierServiceFactoryOptions = FactoryOptions; + +export type SyncNotifierServiceInitOptions = SyncNotifierServiceFactoryOptions; + +export function syncNotifierServiceFactory( + cache: { syncNotifierService?: AbstractSyncNotifierService } & CachedServices, + opts: SyncNotifierServiceInitOptions +): Promise { + return factory(cache, "syncNotifierService", opts, () => + Promise.resolve(new SyncNotifierService()) + ); +} diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 87c1dd17e93..a79028a26d1 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -26,7 +26,7 @@ import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector import { LogService as LogServiceAbstraction } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; diff --git a/apps/browser/src/popup/settings/about.component.html b/apps/browser/src/popup/settings/about.component.html index ed2ceb40bec..c11b68e9c1f 100644 --- a/apps/browser/src/popup/settings/about.component.html +++ b/apps/browser/src/popup/settings/about.component.html @@ -14,7 +14,7 @@

{{ "serverVersion" | i18n }}: {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

@@ -24,7 +24,7 @@ {{ "serverVersion" | i18n }} ({{ "thirdParty" | i18n }}): {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

@@ -36,7 +36,7 @@ {{ "serverVersion" | i18n }} ({{ "selfHosted" | i18n }}): {{ this.serverConfig?.version }} - ({{ "lastSeenOn" | i18n }}: {{ serverConfig.utcDate | date: "mediumDate" }}) + ({{ "lastSeenOn" | i18n: (serverConfig.utcDate | date: "mediumDate") }})

diff --git a/apps/browser/src/popup/vault/add-edit.component.ts b/apps/browser/src/popup/vault/add-edit.component.ts index f5062051c2e..d220a24a0d8 100644 --- a/apps/browser/src/popup/vault/add-edit.component.ts +++ b/apps/browser/src/popup/vault/add-edit.component.ts @@ -12,7 +12,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/apps/browser/src/popup/vault/ciphers.component.ts b/apps/browser/src/popup/vault/ciphers.component.ts index 4346ad7a668..60a92e238c2 100644 --- a/apps/browser/src/popup/vault/ciphers.component.ts +++ b/apps/browser/src/popup/vault/ciphers.component.ts @@ -10,7 +10,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { CipherType } from "@bitwarden/common/enums/cipherType"; @@ -78,7 +78,7 @@ export class CiphersComponent extends BaseCiphersComponent implements OnInit, On async ngOnInit() { this.searchTypeSearch = !this.platformUtilsService.isSafari(); - this.showOrganizations = await this.organizationService.hasOrganizations(); + this.showOrganizations = this.organizationService.hasOrganizations(); this.vaultFilter = this.vaultFilterService.getVaultFilter(); // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.queryParams.pipe(first()).subscribe(async (params) => { diff --git a/apps/browser/src/popup/vault/current-tab.component.ts b/apps/browser/src/popup/vault/current-tab.component.ts index 2f3569841f4..f3e97dd233d 100644 --- a/apps/browser/src/popup/vault/current-tab.component.ts +++ b/apps/browser/src/popup/vault/current-tab.component.ts @@ -4,7 +4,7 @@ import { Router } from "@angular/router"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; @@ -219,7 +219,7 @@ export class CurrentTabComponent implements OnInit, OnDestroy { const otherTypes: CipherType[] = []; const dontShowCards = await this.stateService.getDontShowCardsCurrentTab(); const dontShowIdentities = await this.stateService.getDontShowIdentitiesCurrentTab(); - this.showOrganizations = await this.organizationService.hasOrganizations(); + this.showOrganizations = this.organizationService.hasOrganizations(); if (!dontShowCards) { otherTypes.push(CipherType.Card); } diff --git a/apps/browser/src/popup/vault/share.component.html b/apps/browser/src/popup/vault/share.component.html index ad1447f6f65..dcec42415c0 100644 --- a/apps/browser/src/popup/vault/share.component.html +++ b/apps/browser/src/popup/vault/share.component.html @@ -1,70 +1,76 @@
-
-
- -
-

- {{ "moveToOrganization" | i18n }} -

-
- -
-
-
-
-
-
- {{ "noOrganizationsList" | i18n }} -
+ +
+
+
-
-
- - -
-
- -
-
-

- {{ "collections" | i18n }} -

-
-
- {{ "noCollectionsInList" | i18n }} -
-
-
-
+ {{ "moveToOrganization" | i18n }} + +
+ +
+ +
+
+
+
+ {{ "noOrganizationsList" | i18n }} +
+
+
+
+ + +
+
+
-
-
+
+

+ {{ "collections" | i18n }} +

+
+
+ {{ "noCollectionsInList" | i18n }} +
+
+
+
+ + +
+
+
+ +
diff --git a/apps/browser/src/popup/vault/share.component.ts b/apps/browser/src/popup/vault/share.component.ts index bfff215533e..112b432c0e0 100644 --- a/apps/browser/src/popup/vault/share.component.ts +++ b/apps/browser/src/popup/vault/share.component.ts @@ -7,7 +7,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; @Component({ diff --git a/apps/browser/src/popup/vault/vault-select.component.html b/apps/browser/src/popup/vault/vault-select.component.html index d5698e79288..95d1effd7fd 100644 --- a/apps/browser/src/popup/vault/vault-select.component.html +++ b/apps/browser/src/popup/vault/vault-select.component.html @@ -1,71 +1,70 @@ -
- - - + + + + +
+ diff --git a/apps/browser/src/popup/vault/vault-select.component.ts b/apps/browser/src/popup/vault/vault-select.component.ts index d12463f2c11..04ae04d0fab 100644 --- a/apps/browser/src/popup/vault/vault-select.component.ts +++ b/apps/browser/src/popup/vault/vault-select.component.ts @@ -5,19 +5,19 @@ import { Component, ElementRef, EventEmitter, - NgZone, OnInit, Output, TemplateRef, ViewChild, ViewContainerRef, HostListener, + OnDestroy, } from "@angular/core"; -import { merge } from "rxjs"; +import { BehaviorSubject, concatMap, map, merge, Observable, Subject, takeUntil } from "rxjs"; import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault-filter.model"; -import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { Organization } from "@bitwarden/common/models/domain/organization"; @@ -47,20 +47,22 @@ import { VaultFilterService } from "../../services/vaultFilter.service"; ]), ], }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class VaultSelectComponent implements OnInit { +export class VaultSelectComponent implements OnInit, OnDestroy { @Output() onVaultSelectionChanged = new EventEmitter(); @ViewChild("toggleVaults", { read: ElementRef }) buttonRef: ElementRef; @ViewChild("vaultSelectorTemplate", { read: TemplateRef }) templateRef: TemplateRef; + private _selectedVault = new BehaviorSubject(null); + isOpen = false; loaded = false; - organizations: Organization[]; + organizations$: Observable; + selectedVault$: Observable = this._selectedVault.asObservable(); + vaultFilter: VaultFilter = new VaultFilter(); - vaultFilterDisplay = ""; - enforcePersonalOwnwership = false; + enforcePersonalOwnership = false; overlayPostition: ConnectedPosition[] = [ { originX: "start", @@ -71,22 +73,22 @@ export class VaultSelectComponent implements OnInit { ]; private overlayRef: OverlayRef; + private _destroy = new Subject(); - get show() { + shouldShow(organizations: Organization[]): boolean { return ( - (this.organizations.length > 0 && !this.enforcePersonalOwnwership) || - (this.organizations.length > 1 && this.enforcePersonalOwnwership) + (organizations.length > 0 && !this.enforcePersonalOwnership) || + (organizations.length > 1 && this.enforcePersonalOwnership) ); } constructor( private vaultFilterService: VaultFilterService, private i18nService: I18nService, - private ngZone: NgZone, - private broadcasterService: BroadcasterService, private overlay: Overlay, private viewContainerRef: ViewContainerRef, - private platformUtilsService: PlatformUtilsService + private platformUtilsService: PlatformUtilsService, + private organizationService: OrganizationService ) {} @HostListener("document:keydown.escape", ["$event"]) @@ -98,46 +100,45 @@ export class VaultSelectComponent implements OnInit { } async ngOnInit() { - await this.load(); - this.broadcasterService.subscribe(this.constructor.name, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case "syncCompleted": - await this.load(); - break; - default: - break; - } - }); - }); + this.organizations$ = this.organizationService.organizations$ + .pipe(takeUntil(this._destroy)) + .pipe(map((orgs) => orgs.sort((a, b) => a.name.localeCompare(b.name)))); + + this.organizations$ + .pipe( + concatMap(async (organizations) => { + this.enforcePersonalOwnership = + await this.vaultFilterService.checkForPersonalOwnershipPolicy(); + + if (this.shouldShow(organizations)) { + if (this.enforcePersonalOwnership && !this.vaultFilter.myVaultOnly) { + const firstOrganization = organizations[0]; + this._selectedVault.next(firstOrganization.name); + this.vaultFilterService.setVaultFilter(firstOrganization.id); + this.vaultFilter.selectedOrganizationId = firstOrganization.id; + } else if (this.vaultFilter.myVaultOnly) { + this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault)); + } else if (this.vaultFilter.selectedOrganizationId != null) { + const selectedOrganization = organizations.find( + (o) => o.id === this.vaultFilter.selectedOrganizationId + ); + this._selectedVault.next(selectedOrganization.name); + } else { + this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults)); + } + } + }) + ) + .pipe(takeUntil(this._destroy)) + .subscribe(); + + this.loaded = true; } - async load() { - this.vaultFilter = this.vaultFilterService.getVaultFilter(); - this.organizations = (await this.vaultFilterService.buildOrganizations()).sort((a, b) => - a.name.localeCompare(b.name) - ); - this.enforcePersonalOwnwership = - await this.vaultFilterService.checkForPersonalOwnershipPolicy(); - - if (this.show) { - if (this.enforcePersonalOwnwership && !this.vaultFilter.myVaultOnly) { - this.vaultFilterService.setVaultFilter(this.organizations[0].id); - this.vaultFilter.selectedOrganizationId = this.organizations[0].id; - this.vaultFilterDisplay = this.organizations.find( - (o) => o.id === this.vaultFilter.selectedOrganizationId - ).name; - } else if (this.vaultFilter.myVaultOnly) { - this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault); - } else if (this.vaultFilter.selectedOrganizationId != null) { - this.vaultFilterDisplay = this.organizations.find( - (o) => o.id === this.vaultFilter.selectedOrganizationId - ).name; - } else { - this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults); - } - } - this.loaded = true; + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + this._selectedVault.complete(); } openOverlay() { @@ -191,20 +192,20 @@ export class VaultSelectComponent implements OnInit { this.i18nService.t("disabledOrganizationFilterError") ); } else { - this.vaultFilterDisplay = organization.name; + this._selectedVault.next(organization.name); this.vaultFilterService.setVaultFilter(organization.id); this.onVaultSelectionChanged.emit(); this.close(); } } selectAllVaults() { - this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.allVaults); + this._selectedVault.next(this.i18nService.t(this.vaultFilterService.allVaults)); this.vaultFilterService.setVaultFilter(this.vaultFilterService.allVaults); this.onVaultSelectionChanged.emit(); this.close(); } selectMyVault() { - this.vaultFilterDisplay = this.i18nService.t(this.vaultFilterService.myVault); + this._selectedVault.next(this.i18nService.t(this.vaultFilterService.myVault)); this.vaultFilterService.setVaultFilter(this.vaultFilterService.myVault); this.onVaultSelectionChanged.emit(); this.close(); diff --git a/apps/browser/src/services/vaultFilter.service.ts b/apps/browser/src/services/vaultFilter.service.ts index b921a21292d..77016638d8f 100644 --- a/apps/browser/src/services/vaultFilter.service.ts +++ b/apps/browser/src/services/vaultFilter.service.ts @@ -3,7 +3,7 @@ import { VaultFilterService as BaseVaultFilterService } from "@bitwarden/angular import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 734a030943d..489f92fac9d 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -30,8 +30,8 @@ import { ImportService } from "@bitwarden/common/services/import.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { MemoryStorageService } from "@bitwarden/common/services/memoryStorage.service"; import { NoopMessagingService } from "@bitwarden/common/services/noopMessaging.service"; -import { OrganizationService } from "@bitwarden/common/services/organization.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; +import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; import { ProviderService } from "@bitwarden/common/services/provider.service"; @@ -41,6 +41,7 @@ import { SettingsService } from "@bitwarden/common/services/settings.service"; import { StateService } from "@bitwarden/common/services/state.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service"; +import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service"; import { TokenService } from "@bitwarden/common/services/token.service"; import { TotpService } from "@bitwarden/common/services/totp.service"; import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service"; @@ -113,6 +114,7 @@ export class Main { folderApiService: FolderApiService; userVerificationApiService: UserVerificationApiService; organizationApiService: OrganizationApiServiceAbstraction; + syncNotifierService: SyncNotifierService; constructor() { let p = null; @@ -191,7 +193,9 @@ export class Main { customUserAgent ); - this.organizationApiService = new OrganizationApiService(this.apiService); + this.syncNotifierService = new SyncNotifierService(); + + this.organizationApiService = new OrganizationApiService(this.apiService, this.syncService); this.containerService = new ContainerService(this.cryptoService, this.encryptService); @@ -231,7 +235,7 @@ export class Main { this.providerService = new ProviderService(this.stateService); - this.organizationService = new OrganizationService(this.stateService); + this.organizationService = new OrganizationService(this.stateService, this.syncNotifierService); this.policyService = new PolicyService(this.stateService, this.organizationService); @@ -311,9 +315,9 @@ export class Main { this.logService, this.keyConnectorService, this.stateService, - this.organizationService, this.providerService, this.folderApiService, + this.syncNotifierService, async (expired: boolean) => await this.logout() ); diff --git a/apps/cli/src/commands/convertToKeyConnector.command.ts b/apps/cli/src/commands/convertToKeyConnector.command.ts index 6339fa9b9aa..00fb2f72910 100644 --- a/apps/cli/src/commands/convertToKeyConnector.command.ts +++ b/apps/cli/src/commands/convertToKeyConnector.command.ts @@ -74,7 +74,6 @@ export class ConvertToKeyConnectorCommand { } else if (answer.convert === "leave") { await this.organizationApiService.leave(organization.id); await this.keyConnectorService.removeConvertAccountRequired(); - await this.syncService.fullSync(true); return Response.success(); } else { await this.logout(); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index 7cfcd9fb6e3..510cb10da3f 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -4,7 +4,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { TotpService } from "@bitwarden/common/abstractions/totp.service"; diff --git a/apps/cli/src/commands/import.command.ts b/apps/cli/src/commands/import.command.ts index f3b04a5f564..6fb17fa2cd9 100644 --- a/apps/cli/src/commands/import.command.ts +++ b/apps/cli/src/commands/import.command.ts @@ -2,7 +2,7 @@ import * as program from "commander"; import * as inquirer from "inquirer"; import { ImportService } from "@bitwarden/common/abstractions/import.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { ImportType } from "@bitwarden/common/enums/importOptions"; import { Importer } from "@bitwarden/common/importers/importer"; import { Response } from "@bitwarden/node/cli/models/response"; diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 4e2d7cb02e0..dd779ec0576 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -2,7 +2,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Utils } from "@bitwarden/common/misc/utils"; import { CollectionData } from "@bitwarden/common/models/data/collectionData"; diff --git a/apps/desktop/src/app/vault/add-edit.component.ts b/apps/desktop/src/app/vault/add-edit.component.ts index aa76d2ba7dd..5a2153fadc4 100644 --- a/apps/desktop/src/app/vault/add-edit.component.ts +++ b/apps/desktop/src/app/vault/add-edit.component.ts @@ -11,7 +11,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/apps/desktop/src/app/vault/share.component.html b/apps/desktop/src/app/vault/share.component.html index f80c3b48296..f460703d5d3 100644 --- a/apps/desktop/src/app/vault/share.component.html +++ b/apps/desktop/src/app/vault/share.component.html @@ -1,78 +1,80 @@
- {{ "startTrial" | i18n }} +
diff --git a/apps/web/src/app/accounts/trial-initiation/billing.component.ts b/apps/web/src/app/accounts/trial-initiation/billing.component.ts index 7bf672d09a2..aff798b5b86 100644 --- a/apps/web/src/app/accounts/trial-initiation/billing.component.ts +++ b/apps/web/src/app/accounts/trial-initiation/billing.component.ts @@ -7,8 +7,8 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; diff --git a/apps/web/src/app/common/base.people.component.ts b/apps/web/src/app/common/base.people.component.ts index 6b9370ae814..8d9a3324244 100644 --- a/apps/web/src/app/common/base.people.component.ts +++ b/apps/web/src/app/common/base.people.component.ts @@ -126,7 +126,12 @@ export abstract class BasePeopleComponent< } this.allUsers = response.data != null && response.data.length > 0 ? response.data : []; - this.allUsers.sort(Utils.getSortFunction(this.i18nService, "email")); + this.allUsers.sort( + Utils.getSortFunction( + this.i18nService, + "email" + ) + ); this.allUsers.forEach((u) => { if (!this.statusMap.has(u.status)) { this.statusMap.set(u.status, [u]); diff --git a/apps/web/src/app/components/organization-switcher.component.html b/apps/web/src/app/components/organization-switcher.component.html index cd003ae247e..1cd9c090d59 100644 --- a/apps/web/src/app/components/organization-switcher.component.html +++ b/apps/web/src/app/components/organization-switcher.component.html @@ -45,7 +45,11 @@
    -
  • +
  • ; loaded = false; async ngOnInit() { - await this.load(); - } - - async load() { - const orgs = await this.organizationService.getAll(); - this.organizations = orgs - .filter(canAccessOrgAdmin) - .sort(Utils.getSortFunction(this.i18nService, "name")); + this.organizations$ = this.organizationService.organizations$.pipe( + canAccessAdmin(this.i18nService) + ); this.loaded = true; } diff --git a/apps/web/src/app/layouts/navbar.component.html b/apps/web/src/app/layouts/navbar.component.html index ee428cb5ee8..97b045b9751 100644 --- a/apps/web/src/app/layouts/navbar.component.html +++ b/apps/web/src/app/layouts/navbar.component.html @@ -17,8 +17,12 @@
  • - diff --git a/apps/web/src/app/layouts/navbar.component.ts b/apps/web/src/app/layouts/navbar.component.ts index 3552ce12233..573adfacca6 100644 --- a/apps/web/src/app/layouts/navbar.component.ts +++ b/apps/web/src/app/layouts/navbar.component.ts @@ -1,19 +1,19 @@ -import { Component, NgZone, OnInit } from "@angular/core"; +import { Component, OnInit } from "@angular/core"; +import { Observable } from "rxjs"; -import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { + canAccessAdmin, + OrganizationService, +} from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { TokenService } from "@bitwarden/common/abstractions/token.service"; -import { Utils } from "@bitwarden/common/misc/utils"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { Provider } from "@bitwarden/common/models/domain/provider"; -import { canAccessOrgAdmin } from "../organizations/navigation-permissions"; - @Component({ selector: "app-navbar", templateUrl: "navbar.component.html", @@ -24,7 +24,7 @@ export class NavbarComponent implements OnInit { name: string; email: string; providers: Provider[] = []; - organizations: Organization[] = []; + organizations$: Observable; constructor( private messagingService: MessagingService, @@ -33,9 +33,7 @@ export class NavbarComponent implements OnInit { private providerService: ProviderService, private syncService: SyncService, private organizationService: OrganizationService, - private i18nService: I18nService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone + private i18nService: I18nService ) { this.selfHosted = this.platformUtilsService.isSelfHost(); } @@ -53,24 +51,9 @@ export class NavbarComponent implements OnInit { } this.providers = await this.providerService.getAll(); - this.organizations = await this.buildOrganizations(); - - this.broadcasterService.subscribe(this.constructor.name, async (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case "organizationCreated": - if (this.organizations.length < 1) { - this.organizations = await this.buildOrganizations(); - } - break; - } - }); - }); - } - - async buildOrganizations() { - const allOrgs = await this.organizationService.getAll(); - return allOrgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(this.i18nService, "name")); + this.organizations$ = this.organizationService.organizations$.pipe( + canAccessAdmin(this.i18nService) + ); } lock() { diff --git a/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts b/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts index f55470bbf4c..0e410d9734d 100644 --- a/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts +++ b/apps/web/src/app/organizations/billing/organization-billing-routing.module.ts @@ -1,9 +1,10 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { canAccessBillingTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; + import { PaymentMethodComponent } from "../../settings/payment-method.component"; import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { canAccessBillingTab } from "../navigation-permissions"; import { OrgBillingHistoryViewComponent } from "./organization-billing-history-view.component"; import { OrganizationBillingTabComponent } from "./organization-billing-tab.component"; diff --git a/apps/web/src/app/organizations/billing/organization-subscription.component.ts b/apps/web/src/app/organizations/billing/organization-subscription.component.ts index cf80ff2af1c..7ee3a42da89 100644 --- a/apps/web/src/app/organizations/billing/organization-subscription.component.ts +++ b/apps/web/src/app/organizations/billing/organization-subscription.component.ts @@ -7,8 +7,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { OrganizationApiKeyType } from "@bitwarden/common/enums/organizationApiKeyType"; import { OrganizationConnectionType } from "@bitwarden/common/enums/organizationConnectionType"; @@ -87,7 +87,7 @@ export class OrganizationSubscriptionComponent implements OnInit { } this.loading = true; - this.userOrg = await this.organizationService.get(this.organizationId); + this.userOrg = this.organizationService.get(this.organizationId); if (this.userOrg.canManageBilling) { this.sub = await this.organizationApiService.getSubscription(this.organizationId); } diff --git a/apps/web/src/app/organizations/guards/org-permissions.guard.spec.ts b/apps/web/src/app/organizations/guards/org-permissions.guard.spec.ts index 59f7c5390a9..350f3879f08 100644 --- a/apps/web/src/app/organizations/guards/org-permissions.guard.spec.ts +++ b/apps/web/src/app/organizations/guards/org-permissions.guard.spec.ts @@ -7,7 +7,7 @@ import { import { mock, MockProxy } from "jest-mock-extended"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType"; @@ -57,7 +57,7 @@ describe("Organization Permissions Guard", () => { }); it("blocks navigation if organization does not exist", async () => { - organizationService.get.mockResolvedValue(null); + organizationService.get.mockReturnValue(null); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -66,7 +66,7 @@ describe("Organization Permissions Guard", () => { it("permits navigation if no permissions are specified", async () => { const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -81,7 +81,7 @@ describe("Organization Permissions Guard", () => { }; const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -104,7 +104,7 @@ describe("Organization Permissions Guard", () => { }); const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -124,7 +124,7 @@ describe("Organization Permissions Guard", () => { }), }); const org = orgFactory(); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -141,7 +141,7 @@ describe("Organization Permissions Guard", () => { type: OrganizationUserType.Admin, enabled: false, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); @@ -153,7 +153,7 @@ describe("Organization Permissions Guard", () => { type: OrganizationUserType.Owner, enabled: false, }); - organizationService.get.calledWith(org.id).mockResolvedValue(org); + organizationService.get.calledWith(org.id).mockReturnValue(org); const actual = await organizationPermissionsGuard.canActivate(route, state); diff --git a/apps/web/src/app/organizations/guards/org-permissions.guard.ts b/apps/web/src/app/organizations/guards/org-permissions.guard.ts index 45c2b8f8bf2..594c1d04b8e 100644 --- a/apps/web/src/app/organizations/guards/org-permissions.guard.ts +++ b/apps/web/src/app/organizations/guards/org-permissions.guard.ts @@ -2,13 +2,14 @@ import { Injectable } from "@angular/core"; import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot } from "@angular/router"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { + canAccessOrgAdmin, + OrganizationService, +} from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; -import { canAccessOrgAdmin } from "../navigation-permissions"; - @Injectable({ providedIn: "root", }) @@ -27,7 +28,7 @@ export class OrganizationPermissionsGuard implements CanActivate { await this.syncService.fullSync(false); } - const org = await this.organizationService.get(route.params.organizationId); + const org = this.organizationService.get(route.params.organizationId); if (org == null) { return this.router.createUrlTree(["/"]); } diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.html b/apps/web/src/app/organizations/layouts/organization-layout.component.html index 1a496e7364d..c9088e679a2 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.html +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.html @@ -1,5 +1,5 @@ -
    +
    {{ "vault" | i18n }} - {{ "members" | i18n }} - {{ "groups" | i18n }} - {{ - reportTabLabel | i18n + {{ + "members" | i18n }} - {{ "billing" | i18n }} - {{ + {{ + "groups" | i18n + }} + {{ getReportTabLabel(organization) | i18n }} + {{ + "billing" | i18n + }} + {{ "settings" | i18n }} diff --git a/apps/web/src/app/organizations/layouts/organization-layout.component.ts b/apps/web/src/app/organizations/layouts/organization-layout.component.ts index e0ceb0ad0e7..b9459f5f702 100644 --- a/apps/web/src/app/organizations/layouts/organization-layout.component.ts +++ b/apps/web/src/app/organizations/layouts/organization-layout.component.ts @@ -1,10 +1,6 @@ -import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { concatMap, Subject, takeUntil } from "rxjs"; - -import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; -import { Organization } from "@bitwarden/common/models/domain/organization"; +import { map, mergeMap, Observable, Subject, takeUntil } from "rxjs"; import { canAccessBillingTab, @@ -12,85 +8,67 @@ import { canAccessMembersTab, canAccessReportingTab, canAccessSettingsTab, -} from "../navigation-permissions"; - -const BroadcasterSubscriptionId = "OrganizationLayoutComponent"; + getOrganizationById, + OrganizationService, +} from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; +import { Organization } from "@bitwarden/common/models/domain/organization"; @Component({ selector: "app-organization-layout", templateUrl: "organization-layout.component.html", }) export class OrganizationLayoutComponent implements OnInit, OnDestroy { - private organizationId: string; - private destroy$ = new Subject(); + organization$: Observable; - organization: Organization; + private _destroy = new Subject(); - constructor( - private route: ActivatedRoute, - private organizationService: OrganizationService, - private broadcasterService: BroadcasterService, - private ngZone: NgZone - ) {} + constructor(private route: ActivatedRoute, private organizationService: OrganizationService) {} ngOnInit() { document.body.classList.remove("layout_frontend"); - this.route.params - .pipe( - concatMap(async (params: any) => { - this.organizationId = params.organizationId; - await this.load(); - }), - takeUntil(this.destroy$) - ) - .subscribe(); - this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { - this.ngZone.run(async () => { - switch (message.command) { - case "updatedOrgLicense": - await this.load(); - break; - } - }); - }); + this.organization$ = this.route.params + .pipe(takeUntil(this._destroy)) + .pipe(map((p) => p.organizationId)) + .pipe( + mergeMap((id) => { + return this.organizationService.organizations$ + .pipe(takeUntil(this._destroy)) + .pipe(getOrganizationById(id)); + }) + ); } ngOnDestroy() { - this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); - this.destroy$.next(); - this.destroy$.complete(); + this._destroy.next(); + this._destroy.complete(); } - async load() { - this.organization = await this.organizationService.get(this.organizationId); + canShowSettingsTab(organization: Organization): boolean { + return canAccessSettingsTab(organization); } - get showSettingsTab(): boolean { - return canAccessSettingsTab(this.organization); + canShowMembersTab(organization: Organization): boolean { + return canAccessMembersTab(organization); } - get showMembersTab(): boolean { - return canAccessMembersTab(this.organization); + canShowGroupsTab(organization: Organization): boolean { + return canAccessGroupsTab(organization); } - get showGroupsTab(): boolean { - return canAccessGroupsTab(this.organization); + canShowReportsTab(organization: Organization): boolean { + return canAccessReportingTab(organization); } - get showReportsTab(): boolean { - return canAccessReportingTab(this.organization); + canShowBillingTab(organization: Organization): boolean { + return canAccessBillingTab(organization); } - get showBillingTab(): boolean { - return canAccessBillingTab(this.organization); + getReportTabLabel(organization: Organization): string { + return organization.useEvents ? "reporting" : "reports"; } - get reportTabLabel(): string { - return this.organization.useEvents ? "reporting" : "reports"; - } - - get reportRoute(): string { - return this.organization.useEvents ? "reporting/events" : "reporting/reports"; + getReportRoute(organization: Organization): string { + return organization.useEvents ? "reporting/events" : "reporting/reports"; } } diff --git a/apps/web/src/app/organizations/manage/collection-add-edit.component.ts b/apps/web/src/app/organizations/manage/collection-add-edit.component.ts index 2c77a726b72..5c428abda4f 100644 --- a/apps/web/src/app/organizations/manage/collection-add-edit.component.ts +++ b/apps/web/src/app/organizations/manage/collection-add-edit.component.ts @@ -4,7 +4,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { Utils } from "@bitwarden/common/misc/utils"; import { EncString } from "@bitwarden/common/models/domain/encString"; diff --git a/apps/web/src/app/organizations/manage/collections.component.ts b/apps/web/src/app/organizations/manage/collections.component.ts index 04867a4eabe..d9fe9bdb689 100644 --- a/apps/web/src/app/organizations/manage/collections.component.ts +++ b/apps/web/src/app/organizations/manage/collections.component.ts @@ -7,7 +7,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { CollectionData } from "@bitwarden/common/models/data/collectionData"; diff --git a/apps/web/src/app/organizations/manage/events.component.ts b/apps/web/src/app/organizations/manage/events.component.ts index 6573543d0d0..9ee2152f229 100644 --- a/apps/web/src/app/organizations/manage/events.component.ts +++ b/apps/web/src/app/organizations/manage/events.component.ts @@ -7,7 +7,7 @@ import { ExportService } from "@bitwarden/common/abstractions/export.service"; import { FileDownloadService } from "@bitwarden/common/abstractions/fileDownload/fileDownload.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { Organization } from "@bitwarden/common/models/domain/organization"; diff --git a/apps/web/src/app/organizations/manage/manage.component.ts b/apps/web/src/app/organizations/manage/manage.component.ts index f1e25092596..5e42e3bfe32 100644 --- a/apps/web/src/app/organizations/manage/manage.component.ts +++ b/apps/web/src/app/organizations/manage/manage.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; @Component({ diff --git a/apps/web/src/app/organizations/manage/people.component.ts b/apps/web/src/app/organizations/manage/people.component.ts index ca232c70fc0..c34505fbb41 100644 --- a/apps/web/src/app/organizations/manage/people.component.ts +++ b/apps/web/src/app/organizations/manage/people.component.ts @@ -10,8 +10,8 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/apps/web/src/app/organizations/navigation-permissions.ts b/apps/web/src/app/organizations/navigation-permissions.ts index 3c44e90957d..e69de29bb2d 100644 --- a/apps/web/src/app/organizations/navigation-permissions.ts +++ b/apps/web/src/app/organizations/navigation-permissions.ts @@ -1,31 +0,0 @@ -import { Organization } from "@bitwarden/common/models/domain/organization"; - -export function canAccessMembersTab(org: Organization): boolean { - return org.canManageUsers || org.canManageUsersPassword; -} - -export function canAccessGroupsTab(org: Organization): boolean { - return org.canManageGroups; -} - -export function canAccessReportingTab(org: Organization): boolean { - return org.canAccessReports || org.canAccessEventLogs; -} - -export function canAccessBillingTab(org: Organization): boolean { - return org.canManageBilling; -} - -export function canAccessSettingsTab(org: Organization): boolean { - return org.isOwner; -} - -export function canAccessOrgAdmin(org: Organization): boolean { - return ( - canAccessMembersTab(org) || - canAccessGroupsTab(org) || - canAccessReportingTab(org) || - canAccessBillingTab(org) || - canAccessSettingsTab(org) - ); -} diff --git a/apps/web/src/app/organizations/organization-routing.module.ts b/apps/web/src/app/organizations/organization-routing.module.ts index 33ff52e9826..f21072f23f9 100644 --- a/apps/web/src/app/organizations/organization-routing.module.ts +++ b/apps/web/src/app/organizations/organization-routing.module.ts @@ -2,16 +2,16 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; +import { + canAccessOrgAdmin, + canAccessGroupsTab, + canAccessMembersTab, +} from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { OrganizationPermissionsGuard } from "./guards/org-permissions.guard"; import { OrganizationLayoutComponent } from "./layouts/organization-layout.component"; import { GroupsComponent } from "./manage/groups.component"; import { PeopleComponent } from "./manage/people.component"; -import { - canAccessGroupsTab, - canAccessMembersTab, - canAccessOrgAdmin, -} from "./navigation-permissions"; import { VaultModule } from "./vault/vault.module"; const routes: Routes = [ diff --git a/apps/web/src/app/organizations/policies/master-password.component.ts b/apps/web/src/app/organizations/policies/master-password.component.ts index 90f45beecd2..a28a08ce9fd 100644 --- a/apps/web/src/app/organizations/policies/master-password.component.ts +++ b/apps/web/src/app/organizations/policies/master-password.component.ts @@ -2,7 +2,7 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyType } from "@bitwarden/common/enums/policyType"; import { BasePolicy, BasePolicyComponent } from "./base-policy.component"; diff --git a/apps/web/src/app/organizations/policies/policies.component.ts b/apps/web/src/app/organizations/policies/policies.component.ts index b4e11a46ba0..ec9a1dc2bc9 100644 --- a/apps/web/src/app/organizations/policies/policies.component.ts +++ b/apps/web/src/app/organizations/policies/policies.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { PolicyType } from "@bitwarden/common/enums/policyType"; import { Organization } from "@bitwarden/common/models/domain/organization"; diff --git a/apps/web/src/app/organizations/policies/reset-password.component.ts b/apps/web/src/app/organizations/policies/reset-password.component.ts index b114452fb9a..6f7fb26551b 100644 --- a/apps/web/src/app/organizations/policies/reset-password.component.ts +++ b/apps/web/src/app/organizations/policies/reset-password.component.ts @@ -1,7 +1,7 @@ import { Component } from "@angular/core"; import { UntypedFormBuilder } from "@angular/forms"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyType } from "@bitwarden/common/enums/policyType"; import { Organization } from "@bitwarden/common/models/domain/organization"; diff --git a/apps/web/src/app/organizations/reporting/organization-reporting-routing.module.ts b/apps/web/src/app/organizations/reporting/organization-reporting-routing.module.ts index 88e3e25c9d1..378325a64cf 100644 --- a/apps/web/src/app/organizations/reporting/organization-reporting-routing.module.ts +++ b/apps/web/src/app/organizations/reporting/organization-reporting-routing.module.ts @@ -1,11 +1,11 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { canAccessReportingTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; import { EventsComponent } from "../manage/events.component"; -import { canAccessReportingTab } from "../navigation-permissions"; import { ExposedPasswordsReportComponent } from "../tools/exposed-passwords-report.component"; import { InactiveTwoFactorReportComponent } from "../tools/inactive-two-factor-report.component"; import { ReusedPasswordsReportComponent } from "../tools/reused-passwords-report.component"; diff --git a/apps/web/src/app/organizations/reporting/reporting.component.ts b/apps/web/src/app/organizations/reporting/reporting.component.ts index 880de67c0fd..671e45b7962 100644 --- a/apps/web/src/app/organizations/reporting/reporting.component.ts +++ b/apps/web/src/app/organizations/reporting/reporting.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { concatMap, Subject, takeUntil } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; @Component({ diff --git a/apps/web/src/app/organizations/settings/account.component.html b/apps/web/src/app/organizations/settings/account.component.html index 680cb2badd8..531d95f2672 100644 --- a/apps/web/src/app/organizations/settings/account.component.html +++ b/apps/web/src/app/organizations/settings/account.component.html @@ -56,9 +56,9 @@
    - +
    diff --git a/apps/web/src/app/organizations/settings/account.component.ts b/apps/web/src/app/organizations/settings/account.component.ts index f8a909f1eda..8ca3290931c 100644 --- a/apps/web/src/app/organizations/settings/account.component.ts +++ b/apps/web/src/app/organizations/settings/account.component.ts @@ -2,14 +2,12 @@ import { Component, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; -import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { OrganizationKeysRequest } from "@bitwarden/common/models/request/organizationKeysRequest"; import { OrganizationUpdateRequest } from "@bitwarden/common/models/request/organizationUpdateRequest"; import { OrganizationResponse } from "@bitwarden/common/models/response/organizationResponse"; @@ -39,17 +37,15 @@ export class AccountComponent { loading = true; canUseApi = false; org: OrganizationResponse; - formPromise: Promise; + formPromise: Promise; taxFormPromise: Promise; private organizationId: string; constructor( private modalService: ModalService, - private apiService: ApiService, private i18nService: I18nService, private route: ActivatedRoute, - private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private cryptoService: CryptoService, private logService: LogService, @@ -64,9 +60,7 @@ export class AccountComponent { // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { this.organizationId = params.organizationId; - this.canManageBilling = ( - await this.organizationService.get(this.organizationId) - ).canManageBilling; + this.canManageBilling = this.organizationService.get(this.organizationId).canManageBilling; try { this.org = await this.organizationApiService.get(this.organizationId); this.canUseApi = this.org.useApi; @@ -92,9 +86,7 @@ export class AccountComponent { request.keys = new OrganizationKeysRequest(orgKeys[0], orgKeys[1].encryptedString); } - this.formPromise = this.organizationApiService.save(this.organizationId, request).then(() => { - return this.syncService.fullSync(true); - }); + this.formPromise = this.organizationApiService.save(this.organizationId, request); await this.formPromise; this.platformUtilsService.showToast( "success", diff --git a/apps/web/src/app/organizations/settings/delete-organization.component.ts b/apps/web/src/app/organizations/settings/delete-organization.component.ts index 2b2341a44ac..e67f667b9c9 100644 --- a/apps/web/src/app/organizations/settings/delete-organization.component.ts +++ b/apps/web/src/app/organizations/settings/delete-organization.component.ts @@ -3,8 +3,8 @@ import { Component, EventEmitter, OnInit, Output } from "@angular/core"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { UserVerificationService } from "@bitwarden/common/abstractions/userVerification/userVerification.service.abstraction"; import { CipherType } from "@bitwarden/common/enums/cipherType"; diff --git a/apps/web/src/app/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/organizations/settings/organization-settings-routing.module.ts index c05954dfaac..1d4a4df8520 100644 --- a/apps/web/src/app/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/organizations/settings/organization-settings-routing.module.ts @@ -1,10 +1,10 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; +import { canAccessSettingsTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { OrganizationPermissionsGuard } from "../guards/org-permissions.guard"; -import { canAccessSettingsTab } from "../navigation-permissions"; import { PoliciesComponent } from "../policies"; import { AccountComponent } from "./account.component"; diff --git a/apps/web/src/app/organizations/settings/settings.component.ts b/apps/web/src/app/organizations/settings/settings.component.ts index 770661ffcf7..9988e65dd7c 100644 --- a/apps/web/src/app/organizations/settings/settings.component.ts +++ b/apps/web/src/app/organizations/settings/settings.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { Subject, switchMap, takeUntil } from "rxjs"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; @Component({ diff --git a/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html b/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html index caa2260f526..055cdddb38a 100644 --- a/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html +++ b/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.html @@ -34,7 +34,9 @@ > - +
    diff --git a/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts b/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts index 3821a1e3722..853907452d2 100644 --- a/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts +++ b/apps/web/src/app/organizations/sponsorships/families-for-enterprise-setup.component.ts @@ -1,12 +1,13 @@ -import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; +import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { first } from "rxjs/operators"; +import { Observable, Subject } from "rxjs"; +import { first, map, takeUntil } from "rxjs/operators"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ValidationService } from "@bitwarden/angular/services/validation.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType"; @@ -22,8 +23,7 @@ import { OrganizationPlansComponent } from "../../settings/organization-plans.co selector: "families-for-enterprise-setup", templateUrl: "families-for-enterprise-setup.component.html", }) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class FamiliesForEnterpriseSetupComponent implements OnInit { +export class FamiliesForEnterpriseSetupComponent implements OnInit, OnDestroy { @ViewChild(OrganizationPlansComponent, { static: false }) set organizationPlansComponent(value: OrganizationPlansComponent) { if (!value) { @@ -46,11 +46,14 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit { token: string; existingFamilyOrganizations: Organization[]; + existingFamilyOrganizations$: Observable; showNewOrganization = false; _organizationPlansComponent: OrganizationPlansComponent; _selectedFamilyOrganizationId = ""; + private _destroy = new Subject(); + constructor( private router: Router, private platformUtilsService: PlatformUtilsService, @@ -84,17 +87,24 @@ export class FamiliesForEnterpriseSetupComponent implements OnInit { await this.syncService.fullSync(true); this.badToken = !(await this.apiService.postPreValidateSponsorshipToken(this.token)); this.loading = false; + }); - this.existingFamilyOrganizations = (await this.organizationService.getAll()).filter( - (o) => o.planProductType === ProductType.Families - ); + this.existingFamilyOrganizations$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs.filter((o) => o.planProductType === ProductType.Families)) + ); - if (this.existingFamilyOrganizations.length === 0) { + this.existingFamilyOrganizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { + if (orgs.length === 0) { this.selectedFamilyOrganizationId = "createNew"; } }); } + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } + async submit() { this.formPromise = this.doSubmit(this._selectedFamilyOrganizationId); await this.formPromise; diff --git a/apps/web/src/app/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/organizations/tools/exposed-passwords-report.component.ts index 00c2ce2e3f4..adb7ba60246 100644 --- a/apps/web/src/app/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/organizations/tools/exposed-passwords-report.component.ts @@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { Cipher } from "@bitwarden/common/models/domain/cipher"; diff --git a/apps/web/src/app/organizations/tools/import-export/org-import.component.ts b/apps/web/src/app/organizations/tools/import-export/org-import.component.ts index 27d1c6cd39e..69506b226a3 100644 --- a/apps/web/src/app/organizations/tools/import-export/org-import.component.ts +++ b/apps/web/src/app/organizations/tools/import-export/org-import.component.ts @@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ImportService } from "@bitwarden/common/abstractions/import.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/apps/web/src/app/organizations/tools/inactive-two-factor-report.component.ts b/apps/web/src/app/organizations/tools/inactive-two-factor-report.component.ts index 80fe51d32d3..49ae82ab5f3 100644 --- a/apps/web/src/app/organizations/tools/inactive-two-factor-report.component.ts +++ b/apps/web/src/app/organizations/tools/inactive-two-factor-report.component.ts @@ -5,7 +5,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; diff --git a/apps/web/src/app/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/organizations/tools/reused-passwords-report.component.ts index 98a62cd83ab..74e5ac91bb1 100644 --- a/apps/web/src/app/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/organizations/tools/reused-passwords-report.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { Cipher } from "@bitwarden/common/models/domain/cipher"; diff --git a/apps/web/src/app/organizations/tools/tools.component.ts b/apps/web/src/app/organizations/tools/tools.component.ts index 249e5cd2ca8..e50320d7c80 100644 --- a/apps/web/src/app/organizations/tools/tools.component.ts +++ b/apps/web/src/app/organizations/tools/tools.component.ts @@ -2,7 +2,7 @@ import { Component } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; @Component({ diff --git a/apps/web/src/app/organizations/tools/unsecured-websites-report.component.ts b/apps/web/src/app/organizations/tools/unsecured-websites-report.component.ts index b1e99c50fc5..dbb5e08be54 100644 --- a/apps/web/src/app/organizations/tools/unsecured-websites-report.component.ts +++ b/apps/web/src/app/organizations/tools/unsecured-websites-report.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; diff --git a/apps/web/src/app/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/organizations/tools/weak-passwords-report.component.ts index 9b5d95b6372..3b8cc1490a0 100644 --- a/apps/web/src/app/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/organizations/tools/weak-passwords-report.component.ts @@ -4,7 +4,7 @@ import { ActivatedRoute } from "@angular/router"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; diff --git a/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html b/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html index 72f1bbdbda7..4d24e66764e 100644 --- a/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html +++ b/apps/web/src/app/organizations/users/enroll-master-password-reset.component.html @@ -32,9 +32,9 @@
    - +

    {{ "reportError" | i18n }}...

    diff --git a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html index 2c5547efc7c..0fce002310b 100644 --- a/apps/web/src/app/reports/pages/exposed-passwords-report.component.html +++ b/apps/web/src/app/reports/pages/exposed-passwords-report.component.html @@ -2,9 +2,9 @@

    {{ "exposedPasswordsReport" | i18n }}

    {{ "exposedPasswordsReportDesc" | i18n }}

    - +
    {{ "noExposedPasswords" | i18n }} diff --git a/apps/web/src/app/settings/add-credit.component.ts b/apps/web/src/app/settings/add-credit.component.ts index eeb22e876d2..3d33df2d3ee 100644 --- a/apps/web/src/app/settings/add-credit.component.ts +++ b/apps/web/src/app/settings/add-credit.component.ts @@ -11,7 +11,7 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { PayPalConfig } from "@bitwarden/common/abstractions/environment.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { PaymentMethodType } from "@bitwarden/common/enums/paymentMethodType"; diff --git a/apps/web/src/app/settings/change-kdf.component.html b/apps/web/src/app/settings/change-kdf.component.html index b06cf01d060..1b3b62a03fb 100644 --- a/apps/web/src/app/settings/change-kdf.component.html +++ b/apps/web/src/app/settings/change-kdf.component.html @@ -71,7 +71,7 @@
- + diff --git a/apps/web/src/app/settings/change-password.component.html b/apps/web/src/app/settings/change-password.component.html index 6f36236a7ae..e74881db025 100644 --- a/apps/web/src/app/settings/change-password.component.html +++ b/apps/web/src/app/settings/change-password.component.html @@ -100,7 +100,7 @@ - + diff --git a/apps/web/src/app/settings/change-password.component.ts b/apps/web/src/app/settings/change-password.component.ts index 8044c3cf3ad..45672940f15 100644 --- a/apps/web/src/app/settings/change-password.component.ts +++ b/apps/web/src/app/settings/change-password.component.ts @@ -10,8 +10,8 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/apps/web/src/app/settings/emergency-access-add-edit.component.html b/apps/web/src/app/settings/emergency-access-add-edit.component.html index 410489321a1..b438cee937c 100644 --- a/apps/web/src/app/settings/emergency-access-add-edit.component.html +++ b/apps/web/src/app/settings/emergency-access-add-edit.component.html @@ -100,9 +100,15 @@
- {{ - "submit" | i18n - }} + diff --git a/apps/web/src/app/settings/organization-plans.component.ts b/apps/web/src/app/settings/organization-plans.component.ts index 7dc6f28e7fb..485afd12b15 100644 --- a/apps/web/src/app/settings/organization-plans.component.ts +++ b/apps/web/src/app/settings/organization-plans.component.ts @@ -7,8 +7,8 @@ import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; diff --git a/apps/web/src/app/settings/premium.component.html b/apps/web/src/app/settings/premium.component.html index 63995b457ac..13266fd9ca5 100644 --- a/apps/web/src/app/settings/premium.component.html +++ b/apps/web/src/app/settings/premium.component.html @@ -68,9 +68,9 @@ "licenseFileDesc" | i18n: "bitwarden_premium_license.json" }}
- +
@@ -118,7 +118,7 @@

{{ "paymentChargedAnnually" | i18n }} - + diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 6e9fa21b1f5..2a2fc4d2786 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -2,7 +2,7 @@ import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { BroadcasterService } from "@bitwarden/common/abstractions/broadcaster.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { TokenService } from "@bitwarden/common/abstractions/token.service"; diff --git a/apps/web/src/app/settings/sponsored-families.component.html b/apps/web/src/app/settings/sponsored-families.component.html index 9bd2bb70274..f0151dfddd0 100644 --- a/apps/web/src/app/settings/sponsored-families.component.html +++ b/apps/web/src/app/settings/sponsored-families.component.html @@ -22,7 +22,7 @@ [appApiAction]="formPromise" [formGroup]="sponsorshipForm" ngNativeValidate - *ngIf="anyOrgsAvailable" + *ngIf="anyOrgsAvailable$ | async" >
@@ -34,7 +34,9 @@ required > - +
@@ -74,7 +76,7 @@
- +
@@ -86,12 +88,12 @@ - + diff --git a/apps/web/src/app/settings/sponsored-families.component.ts b/apps/web/src/app/settings/sponsored-families.component.ts index d2233fc6c8a..b0cf27e9639 100644 --- a/apps/web/src/app/settings/sponsored-families.component.ts +++ b/apps/web/src/app/settings/sponsored-families.component.ts @@ -1,30 +1,40 @@ -import { Component, OnInit } from "@angular/core"; -import { UntypedFormBuilder, UntypedFormGroup, Validators } from "@angular/forms"; +import { Component, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms"; +import { map, Observable, Subject, takeUntil } from "rxjs"; import { notAllowedValueAsync } from "@bitwarden/angular/validators/notAllowedValueAsync.validator"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { SyncService } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; import { PlanSponsorshipType } from "@bitwarden/common/enums/planSponsorshipType"; import { Organization } from "@bitwarden/common/models/domain/organization"; +interface RequestSponsorshipForm { + selectedSponsorshipOrgId: FormControl; + sponsorshipEmail: FormControl; +} + @Component({ selector: "app-sponsored-families", templateUrl: "sponsored-families.component.html", }) -export class SponsoredFamiliesComponent implements OnInit { +export class SponsoredFamiliesComponent implements OnInit, OnDestroy { loading = false; - availableSponsorshipOrgs: Organization[] = []; - activeSponsorshipOrgs: Organization[] = []; + availableSponsorshipOrgs$: Observable; + activeSponsorshipOrgs$: Observable; + anyOrgsAvailable$: Observable; + anyActiveSponsorships$: Observable; // Conditional display properties - formPromise: Promise; + formPromise: Promise; - sponsorshipForm: UntypedFormGroup; + sponsorshipForm: FormGroup; + + private _destroy = new Subject(); constructor( private apiService: ApiService, @@ -32,31 +42,50 @@ export class SponsoredFamiliesComponent implements OnInit { private platformUtilsService: PlatformUtilsService, private syncService: SyncService, private organizationService: OrganizationService, - private formBuilder: UntypedFormBuilder, + private formBuilder: FormBuilder, private stateService: StateService ) { - this.sponsorshipForm = this.formBuilder.group({ - selectedSponsorshipOrgId: [ - "", - { - validators: [Validators.required], - }, - ], - sponsorshipEmail: [ - "", - { - validators: [Validators.email], - asyncValidators: [ - notAllowedValueAsync(async () => await this.stateService.getEmail(), true), - ], - updateOn: "blur", - }, - ], + this.sponsorshipForm = this.formBuilder.group({ + selectedSponsorshipOrgId: new FormControl("", { + validators: [Validators.required], + }), + sponsorshipEmail: new FormControl("", { + validators: [Validators.email], + asyncValidators: [ + notAllowedValueAsync(async () => await this.stateService.getEmail(), true), + ], + updateOn: "blur", + }), }); } async ngOnInit() { - await this.load(); + this.availableSponsorshipOrgs$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs.filter((o) => o.familySponsorshipAvailable)) + ); + + this.availableSponsorshipOrgs$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { + if (orgs.length === 1) { + this.sponsorshipForm.patchValue({ + selectedSponsorshipOrgId: orgs[0].id, + }); + } + }); + + this.anyOrgsAvailable$ = this.availableSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); + + this.activeSponsorshipOrgs$ = this.organizationService.organizations$.pipe( + map((orgs) => orgs.filter((o) => o.familySponsorshipFriendlyName !== null)) + ); + + this.anyActiveSponsorships$ = this.activeSponsorshipOrgs$.pipe(map((orgs) => orgs.length > 0)); + + this.loading = false; + } + + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); } async submit() { @@ -73,50 +102,23 @@ export class SponsoredFamiliesComponent implements OnInit { this.platformUtilsService.showToast("success", null, this.i18nService.t("sponsorshipCreated")); this.formPromise = null; this.resetForm(); - await this.load(true); + await this.forceReload(); } - async load(forceReload = false) { - if (this.loading) { - return; - } - + async forceReload() { this.loading = true; - if (forceReload) { - await this.syncService.fullSync(true); - } - - const allOrgs = await this.organizationService.getAll(); - this.availableSponsorshipOrgs = allOrgs.filter((org) => org.familySponsorshipAvailable); - - this.activeSponsorshipOrgs = allOrgs.filter( - (org) => org.familySponsorshipFriendlyName !== null - ); - - if (this.availableSponsorshipOrgs.length === 1) { - this.sponsorshipForm.patchValue({ - selectedSponsorshipOrgId: this.availableSponsorshipOrgs[0].id, - }); - } + await this.syncService.fullSync(true); this.loading = false; } get sponsorshipEmailControl() { - return this.sponsorshipForm.controls["sponsorshipEmail"]; + return this.sponsorshipForm.controls.sponsorshipEmail; } private async resetForm() { this.sponsorshipForm.reset(); } - get anyActiveSponsorships(): boolean { - return this.activeSponsorshipOrgs.length > 0; - } - - get anyOrgsAvailable(): boolean { - return this.availableSponsorshipOrgs.length > 0; - } - get isSelfHosted(): boolean { return this.platformUtilsService.isSelfHost(); } diff --git a/apps/web/src/app/settings/two-factor-setup.component.html b/apps/web/src/app/settings/two-factor-setup.component.html index 931296bdbdf..fb71ac5c394 100644 --- a/apps/web/src/app/settings/two-factor-setup.component.html +++ b/apps/web/src/app/settings/two-factor-setup.component.html @@ -92,9 +92,15 @@ {{ "deviceVerificationDesc" | i18n }} - + diff --git a/apps/web/src/app/shared/shared.module.ts b/apps/web/src/app/shared/shared.module.ts index 7db6a1911cb..68c558f226d 100644 --- a/apps/web/src/app/shared/shared.module.ts +++ b/apps/web/src/app/shared/shared.module.ts @@ -12,7 +12,6 @@ import { ButtonModule, CalloutModule, FormFieldModule, - SubmitButtonModule, MenuModule, TabsModule, IconModule, @@ -45,7 +44,6 @@ import "./locales"; ButtonModule, MenuModule, FormFieldModule, - SubmitButtonModule, IconModule, TabsModule, ], @@ -65,7 +63,6 @@ import "./locales"; ButtonModule, MenuModule, FormFieldModule, - SubmitButtonModule, IconModule, TabsModule, ], diff --git a/apps/web/src/app/vault/add-edit.component.ts b/apps/web/src/app/vault/add-edit.component.ts index 0b7642eb97e..2a01a8ec590 100644 --- a/apps/web/src/app/vault/add-edit.component.ts +++ b/apps/web/src/app/vault/add-edit.component.ts @@ -9,7 +9,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; diff --git a/apps/web/src/app/vault/bulk-share.component.ts b/apps/web/src/app/vault/bulk-share.component.ts index eccdf027b2b..abf17fba9b0 100644 --- a/apps/web/src/app/vault/bulk-share.component.ts +++ b/apps/web/src/app/vault/bulk-share.component.ts @@ -4,11 +4,12 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CollectionView } from "@bitwarden/common/models/view/collectionView"; +import { Checkable, isChecked } from "@bitwarden/common/types/checkable"; @Component({ selector: "app-vault-bulk-share", @@ -20,10 +21,10 @@ export class BulkShareComponent implements OnInit { @Output() onShared = new EventEmitter(); nonShareableCount = 0; - collections: CollectionView[] = []; + collections: Checkable[] = []; organizations: Organization[] = []; shareableCiphers: CipherView[] = []; - formPromise: Promise; + formPromise: Promise; private writeableCollections: CollectionView[] = []; @@ -66,9 +67,7 @@ export class BulkShareComponent implements OnInit { } async submit() { - const checkedCollectionIds = this.collections - .filter((c) => (c as any).checked) - .map((c) => c.id); + const checkedCollectionIds = this.collections.filter(isChecked).map((c) => c.id); try { this.formPromise = this.cipherService.shareManyWithServer( this.shareableCiphers, @@ -90,8 +89,8 @@ export class BulkShareComponent implements OnInit { } } - check(c: CollectionView, select?: boolean) { - (c as any).checked = select == null ? !(c as any).checked : select; + check(c: Checkable, select?: boolean) { + c.checked = select == null ? !c.checked : select; } selectAll(select: boolean) { @@ -106,7 +105,7 @@ export class BulkShareComponent implements OnInit { this.collections != null ) { for (let i = 0; i < this.collections.length; i++) { - if ((this.collections[i] as any).checked) { + if (this.collections[i].checked) { return true; } } diff --git a/apps/web/src/app/vault/ciphers.component.ts b/apps/web/src/app/vault/ciphers.component.ts index 0ccb487ac7a..624ea1e65e6 100644 --- a/apps/web/src/app/vault/ciphers.component.ts +++ b/apps/web/src/app/vault/ciphers.component.ts @@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { EventService } from "@bitwarden/common/abstractions/event.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; diff --git a/apps/web/src/app/vault/share.component.html b/apps/web/src/app/vault/share.component.html index 8d533e12004..77fe2f0ef83 100644 --- a/apps/web/src/app/vault/share.component.html +++ b/apps/web/src/app/vault/share.component.html @@ -15,78 +15,87 @@ - -
+ + + + + + +
+ + + {{ c.name }} +
-
- {{ "noCollectionsInList" | i18n }} + - - - - - - - -
- - - {{ c.name }} -
-
- +
diff --git a/apps/web/src/app/vault/share.component.ts b/apps/web/src/app/vault/share.component.ts index 018e4a6f569..6d22c0fe50d 100644 --- a/apps/web/src/app/vault/share.component.ts +++ b/apps/web/src/app/vault/share.component.ts @@ -5,7 +5,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { CollectionView } from "@bitwarden/common/models/view/collectionView"; diff --git a/apps/web/src/app/vault/vault-filter/organization-filter/organization-options.component.ts b/apps/web/src/app/vault/vault-filter/organization-filter/organization-options.component.ts index efffd7d54a2..fb1009fa515 100644 --- a/apps/web/src/app/vault/vault-filter/organization-filter/organization-options.component.ts +++ b/apps/web/src/app/vault/vault-filter/organization-filter/organization-options.component.ts @@ -102,9 +102,7 @@ export class OrganizationOptionsComponent { } try { - this.actionPromise = this.organizationApiService.leave(org.id).then(() => { - return this.syncService.fullSync(true); - }); + this.actionPromise = this.organizationApiService.leave(org.id); await this.actionPromise; this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); await this.load(); diff --git a/apps/web/src/app/vault/vault-filter/shared/vault-filter.service.ts b/apps/web/src/app/vault/vault-filter/shared/vault-filter.service.ts index b6c2f7cfa70..d8bb48ce663 100644 --- a/apps/web/src/app/vault/vault-filter/shared/vault-filter.service.ts +++ b/apps/web/src/app/vault/vault-filter/shared/vault-filter.service.ts @@ -8,7 +8,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { CollectionData } from "@bitwarden/common/models/data/collectionData"; diff --git a/apps/web/src/app/vault/vault.component.ts b/apps/web/src/app/vault/vault.component.ts index c9d6b590390..892a4fac8dc 100644 --- a/apps/web/src/app/vault/vault.component.ts +++ b/apps/web/src/app/vault/vault.component.ts @@ -17,7 +17,7 @@ import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { StateService } from "@bitwarden/common/abstractions/state.service"; diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index bfd0d1a0d82..9edbc7fe94a 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -572,8 +572,8 @@ "loginWithDevice" : { "message": "Log in with device" }, - "loginWithDevciceEnabledInfo": { - "message": "Log in with device must be enabled in the settings of the Biwarden mobile app. Need another option?" + "loginWithDeviceEnabledInfo": { + "message": "Log in with device must be enabled in the settings of the Bitwarden mobile app. Need another option?" }, "createAccount": { "message": "Create Account" diff --git a/apps/web/webpack.config.js b/apps/web/webpack.config.js index a7b6212943e..3b472c45d4e 100644 --- a/apps/web/webpack.config.js +++ b/apps/web/webpack.config.js @@ -273,7 +273,8 @@ const devServer = https://app.simplelogin.io/api/alias/random/new https://quack.duckduckgo.com/api/email/addresses https://app.anonaddy.com/api/v1/aliases - https://api.fastmail.com; + https://api.fastmail.com + https://quack.duckduckgo.com/api/email/addresses; object-src 'self' blob:;`, diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html index 54fe44e073c..3d27448753f 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html +++ b/bitwarden_license/bit-web/src/app/organizations/manage/scim.component.html @@ -81,7 +81,13 @@ {{ "scimApiKeyHelperText" | i18n }} - + diff --git a/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.ts b/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.ts index f05748f8448..4a24c52ba8b 100644 --- a/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.ts +++ b/bitwarden_license/bit-web/src/app/organizations/manage/sso.component.ts @@ -13,8 +13,8 @@ import { SelectOptions } from "@bitwarden/angular/interfaces/selectOptions"; import { ControlsOf } from "@bitwarden/angular/types/controls-of"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { OpenIdConnectRedirectBehavior, diff --git a/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts b/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts index f806aff5741..254fac4989e 100644 --- a/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/organizations/organizations-routing.module.ts @@ -2,11 +2,11 @@ import { NgModule } from "@angular/core"; import { RouterModule, Routes } from "@angular/router"; import { AuthGuard } from "@bitwarden/angular/guards/auth.guard"; +import { canAccessSettingsTab } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { OrganizationPermissionsGuard } from "src/app/organizations/guards/org-permissions.guard"; import { OrganizationLayoutComponent } from "src/app/organizations/layouts/organization-layout.component"; -import { canAccessSettingsTab } from "src/app/organizations/navigation-permissions"; import { SettingsComponent } from "src/app/organizations/settings/settings.component"; import { ScimComponent } from "./manage/scim.component"; diff --git a/bitwarden_license/bit-web/src/app/providers/clients/clients.component.ts b/bitwarden_license/bit-web/src/app/providers/clients/clients.component.ts index 697b2ac357d..f98a7f3ac18 100644 --- a/bitwarden_license/bit-web/src/app/providers/clients/clients.component.ts +++ b/bitwarden_license/bit-web/src/app/providers/clients/clients.component.ts @@ -7,8 +7,8 @@ import { ValidationService } from "@bitwarden/angular/services/validation.servic import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { ProviderService } from "@bitwarden/common/abstractions/provider.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; diff --git a/libs/angular/src/components/add-edit.component.ts b/libs/angular/src/components/add-edit.component.ts index 8f559dcbe7f..7519b426352 100644 --- a/libs/angular/src/components/add-edit.component.ts +++ b/libs/angular/src/components/add-edit.component.ts @@ -9,7 +9,7 @@ import { FolderService } from "@bitwarden/common/abstractions/folder/folder.serv import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService } from "@bitwarden/common/abstractions/messaging.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordRepromptService } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; diff --git a/libs/angular/src/components/export-scope-callout.component.ts b/libs/angular/src/components/export-scope-callout.component.ts index c77be2e77d3..74e862ec81c 100644 --- a/libs/angular/src/components/export-scope-callout.component.ts +++ b/libs/angular/src/components/export-scope-callout.component.ts @@ -1,6 +1,6 @@ import { Component, Input, OnInit } from "@angular/core"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { StateService } from "@bitwarden/common/abstractions/state.service"; @Component({ @@ -23,7 +23,7 @@ export class ExportScopeCalloutComponent implements OnInit { ) {} async ngOnInit(): Promise { - if (!(await this.organizationService.hasOrganizations())) { + if (!this.organizationService.hasOrganizations()) { return; } this.scopeConfig = @@ -31,7 +31,7 @@ export class ExportScopeCalloutComponent implements OnInit { ? { title: "exportingOrganizationVaultTitle", description: "exportingOrganizationVaultDescription", - scopeIdentifier: (await this.organizationService.get(this.organizationId)).name, + scopeIdentifier: this.organizationService.get(this.organizationId).name, } : { title: "exportingPersonalVaultTitle", diff --git a/libs/angular/src/components/remove-password.component.ts b/libs/angular/src/components/remove-password.component.ts index 20d7cc247ab..5294904f17b 100644 --- a/libs/angular/src/components/remove-password.component.ts +++ b/libs/angular/src/components/remove-password.component.ts @@ -1,7 +1,6 @@ import { Directive, OnInit } from "@angular/core"; import { Router } from "@angular/router"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { KeyConnectorService } from "@bitwarden/common/abstractions/keyConnector.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; @@ -23,7 +22,6 @@ export class RemovePasswordComponent implements OnInit { constructor( private router: Router, private stateService: StateService, - private apiService: ApiService, private syncService: SyncService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, @@ -70,9 +68,7 @@ export class RemovePasswordComponent implements OnInit { try { this.leaving = true; - this.actionPromise = this.organizationApiService.leave(this.organization.id).then(() => { - return this.syncService.fullSync(true); - }); + this.actionPromise = this.organizationApiService.leave(this.organization.id); await this.actionPromise; this.platformUtilsService.showToast("success", null, this.i18nService.t("leftOrganization")); await this.keyConnectorService.removeConvertAccountRequired(); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 55e3835270f..e9e6038b2f8 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -1,29 +1,33 @@ -import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; +import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/abstractions/log.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType"; import { Utils } from "@bitwarden/common/misc/utils"; import { Organization } from "@bitwarden/common/models/domain/organization"; import { CipherView } from "@bitwarden/common/models/view/cipherView"; import { CollectionView } from "@bitwarden/common/models/view/collectionView"; +import { Checkable, isChecked } from "@bitwarden/common/types/checkable"; @Directive() -export class ShareComponent implements OnInit { +export class ShareComponent implements OnInit, OnDestroy { @Input() cipherId: string; @Input() organizationId: string; @Output() onSharedCipher = new EventEmitter(); - formPromise: Promise; + formPromise: Promise; cipher: CipherView; - collections: CollectionView[] = []; - organizations: Organization[] = []; + collections: Checkable[] = []; + organizations$: Observable; - protected writeableCollections: CollectionView[] = []; + protected writeableCollections: Checkable[] = []; + + private _destroy = new Subject(); constructor( protected collectionService: CollectionService, @@ -38,24 +42,37 @@ export class ShareComponent implements OnInit { await this.load(); } + ngOnDestroy(): void { + this._destroy.next(); + this._destroy.complete(); + } + async load() { const allCollections = await this.collectionService.getAllDecrypted(); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); - const orgs = await this.organizationService.getAll(); - this.organizations = orgs - .sort(Utils.getSortFunction(this.i18nService, "name")) - .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed); + + this.organizations$ = this.organizationService.organizations$.pipe( + map((orgs) => { + return orgs + .filter((o) => o.enabled && o.status === OrganizationUserStatusType.Confirmed) + .sort(Utils.getSortFunction(this.i18nService, "name")); + }) + ); + + this.organizations$.pipe(takeUntil(this._destroy)).subscribe((orgs) => { + if (this.organizationId == null && orgs.length > 0) { + this.organizationId = orgs[0].id; + } + }); const cipherDomain = await this.cipherService.get(this.cipherId); this.cipher = await cipherDomain.decrypt(); - if (this.organizationId == null && this.organizations.length > 0) { - this.organizationId = this.organizations[0].id; - } + this.filterCollections(); } filterCollections() { - this.writeableCollections.forEach((c) => ((c as any).checked = false)); + this.writeableCollections.forEach((c) => (c.checked = false)); if (this.organizationId == null || this.writeableCollections.length === 0) { this.collections = []; } else { @@ -66,9 +83,7 @@ export class ShareComponent implements OnInit { } async submit(): Promise { - const selectedCollectionIds = this.collections - .filter((c) => !!(c as any).checked) - .map((c) => c.id); + const selectedCollectionIds = this.collections.filter(isChecked).map((c) => c.id); if (selectedCollectionIds.length === 0) { this.platformUtilsService.showToast( "error", @@ -80,9 +95,9 @@ export class ShareComponent implements OnInit { const cipherDomain = await this.cipherService.get(this.cipherId); const cipherView = await cipherDomain.decrypt(); + const orgs = await firstValueFrom(this.organizations$); const orgName = - this.organizations.find((o) => o.id === this.organizationId)?.name ?? - this.i18nService.t("organization"); + orgs.find((o) => o.id === this.organizationId)?.name ?? this.i18nService.t("organization"); try { this.formPromise = this.cipherService @@ -106,7 +121,7 @@ export class ShareComponent implements OnInit { get canSave() { if (this.collections != null) { for (let i = 0; i < this.collections.length; i++) { - if ((this.collections[i] as any).checked) { + if (this.collections[i].checked) { return true; } } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b53aac0866a..213f5617497 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -32,8 +32,8 @@ import { KeyConnectorService as KeyConnectorServiceAbstraction } from "@bitwarde import { LogService } from "@bitwarden/common/abstractions/log.service"; import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/abstractions/messaging.service"; import { NotificationsService as NotificationsServiceAbstraction } from "@bitwarden/common/abstractions/notifications.service"; -import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization-api.service.abstraction"; +import { OrganizationService as OrganizationServiceAbstraction } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PasswordGenerationService as PasswordGenerationServiceAbstraction } from "@bitwarden/common/abstractions/passwordGeneration.service"; import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@bitwarden/common/abstractions/passwordReprompt.service"; import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service"; @@ -50,6 +50,7 @@ import { StateService as StateServiceAbstraction } from "@bitwarden/common/abstr import { StateMigrationService as StateMigrationServiceAbstraction } from "@bitwarden/common/abstractions/stateMigration.service"; import { AbstractStorageService } from "@bitwarden/common/abstractions/storage.service"; import { SyncService as SyncServiceAbstraction } from "@bitwarden/common/abstractions/sync/sync.service.abstraction"; +import { SyncNotifierService as SyncNotifierServiceAbstraction } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction"; import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/abstractions/token.service"; import { TotpService as TotpServiceAbstraction } from "@bitwarden/common/abstractions/totp.service"; import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/abstractions/twoFactor.service"; @@ -84,8 +85,8 @@ import { FolderService } from "@bitwarden/common/services/folder/folder.service" import { FormValidationErrorsService } from "@bitwarden/common/services/formValidationErrors.service"; import { KeyConnectorService } from "@bitwarden/common/services/keyConnector.service"; import { NotificationsService } from "@bitwarden/common/services/notifications.service"; -import { OrganizationService } from "@bitwarden/common/services/organization.service"; import { OrganizationApiService } from "@bitwarden/common/services/organization/organization-api.service"; +import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; import { PasswordGenerationService } from "@bitwarden/common/services/passwordGeneration.service"; import { PolicyApiService } from "@bitwarden/common/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/services/policy/policy.service"; @@ -96,6 +97,7 @@ import { SettingsService } from "@bitwarden/common/services/settings.service"; import { StateService } from "@bitwarden/common/services/state.service"; import { StateMigrationService } from "@bitwarden/common/services/stateMigration.service"; import { SyncService } from "@bitwarden/common/services/sync/sync.service"; +import { SyncNotifierService } from "@bitwarden/common/services/sync/syncNotifier.service"; import { TokenService } from "@bitwarden/common/services/token.service"; import { TotpService } from "@bitwarden/common/services/totp.service"; import { TwoFactorService } from "@bitwarden/common/services/twoFactor.service"; @@ -338,9 +340,9 @@ import { ValidationService } from "./validation.service"; LogService, KeyConnectorServiceAbstraction, StateServiceAbstraction, - OrganizationServiceAbstraction, ProviderServiceAbstraction, FolderApiServiceAbstraction, + SyncNotifierServiceAbstraction, LOGOUT_CALLBACK, ], }, @@ -506,7 +508,7 @@ import { ValidationService } from "./validation.service"; { provide: OrganizationServiceAbstraction, useClass: OrganizationService, - deps: [StateServiceAbstraction], + deps: [StateServiceAbstraction, SyncNotifierServiceAbstraction], }, { provide: ProviderServiceAbstraction, @@ -534,7 +536,15 @@ import { ValidationService } from "./validation.service"; { provide: OrganizationApiServiceAbstraction, useClass: OrganizationApiService, - deps: [ApiServiceAbstraction], + // This is a slightly odd dependency tree for a specialized api service + // it depends on SyncService so that new data can be retrieved through the sync + // rather than updating the OrganizationService directly. Instead OrganizationService + // subscribes to sync notifications and will update itself based on that. + deps: [ApiServiceAbstraction, SyncServiceAbstraction], + }, + { + provide: SyncNotifierServiceAbstraction, + useClass: SyncNotifierService, }, { provide: ConfigServiceAbstraction, diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index b67d673b9b3..9155582a610 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -4,7 +4,7 @@ import { firstValueFrom, from, mergeMap, Observable } from "rxjs"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/abstractions/folder/folder.service.abstraction"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { StateService } from "@bitwarden/common/abstractions/state.service"; import { PolicyType } from "@bitwarden/common/enums/policyType"; @@ -37,8 +37,8 @@ export class VaultFilterService { return new Set(await this.stateService.getCollapsedGroupings()); } - async buildOrganizations(): Promise { - return await this.organizationService.getAll(); + buildOrganizations(): Promise { + return this.organizationService.getAll(); } buildNestedFolders(organizationId?: string): Observable> { diff --git a/libs/common/spec/importers/bitwardenPasswordProtectedImporter.spec.ts b/libs/common/spec/importers/bitwardenPasswordProtectedImporter.spec.ts index 820f8db38cc..67e03d030c5 100644 --- a/libs/common/spec/importers/bitwardenPasswordProtectedImporter.spec.ts +++ b/libs/common/spec/importers/bitwardenPasswordProtectedImporter.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Substitute, Arg, SubstituteOf } from "@fluffy-spoon/substitute"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; diff --git a/libs/common/spec/models/domain/cipher.spec.ts b/libs/common/spec/models/domain/cipher.spec.ts index c7c0d36714f..0158945d8ce 100644 --- a/libs/common/spec/models/domain/cipher.spec.ts +++ b/libs/common/spec/models/domain/cipher.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg } from "@fluffy-spoon/substitute"; +import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { CipherRepromptType } from "@bitwarden/common/enums/cipherRepromptType"; import { CipherType } from "@bitwarden/common/enums/cipherType"; diff --git a/libs/common/spec/models/domain/encString.spec.ts b/libs/common/spec/models/domain/encString.spec.ts index adcfaf0706e..413d091d68b 100644 --- a/libs/common/spec/models/domain/encString.spec.ts +++ b/libs/common/spec/models/domain/encString.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg } from "@fluffy-spoon/substitute"; +import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { mock, MockProxy } from "jest-mock-extended"; import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; diff --git a/libs/common/spec/models/domain/login.spec.ts b/libs/common/spec/models/domain/login.spec.ts index 6021a4b2908..b2235e75831 100644 --- a/libs/common/spec/models/domain/login.spec.ts +++ b/libs/common/spec/models/domain/login.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg } from "@fluffy-spoon/substitute"; +import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { UriMatchType } from "@bitwarden/common/enums/uriMatchType"; import { LoginData } from "@bitwarden/common/models/data/loginData"; diff --git a/libs/common/spec/models/domain/send.spec.ts b/libs/common/spec/models/domain/send.spec.ts index 575821abc96..903043f86c8 100644 --- a/libs/common/spec/models/domain/send.spec.ts +++ b/libs/common/spec/models/domain/send.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg, SubstituteOf } from "@fluffy-spoon/substitute"; +import { Substitute, Arg, SubstituteOf } from "@fluffy-spoon/substitute"; import { AbstractEncryptService } from "@bitwarden/common/abstractions/abstractEncrypt.service"; import { CryptoService } from "@bitwarden/common/abstractions/crypto.service"; diff --git a/libs/common/spec/models/domain/sendAccess.spec.ts b/libs/common/spec/models/domain/sendAccess.spec.ts index 7772fe428ac..32b01d13fd4 100644 --- a/libs/common/spec/models/domain/sendAccess.spec.ts +++ b/libs/common/spec/models/domain/sendAccess.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg } from "@fluffy-spoon/substitute"; +import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { SendType } from "@bitwarden/common/enums/sendType"; import { SendAccess } from "@bitwarden/common/models/domain/sendAccess"; diff --git a/libs/common/spec/services/import.service.spec.ts b/libs/common/spec/services/import.service.spec.ts index 654c1b6124f..2b47518bc5d 100644 --- a/libs/common/spec/services/import.service.spec.ts +++ b/libs/common/spec/services/import.service.spec.ts @@ -1,4 +1,4 @@ -import Substitute, { SubstituteOf } from "@fluffy-spoon/substitute"; +import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CipherService } from "@bitwarden/common/abstractions/cipher.service"; diff --git a/libs/common/spec/services/organization/organization.service.spec.ts b/libs/common/spec/services/organization/organization.service.spec.ts new file mode 100644 index 00000000000..c9b191bfe78 --- /dev/null +++ b/libs/common/spec/services/organization/organization.service.spec.ts @@ -0,0 +1,210 @@ +import { MockProxy, mock, any, mockClear, matches } from "jest-mock-extended"; +import { BehaviorSubject, firstValueFrom, Subject } from "rxjs"; + +import { StateService } from "@bitwarden/common/abstractions/state.service"; +import { SyncNotifierService } from "@bitwarden/common/abstractions/sync/syncNotifier.service.abstraction"; +import { OrganizationData } from "@bitwarden/common/models/data/organizationData"; +import { SyncResponse } from "@bitwarden/common/models/response/syncResponse"; +import { OrganizationService } from "@bitwarden/common/services/organization/organization.service"; +import { SyncEventArgs } from "@bitwarden/common/types/syncEventArgs"; + +describe("Organization Service", () => { + let organizationService: OrganizationService; + + let stateService: MockProxy; + let activeAccount: BehaviorSubject; + let activeAccountUnlocked: BehaviorSubject; + let syncNotifierService: MockProxy; + let sync: Subject; + + const resetStateService = async ( + customizeStateService: (stateService: MockProxy) => void + ) => { + mockClear(stateService); + stateService = mock(); + stateService.activeAccount$ = activeAccount; + stateService.activeAccountUnlocked$ = activeAccountUnlocked; + customizeStateService(stateService); + organizationService = new OrganizationService(stateService, syncNotifierService); + await new Promise((r) => setTimeout(r, 50)); + }; + + beforeEach(() => { + activeAccount = new BehaviorSubject("123"); + activeAccountUnlocked = new BehaviorSubject(true); + + stateService = mock(); + stateService.activeAccount$ = activeAccount; + stateService.activeAccountUnlocked$ = activeAccountUnlocked; + + stateService.getOrganizations.calledWith(any()).mockResolvedValue({ + "1": organizationData("1", "Test Org"), + }); + + sync = new Subject(); + + syncNotifierService = mock(); + syncNotifierService.sync$ = sync; + + organizationService = new OrganizationService(stateService, syncNotifierService); + }); + + afterEach(() => { + activeAccount.complete(); + activeAccountUnlocked.complete(); + }); + + it("getAll", async () => { + const orgs = await organizationService.getAll(); + expect(orgs).toHaveLength(1); + const org = orgs[0]; + expect(org).toEqual({ + id: "1", + name: "Test Org", + identifier: "test", + }); + }); + + describe("canManageSponsorships", () => { + it("can because one is available", async () => { + await resetStateService((stateService) => { + stateService.getOrganizations.mockResolvedValue({ + "1": { ...organizationData("1", "Org"), familySponsorshipAvailable: true }, + }); + }); + + const result = await organizationService.canManageSponsorships(); + expect(result).toBe(true); + }); + + it("can because one is used", async () => { + await resetStateService((stateService) => { + stateService.getOrganizations.mockResolvedValue({ + "1": { ...organizationData("1", "Test Org"), familySponsorshipFriendlyName: "Something" }, + }); + }); + + const result = await organizationService.canManageSponsorships(); + expect(result).toBe(true); + }); + + it("can not because one isn't available or taken", async () => { + await resetStateService((stateService) => { + stateService.getOrganizations.mockResolvedValue({ + "1": { ...organizationData("1", "Org"), familySponsorshipFriendlyName: null }, + }); + }); + + const result = await organizationService.canManageSponsorships(); + expect(result).toBe(false); + }); + }); + + describe("get", () => { + it("exists", async () => { + const result = organizationService.get("1"); + + expect(result).toEqual({ + id: "1", + name: "Test Org", + identifier: "test", + }); + }); + + it("does not exist", async () => { + const result = organizationService.get("2"); + + expect(result).toBe(undefined); + }); + }); + + it("upsert", async () => { + await organizationService.upsert(organizationData("2", "Test 2")); + + expect(await firstValueFrom(organizationService.organizations$)).toEqual([ + { + id: "1", + name: "Test Org", + identifier: "test", + }, + { + id: "2", + name: "Test 2", + identifier: "test", + }, + ]); + }); + + describe("getByIdentifier", () => { + it("exists", async () => { + const result = organizationService.getByIdentifier("test"); + + expect(result).toEqual({ + id: "1", + name: "Test Org", + identifier: "test", + }); + }); + + it("does not exist", async () => { + const result = organizationService.getByIdentifier("blah"); + + expect(result).toBeUndefined(); + }); + }); + + describe("delete", () => { + it("exists", async () => { + await organizationService.delete("1"); + + expect(stateService.getOrganizations).toHaveBeenCalledTimes(2); + + expect(stateService.setOrganizations).toHaveBeenCalledTimes(1); + }); + + it("does not exist", async () => { + organizationService.delete("1"); + + expect(stateService.getOrganizations).toHaveBeenCalledTimes(2); + }); + }); + + describe("syncEvent works", () => { + it("Complete event updates data", async () => { + sync.next({ + status: "Completed", + successfully: true, + data: new SyncResponse({ + profile: { + organizations: [ + { + id: "1", + name: "Updated Name", + }, + ], + }, + }), + }); + + await new Promise((r) => setTimeout(r, 500)); + + expect(stateService.setOrganizations).toHaveBeenCalledTimes(1); + + expect(stateService.setOrganizations).toHaveBeenLastCalledWith( + matches((organizationData: { [id: string]: OrganizationData }) => { + const organization = organizationData["1"]; + return organization?.name === "Updated Name"; + }) + ); + }); + }); + + function organizationData(id: string, name: string) { + const data = new OrganizationData({} as any); + data.id = id; + data.name = name; + data.identifier = "test"; + + return data; + } +}); diff --git a/libs/common/spec/utils.ts b/libs/common/spec/utils.ts index 0ea16b4d725..4f9dc4076f1 100644 --- a/libs/common/spec/utils.ts +++ b/libs/common/spec/utils.ts @@ -1,4 +1,4 @@ -import Substitute, { Arg } from "@fluffy-spoon/substitute"; +import { Substitute, Arg } from "@fluffy-spoon/substitute"; import { EncString } from "@bitwarden/common/models/domain/encString"; diff --git a/libs/common/spec/web/services/webCryptoFunction.service.spec.ts b/libs/common/spec/web/services/webCryptoFunction.service.spec.ts index 964364a1faf..ce131472d4b 100644 --- a/libs/common/spec/web/services/webCryptoFunction.service.spec.ts +++ b/libs/common/spec/web/services/webCryptoFunction.service.spec.ts @@ -1,4 +1,4 @@ -import Substitute from "@fluffy-spoon/substitute"; +import { Substitute } from "@fluffy-spoon/substitute"; import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service"; import { Utils } from "@bitwarden/common/misc/utils"; diff --git a/libs/common/src/abstractions/organization.service.ts b/libs/common/src/abstractions/organization.service.ts deleted file mode 100644 index e0b5fde4369..00000000000 --- a/libs/common/src/abstractions/organization.service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { OrganizationData } from "../models/data/organizationData"; -import { Organization } from "../models/domain/organization"; - -export abstract class OrganizationService { - get: (id: string) => Promise; - getByIdentifier: (identifier: string) => Promise; - getAll: (userId?: string) => Promise; - save: (orgs: { [id: string]: OrganizationData }) => Promise; - canManageSponsorships: () => Promise; - hasOrganizations: (userId?: string) => Promise; -} diff --git a/libs/common/src/abstractions/organization/organization.service.abstraction.ts b/libs/common/src/abstractions/organization/organization.service.abstraction.ts new file mode 100644 index 00000000000..93a61437904 --- /dev/null +++ b/libs/common/src/abstractions/organization/organization.service.abstraction.ts @@ -0,0 +1,55 @@ +import { map, Observable } from "rxjs"; + +import { Utils } from "../../misc/utils"; +import { Organization } from "../../models/domain/organization"; +import { I18nService } from "../i18n.service"; + +export function canAccessSettingsTab(org: Organization): boolean { + return org.isOwner; +} + +export function canAccessMembersTab(org: Organization): boolean { + return org.canManageUsers || org.canManageUsersPassword; +} + +export function canAccessGroupsTab(org: Organization): boolean { + return org.canManageGroups; +} + +export function canAccessReportingTab(org: Organization): boolean { + return org.canAccessReports || org.canAccessEventLogs; +} + +export function canAccessBillingTab(org: Organization): boolean { + return org.canManageBilling; +} + +export function canAccessOrgAdmin(org: Organization): boolean { + return ( + canAccessMembersTab(org) || + canAccessGroupsTab(org) || + canAccessReportingTab(org) || + canAccessBillingTab(org) || + canAccessSettingsTab(org) + ); +} + +export function getOrganizationById(id: string) { + return map((orgs) => orgs.find((o) => o.id === id)); +} + +export function canAccessAdmin(i18nService: I18nService) { + return map((orgs) => + orgs.filter(canAccessOrgAdmin).sort(Utils.getSortFunction(i18nService, "name")) + ); +} + +export abstract class OrganizationService { + organizations$: Observable; + + get: (id: string) => Organization; + getByIdentifier: (identifier: string) => Organization; + getAll: (userId?: string) => Promise; + canManageSponsorships: () => Promise; + hasOrganizations: () => boolean; +} diff --git a/libs/common/src/abstractions/state.service.ts b/libs/common/src/abstractions/state.service.ts index 03ccfe0abf4..5b844dc1338 100644 --- a/libs/common/src/abstractions/state.service.ts +++ b/libs/common/src/abstractions/state.service.ts @@ -273,7 +273,13 @@ export abstract class StateService { setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; setOrganizationInvitation: (value: any, options?: StorageOptions) => Promise; + /** + * @deprecated Do not call this directly, use OrganizationService + */ getOrganizations: (options?: StorageOptions) => Promise<{ [id: string]: OrganizationData }>; + /** + * @deprecated Do not call this directly, use OrganizationService + */ setOrganizations: ( value: { [id: string]: OrganizationData }, options?: StorageOptions diff --git a/libs/common/src/abstractions/sync/sync.service.abstraction.ts b/libs/common/src/abstractions/sync/sync.service.abstraction.ts index 51583fd3c41..90123f9876a 100644 --- a/libs/common/src/abstractions/sync/sync.service.abstraction.ts +++ b/libs/common/src/abstractions/sync/sync.service.abstraction.ts @@ -1,17 +1,12 @@ -import { Observable } from "rxjs"; - import { SyncCipherNotification, SyncFolderNotification, SyncSendNotification, } from "../../models/response/notificationResponse"; -import { SyncEventArgs } from "../../types/syncEventArgs"; export abstract class SyncService { syncInProgress: boolean; - sync$: Observable; - getLastSync: () => Promise; setLastSync: (date: Date, userId?: string) => Promise; fullSync: (forceSync: boolean, allowThrowOnError?: boolean) => Promise; diff --git a/libs/common/src/abstractions/sync/syncNotifier.service.abstraction.ts b/libs/common/src/abstractions/sync/syncNotifier.service.abstraction.ts new file mode 100644 index 00000000000..9a8261961e1 --- /dev/null +++ b/libs/common/src/abstractions/sync/syncNotifier.service.abstraction.ts @@ -0,0 +1,8 @@ +import { Observable } from "rxjs"; + +import { SyncEventArgs } from "../../types/syncEventArgs"; + +export abstract class SyncNotifierService { + sync$: Observable; + next: (event: SyncEventArgs) => void; +} diff --git a/libs/common/src/misc/utils.ts b/libs/common/src/misc/utils.ts index 454def36551..2c989be9cb7 100644 --- a/libs/common/src/misc/utils.ts +++ b/libs/common/src/misc/utils.ts @@ -310,8 +310,11 @@ export class Utils { return map; } - static getSortFunction(i18nService: I18nService, prop: string) { - return (a: any, b: any) => { + static getSortFunction( + i18nService: I18nService, + prop: { [K in keyof T]: T[K] extends string ? K : never }[keyof T] + ): (a: T, b: T) => number { + return (a, b) => { if (a[prop] == null && b[prop] != null) { return -1; } @@ -322,9 +325,10 @@ export class Utils { return 0; } + // The `as unknown as string` here is unfortunate because typescript doesn't property understand that the return of T[prop] will be a string return i18nService.collator - ? i18nService.collator.compare(a[prop], b[prop]) - : a[prop].localeCompare(b[prop]); + ? i18nService.collator.compare(a[prop] as unknown as string, b[prop] as unknown as string) + : (a[prop] as unknown as string).localeCompare(b[prop] as unknown as string); }; } diff --git a/libs/common/src/services/config/config.service.ts b/libs/common/src/services/config/config.service.ts index 9f9e0938b5a..a5ac5445713 100644 --- a/libs/common/src/services/config/config.service.ts +++ b/libs/common/src/services/config/config.service.ts @@ -48,14 +48,16 @@ export class ConfigService implements ConfigServiceAbstraction { } private async fetchServerConfig(): Promise { - const response = await this.configApiService.get(); - const data = new ServerConfigData(response); + try { + const response = await this.configApiService.get(); - if (data != null) { - await this.stateService.setServerConfig(data); - return new ServerConfig(data); + if (response != null) { + const data = new ServerConfigData(response); + await this.stateService.setServerConfig(data); + return new ServerConfig(data); + } + } catch { + return null; } - - return null; } } diff --git a/libs/common/src/services/event.service.ts b/libs/common/src/services/event.service.ts index 9169bfe696b..4279da86421 100644 --- a/libs/common/src/services/event.service.ts +++ b/libs/common/src/services/event.service.ts @@ -2,7 +2,7 @@ import { ApiService } from "../abstractions/api.service"; import { CipherService } from "../abstractions/cipher.service"; import { EventService as EventServiceAbstraction } from "../abstractions/event.service"; import { LogService } from "../abstractions/log.service"; -import { OrganizationService } from "../abstractions/organization.service"; +import { OrganizationService } from "../abstractions/organization/organization.service.abstraction"; import { StateService } from "../abstractions/state.service"; import { EventType } from "../enums/eventType"; import { EventData } from "../models/data/eventData"; diff --git a/libs/common/src/services/keyConnector.service.ts b/libs/common/src/services/keyConnector.service.ts index 8be5993528a..fd4b7ffa84d 100644 --- a/libs/common/src/services/keyConnector.service.ts +++ b/libs/common/src/services/keyConnector.service.ts @@ -3,7 +3,7 @@ import { CryptoService } from "../abstractions/crypto.service"; import { CryptoFunctionService } from "../abstractions/cryptoFunction.service"; import { KeyConnectorService as KeyConnectorServiceAbstraction } from "../abstractions/keyConnector.service"; import { LogService } from "../abstractions/log.service"; -import { OrganizationService } from "../abstractions/organization.service"; +import { OrganizationService } from "../abstractions/organization/organization.service.abstraction"; import { StateService } from "../abstractions/state.service"; import { TokenService } from "../abstractions/token.service"; import { OrganizationUserType } from "../enums/organizationUserType"; diff --git a/libs/common/src/services/organization.service.ts b/libs/common/src/services/organization.service.ts deleted file mode 100644 index 4397fcb8b13..00000000000 --- a/libs/common/src/services/organization.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { OrganizationService as OrganizationServiceAbstraction } from "../abstractions/organization.service"; -import { StateService } from "../abstractions/state.service"; -import { OrganizationData } from "../models/data/organizationData"; -import { Organization } from "../models/domain/organization"; - -export class OrganizationService implements OrganizationServiceAbstraction { - constructor(private stateService: StateService) {} - - async get(id: string): Promise { - const organizations = await this.stateService.getOrganizations(); - // eslint-disable-next-line - if (organizations == null || !organizations.hasOwnProperty(id)) { - return null; - } - - return new Organization(organizations[id]); - } - - async getByIdentifier(identifier: string): Promise { - const organizations = await this.getAll(); - if (organizations == null || organizations.length === 0) { - return null; - } - - return organizations.find((o) => o.identifier === identifier); - } - - async getAll(userId?: string): Promise { - const organizations = await this.stateService.getOrganizations({ userId: userId }); - const response: Organization[] = []; - for (const id in organizations) { - // eslint-disable-next-line - if (organizations.hasOwnProperty(id) && !organizations[id].isProviderUser) { - response.push(new Organization(organizations[id])); - } - } - const sortedResponse = response.sort((a, b) => a.name.localeCompare(b.name)); - return sortedResponse; - } - - async save(organizations: { [id: string]: OrganizationData }) { - return await this.stateService.setOrganizations(organizations); - } - - async canManageSponsorships(): Promise { - const orgs = await this.getAll(); - return orgs.some( - (o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null - ); - } - - async hasOrganizations(userId?: string): Promise { - const organizations = await this.getAll(userId); - return organizations.length > 0; - } -} diff --git a/libs/common/src/services/organization/organization-api.service.ts b/libs/common/src/services/organization/organization-api.service.ts index 4efc3325235..f15d76c27b6 100644 --- a/libs/common/src/services/organization/organization-api.service.ts +++ b/libs/common/src/services/organization/organization-api.service.ts @@ -1,5 +1,6 @@ import { ApiService } from "../../abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "../../abstractions/organization/organization-api.service.abstraction"; +import { SyncService } from "../../abstractions/sync/sync.service.abstraction"; import { OrganizationApiKeyType } from "../../enums/organizationApiKeyType"; import { ImportDirectoryRequest } from "../../models/request/importDirectoryRequest"; import { OrganizationSsoRequest } from "../../models/request/organization/organizationSsoRequest"; @@ -28,7 +29,7 @@ import { PaymentResponse } from "../../models/response/paymentResponse"; import { TaxInfoResponse } from "../../models/response/taxInfoResponse"; export class OrganizationApiService implements OrganizationApiServiceAbstraction { - constructor(private apiService: ApiService) {} + constructor(private apiService: ApiService, private syncService: SyncService) {} async get(id: string): Promise { const r = await this.apiService.send("GET", "/organizations/" + id, null, true, true); @@ -80,6 +81,8 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction async create(request: OrganizationCreateRequest): Promise { const r = await this.apiService.send("POST", "/organizations", request, true, true); + // Forcing a sync will notify organization service that they need to repull + await this.syncService.fullSync(true); return new OrganizationResponse(r); } @@ -90,7 +93,9 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction async save(id: string, request: OrganizationUpdateRequest): Promise { const r = await this.apiService.send("PUT", "/organizations/" + id, request, true, true); - return new OrganizationResponse(r); + const data = new OrganizationResponse(r); + await this.syncService.fullSync(true); + return data; } async updatePayment(id: string, request: PaymentRequest): Promise { @@ -144,7 +149,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction } async verifyBank(id: string, request: VerifyBankRequest): Promise { - return this.apiService.send( + await this.apiService.send( "POST", "/organizations/" + id + "/verify-bank", request, @@ -162,15 +167,17 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction } async leave(id: string): Promise { - return this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false); + await this.apiService.send("POST", "/organizations/" + id + "/leave", null, true, false); + await this.syncService.fullSync(true); } async delete(id: string, request: SecretVerificationRequest): Promise { - return this.apiService.send("DELETE", "/organizations/" + id, request, true, false); + await this.apiService.send("DELETE", "/organizations/" + id, request, true, false); + await this.syncService.fullSync(true); } async updateLicense(id: string, data: FormData): Promise { - return this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false); + await this.apiService.send("POST", "/organizations/" + id + "/license", data, true, false); } async importDirectory(organizationId: string, request: ImportDirectoryRequest): Promise { @@ -223,6 +230,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction } async updateTaxInfo(id: string, request: OrganizationTaxInfoUpdateRequest): Promise { + // Can't broadcast anything because the response doesn't have content return this.apiService.send("PUT", "/organizations/" + id + "/tax", request, true, false); } @@ -242,6 +250,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction true, true ); + // Not broadcasting anything because data on this response doesn't correspond to `Organization` return new OrganizationKeysResponse(r); } @@ -258,6 +267,7 @@ export class OrganizationApiService implements OrganizationApiServiceAbstraction true, true ); + // Not broadcasting anything because data on this response doesn't correspond to `Organization` return new OrganizationSsoResponse(r); } } diff --git a/libs/common/src/services/organization/organization.service.ts b/libs/common/src/services/organization/organization.service.ts new file mode 100644 index 00000000000..e5ae50d5246 --- /dev/null +++ b/libs/common/src/services/organization/organization.service.ts @@ -0,0 +1,119 @@ +import { BehaviorSubject, concatMap, filter } from "rxjs"; + +import { OrganizationService as OrganizationServiceAbstraction } from "../../abstractions/organization/organization.service.abstraction"; +import { StateService } from "../../abstractions/state.service"; +import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction"; +import { OrganizationData } from "../../models/data/organizationData"; +import { Organization } from "../../models/domain/organization"; +import { isSuccessfullyCompleted } from "../../types/syncEventArgs"; + +export class OrganizationService implements OrganizationServiceAbstraction { + private _organizations = new BehaviorSubject([]); + + organizations$ = this._organizations.asObservable(); + + constructor( + private stateService: StateService, + private syncNotifierService: SyncNotifierService + ) { + this.stateService.activeAccountUnlocked$ + .pipe( + concatMap(async (unlocked) => { + if (!unlocked) { + this._organizations.next([]); + return; + } + + const data = await this.stateService.getOrganizations(); + this.updateObservables(data); + }) + ) + .subscribe(); + + this.syncNotifierService.sync$ + .pipe( + filter(isSuccessfullyCompleted), + concatMap(async ({ data }) => { + const { profile } = data; + const organizations: { [id: string]: OrganizationData } = {}; + profile.organizations.forEach((o) => { + organizations[o.id] = new OrganizationData(o); + }); + + profile.providerOrganizations.forEach((o) => { + if (organizations[o.id] == null) { + organizations[o.id] = new OrganizationData(o); + organizations[o.id].isProviderUser = true; + } + }); + + await this.updateStateAndObservables(organizations); + }) + ) + .subscribe(); + } + + async getAll(userId?: string): Promise { + const organizationsMap = await this.stateService.getOrganizations({ userId: userId }); + return Object.values(organizationsMap || {}).map((o) => new Organization(o)); + } + + async canManageSponsorships(): Promise { + const organizations = this._organizations.getValue(); + return organizations.some( + (o) => o.familySponsorshipAvailable || o.familySponsorshipFriendlyName !== null + ); + } + + hasOrganizations(): boolean { + const organizations = this._organizations.getValue(); + return organizations.length > 0; + } + + async upsert(organization: OrganizationData): Promise { + let organizations = await this.stateService.getOrganizations(); + if (organizations == null) { + organizations = {}; + } + + organizations[organization.id] = organization; + + await this.updateStateAndObservables(organizations); + } + + async delete(id: string): Promise { + const organizations = await this.stateService.getOrganizations(); + if (organizations == null) { + return; + } + + if (organizations[id] == null) { + return; + } + + delete organizations[id]; + await this.updateStateAndObservables(organizations); + } + + get(id: string): Organization { + const organizations = this._organizations.getValue(); + + return organizations.find((organization) => organization.id === id); + } + + getByIdentifier(identifier: string): Organization { + const organizations = this._organizations.getValue(); + + return organizations.find((organization) => organization.identifier === identifier); + } + + private async updateStateAndObservables(organizationsMap: { [id: string]: OrganizationData }) { + await this.stateService.setOrganizations(organizationsMap); + this.updateObservables(organizationsMap); + } + + private updateObservables(organizationsMap: { [id: string]: OrganizationData }) { + const organizations = Object.values(organizationsMap || {}).map((o) => new Organization(o)); + this._organizations.next(organizations); + } +} diff --git a/libs/common/src/services/policy/policy-api.service.ts b/libs/common/src/services/policy/policy-api.service.ts index 777108571f3..7cf3a2b0659 100644 --- a/libs/common/src/services/policy/policy-api.service.ts +++ b/libs/common/src/services/policy/policy-api.service.ts @@ -1,5 +1,5 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { OrganizationService } from "@bitwarden/common/abstractions/organization.service"; +import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction"; import { InternalPolicyService } from "@bitwarden/common/abstractions/policy/policy.service.abstraction"; import { StateService } from "@bitwarden/common/abstractions/state.service"; diff --git a/libs/common/src/services/policy/policy.service.ts b/libs/common/src/services/policy/policy.service.ts index dcc3b3686ac..f64238893fb 100644 --- a/libs/common/src/services/policy/policy.service.ts +++ b/libs/common/src/services/policy/policy.service.ts @@ -1,4 +1,4 @@ -import { OrganizationService } from "../../abstractions/organization.service"; +import { OrganizationService } from "../../abstractions/organization/organization.service.abstraction"; import { InternalPolicyService as InternalPolicyServiceAbstraction } from "../../abstractions/policy/policy.service.abstraction"; import { StateService } from "../../abstractions/state.service"; import { OrganizationUserStatusType } from "../../enums/organizationUserStatusType"; diff --git a/libs/common/src/services/state.service.ts b/libs/common/src/services/state.service.ts index 1f52a27ad67..cbae46ed139 100644 --- a/libs/common/src/services/state.service.ts +++ b/libs/common/src/services/state.service.ts @@ -1927,12 +1927,18 @@ export class StateService< ); } + /** + * @deprecated Do not call this directly, use OrganizationService + */ async getOrganizations(options?: StorageOptions): Promise<{ [id: string]: OrganizationData }> { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) )?.data?.organizations; } + /** + * @deprecated Do not call this directly, use OrganizationService + */ async setOrganizations( value: { [id: string]: OrganizationData }, options?: StorageOptions diff --git a/libs/common/src/services/sync/sync.service.ts b/libs/common/src/services/sync/sync.service.ts index fc460f39206..736a3ceeeaa 100644 --- a/libs/common/src/services/sync/sync.service.ts +++ b/libs/common/src/services/sync/sync.service.ts @@ -1,5 +1,3 @@ -import { Subject } from "rxjs"; - import { ApiService } from "../../abstractions/api.service"; import { CipherService } from "../../abstractions/cipher.service"; import { CollectionService } from "../../abstractions/collection.service"; @@ -9,18 +7,17 @@ import { InternalFolderService } from "../../abstractions/folder/folder.service. import { KeyConnectorService } from "../../abstractions/keyConnector.service"; import { LogService } from "../../abstractions/log.service"; import { MessagingService } from "../../abstractions/messaging.service"; -import { OrganizationService } from "../../abstractions/organization.service"; import { InternalPolicyService } from "../../abstractions/policy/policy.service.abstraction"; import { ProviderService } from "../../abstractions/provider.service"; import { SendService } from "../../abstractions/send.service"; import { SettingsService } from "../../abstractions/settings.service"; import { StateService } from "../../abstractions/state.service"; import { SyncService as SyncServiceAbstraction } from "../../abstractions/sync/sync.service.abstraction"; +import { SyncNotifierService } from "../../abstractions/sync/syncNotifier.service.abstraction"; import { sequentialize } from "../../misc/sequentialize"; import { CipherData } from "../../models/data/cipherData"; import { CollectionData } from "../../models/data/collectionData"; import { FolderData } from "../../models/data/folderData"; -import { OrganizationData } from "../../models/data/organizationData"; import { PolicyData } from "../../models/data/policyData"; import { ProviderData } from "../../models/data/providerData"; import { SendData } from "../../models/data/sendData"; @@ -36,15 +33,10 @@ import { import { PolicyResponse } from "../../models/response/policyResponse"; import { ProfileResponse } from "../../models/response/profileResponse"; import { SendResponse } from "../../models/response/sendResponse"; -import { SyncEventArgs } from "../../types/syncEventArgs"; export class SyncService implements SyncServiceAbstraction { syncInProgress = false; - private _sync = new Subject(); - - sync$ = this._sync.asObservable(); - constructor( private apiService: ApiService, private settingsService: SettingsService, @@ -58,9 +50,9 @@ export class SyncService implements SyncServiceAbstraction { private logService: LogService, private keyConnectorService: KeyConnectorService, private stateService: StateService, - private organizationService: OrganizationService, private providerService: ProviderService, private folderApiService: FolderApiServiceAbstraction, + private syncNotifierService: SyncNotifierService, private logoutCallback: (expired: boolean) => Promise ) {} @@ -84,8 +76,10 @@ export class SyncService implements SyncServiceAbstraction { @sequentialize(() => "fullSync") async fullSync(forceSync: boolean, allowThrowOnError = false): Promise { this.syncStarted(); + this.syncNotifierService.next({ status: "Started" }); const isAuthenticated = await this.stateService.getIsAuthenticated(); if (!isAuthenticated) { + this.syncNotifierService.next({ status: "Completed", successfully: false }); return this.syncCompleted(false); } @@ -101,6 +95,7 @@ export class SyncService implements SyncServiceAbstraction { if (!needsSync) { await this.setLastSync(now); + this.syncNotifierService.next({ status: "Completed", successfully: false }); return this.syncCompleted(false); } @@ -117,11 +112,13 @@ export class SyncService implements SyncServiceAbstraction { await this.syncPolicies(response.policies); await this.setLastSync(now); + this.syncNotifierService.next({ status: "Completed", successfully: true, data: response }); return this.syncCompleted(true); } catch (e) { if (allowThrowOnError) { throw e; } else { + this.syncNotifierService.next({ status: "Completed", successfully: false }); return this.syncCompleted(false); } } @@ -272,13 +269,11 @@ export class SyncService implements SyncServiceAbstraction { private syncStarted() { this.syncInProgress = true; this.messagingService.send("syncStarted"); - this._sync.next({ status: "Started" }); } private syncCompleted(successfully: boolean): boolean { this.syncInProgress = false; this.messagingService.send("syncCompleted", { successfully: successfully }); - this._sync.next({ status: successfully ? "SuccessfullyCompleted" : "UnsuccessfullyCompleted" }); return successfully; } @@ -320,24 +315,11 @@ export class SyncService implements SyncServiceAbstraction { await this.stateService.setForcePasswordReset(response.forcePasswordReset); await this.keyConnectorService.setUsesKeyConnector(response.usesKeyConnector); - const organizations: { [id: string]: OrganizationData } = {}; - response.organizations.forEach((o) => { - organizations[o.id] = new OrganizationData(o); - }); - const providers: { [id: string]: ProviderData } = {}; response.providers.forEach((p) => { providers[p.id] = new ProviderData(p); }); - response.providerOrganizations.forEach((o) => { - if (organizations[o.id] == null) { - organizations[o.id] = new OrganizationData(o); - organizations[o.id].isProviderUser = true; - } - }); - - await this.organizationService.save(organizations); await this.providerService.save(providers); if (await this.keyConnectorService.userNeedsMigration()) { diff --git a/libs/common/src/services/sync/syncNotifier.service.ts b/libs/common/src/services/sync/syncNotifier.service.ts new file mode 100644 index 00000000000..996a54d656d --- /dev/null +++ b/libs/common/src/services/sync/syncNotifier.service.ts @@ -0,0 +1,18 @@ +import { Subject } from "rxjs"; + +import { SyncNotifierService as SyncNotifierServiceAbstraction } from "../../abstractions/sync/syncNotifier.service.abstraction"; +import { SyncEventArgs } from "../../types/syncEventArgs"; + +/** + * This class should most likely have 0 dependencies because it will hopefully + * be rolled into SyncService once upon a time. + */ +export class SyncNotifierService implements SyncNotifierServiceAbstraction { + private _sync = new Subject(); + + sync$ = this._sync.asObservable(); + + next(event: SyncEventArgs): void { + this._sync.next(event); + } +} diff --git a/libs/common/src/types/checkable.ts b/libs/common/src/types/checkable.ts new file mode 100644 index 00000000000..bea2e5a0740 --- /dev/null +++ b/libs/common/src/types/checkable.ts @@ -0,0 +1,9 @@ +type CheckableBase = { + checked?: boolean; +}; + +export type Checkable = T & CheckableBase; + +export function isChecked(item: CheckableBase): boolean { + return !!item.checked; +} diff --git a/libs/common/src/types/syncEventArgs.ts b/libs/common/src/types/syncEventArgs.ts index 3361a3358f1..4e28ae4be7e 100644 --- a/libs/common/src/types/syncEventArgs.ts +++ b/libs/common/src/types/syncEventArgs.ts @@ -1,15 +1,38 @@ -import { filter } from "rxjs"; +import { SyncResponse } from "../models/response/syncResponse"; -export type SyncStatus = "Started" | "SuccessfullyCompleted" | "UnsuccessfullyCompleted"; +type SyncStatus = "Started" | "Completed"; -export type SyncEventArgs = { - status: SyncStatus; +type SyncEventArgsBase = { + status: T; }; +type SyncCompletedEventArgsBase = SyncEventArgsBase<"Completed"> & { + successfully: T; +}; + +type SyncSuccessfullyCompletedEventArgs = SyncCompletedEventArgsBase & { + data: SyncResponse; +}; + +export type SyncEventArgs = + | SyncSuccessfullyCompletedEventArgs + | SyncCompletedEventArgsBase + | SyncEventArgsBase<"Started">; + /** * Helper function to filter only on successfully completed syncs - * @returns a function that can be used in a `.pipe()` from an observable + * @returns a function that can be used in a `.pipe(filter(...))` from an observable + * @example + * ``` + * of({ status: "Completed", successfully: true, data: new SyncResponse() }) + * .pipe(filter(isSuccessfullyCompleted)) + * .subscribe(event => { + * console.log(event.data); + * }); + * ``` */ -export function onlySuccessfullyCompleted() { - return filter((syncEvent) => syncEvent.status === "SuccessfullyCompleted"); +export function isSuccessfullyCompleted( + syncEvent: SyncEventArgs +): syncEvent is SyncSuccessfullyCompletedEventArgs { + return syncEvent.status === "Completed" && syncEvent.successfully; } diff --git a/libs/components/src/button/button.component.html b/libs/components/src/button/button.component.html new file mode 100644 index 00000000000..4875c159e92 --- /dev/null +++ b/libs/components/src/button/button.component.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/libs/components/src/button/button.directive.spec.ts b/libs/components/src/button/button.component.spec.ts similarity index 65% rename from libs/components/src/button/button.directive.spec.ts rename to libs/components/src/button/button.component.spec.ts index a7c3024e480..48aa8928e90 100644 --- a/libs/components/src/button/button.directive.spec.ts +++ b/libs/components/src/button/button.component.spec.ts @@ -8,6 +8,7 @@ describe("Button", () => { let fixture: ComponentFixture; let testAppComponent: TestApp; let buttonDebugElement: DebugElement; + let disabledButtonDebugElement: DebugElement; let linkDebugElement: DebugElement; beforeEach(waitForAsync(() => { @@ -20,6 +21,7 @@ describe("Button", () => { fixture = TestBed.createComponent(TestApp); testAppComponent = fixture.debugElement.componentInstance; buttonDebugElement = fixture.debugElement.query(By.css("button")); + disabledButtonDebugElement = fixture.debugElement.query(By.css("button#disabled")); linkDebugElement = fixture.debugElement.query(By.css("a")); })); @@ -60,16 +62,67 @@ describe("Button", () => { expect(buttonDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); expect(linkDebugElement.nativeElement.classList.contains("tw-block")).toBe(false); }); + + it("should not be disabled when loading and disabled are false", () => { + testAppComponent.loading = false; + testAppComponent.disabled = false; + fixture.detectChanges(); + + expect(buttonDebugElement.attributes["loading"]).toBeFalsy(); + expect(linkDebugElement.attributes["loading"]).toBeFalsy(); + expect(buttonDebugElement.nativeElement.disabled).toBeFalsy(); + }); + + it("should be disabled when disabled is true", () => { + testAppComponent.disabled = true; + fixture.detectChanges(); + + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); + // Anchor tags cannot be disabled. + }); + + it("should be disabled when attribute disabled is true", () => { + expect(disabledButtonDebugElement.nativeElement.disabled).toBeTruthy(); + }); + + it("should be disabled when loading is true", () => { + testAppComponent.loading = true; + fixture.detectChanges(); + + expect(buttonDebugElement.nativeElement.disabled).toBeTruthy(); + }); }); @Component({ selector: "test-app", template: ` - - Link + + + Link + + + `, }) class TestApp { buttonType: string; block: boolean; + disabled: boolean; + loading: boolean; } diff --git a/libs/components/src/button/button.directive.ts b/libs/components/src/button/button.component.ts similarity index 82% rename from libs/components/src/button/button.directive.ts rename to libs/components/src/button/button.component.ts index 5c4b0039aef..eeba83b8156 100644 --- a/libs/components/src/button/button.directive.ts +++ b/libs/components/src/button/button.component.ts @@ -1,4 +1,4 @@ -import { Input, HostBinding, Directive } from "@angular/core"; +import { Input, HostBinding, Component } from "@angular/core"; export type ButtonTypes = "primary" | "secondary" | "danger"; @@ -38,10 +38,11 @@ const buttonStyles: Record = { ], }; -@Directive({ +@Component({ selector: "button[bitButton], a[bitButton]", + templateUrl: "button.component.html", }) -export class ButtonDirective { +export class ButtonComponent { @HostBinding("class") get classList() { return [ "tw-font-semibold", @@ -65,6 +66,14 @@ export class ButtonDirective { .concat(buttonStyles[this.buttonType ?? "secondary"]); } + @HostBinding("attr.disabled") + get disabledAttr() { + const disabled = this.disabled != null && this.disabled !== false; + return disabled || this.loading ? true : null; + } + @Input() buttonType: ButtonTypes = null; @Input() block?: boolean; + @Input() loading = false; + @Input() disabled = false; } diff --git a/libs/components/src/button/button.module.ts b/libs/components/src/button/button.module.ts index c9c3822abfa..448e7c9dcf6 100644 --- a/libs/components/src/button/button.module.ts +++ b/libs/components/src/button/button.module.ts @@ -1,11 +1,11 @@ import { CommonModule } from "@angular/common"; import { NgModule } from "@angular/core"; -import { ButtonDirective } from "./button.directive"; +import { ButtonComponent } from "./button.component"; @NgModule({ imports: [CommonModule], - exports: [ButtonDirective], - declarations: [ButtonDirective], + exports: [ButtonComponent], + declarations: [ButtonComponent], }) export class ButtonModule {} diff --git a/libs/components/src/button/button.stories.ts b/libs/components/src/button/button.stories.ts index f09b8701b1b..4b9b88d48b1 100644 --- a/libs/components/src/button/button.stories.ts +++ b/libs/components/src/button/button.stories.ts @@ -1,12 +1,14 @@ import { Meta, Story } from "@storybook/angular"; -import { ButtonDirective } from "./button.directive"; +import { ButtonComponent } from "./button.component"; export default { title: "Component Library/Button", - component: ButtonDirective, + component: ButtonComponent, args: { buttonType: "primary", + disabled: false, + loading: false, }, parameters: { design: { @@ -16,11 +18,11 @@ export default { }, } as Meta; -const Template: Story = (args: ButtonDirective) => ({ +const Template: Story = (args: ButtonComponent) => ({ props: args, template: ` - - Link + + Link `, }); @@ -39,21 +41,50 @@ Danger.args = { buttonType: "danger", }; -const DisabledTemplate: Story = (args) => ({ +const AllStylesTemplate: Story = (args) => ({ props: args, template: ` - - - + + + `, }); -export const Disabled = DisabledTemplate.bind({}); -Disabled.args = { - size: "small", +export const Loading = AllStylesTemplate.bind({}); +Loading.args = { + disabled: false, + loading: true, }; -const BlockTemplate: Story = (args: ButtonDirective) => ({ +export const Disabled = AllStylesTemplate.bind({}); +Disabled.args = { + disabled: true, + loading: false, +}; + +const DisabledWithAttributeTemplate: Story = (args) => ({ + props: args, + template: ` + + + + + + + + + + + `, +}); + +export const DisabledWithAttribute = DisabledWithAttributeTemplate.bind({}); +DisabledWithAttribute.args = { + disabled: true, + loading: false, +}; + +const BlockTemplate: Story = (args: ButtonComponent) => ({ props: args, template: ` diff --git a/libs/components/src/button/index.ts b/libs/components/src/button/index.ts index 1bdd62ddbcf..ff86120cb11 100644 --- a/libs/components/src/button/index.ts +++ b/libs/components/src/button/index.ts @@ -1,2 +1,2 @@ -export * from "./button.directive"; +export * from "./button.component"; export * from "./button.module"; diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 458e1ab8c5c..271d923079e 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -19,7 +19,7 @@ > -
+
diff --git a/libs/components/src/dialog/dialog/dialog.component.ts b/libs/components/src/dialog/dialog/dialog.component.ts index 3727b08b5b7..a457ed23650 100644 --- a/libs/components/src/dialog/dialog/dialog.component.ts +++ b/libs/components/src/dialog/dialog/dialog.component.ts @@ -1,3 +1,4 @@ +import { coerceBooleanProperty } from "@angular/cdk/coercion"; import { Component, Input } from "@angular/core"; @Component({ @@ -7,6 +8,14 @@ import { Component, Input } from "@angular/core"; export class DialogComponent { @Input() dialogSize: "small" | "default" | "large" = "default"; + private _disablePadding: boolean; + @Input() set disablePadding(value: boolean) { + this._disablePadding = coerceBooleanProperty(value); + } + get disablePadding() { + return this._disablePadding; + } + get width() { switch (this.dialogSize) { case "small": { diff --git a/libs/components/src/dialog/dialog/dialog.stories.ts b/libs/components/src/dialog/dialog/dialog.stories.ts index 8e2826111fe..a97b054f358 100644 --- a/libs/components/src/dialog/dialog/dialog.stories.ts +++ b/libs/components/src/dialog/dialog/dialog.stories.ts @@ -5,6 +5,7 @@ import { I18nService } from "@bitwarden/common/abstractions/i18n.service"; import { ButtonModule } from "../../button"; import { IconButtonModule } from "../../icon-button"; import { SharedModule } from "../../shared"; +import { TabsModule } from "../../tabs"; import { I18nMockService } from "../../utils/i18n-mock.service"; import { DialogCloseDirective } from "../directives/dialog-close.directive"; import { DialogTitleContainerDirective } from "../directives/dialog-title-container.directive"; @@ -16,7 +17,7 @@ export default { component: DialogComponent, decorators: [ moduleMetadata({ - imports: [ButtonModule, SharedModule, IconButtonModule], + imports: [ButtonModule, SharedModule, IconButtonModule, TabsModule], declarations: [DialogTitleContainerDirective, DialogCloseDirective], providers: [ { @@ -33,6 +34,13 @@ export default { args: { dialogSize: "small", }, + argTypes: { + _disablePadding: { + table: { + disable: true, + }, + }, + }, parameters: { design: { type: "figma", @@ -44,7 +52,7 @@ export default { const Template: Story = (args: DialogComponent) => ({ props: args, template: ` - + {{title}} Dialog body text goes here.
@@ -83,7 +91,7 @@ Large.args = { const TemplateScrolling: Story = (args: DialogComponent) => ({ props: args, template: ` - + Scrolling Example Dialog body text goes here.
@@ -104,3 +112,37 @@ export const ScrollingContent = TemplateScrolling.bind({}); ScrollingContent.args = { dialogSize: "small", }; + +const TemplateTabbed: Story = (args: DialogComponent) => ({ + props: args, + template: ` + + Tab Content Example + + + First Tab Content + Second Tab Content + Third Tab Content + + +
+ + +
+
+ `, +}); + +export const TabContent = TemplateTabbed.bind({}); +TabContent.args = { + dialogSize: "large", + disablePadding: true, +}; +TabContent.story = { + parameters: { + docs: { + storyDescription: `An example of using the \`bitTabGroup\` component within the Dialog. The content padding should be + disabled (via \`disablePadding\`) so that the tabs are flush against the dialog title.`, + }, + }, +}; diff --git a/libs/components/src/icon-button/icon-button.component.ts b/libs/components/src/icon-button/icon-button.component.ts index cd6f1be402a..6696935257f 100644 --- a/libs/components/src/icon-button/icon-button.component.ts +++ b/libs/components/src/icon-button/icon-button.component.ts @@ -72,7 +72,7 @@ const sizes: Record = { @Component({ selector: "button[bitIconButton]", - template: ``, + template: ``, }) export class BitIconButtonComponent { @Input("bitIconButton") icon: string; @@ -106,10 +106,15 @@ export class BitIconButtonComponent { "before:tw-rounded-md", "before:tw-transition", "before:tw-ring", + "before:tw-ring-transparent", "focus-visible:before:tw-ring-text-contrast", "focus-visible:tw-z-10", ] .concat(styles[this.buttonType]) .concat(sizes[this.size]); } + + get iconClass() { + return [this.icon, "!tw-m-0"]; + } } diff --git a/libs/components/src/index.ts b/libs/components/src/index.ts index 1f1ae6a8d2b..264c655d80f 100644 --- a/libs/components/src/index.ts +++ b/libs/components/src/index.ts @@ -7,7 +7,6 @@ export * from "./icon"; export * from "./icon-button"; export * from "./menu"; export * from "./dialog"; -export * from "./submit-button"; export * from "./link"; export * from "./tabs"; export * from "./toggle-group"; diff --git a/libs/components/src/submit-button/index.ts b/libs/components/src/submit-button/index.ts deleted file mode 100644 index ae7d96d2c1a..00000000000 --- a/libs/components/src/submit-button/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./submit-button.module"; diff --git a/libs/components/src/submit-button/submit-button.component.html b/libs/components/src/submit-button/submit-button.component.html deleted file mode 100644 index 9d9657ba7ee..00000000000 --- a/libs/components/src/submit-button/submit-button.component.html +++ /dev/null @@ -1,16 +0,0 @@ - diff --git a/libs/components/src/submit-button/submit-button.component.ts b/libs/components/src/submit-button/submit-button.component.ts deleted file mode 100644 index 27408349da7..00000000000 --- a/libs/components/src/submit-button/submit-button.component.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Component, HostBinding, Input } from "@angular/core"; - -import { ButtonTypes } from "../button"; - -@Component({ - selector: "bit-submit-button", - templateUrl: "./submit-button.component.html", -}) -export class SubmitButtonComponent { - @Input() buttonType: ButtonTypes = "primary"; - @Input() disabled = false; - @Input() loading: boolean; - - @Input() block?: boolean; - - @HostBinding("class") get classList() { - return this.block == null || this.block === false ? [] : ["tw-w-full", "tw-block"]; - } -} diff --git a/libs/components/src/submit-button/submit-button.module.ts b/libs/components/src/submit-button/submit-button.module.ts deleted file mode 100644 index c7ab7567e64..00000000000 --- a/libs/components/src/submit-button/submit-button.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CommonModule } from "@angular/common"; -import { NgModule } from "@angular/core"; - -import { ButtonModule } from "../button"; - -import { SubmitButtonComponent } from "./submit-button.component"; - -@NgModule({ - imports: [CommonModule, ButtonModule], - exports: [SubmitButtonComponent], - declarations: [SubmitButtonComponent], -}) -export class SubmitButtonModule {} diff --git a/libs/components/src/submit-button/submit-button.stories.ts b/libs/components/src/submit-button/submit-button.stories.ts deleted file mode 100644 index cf19b1c8e4b..00000000000 --- a/libs/components/src/submit-button/submit-button.stories.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { Meta, moduleMetadata, Story } from "@storybook/angular"; - -import { SubmitButtonComponent } from "./submit-button.component"; -import { SubmitButtonModule } from "./submit-button.module"; - -export default { - title: "Component Library/Submit Button", - component: SubmitButtonComponent, - decorators: [ - moduleMetadata({ - imports: [SubmitButtonModule], - }), - ], - args: { - buttonType: "primary", - loading: false, - block: false, - }, - parameters: { - design: { - type: "figma", - url: "https://www.figma.com/file/Zt3YSeb6E6lebAffrNLa0h/Tailwind-Component-Library?node-id=1881%3A16733", - }, - }, -} as Meta; - -const Template: Story = (args: SubmitButtonComponent) => ({ - props: args, - template: ` - Submit - `, -}); - -export const Primary = Template.bind({}); -Primary.args = {}; - -export const Loading = Template.bind({}); -Loading.args = { - loading: true, -}; - -export const Disabled = Template.bind({}); -Disabled.args = { - disabled: true, -};