[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 <djsmith85@users.noreply.github.com>

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
Co-authored-by: William Martin <contact@willmartian.com>
This commit is contained in:
Daniel James Smith 2023-10-19 11:17:23 +02:00 committed by GitHub
parent d0e72f5554
commit 9e290a3fed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 299 additions and 328 deletions

View File

@ -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";

View File

@ -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";

View File

@ -1,4 +1,4 @@
import { ImportApiService, ImportApiServiceAbstraction } from "@bitwarden/importer";
import { ImportApiService, ImportApiServiceAbstraction } from "@bitwarden/importer/core";
import {
ApiServiceInitOptions,

View File

@ -1,4 +1,4 @@
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer";
import { ImportService, ImportServiceAbstraction } from "@bitwarden/importer/core";
import {
cryptoServiceFactory,

View File

@ -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

View File

@ -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";

View File

@ -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";

View File

@ -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/*"]
}

View File

@ -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

View File

@ -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",

View File

@ -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 {}

View File

@ -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<void> {
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();
}
}

View File

@ -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 {}

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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 {}

View File

@ -0,0 +1,18 @@
<h1 bitTypography="h1">{{ "importData" | i18n }}</h1>
<tools-import
(formDisabled)="this.disabled = $event"
(formLoading)="this.loading = $event"
(onSuccessfulImport)="this.onSuccessfulImport($event)"
organizationId="{{ routeOrgId }}"
></tools-import>
<button
[disabled]="disabled"
[loading]="loading"
form="importForm"
bitButton
type="submit"
bitFormButton
buttonType="primary"
>
{{ "importData" | i18n }}
</button>

View File

@ -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<void> {
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"]);
}
}
}

View File

@ -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 {}

View File

@ -32,7 +32,7 @@ app-password-generator-history {
}
}
app-import {
tools-import {
textarea {
height: 150px;
}

View File

@ -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/*"]
}

View File

@ -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";

View File

@ -1,3 +1,4 @@
export * from "./async-actions.module";
export * from "./bit-action.directive";
export * from "./form-button.directive";
export * from "./bit-submit.directive";

View File

@ -0,0 +1,4 @@
th {
text-align: inherit;
text-align: -webkit-match-parent;
}

View File

@ -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",
];
}

View File

@ -159,6 +159,7 @@
}
@import "./search/search.component.css";
@import "./table/table.component.css";
/**
* tw-break-words does not work with table cells:

View File

@ -1,4 +1,4 @@
<form (submit)="submit()">
<form [formGroup]="formGroup" [bitSubmit]="submit">
<bit-dialog>
<span bitDialogTitle>
{{ "confirmVaultImport" | i18n }}
@ -12,7 +12,7 @@
bitInput
type="password"
name="filePassword"
[formControl]="filePassword"
formControlName="filePassword"
appAutofocus
appInputVerbatim
/>

View File

@ -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);
};
}

View File

@ -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<ErrorListItem>();

View File

@ -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<ResultList>();

View File

@ -1,9 +1,7 @@
<h1 bitTypography="h1">{{ "importData" | i18n }}</h1>
<bit-callout type="info" *ngIf="importBlockedByPolicy">
{{ "personalOwnershipPolicyInEffectImports" | i18n }}
</bit-callout>
<form [formGroup]="formGroup" [bitSubmit]="submit">
<form [formGroup]="formGroup" [bitSubmit]="submit" id="importForm">
<bit-form-field>
<bit-label
>{{ "importDestination" | i18n }}
@ -349,12 +347,7 @@
<bit-form-field>
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
<div class="file-selector">
<button
bitButton
type="button"
class="btn btn-outline-primary"
(click)="fileSelector.click()"
>
<button bitButton type="button" buttonType="secondary" (click)="fileSelector.click()">
{{ "chooseFile" | i18n }}
</button>
{{ this.fileSelected ? this.fileSelected.name : ("noFileChosen" | i18n) }}
@ -380,13 +373,4 @@
formControlName="fileContents"
></textarea>
</bit-form-field>
<button
bitButton
bitFormButton
type="submit"
buttonType="primary"
[disabled]="importBlockedByPolicy"
>
{{ "importData" | i18n }}
</button>
</form>

View File

@ -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<CollectionView[]>;
organizations$: Observable<Organization[]>;
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<void>();
private _importBlockedByPolicy = false;
@ -68,10 +139,31 @@ export class ImportComponent implements OnInit, OnDestroy {
file: [],
});
@ViewChild(BitSubmitDirective)
private bitSubmit: BitSubmitDirective;
@Output()
formLoading = new EventEmitter<boolean>();
@Output()
formDisabled = new EventEmitter<boolean>();
@Output()
onSuccessfulImport = new EventEmitter<string>();
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<void> {
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<unknown, Error>(ImportErrorDialogComponent, {
data: e,

View File

@ -0,0 +1,3 @@
export * from "./dialog";
export { ImportComponent } from "./import.component";

View File

@ -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";

View File

@ -0,0 +1,2 @@
export { ImportType, ImportOption } from "./import-options";
export { ImportResult } from "./import-result";

View File

@ -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";

View File

@ -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"]
}

View File

@ -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"]
},

View File

@ -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"]