From 9e290a3fed3f7d247374138245259d7b34c4d0cd Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 19 Oct 2023 11:17:23 +0200 Subject: [PATCH] [PM-4222] Make importer UI reusable (#6504) * Split up import/export into separate modules * Fix routing and apply PR feedback * Renamed OrganizationExport exports to OrganizationVaultExport * Make import dialogs standalone and move them to libs/importer * Make import.component re-usable - Move functionality which was previously present on the org-import.component into import.component - Move import.component into libs/importer Make import.component standalone Create import-web.component to represent Web UI Fix module imports and routing Remove unused org-import-files * Renamed filenames according to export rename * Make ImportWebComponent standalone, simplify routing * Pass organizationId as Input to ImportComponent * use formLoading and formDisabled outputs * Emit an event when the import succeeds Remove Angular router from base-component as other clients might not have routing (i.e. desktop) Move logic that happened on web successful import into the import-web.component * fix table themes on desktop & browser * fix fileSelector button styles * update selectors to use tools prefix; remove unused selectors * Wall off UI components in libs/importer Create barrel-file for libs/importer/components Remove components and dialog exports from libs/importer/index.ts Extend libs/shared/tsconfig.libs.json to include @bitwarden/importer/ui -> libs/importer/components Extend apps/web/tsconfig.ts to include @bitwarden/importer/ui Update all usages * Rename @bitwarden/importer to @bitwarden/importer/core Create more barrel files in libs/importer/* Update imports within libs/importer Extend tsconfig files Update imports in web, desktop, browser and cli * Lazy-load the ImportWebComponent via both routes * Use SharedModule as import in import-web.component * File selector should be displayed as secondary * Use bitSubmit to override submit preventDefault (#6607) Co-authored-by: Daniel James Smith --------- Co-authored-by: Daniel James Smith Co-authored-by: William Martin --- .../browser/src/background/main.background.ts | 2 +- .../src/popup/services/services.module.ts | 2 +- .../import-api-service.factory.ts | 2 +- .../import-service.factory.ts | 2 +- apps/browser/tsconfig.json | 3 +- apps/cli/src/bw.ts | 2 +- apps/cli/src/tools/import.command.ts | 2 +- apps/cli/tsconfig.json | 2 +- apps/desktop/tsconfig.json | 3 +- .../organization-settings-routing.module.ts | 11 +- .../tools/import/org-import-routing.module.ts | 25 ---- .../tools/import/org-import.component.ts | 99 ------------- .../tools/import/org-import.module.ts | 44 ------ apps/web/src/app/oss-routing.module.ts | 6 +- .../dialog/file-password-prompt.component.ts | 20 --- .../app/tools/import/import-routing.module.ts | 17 --- .../tools/import/import-web.component.html | 18 +++ .../app/tools/import/import-web.component.ts | 51 +++++++ .../web/src/app/tools/import/import.module.ts | 54 ------- apps/web/src/scss/pages.scss | 2 +- apps/web/tsconfig.json | 3 +- .../src/services/jslib-services.module.ts | 2 +- libs/components/src/async-actions/index.ts | 1 + libs/components/src/table/table.component.css | 4 + libs/components/src/table/table.component.ts | 2 + libs/components/src/tw-theme.css | 1 + .../file-password-prompt.component.html | 4 +- .../dialog/file-password-prompt.component.ts | 43 ++++++ .../dialog/import-error-dialog.component.html | 0 .../dialog/import-error-dialog.component.ts | 7 +- .../import-success-dialog.component.html | 0 .../dialog/import-success-dialog.component.ts | 9 +- .../importer/src/components}/dialog/index.ts | 0 .../src/components}/import.component.html | 20 +-- .../src/components}/import.component.ts | 135 +++++++++++++++--- libs/importer/src/components/index.ts | 3 + libs/importer/src/index.ts | 10 +- libs/importer/src/models/index.ts | 2 + libs/importer/src/services/index.ts | 5 + libs/shared/tsconfig.libs.json | 3 +- tsconfig.eslint.json | 3 +- tsconfig.json | 3 +- 42 files changed, 299 insertions(+), 328 deletions(-) delete mode 100644 apps/web/src/app/admin-console/organizations/tools/import/org-import-routing.module.ts delete mode 100644 apps/web/src/app/admin-console/organizations/tools/import/org-import.component.ts delete mode 100644 apps/web/src/app/admin-console/organizations/tools/import/org-import.module.ts delete mode 100644 apps/web/src/app/tools/import/dialog/file-password-prompt.component.ts delete mode 100644 apps/web/src/app/tools/import/import-routing.module.ts create mode 100644 apps/web/src/app/tools/import/import-web.component.html create mode 100644 apps/web/src/app/tools/import/import-web.component.ts delete mode 100644 apps/web/src/app/tools/import/import.module.ts create mode 100644 libs/components/src/table/table.component.css rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/file-password-prompt.component.html (90%) create mode 100644 libs/importer/src/components/dialog/file-password-prompt.component.ts rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/import-error-dialog.component.html (100%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/import-error-dialog.component.ts (72%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/import-success-dialog.component.html (100%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/import-success-dialog.component.ts (84%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/dialog/index.ts (100%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/import.component.html (97%) rename {apps/web/src/app/tools/import => libs/importer/src/components}/import.component.ts (78%) create mode 100644 libs/importer/src/components/index.ts create mode 100644 libs/importer/src/models/index.ts create mode 100644 libs/importer/src/services/index.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index e00beaefe4..9d4cba04e7 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -113,7 +113,7 @@ import { ImportApiService, ImportServiceAbstraction, ImportService, -} from "@bitwarden/importer"; +} from "@bitwarden/importer/core"; import { BrowserOrganizationService } from "../admin-console/services/browser-organization.service"; import { BrowserPolicyService } from "../admin-console/services/browser-policy.service"; diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 6eb0e075c1..2622b8ef13 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -84,7 +84,7 @@ import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.serv import { FolderApiService } from "@bitwarden/common/vault/services/folder/folder-api.service"; import { DialogService } from "@bitwarden/components"; import { VaultExportServiceAbstraction } from "@bitwarden/exporter/vault-export"; -import { ImportServiceAbstraction } from "@bitwarden/importer"; +import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { BrowserOrganizationService } from "../../admin-console/services/browser-organization.service"; import { BrowserPolicyService } from "../../admin-console/services/browser-policy.service"; diff --git a/apps/browser/src/tools/background/service_factories/import-api-service.factory.ts b/apps/browser/src/tools/background/service_factories/import-api-service.factory.ts index 4344647bbe..00954a0dc6 100644 --- a/apps/browser/src/tools/background/service_factories/import-api-service.factory.ts +++ b/apps/browser/src/tools/background/service_factories/import-api-service.factory.ts @@ -1,4 +1,4 @@ -import { ImportApiService, ImportApiServiceAbstraction } from "@bitwarden/importer"; +import { ImportApiService, ImportApiServiceAbstraction } from "@bitwarden/importer/core"; import { ApiServiceInitOptions, diff --git a/apps/browser/src/tools/background/service_factories/import-service.factory.ts b/apps/browser/src/tools/background/service_factories/import-service.factory.ts index 3dc9bbd4f0..7f5328f4d0 100644 --- a/apps/browser/src/tools/background/service_factories/import-service.factory.ts +++ b/apps/browser/src/tools/background/service_factories/import-service.factory.ts @@ -1,4 +1,4 @@ -import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer"; +import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core"; import { cryptoServiceFactory, diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index 357be6c528..3ad2be7c02 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -15,7 +15,8 @@ "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], "@bitwarden/exporter/*": ["../../libs/exporter/src/*"], - "@bitwarden/importer": ["../../libs/importer/src"], + "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/vault": ["../../libs/vault/src"] }, "useDefineForClassFields": false diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index ca50169019..ffaec215e2 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -74,7 +74,7 @@ import { ImportApiServiceAbstraction, ImportService, ImportServiceAbstraction, -} from "@bitwarden/importer"; +} from "@bitwarden/importer/core"; import { NodeCryptoFunctionService } from "@bitwarden/node/services/node-crypto-function.service"; import { CliConfigService } from "./platform/services/cli-config.service"; diff --git a/apps/cli/src/tools/import.command.ts b/apps/cli/src/tools/import.command.ts index e3f24b960f..1be562239f 100644 --- a/apps/cli/src/tools/import.command.ts +++ b/apps/cli/src/tools/import.command.ts @@ -3,7 +3,7 @@ import * as inquirer from "inquirer"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer"; +import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer/core"; import { Response } from "../models/response"; import { MessageResponse } from "../models/response/message.response"; diff --git a/apps/cli/tsconfig.json b/apps/cli/tsconfig.json index 420496ad93..395d91564a 100644 --- a/apps/cli/tsconfig.json +++ b/apps/cli/tsconfig.json @@ -14,7 +14,7 @@ "paths": { "@bitwarden/common/spec": ["../../libs/common/spec"], "@bitwarden/common/*": ["../../libs/common/src/*"], - "@bitwarden/importer": ["../../libs/importer/src"], + "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/exporter/*": ["../../libs/exporter/src/*"], "@bitwarden/node/*": ["../../libs/node/src/*"] } diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 67bfba6442..b479905f89 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -15,7 +15,8 @@ "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], "@bitwarden/exporter/*": ["../../libs/exporter/src/*"], - "@bitwarden/importer": ["../../libs/importer/src"], + "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/vault": ["../../libs/vault/src"] }, "useDefineForClassFields": false diff --git a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts index 1606d86497..d853b942ce 100644 --- a/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts +++ b/apps/web/src/app/admin-console/organizations/settings/organization-settings-routing.module.ts @@ -48,8 +48,15 @@ const routes: Routes = [ children: [ { path: "import", - loadChildren: () => - import("../tools/import/org-import.module").then((m) => m.OrganizationImportModule), + loadComponent: () => + import("../../../tools/import/import-web.component").then( + (mod) => mod.ImportWebComponent + ), + canActivate: [OrganizationPermissionsGuard], + data: { + titleId: "importData", + organizationPermissions: (org: Organization) => org.canAccessImportExport, + }, }, { path: "export", diff --git a/apps/web/src/app/admin-console/organizations/tools/import/org-import-routing.module.ts b/apps/web/src/app/admin-console/organizations/tools/import/org-import-routing.module.ts deleted file mode 100644 index 9b4b4b5c78..0000000000 --- a/apps/web/src/app/admin-console/organizations/tools/import/org-import-routing.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; - -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; - -import { OrganizationPermissionsGuard } from "../../guards/org-permissions.guard"; - -import { OrganizationImportComponent } from "./org-import.component"; - -const routes: Routes = [ - { - path: "", - component: OrganizationImportComponent, - canActivate: [OrganizationPermissionsGuard], - data: { - titleId: "importData", - organizationPermissions: (org: Organization) => org.canAccessImportExport, - }, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], -}) -export class OrganizationImportRoutingModule {} diff --git a/apps/web/src/app/admin-console/organizations/tools/import/org-import.component.ts b/apps/web/src/app/admin-console/organizations/tools/import/org-import.component.ts deleted file mode 100644 index b514f2bcf3..0000000000 --- a/apps/web/src/app/admin-console/organizations/tools/import/org-import.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Component } from "@angular/core"; -import { FormBuilder } from "@angular/forms"; -import { ActivatedRoute, Router } from "@angular/router"; -import { switchMap, takeUntil } from "rxjs/operators"; - -import { - canAccessVaultTab, - OrganizationService, -} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; -import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; -import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; -import { DialogService } from "@bitwarden/components"; -import { ImportServiceAbstraction } from "@bitwarden/importer"; - -import { ImportComponent } from "../../../../tools/import/import.component"; - -@Component({ - selector: "app-org-import", - templateUrl: "../../../../tools/import/import.component.html", -}) -// eslint-disable-next-line rxjs-angular/prefer-takeuntil -export class OrganizationImportComponent extends ImportComponent { - organization: Organization; - - protected get importBlockedByPolicy(): boolean { - return false; - } - - constructor( - i18nService: I18nService, - importService: ImportServiceAbstraction, - router: Router, - private route: ActivatedRoute, - platformUtilsService: PlatformUtilsService, - policyService: PolicyService, - organizationService: OrganizationService, - logService: LogService, - syncService: SyncService, - dialogService: DialogService, - folderService: FolderService, - collectionService: CollectionService, - formBuilder: FormBuilder - ) { - super( - i18nService, - importService, - router, - platformUtilsService, - policyService, - logService, - syncService, - dialogService, - folderService, - collectionService, - organizationService, - formBuilder - ); - } - - ngOnInit() { - this.route.params - .pipe( - switchMap((params) => this.organizationService.get$(params.organizationId)), - takeUntil(this.destroy$) - ) - .subscribe((organization) => { - this.organizationId = organization.id; - this.organization = organization; - }); - super.ngOnInit(); - } - - protected async onSuccessfulImport(): Promise { - if (canAccessVaultTab(this.organization)) { - await this.router.navigate(["organizations", this.organizationId, "vault"]); - } else { - this.fileSelected = null; - } - } - - protected async performImport() { - const confirmed = await this.dialogService.openSimpleDialog({ - title: { key: "warning" }, - content: { key: "importWarning", placeholders: [this.organization.name] }, - type: "warning", - }); - - if (!confirmed) { - return; - } - await super.performImport(); - } -} diff --git a/apps/web/src/app/admin-console/organizations/tools/import/org-import.module.ts b/apps/web/src/app/admin-console/organizations/tools/import/org-import.module.ts deleted file mode 100644 index 66a22158e2..0000000000 --- a/apps/web/src/app/admin-console/organizations/tools/import/org-import.module.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { - ImportService, - ImportServiceAbstraction, - ImportApiService, - ImportApiServiceAbstraction, -} from "@bitwarden/importer"; - -import { LooseComponentsModule, SharedModule } from "../../../../shared"; - -import { OrganizationImportRoutingModule } from "./org-import-routing.module"; -import { OrganizationImportComponent } from "./org-import.component"; - -@NgModule({ - imports: [SharedModule, LooseComponentsModule, OrganizationImportRoutingModule], - declarations: [OrganizationImportComponent], - providers: [ - { - provide: ImportApiServiceAbstraction, - useClass: ImportApiService, - deps: [ApiService], - }, - { - provide: ImportServiceAbstraction, - useClass: ImportService, - deps: [ - CipherService, - FolderService, - ImportApiServiceAbstraction, - I18nService, - CollectionService, - CryptoService, - ], - }, - ], -}) -export class OrganizationImportModule {} diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index d8a57cd3fb..eee22c0d46 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -255,7 +255,11 @@ const routes: Routes = [ { path: "", pathMatch: "full", redirectTo: "generator" }, { path: "import", - loadChildren: () => import("./tools/import/import.module").then((m) => m.ImportModule), + loadComponent: () => + import("./tools/import/import-web.component").then((mod) => mod.ImportWebComponent), + data: { + titleId: "importData", + }, }, { path: "export", diff --git a/apps/web/src/app/tools/import/dialog/file-password-prompt.component.ts b/apps/web/src/app/tools/import/dialog/file-password-prompt.component.ts deleted file mode 100644 index d0cbfe20fd..0000000000 --- a/apps/web/src/app/tools/import/dialog/file-password-prompt.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DialogRef } from "@angular/cdk/dialog"; -import { Component } from "@angular/core"; -import { FormControl, Validators } from "@angular/forms"; - -@Component({ - templateUrl: "file-password-prompt.component.html", -}) -export class FilePasswordPromptComponent { - filePassword = new FormControl("", Validators.required); - - constructor(public dialogRef: DialogRef) {} - - submit() { - this.filePassword.markAsTouched(); - if (!this.filePassword.valid) { - return; - } - this.dialogRef.close(this.filePassword.value); - } -} diff --git a/apps/web/src/app/tools/import/import-routing.module.ts b/apps/web/src/app/tools/import/import-routing.module.ts deleted file mode 100644 index 82d0a73384..0000000000 --- a/apps/web/src/app/tools/import/import-routing.module.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { NgModule } from "@angular/core"; -import { RouterModule, Routes } from "@angular/router"; - -import { ImportComponent } from "./import.component"; - -const routes: Routes = [ - { - path: "", - component: ImportComponent, - data: { titleId: "importData" }, - }, -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], -}) -export class ImportRoutingModule {} diff --git a/apps/web/src/app/tools/import/import-web.component.html b/apps/web/src/app/tools/import/import-web.component.html new file mode 100644 index 0000000000..1d655db324 --- /dev/null +++ b/apps/web/src/app/tools/import/import-web.component.html @@ -0,0 +1,18 @@ +

{{ "importData" | i18n }}

+ + diff --git a/apps/web/src/app/tools/import/import-web.component.ts b/apps/web/src/app/tools/import/import-web.component.ts new file mode 100644 index 0000000000..952a717cd9 --- /dev/null +++ b/apps/web/src/app/tools/import/import-web.component.ts @@ -0,0 +1,51 @@ +import { Component, OnInit } from "@angular/core"; +import { ActivatedRoute, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; + +import { + OrganizationService, + canAccessVaultTab, +} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { ImportComponent } from "@bitwarden/importer/ui"; + +import { SharedModule } from "../../shared"; + +@Component({ + templateUrl: "import-web.component.html", + standalone: true, + imports: [SharedModule, ImportComponent], +}) +export class ImportWebComponent implements OnInit { + protected routeOrgId: string = null; + protected loading = false; + protected disabled = false; + + constructor( + private route: ActivatedRoute, + private organizationService: OrganizationService, + private router: Router + ) {} + + ngOnInit(): void { + this.routeOrgId = this.route.snapshot.paramMap.get("organizationId"); + } + + /** + * Callback that is called after a successful import. + */ + protected async onSuccessfulImport(organizationId: string): Promise { + if (!organizationId) { + await this.router.navigate(["vault"]); + return; + } + + const organization = await firstValueFrom(this.organizationService.get$(organizationId)); + if (organization == null) { + return; + } + + if (canAccessVaultTab(organization)) { + await this.router.navigate(["organizations", organizationId, "vault"]); + } + } +} diff --git a/apps/web/src/app/tools/import/import.module.ts b/apps/web/src/app/tools/import/import.module.ts deleted file mode 100644 index 34f71cfab7..0000000000 --- a/apps/web/src/app/tools/import/import.module.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { NgModule } from "@angular/core"; - -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; -import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { - ImportService, - ImportServiceAbstraction, - ImportApiService, - ImportApiServiceAbstraction, -} from "@bitwarden/importer"; - -import { LooseComponentsModule, SharedModule } from "../../shared"; - -import { - ImportErrorDialogComponent, - ImportSuccessDialogComponent, - FilePasswordPromptComponent, -} from "./dialog"; -import { ImportRoutingModule } from "./import-routing.module"; -import { ImportComponent } from "./import.component"; - -@NgModule({ - imports: [SharedModule, LooseComponentsModule, ImportRoutingModule], - declarations: [ - ImportComponent, - FilePasswordPromptComponent, - ImportErrorDialogComponent, - ImportSuccessDialogComponent, - ], - providers: [ - { - provide: ImportApiServiceAbstraction, - useClass: ImportApiService, - deps: [ApiService], - }, - { - provide: ImportServiceAbstraction, - useClass: ImportService, - deps: [ - CipherService, - FolderService, - ImportApiServiceAbstraction, - I18nService, - CollectionService, - CryptoService, - ], - }, - ], -}) -export class ImportModule {} diff --git a/apps/web/src/scss/pages.scss b/apps/web/src/scss/pages.scss index 4dbee2ac50..684d45a1a6 100644 --- a/apps/web/src/scss/pages.scss +++ b/apps/web/src/scss/pages.scss @@ -32,7 +32,7 @@ app-password-generator-history { } } -app-import { +tools-import { textarea { height: 150px; } diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 67d2451158..a1ea71f4bb 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -10,7 +10,8 @@ "@bitwarden/common/*": ["../../libs/common/src/*"], "@bitwarden/components": ["../../libs/components/src"], "@bitwarden/exporter/*": ["../../libs/exporter/src/*"], - "@bitwarden/importer": ["../../libs/importer/src"], + "@bitwarden/importer/core": ["../../libs/importer/src"], + "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["src/*"] } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index fbaf3411ec..e52e9c394e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -154,7 +154,7 @@ import { ImportApiServiceAbstraction, ImportService, ImportServiceAbstraction, -} from "@bitwarden/importer"; +} from "@bitwarden/importer/core"; import { PasswordRepromptService } from "@bitwarden/vault"; import { AuthGuard } from "../auth/guards/auth.guard"; diff --git a/libs/components/src/async-actions/index.ts b/libs/components/src/async-actions/index.ts index 6515ffc47c..05f49902a7 100644 --- a/libs/components/src/async-actions/index.ts +++ b/libs/components/src/async-actions/index.ts @@ -1,3 +1,4 @@ export * from "./async-actions.module"; export * from "./bit-action.directive"; export * from "./form-button.directive"; +export * from "./bit-submit.directive"; diff --git a/libs/components/src/table/table.component.css b/libs/components/src/table/table.component.css new file mode 100644 index 0000000000..e764a9130e --- /dev/null +++ b/libs/components/src/table/table.component.css @@ -0,0 +1,4 @@ +th { + text-align: inherit; + text-align: -webkit-match-parent; +} diff --git a/libs/components/src/table/table.component.ts b/libs/components/src/table/table.component.ts index 9f36d0a70f..b4d6d1931d 100644 --- a/libs/components/src/table/table.component.ts +++ b/libs/components/src/table/table.component.ts @@ -39,6 +39,8 @@ export class TableComponent implements OnDestroy, AfterContentChecked { "tw-w-full", "tw-leading-normal", "tw-text-main", + "tw-border-collapse", + "tw-text-start", this.layout === "auto" ? "tw-table-auto" : "tw-table-fixed", ]; } diff --git a/libs/components/src/tw-theme.css b/libs/components/src/tw-theme.css index 1ff2064fdb..14b5fcb433 100644 --- a/libs/components/src/tw-theme.css +++ b/libs/components/src/tw-theme.css @@ -159,6 +159,7 @@ } @import "./search/search.component.css"; +@import "./table/table.component.css"; /** * tw-break-words does not work with table cells: diff --git a/apps/web/src/app/tools/import/dialog/file-password-prompt.component.html b/libs/importer/src/components/dialog/file-password-prompt.component.html similarity index 90% rename from apps/web/src/app/tools/import/dialog/file-password-prompt.component.html rename to libs/importer/src/components/dialog/file-password-prompt.component.html index 6c849da69d..823d6ebec1 100644 --- a/apps/web/src/app/tools/import/dialog/file-password-prompt.component.html +++ b/libs/importer/src/components/dialog/file-password-prompt.component.html @@ -1,4 +1,4 @@ -
+ {{ "confirmVaultImport" | i18n }} @@ -12,7 +12,7 @@ bitInput type="password" name="filePassword" - [formControl]="filePassword" + formControlName="filePassword" appAutofocus appInputVerbatim /> diff --git a/libs/importer/src/components/dialog/file-password-prompt.component.ts b/libs/importer/src/components/dialog/file-password-prompt.component.ts new file mode 100644 index 0000000000..864fdafab0 --- /dev/null +++ b/libs/importer/src/components/dialog/file-password-prompt.component.ts @@ -0,0 +1,43 @@ +import { DialogRef } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; +import { Component } from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { + AsyncActionsModule, + ButtonModule, + DialogModule, + FormFieldModule, + IconButtonModule, +} from "@bitwarden/components"; + +@Component({ + templateUrl: "file-password-prompt.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + DialogModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + ReactiveFormsModule, + ], +}) +export class FilePasswordPromptComponent { + formGroup = this.formBuilder.group({ + filePassword: ["", Validators.required], + }); + + constructor(public dialogRef: DialogRef, protected formBuilder: FormBuilder) {} + + submit = () => { + this.formGroup.markAsTouched(); + if (!this.formGroup.valid) { + return; + } + this.dialogRef.close(this.formGroup.value.filePassword); + }; +} diff --git a/apps/web/src/app/tools/import/dialog/import-error-dialog.component.html b/libs/importer/src/components/dialog/import-error-dialog.component.html similarity index 100% rename from apps/web/src/app/tools/import/dialog/import-error-dialog.component.html rename to libs/importer/src/components/dialog/import-error-dialog.component.html diff --git a/apps/web/src/app/tools/import/dialog/import-error-dialog.component.ts b/libs/importer/src/components/dialog/import-error-dialog.component.ts similarity index 72% rename from apps/web/src/app/tools/import/dialog/import-error-dialog.component.ts rename to libs/importer/src/components/dialog/import-error-dialog.component.ts index abb68cf53b..4d766e3619 100644 --- a/apps/web/src/app/tools/import/dialog/import-error-dialog.component.ts +++ b/libs/importer/src/components/dialog/import-error-dialog.component.ts @@ -1,7 +1,9 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; import { Component, Inject, OnInit } from "@angular/core"; -import { TableDataSource } from "@bitwarden/components"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ButtonModule, DialogModule, TableDataSource, TableModule } from "@bitwarden/components"; export interface ErrorListItem { type: string; @@ -9,8 +11,9 @@ export interface ErrorListItem { } @Component({ - selector: "app-import-error-dialog", templateUrl: "./import-error-dialog.component.html", + standalone: true, + imports: [CommonModule, JslibModule, DialogModule, TableModule, ButtonModule], }) export class ImportErrorDialogComponent implements OnInit { protected dataSource = new TableDataSource(); diff --git a/apps/web/src/app/tools/import/dialog/import-success-dialog.component.html b/libs/importer/src/components/dialog/import-success-dialog.component.html similarity index 100% rename from apps/web/src/app/tools/import/dialog/import-success-dialog.component.html rename to libs/importer/src/components/dialog/import-success-dialog.component.html diff --git a/apps/web/src/app/tools/import/dialog/import-success-dialog.component.ts b/libs/importer/src/components/dialog/import-success-dialog.component.ts similarity index 84% rename from apps/web/src/app/tools/import/dialog/import-success-dialog.component.ts rename to libs/importer/src/components/dialog/import-success-dialog.component.ts index 215784cb6f..4d10002da4 100644 --- a/apps/web/src/app/tools/import/dialog/import-success-dialog.component.ts +++ b/libs/importer/src/components/dialog/import-success-dialog.component.ts @@ -1,9 +1,12 @@ import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { CommonModule } from "@angular/common"; import { Component, Inject, OnInit } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { CipherType } from "@bitwarden/common/vault/enums/cipher-type"; -import { TableDataSource } from "@bitwarden/components"; -import { ImportResult } from "@bitwarden/importer"; +import { ButtonModule, DialogModule, TableDataSource, TableModule } from "@bitwarden/components"; + +import { ImportResult } from "../../models"; export interface ResultList { icon: string; @@ -13,6 +16,8 @@ export interface ResultList { @Component({ templateUrl: "./import-success-dialog.component.html", + standalone: true, + imports: [CommonModule, JslibModule, DialogModule, TableModule, ButtonModule], }) export class ImportSuccessDialogComponent implements OnInit { protected dataSource = new TableDataSource(); diff --git a/apps/web/src/app/tools/import/dialog/index.ts b/libs/importer/src/components/dialog/index.ts similarity index 100% rename from apps/web/src/app/tools/import/dialog/index.ts rename to libs/importer/src/components/dialog/index.ts diff --git a/apps/web/src/app/tools/import/import.component.html b/libs/importer/src/components/import.component.html similarity index 97% rename from apps/web/src/app/tools/import/import.component.html rename to libs/importer/src/components/import.component.html index c9e3285c29..83e119fcc5 100644 --- a/apps/web/src/app/tools/import/import.component.html +++ b/libs/importer/src/components/import.component.html @@ -1,9 +1,7 @@ -

{{ "importData" | i18n }}

- {{ "personalOwnershipPolicyInEffectImports" | i18n }} - + {{ "importDestination" | i18n }} @@ -349,12 +347,7 @@ {{ "selectImportFile" | i18n }}
- {{ this.fileSelected ? this.fileSelected.name : ("noFileChosen" | i18n) }} @@ -380,13 +373,4 @@ formControlName="fileContents" > - diff --git a/apps/web/src/app/tools/import/import.component.ts b/libs/importer/src/components/import.component.ts similarity index 78% rename from apps/web/src/app/tools/import/import.component.ts rename to libs/importer/src/components/import.component.ts index 1f71b2fd77..3a111720ab 100644 --- a/apps/web/src/app/tools/import/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -1,10 +1,20 @@ -import { Component, OnDestroy, OnInit } from "@angular/core"; -import { FormBuilder, Validators } from "@angular/forms"; -import { Router } from "@angular/router"; +import { CommonModule } from "@angular/common"; +import { + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, + ViewChild, +} from "@angular/core"; +import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms"; import * as JSZip from "jszip"; import { concat, Observable, Subject, lastValueFrom, combineLatest } from "rxjs"; import { map, takeUntil } from "rxjs/operators"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { canAccessImportExport, OrganizationService, @@ -12,22 +22,35 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { DialogService } from "@bitwarden/components"; import { - ImportOption, - ImportResult, + AsyncActionsModule, + BitSubmitDirective, + ButtonModule, + CalloutModule, + DialogService, + FormFieldModule, + IconButtonModule, + SelectModule, +} from "@bitwarden/components"; + +import { ImportOption, ImportResult, ImportType } from "../models"; +import { + ImportApiService, + ImportApiServiceAbstraction, + ImportService, ImportServiceAbstraction, - ImportType, -} from "@bitwarden/importer"; +} from "../services"; import { FilePasswordPromptComponent, @@ -36,8 +59,39 @@ import { } from "./dialog"; @Component({ - selector: "app-import", + selector: "tools-import", templateUrl: "import.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + FormFieldModule, + AsyncActionsModule, + ButtonModule, + IconButtonModule, + SelectModule, + CalloutModule, + ReactiveFormsModule, + ], + providers: [ + { + provide: ImportApiServiceAbstraction, + useClass: ImportApiService, + deps: [ApiService], + }, + { + provide: ImportServiceAbstraction, + useClass: ImportService, + deps: [ + CipherService, + FolderService, + ImportApiServiceAbstraction, + I18nService, + CollectionService, + CryptoService, + ], + }, + ], }) export class ImportComponent implements OnInit, OnDestroy { featuredImportOptions: ImportOption[]; @@ -49,7 +103,24 @@ export class ImportComponent implements OnInit, OnDestroy { collections$: Observable; organizations$: Observable; - protected organizationId: string = null; + private _organizationId: string; + + get organizationId(): string { + return this._organizationId; + } + + @Input() set organizationId(value: string) { + this._organizationId = value; + this.organizationService + .get$(this._organizationId) + .pipe(takeUntil(this.destroy$)) + .subscribe((organization) => { + this._organizationId = organization?.id; + this.organization = organization; + }); + } + + protected organization: Organization; protected destroy$ = new Subject(); private _importBlockedByPolicy = false; @@ -68,10 +139,31 @@ export class ImportComponent implements OnInit, OnDestroy { file: [], }); + @ViewChild(BitSubmitDirective) + private bitSubmit: BitSubmitDirective; + + @Output() + formLoading = new EventEmitter(); + + @Output() + formDisabled = new EventEmitter(); + + @Output() + onSuccessfulImport = new EventEmitter(); + + ngAfterViewInit(): void { + this.bitSubmit.loading$.pipe(takeUntil(this.destroy$)).subscribe((loading) => { + this.formLoading.emit(loading); + }); + + this.bitSubmit.disabled$.pipe(takeUntil(this.destroy$)).subscribe((disabled) => { + this.formDisabled.emit(disabled); + }); + } + constructor( protected i18nService: I18nService, protected importService: ImportServiceAbstraction, - protected router: Router, protected platformUtilsService: PlatformUtilsService, protected policyService: PolicyService, private logService: LogService, @@ -87,13 +179,6 @@ export class ImportComponent implements OnInit, OnDestroy { return this._importBlockedByPolicy; } - /** - * Callback that is called after a successful import. - */ - protected async onSuccessfulImport(): Promise { - await this.router.navigate(["vault"]); - } - ngOnInit() { this.setImportOptions(); @@ -167,6 +252,18 @@ export class ImportComponent implements OnInit, OnDestroy { }; protected async performImport() { + if (this.organization) { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "warning" }, + content: { key: "importWarning", placeholders: [this.organization.name] }, + type: "warning", + }); + + if (!confirmed) { + return; + } + } + if (this.importBlockedByPolicy) { this.platformUtilsService.showToast( "error", @@ -246,7 +343,7 @@ export class ImportComponent implements OnInit, OnDestroy { }); this.syncService.fullSync(true); - await this.onSuccessfulImport(); + this.onSuccessfulImport.emit(this._organizationId); } catch (e) { this.dialogService.open(ImportErrorDialogComponent, { data: e, diff --git a/libs/importer/src/components/index.ts b/libs/importer/src/components/index.ts new file mode 100644 index 0000000000..a2e59f1714 --- /dev/null +++ b/libs/importer/src/components/index.ts @@ -0,0 +1,3 @@ +export * from "./dialog"; + +export { ImportComponent } from "./import.component"; diff --git a/libs/importer/src/index.ts b/libs/importer/src/index.ts index 4586407659..4c6c4131ba 100644 --- a/libs/importer/src/index.ts +++ b/libs/importer/src/index.ts @@ -1,11 +1,5 @@ -export { ImportType, ImportOption } from "./models/import-options"; +export * from "./models"; -export { ImportResult } from "./models/import-result"; - -export { ImportApiServiceAbstraction } from "./services/import-api.service.abstraction"; -export { ImportApiService } from "./services/import-api.service"; - -export { ImportServiceAbstraction } from "./services/import.service.abstraction"; -export { ImportService } from "./services/import.service"; +export * from "./services"; export { Importer } from "./importers/importer"; diff --git a/libs/importer/src/models/index.ts b/libs/importer/src/models/index.ts new file mode 100644 index 0000000000..95eb910957 --- /dev/null +++ b/libs/importer/src/models/index.ts @@ -0,0 +1,2 @@ +export { ImportType, ImportOption } from "./import-options"; +export { ImportResult } from "./import-result"; diff --git a/libs/importer/src/services/index.ts b/libs/importer/src/services/index.ts new file mode 100644 index 0000000000..7b1244867f --- /dev/null +++ b/libs/importer/src/services/index.ts @@ -0,0 +1,5 @@ +export { ImportApiServiceAbstraction } from "./import-api.service.abstraction"; +export { ImportApiService } from "./import-api.service"; + +export { ImportServiceAbstraction } from "./import.service.abstraction"; +export { ImportService } from "./import.service"; diff --git a/libs/shared/tsconfig.libs.json b/libs/shared/tsconfig.libs.json index 85d5554270..1addd88d4c 100644 --- a/libs/shared/tsconfig.libs.json +++ b/libs/shared/tsconfig.libs.json @@ -7,7 +7,8 @@ "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], "@bitwarden/exporter/*": ["../exporter/src/*"], - "@bitwarden/importer": ["../importer/src"], + "@bitwarden/importer/core": ["../importer/src"], + "@bitwarden/importer/ui": ["../importer/src/components"], "@bitwarden/node/*": ["../node/src/*"], "@bitwarden/vault": ["../vault/src"] } diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index d96a144ebc..0ccac14430 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -20,7 +20,8 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/exporter/*": ["./libs/exporter/src/*"], - "@bitwarden/importer": ["./libs/importer/src"], + "@bitwarden/importer/core": ["./libs/importer/src"], + "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/vault": ["./libs/vault/src"] }, diff --git a/tsconfig.json b/tsconfig.json index d6e4db99f5..f6f6b4ee30 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,8 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/exporter/*": ["./libs/exporter/src/*"], - "@bitwarden/importer": ["./libs/importer/src"], + "@bitwarden/importer/core": ["./libs/importer/src"], + "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/node/*": ["./libs/node/src/*"], "@bitwarden/web-vault/*": ["./apps/web/src/*"], "@bitwarden/vault": ["./libs/vault/src"]