diff --git a/apps/web/src/app/organizations/manage/member-dialog/index.ts b/apps/web/src/app/organizations/manage/member-dialog/index.ts new file mode 100644 index 00000000000..455d477c962 --- /dev/null +++ b/apps/web/src/app/organizations/manage/member-dialog/index.ts @@ -0,0 +1,2 @@ +export * from "./member-dialog.component"; +export * from "./member-dialog.module"; diff --git a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.html b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.html new file mode 100644 index 00000000000..22ed712c76b --- /dev/null +++ b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.html @@ -0,0 +1,433 @@ +
+ + + {{ title }} + {{ + params.name + }} + {{ "revoked" | i18n }} + +
+ + + {{ "loading" | i18n }} + + + + +

{{ "inviteUserDesc" | i18n }}

+
+ + + {{ "inviteMultipleEmailDesc" | i18n: "20" }} +
+
+

+ {{ "userType" | i18n }} + + + +

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

+ {{ "permissions" | i18n }} +

+
+
+
+ +
+ + +
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+

+
+ {{ "accessControl" | i18n }} + + + +
+
+ + +
+

+
+
+ + +
+
+ + +
+
+ +
+ {{ "noCollectionsInList" | i18n }} +
+ + + + + + + + + + + + + + + + + +
 {{ "name" | i18n }}{{ "hidePasswords" | i18n }}{{ "readOnly" | i18n }}
+ + + {{ c.name }} + + + + +
+
+
+ Groups + Collections +
+
+
+ + +
+ + + +
+
+
+
diff --git a/apps/web/src/app/organizations/manage/user-add-edit.component.ts b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.ts similarity index 69% rename from apps/web/src/app/organizations/manage/user-add-edit.component.ts rename to apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.ts index 4a9d01efb5d..1f0a559af26 100644 --- a/apps/web/src/app/organizations/manage/user-add-edit.component.ts +++ b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.component.ts @@ -1,4 +1,6 @@ -import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; +import { Component, Inject, OnInit } from "@angular/core"; +import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { CollectionService } from "@bitwarden/common/abstractions/collection.service"; @@ -15,21 +17,35 @@ import { OrganizationUserUpdateRequest } from "@bitwarden/common/models/request/ import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request"; import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/models/view/collection.view"; +import { DialogService } from "@bitwarden/components"; + +export enum MemberDialogTab { + Role = 0, + Groups = 1, + Collections = 2, +} + +export interface MemberDialogParams { + name: string; + organizationId: string; + organizationUserId: string; + usesKeyConnector: boolean; + initialTab?: MemberDialogTab; +} + +export enum MemberDialogResult { + Saved = "saved", + Canceled = "canceled", + Deleted = "deleted", + Revoked = "revoked", + Restored = "restored", +} @Component({ - selector: "app-user-add-edit", - templateUrl: "user-add-edit.component.html", + selector: "app-member-dialog", + templateUrl: "member-dialog.component.html", }) -export class UserAddEditComponent implements OnInit { - @Input() name: string; - @Input() organizationUserId: string; - @Input() organizationId: string; - @Input() usesKeyConnector = false; - @Output() onSavedUser = new EventEmitter(); - @Output() onDeletedUser = new EventEmitter(); - @Output() onRevokedUser = new EventEmitter(); - @Output() onRestoredUser = new EventEmitter(); - +export class MemberDialogComponent implements OnInit { loading = true; editMode = false; isRevoked = false; @@ -40,10 +56,12 @@ export class UserAddEditComponent implements OnInit { showCustom = false; access: "all" | "selected" = "selected"; collections: CollectionView[] = []; - formPromise: Promise; - deletePromise: Promise; organizationUserType = OrganizationUserType; + protected tabIndex: MemberDialogTab; + // Stub, to be filled out in upcoming PRs + protected formGroup = this.formBuilder.group({}); + manageAllCollectionsCheckboxes = [ { id: "createNewCollections", @@ -80,24 +98,28 @@ export class UserAddEditComponent implements OnInit { } constructor( + @Inject(DIALOG_DATA) protected params: MemberDialogParams, + private dialogRef: DialogRef, private apiService: ApiService, private i18nService: I18nService, private collectionService: CollectionService, private platformUtilsService: PlatformUtilsService, - private logService: LogService + private logService: LogService, + private formBuilder: FormBuilder ) {} async ngOnInit() { - this.editMode = this.loading = this.organizationUserId != null; + this.editMode = this.loading = this.params.organizationUserId != null; + this.tabIndex = this.params.initialTab ?? MemberDialogTab.Role; await this.loadCollections(); if (this.editMode) { this.editMode = true; - this.title = this.i18nService.t("editUser"); + this.title = this.i18nService.t("editMember"); try { const user = await this.apiService.getOrganizationUser( - this.organizationId, - this.organizationUserId + this.params.organizationId, + this.params.organizationUserId ); this.access = user.accessAll ? "all" : "selected"; this.type = user.type; @@ -119,14 +141,14 @@ export class UserAddEditComponent implements OnInit { this.logService.error(e); } } else { - this.title = this.i18nService.t("inviteUser"); + this.title = this.i18nService.t("inviteMember"); } this.loading = false; } async loadCollections() { - const response = await this.apiService.getCollections(this.organizationId); + const response = await this.apiService.getCollections(this.params.organizationId); const collections = response.data.map( (r) => new Collection(new CollectionData(r as CollectionDetailsResponse)) ); @@ -162,7 +184,7 @@ export class UserAddEditComponent implements OnInit { } } - async submit() { + submit = async () => { let collections: SelectionReadOnlyRequest[] = null; if (this.access !== "all") { collections = this.collections @@ -180,9 +202,9 @@ export class UserAddEditComponent implements OnInit { request.permissions ?? new PermissionsApi(), request.type !== OrganizationUserType.Custom ); - this.formPromise = this.apiService.putOrganizationUser( - this.organizationId, - this.organizationUserId, + await this.apiService.putOrganizationUser( + this.params.organizationId, + this.params.organizationUserId, request ); } else { @@ -195,31 +217,31 @@ export class UserAddEditComponent implements OnInit { request.type !== OrganizationUserType.Custom ); request.collections = collections; - this.formPromise = this.apiService.postOrganizationUserInvite(this.organizationId, request); + await this.apiService.postOrganizationUserInvite(this.params.organizationId, request); } - await this.formPromise; + this.platformUtilsService.showToast( "success", null, - this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.name) + this.i18nService.t(this.editMode ? "editedUserId" : "invitedUsers", this.params.name) ); - this.onSavedUser.emit(); + this.close(MemberDialogResult.Saved); } catch (e) { this.logService.error(e); } - } + }; - async delete() { + delete = async () => { if (!this.editMode) { return; } - const message = this.usesKeyConnector + const message = this.params.usesKeyConnector ? "removeUserConfirmationKeyConnector" : "removeOrgUserConfirmation"; const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t(message), - this.i18nService.t("removeUserIdAccess", this.name), + this.i18nService.t("removeUserIdAccess", this.params.name), this.i18nService.t("yes"), this.i18nService.t("no"), "warning" @@ -229,30 +251,30 @@ export class UserAddEditComponent implements OnInit { } try { - this.deletePromise = this.apiService.deleteOrganizationUser( - this.organizationId, - this.organizationUserId + await this.apiService.deleteOrganizationUser( + this.params.organizationId, + this.params.organizationUserId ); - await this.deletePromise; + this.platformUtilsService.showToast( "success", null, - this.i18nService.t("removedUserId", this.name) + this.i18nService.t("removedUserId", this.params.name) ); - this.onDeletedUser.emit(); + this.close(MemberDialogResult.Deleted); } catch (e) { this.logService.error(e); } - } + }; - async revoke() { + revoke = async () => { if (!this.editMode) { return; } const confirmed = await this.platformUtilsService.showDialog( this.i18nService.t("revokeUserConfirmation"), - this.i18nService.t("revokeUserId", this.name), + this.i18nService.t("revokeUserId", this.params.name), this.i18nService.t("revokeAccess"), this.i18nService.t("cancel"), "warning" @@ -262,43 +284,63 @@ export class UserAddEditComponent implements OnInit { } try { - this.formPromise = this.apiService.revokeOrganizationUser( - this.organizationId, - this.organizationUserId + await this.apiService.revokeOrganizationUser( + this.params.organizationId, + this.params.organizationUserId ); - await this.formPromise; + this.platformUtilsService.showToast( "success", null, - this.i18nService.t("revokedUserId", this.name) + this.i18nService.t("revokedUserId", this.params.name) ); this.isRevoked = true; - this.onRevokedUser.emit(); + this.close(MemberDialogResult.Revoked); } catch (e) { this.logService.error(e); } - } + }; - async restore() { + restore = async () => { if (!this.editMode) { return; } try { - this.formPromise = this.apiService.restoreOrganizationUser( - this.organizationId, - this.organizationUserId + await this.apiService.restoreOrganizationUser( + this.params.organizationId, + this.params.organizationUserId ); - await this.formPromise; + this.platformUtilsService.showToast( "success", null, - this.i18nService.t("restoredUserId", this.name) + this.i18nService.t("restoredUserId", this.params.name) ); this.isRevoked = false; - this.onRestoredUser.emit(); + this.close(MemberDialogResult.Restored); } catch (e) { this.logService.error(e); } + }; + + protected async cancel() { + this.close(MemberDialogResult.Canceled); + } + + private close(result: MemberDialogResult) { + this.dialogRef.close(result); } } + +/** + * Strongly typed helper to open a UserDialog + * @param dialogService Instance of the dialog service that will be used to open the dialog + * @param config Configuration for the dialog + */ +export function openUserAddEditDialog( + dialogService: DialogService, + config: DialogConfig +) { + return dialogService.open(MemberDialogComponent, config); +} diff --git a/apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts new file mode 100644 index 00000000000..71c0046557d --- /dev/null +++ b/apps/web/src/app/organizations/manage/member-dialog/member-dialog.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from "@angular/core"; + +import { SharedModule } from "../../../shared/shared.module"; + +import { MemberDialogComponent } from "./member-dialog.component"; +import { NestedCheckboxComponent } from "./nested-checkbox.component"; + +@NgModule({ + declarations: [MemberDialogComponent, NestedCheckboxComponent], + imports: [SharedModule], + exports: [MemberDialogComponent], +}) +export class UserDialogModule {} diff --git a/apps/web/src/app/components/nested-checkbox.component.html b/apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.html similarity index 100% rename from apps/web/src/app/components/nested-checkbox.component.html rename to apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.html diff --git a/apps/web/src/app/components/nested-checkbox.component.ts b/apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.ts similarity index 100% rename from apps/web/src/app/components/nested-checkbox.component.ts rename to apps/web/src/app/organizations/manage/member-dialog/nested-checkbox.component.ts diff --git a/apps/web/src/app/organizations/manage/organization-manage.module.ts b/apps/web/src/app/organizations/manage/organization-manage.module.ts index 58c0ad161a4..7d2615904fb 100644 --- a/apps/web/src/app/organizations/manage/organization-manage.module.ts +++ b/apps/web/src/app/organizations/manage/organization-manage.module.ts @@ -4,9 +4,10 @@ import { NgModule } from "@angular/core"; import { SharedModule } from "../../shared"; import { EntityUsersComponent } from "./entity-users.component"; +import { UserDialogModule } from "./member-dialog"; @NgModule({ - imports: [SharedModule, ScrollingModule], + imports: [SharedModule, ScrollingModule, UserDialogModule], declarations: [EntityUsersComponent], exports: [EntityUsersComponent], }) diff --git a/apps/web/src/app/organizations/manage/people.component.ts b/apps/web/src/app/organizations/manage/people.component.ts index 8fb4e789a5f..358b06aa611 100644 --- a/apps/web/src/app/organizations/manage/people.component.ts +++ b/apps/web/src/app/organizations/manage/people.component.ts @@ -1,6 +1,6 @@ import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { combineLatest, concatMap, Subject, takeUntil } from "rxjs"; +import { combineLatest, concatMap, lastValueFrom, Subject, takeUntil } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; @@ -26,6 +26,7 @@ import { OrganizationUserConfirmRequest } from "@bitwarden/common/models/request import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { OrganizationUserBulkResponse } from "@bitwarden/common/models/response/organization-user-bulk.response"; import { OrganizationUserUserDetailsResponse } from "@bitwarden/common/models/response/organization-user.response"; +import { DialogService } from "@bitwarden/components"; import { BasePeopleComponent } from "../../common/base.people.component"; @@ -34,8 +35,8 @@ import { BulkRemoveComponent } from "./bulk/bulk-remove.component"; import { BulkRestoreRevokeComponent } from "./bulk/bulk-restore-revoke.component"; import { BulkStatusComponent } from "./bulk/bulk-status.component"; import { EntityEventsComponent } from "./entity-events.component"; +import { openUserAddEditDialog, MemberDialogResult } from "./member-dialog/member-dialog.component"; import { ResetPasswordComponent } from "./reset-password.component"; -import { UserAddEditComponent } from "./user-add-edit.component"; import { UserGroupsComponent } from "./user-groups.component"; @Component({ @@ -46,7 +47,6 @@ export class PeopleComponent extends BasePeopleComponent implements OnInit, OnDestroy { - @ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef; @ViewChild("groupsTemplate", { read: ViewContainerRef, static: true }) groupsModalRef: ViewContainerRef; @ViewChild("eventsTemplate", { read: ViewContainerRef, static: true }) @@ -93,7 +93,8 @@ export class PeopleComponent private syncService: SyncService, stateService: StateService, private organizationService: OrganizationService, - private organizationApiService: OrganizationApiServiceAbstraction + private organizationApiService: OrganizationApiServiceAbstraction, + private dialogService: DialogService ) { super( apiService, @@ -240,36 +241,26 @@ export class PeopleComponent } async edit(user: OrganizationUserUserDetailsResponse) { - const [modal] = await this.modalService.openViewRef( - UserAddEditComponent, - this.addEditModalRef, - (comp) => { - comp.name = this.userNamePipe.transform(user); - comp.organizationId = this.organizationId; - comp.organizationUserId = user != null ? user.id : null; - comp.usesKeyConnector = user?.usesKeyConnector; - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onSavedUser.subscribe(() => { - modal.close(); - this.load(); - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onDeletedUser.subscribe(() => { - modal.close(); - this.removeUser(user); - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onRevokedUser.subscribe(() => { - modal.close(); - this.load(); - }); - // eslint-disable-next-line rxjs-angular/prefer-takeuntil - comp.onRestoredUser.subscribe(() => { - modal.close(); - this.load(); - }); - } - ); + const dialog = openUserAddEditDialog(this.dialogService, { + data: { + name: this.userNamePipe.transform(user), + organizationId: this.organizationId, + organizationUserId: user != null ? user.id : null, + usesKeyConnector: user?.usesKeyConnector, + }, + }); + + const result = await lastValueFrom(dialog.closed); + switch (result) { + case MemberDialogResult.Deleted: + this.removeUser(user); + break; + case MemberDialogResult.Saved: + case MemberDialogResult.Revoked: + case MemberDialogResult.Restored: + this.load(); + break; + } } async groups(user: OrganizationUserUserDetailsResponse) { diff --git a/apps/web/src/app/organizations/manage/user-add-edit.component.html b/apps/web/src/app/organizations/manage/user-add-edit.component.html deleted file mode 100644 index 7d15eaa22f3..00000000000 --- a/apps/web/src/app/organizations/manage/user-add-edit.component.html +++ /dev/null @@ -1,438 +0,0 @@ - diff --git a/apps/web/src/app/shared/loose-components.module.ts b/apps/web/src/app/shared/loose-components.module.ts index 123a28a76d7..5db80888a06 100644 --- a/apps/web/src/app/shared/loose-components.module.ts +++ b/apps/web/src/app/shared/loose-components.module.ts @@ -19,7 +19,6 @@ import { UpdatePasswordComponent } from "../accounts/update-password.component"; import { UpdateTempPasswordComponent } from "../accounts/update-temp-password.component"; import { VerifyEmailTokenComponent } from "../accounts/verify-email-token.component"; import { VerifyRecoverDeleteComponent } from "../accounts/verify-recover-delete.component"; -import { NestedCheckboxComponent } from "../components/nested-checkbox.component"; import { OrganizationSwitcherComponent } from "../components/organization-switcher.component"; import { PasswordRepromptComponent } from "../components/password-reprompt.component"; import { PremiumBadgeComponent } from "../components/premium-badge.component"; @@ -40,7 +39,6 @@ import { EventsComponent as OrgEventsComponent } from "../organizations/manage/e import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component"; import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component"; import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component"; -import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component"; import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component"; import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component"; import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component"; @@ -172,7 +170,6 @@ import { SharedModule } from "./shared.module"; HintComponent, LockComponent, NavbarComponent, - NestedCheckboxComponent, OrganizationSwitcherComponent, OrgAddEditComponent, OrganizationLayoutComponent, @@ -194,7 +191,6 @@ import { SharedModule } from "./shared.module"; OrgReusedPasswordsReportComponent, OrgToolsComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserAddEditComponent, OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, GeneratorComponent, @@ -292,7 +288,6 @@ import { SharedModule } from "./shared.module"; HintComponent, LockComponent, NavbarComponent, - NestedCheckboxComponent, OrganizationSwitcherComponent, OrgAddEditComponent, OrganizationLayoutComponent, @@ -314,7 +309,6 @@ import { SharedModule } from "./shared.module"; OrgReusedPasswordsReportComponent, OrgToolsComponent, OrgUnsecuredWebsitesReportComponent, - OrgUserAddEditComponent, OrgUserConfirmComponent, OrgWeakPasswordsReportComponent, GeneratorComponent, diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 670d336c7e7..c96137a3e61 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -2434,11 +2434,11 @@ "deleteCollectionConfirmation": { "message": "Are you sure you want to delete this collection?" }, - "editUser": { - "message": "Edit user" + "editMember": { + "message": "Edit member" }, - "inviteUser": { - "message": "Invite user" + "inviteMember": { + "message": "Invite member" }, "inviteUserDesc": { "message": "Invite a new user to your organization by entering their Bitwarden account email address below. If they do not have a Bitwarden account already, they will be prompted to create a new account." diff --git a/libs/components/src/dialog/dialog/dialog.component.html b/libs/components/src/dialog/dialog/dialog.component.html index 271d923079e..f63f48343bc 100644 --- a/libs/components/src/dialog/dialog/dialog.component.html +++ b/libs/components/src/dialog/dialog/dialog.component.html @@ -1,6 +1,6 @@