mirror of
https://github.com/bitwarden/browser
synced 2025-12-16 08:13:42 +00:00
[EC-549] Member details collections tab (#4207)
* [EC-784] Introduce OrganizationUserService and abstraction * [EC-784] Move API response models into abstraction folder * [EC-784] Register OrganizationUserService in JsLib * [EC-784] Add OrganizationUserService to CLI Main * [EC-784] Move getOrganizationUser() - Move getOrganizationUser() implementation to OrganizationUserService - Update any references to the API service in the CLI and Web projects * [EC-784] Move getOrganizationUserGroups() * [EC-784] Move and rename getOrganizationUsers() * [EC-784] Move getOrganizationUserResetPasswordDetails() * [EC-784] Move OrganizationUser API request models into abstraction folder * [EC-784] Move postOrganizationUserInvite() * [EC-784] Move postOrganizationUserReinvite() * [EC-784] Move postManyOrganizationUserReinvite() Also tweak the signature to avoid exposing the API request model * [EC-784] Move postOrganizationUserAccept() * [EC-784] Move postOrganizationUserConfirm() * [EC-784] Move postOrganizationUsersPublicKey() Also modify signature to avoid exposing API request model * [EC-784] Move postOrganizationUserBulkConfirm() * [EC-784] Move putOrganizationUser() * [EC-784] Move putOrganizationUserGroups() * [EC-784] Update abstraction method definitions to use abstract keyword * [EC-784] Move putOrganizationUserResetPasswordEnrollment() * [EC-784] Move putOrganizationUserResetPassword() * [EC-784] Move deleteOrganizationUser() * [EC-784] Move deleteManyOrganizationUsers() * [EC-784] Move revokeOrganizationUser() * [EC-784] Move revokeManyOrganizationUsers() * [EC-784] Move restoreOrganizationUser() * [EC-784] Move restoreManyOrganizationUsers() * [EC-784] Move internal OrganizationUserBulkRequest model out of service abstraction * [EC-784] Rename organizationUser folder to organization-user * [EC-549] feat: add unconnected access selector * [EC-549] fix: old user group dialog not working * [EC-549] feat: add support for showing collections * [EC-549] feat: rewrite and implement saving and inviting * [EC-549] feat: implement support for access all collections * [EC-549] feat: remove collection form from role tab * [EC-549] chore: clean up comments * [EC-549] fix: revert changes to access selector story * [EC-549] feat: handle organizations that dont use groups Co-authored-by: Shane Melton <smelton@bitwarden.com>
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
export * from "./group/group.service";
|
export * from "./group/group.service";
|
||||||
export * from "./collection-admin.service";
|
export * from "./collection-admin.service";
|
||||||
|
export * from "./user-admin.service";
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||||
|
import {
|
||||||
|
OrganizationUserInviteRequest,
|
||||||
|
OrganizationUserUpdateRequest,
|
||||||
|
} from "@bitwarden/common/abstractions/organization-user/requests";
|
||||||
|
import { OrganizationUserDetailsResponse } from "@bitwarden/common/abstractions/organization-user/responses";
|
||||||
|
|
||||||
|
import { CoreOrganizationModule } from "../core-organization.module";
|
||||||
|
import { OrganizationUserAdminView } from "../views/user-admin-view";
|
||||||
|
|
||||||
|
@Injectable({ providedIn: CoreOrganizationModule })
|
||||||
|
export class UserAdminService {
|
||||||
|
constructor(private organizationUserService: OrganizationUserService) {}
|
||||||
|
|
||||||
|
async get(
|
||||||
|
organizationId: string,
|
||||||
|
organizationUserId: string
|
||||||
|
): Promise<OrganizationUserAdminView | undefined> {
|
||||||
|
const userResponse = await this.organizationUserService.getOrganizationUser(
|
||||||
|
organizationId,
|
||||||
|
organizationUserId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (userResponse == null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [view] = await this.decryptMany(organizationId, [userResponse]);
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(user: OrganizationUserAdminView): Promise<void> {
|
||||||
|
const request = new OrganizationUserUpdateRequest();
|
||||||
|
request.accessAll = user.accessAll;
|
||||||
|
request.permissions = user.permissions;
|
||||||
|
request.type = user.type;
|
||||||
|
request.collections = user.collections;
|
||||||
|
|
||||||
|
await this.organizationUserService.putOrganizationUser(user.organizationId, user.id, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async invite(emails: string[], user: OrganizationUserAdminView): Promise<void> {
|
||||||
|
const request = new OrganizationUserInviteRequest();
|
||||||
|
request.emails = emails;
|
||||||
|
request.accessAll = user.accessAll;
|
||||||
|
request.permissions = user.permissions;
|
||||||
|
request.type = user.type;
|
||||||
|
request.collections = user.collections;
|
||||||
|
|
||||||
|
await this.organizationUserService.postOrganizationUserInvite(user.organizationId, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async decryptMany(
|
||||||
|
organizationId: string,
|
||||||
|
users: OrganizationUserDetailsResponse[]
|
||||||
|
): Promise<OrganizationUserAdminView[]> {
|
||||||
|
const promises = users.map(async (u) => {
|
||||||
|
const view = new OrganizationUserAdminView();
|
||||||
|
|
||||||
|
view.id = u.id;
|
||||||
|
view.organizationId = organizationId;
|
||||||
|
view.userId = u.userId;
|
||||||
|
view.type = u.type;
|
||||||
|
view.status = u.status;
|
||||||
|
view.accessAll = u.accessAll;
|
||||||
|
view.permissions = u.permissions;
|
||||||
|
view.resetPasswordEnrolled = u.resetPasswordEnrolled;
|
||||||
|
view.collections = u.collections.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
hidePasswords: c.hidePasswords,
|
||||||
|
readOnly: c.readOnly,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return view;
|
||||||
|
});
|
||||||
|
|
||||||
|
return await Promise.all(promises);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from "./collection-access-selection.view";
|
export * from "./collection-access-selection.view";
|
||||||
export * from "./collection-admin.view";
|
export * from "./collection-admin.view";
|
||||||
export * from "./group.view";
|
export * from "./group.view";
|
||||||
|
export * from "./organization-user.view";
|
||||||
|
export * from "./user-admin-view";
|
||||||
|
|||||||
18
apps/web/src/app/organizations/core/views/user-admin-view.ts
Normal file
18
apps/web/src/app/organizations/core/views/user-admin-view.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||||
|
import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api";
|
||||||
|
|
||||||
|
import { CollectionAccessSelectionView } from "./collection-access-selection.view";
|
||||||
|
|
||||||
|
export class OrganizationUserAdminView {
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
type: OrganizationUserType;
|
||||||
|
status: OrganizationUserStatusType;
|
||||||
|
accessAll: boolean;
|
||||||
|
permissions: PermissionsApi;
|
||||||
|
resetPasswordEnrolled: boolean;
|
||||||
|
|
||||||
|
collections: CollectionAccessSelectionView[] = [];
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
<form [formGroup]="formGroup" [bitSubmit]="submit">
|
||||||
<bit-dialog dialogSize="large" [disablePadding]="!loading">
|
<bit-dialog [disablePadding]="!loading">
|
||||||
<span bitDialogTitle>
|
<span bitDialogTitle>
|
||||||
{{ title }}
|
{{ title }}
|
||||||
<span class="tw-text-sm tw-normal-case tw-text-muted" *ngIf="!loading && params.name">{{
|
<span class="tw-text-sm tw-normal-case tw-text-muted" *ngIf="!loading && params.name">{{
|
||||||
@@ -114,6 +114,7 @@
|
|||||||
id="userTypeCustom"
|
id="userTypeCustom"
|
||||||
[value]="organizationUserType.Custom"
|
[value]="organizationUserType.Custom"
|
||||||
[(ngModel)]="type"
|
[(ngModel)]="type"
|
||||||
|
[ngModelOptions]="{ standalone: true }"
|
||||||
[attr.disabled]="!canUseCustomPermissions || null"
|
[attr.disabled]="!canUseCustomPermissions || null"
|
||||||
/>
|
/>
|
||||||
<label class="form-check-label" for="userTypeCustom">
|
<label class="form-check-label" for="userTypeCustom">
|
||||||
@@ -283,112 +284,41 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<h3 class="mt-4 d-flex">
|
|
||||||
<div class="mb-3">
|
|
||||||
{{ "accessControl" | i18n }}
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
|
||||||
href="https://bitwarden.com/help/user-types-access-control/#access-control"
|
|
||||||
>
|
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
|
||||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
|
||||||
{{ "selectAll" | i18n }}
|
|
||||||
</button>
|
|
||||||
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
|
|
||||||
{{ "unselectAll" | i18n }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</h3>
|
|
||||||
<div class="form-group" [ngClass]="{ 'mb-0': access !== 'selected' }">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="access"
|
|
||||||
id="accessAll"
|
|
||||||
value="all"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
[(ngModel)]="access"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="accessAll">
|
|
||||||
{{ "userAccessAllItems" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="radio"
|
|
||||||
name="access"
|
|
||||||
id="accessSelected"
|
|
||||||
value="selected"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
[(ngModel)]="access"
|
|
||||||
/>
|
|
||||||
<label class="form-check-label" for="accessSelected">
|
|
||||||
{{ "userAccessSelectedCollections" | i18n }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ng-container *ngIf="access === 'selected'">
|
|
||||||
<div *ngIf="!collections || !collections.length">
|
|
||||||
{{ "noCollectionsInList" | i18n }}
|
|
||||||
</div>
|
|
||||||
<table
|
|
||||||
class="table table-hover table-list mb-0"
|
|
||||||
*ngIf="collections && collections.length"
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>{{ "name" | i18n }}</th>
|
|
||||||
<th width="100" class="text-center">{{ "hidePasswords" | i18n }}</th>
|
|
||||||
<th width="100" class="text-center">{{ "readOnly" | i18n }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr *ngFor="let c of collections; let i = index">
|
|
||||||
<td class="table-list-checkbox" (click)="check(c)">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
[(ngModel)]="$any(c).checked"
|
|
||||||
name="Collection[{{ i }}].Checked"
|
|
||||||
appStopProp
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td (click)="check(c)">
|
|
||||||
{{ c.name }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
[(ngModel)]="c.hidePasswords"
|
|
||||||
name="Collection[{{ i }}].HidePasswords"
|
|
||||||
[disabled]="!$any(c).checked"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[ngModelOptions]="{ standalone: true }"
|
|
||||||
[(ngModel)]="c.readOnly"
|
|
||||||
name="Collection[{{ i }}].ReadOnly"
|
|
||||||
[disabled]="!$any(c).checked"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
|
||||||
</bit-tab>
|
</bit-tab>
|
||||||
<bit-tab [label]="'groups' | i18n">Groups</bit-tab>
|
<bit-tab *ngIf="organization.useGroups" [label]="'groups' | i18n"> Groups </bit-tab>
|
||||||
<bit-tab [label]="'collections' | i18n">Collections</bit-tab>
|
<bit-tab [label]="'collections' | i18n">
|
||||||
|
<div *ngIf="organization.useGroups" class="tw-mb-6">
|
||||||
|
{{ "userPermissionOverrideHelper" | i18n }}
|
||||||
|
</div>
|
||||||
|
<div class="tw-mb-6">
|
||||||
|
<!-- TODO: Replace with bit-checkbox when feature has been merged into feature branch -->
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" formControlName="accessAllCollections" />
|
||||||
|
<span class="tw-bold"
|
||||||
|
>{{ "accessAllCollectionsDesc" | i18n }}
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
appA11yTitle="{{ 'learnMore' | i18n }}"
|
||||||
|
href="https://bitwarden.com/help/user-types-access-control/#access-control"
|
||||||
|
>
|
||||||
|
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div class="tw-text-muted">{{ "accessAllCollectionsHelp" | i18n }}</div>
|
||||||
|
</div>
|
||||||
|
<bit-access-selector
|
||||||
|
*ngIf="!accessAllCollections"
|
||||||
|
[permissionMode]="PermissionMode.Edit"
|
||||||
|
formControlName="access"
|
||||||
|
[showGroupColumn]="organization.useGroups"
|
||||||
|
[items]="accessItems"
|
||||||
|
[columnHeader]="'collection' | i18n"
|
||||||
|
[selectorLabelText]="'selectCollections' | i18n"
|
||||||
|
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||||
|
></bit-access-selector
|
||||||
|
></bit-tab>
|
||||||
</bit-tab-group>
|
</bit-tab-group>
|
||||||
</div>
|
</div>
|
||||||
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||||
|
|||||||
@@ -1,16 +1,13 @@
|
|||||||
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
import { Component, Inject, OnInit } from "@angular/core";
|
import { Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
import { FormBuilder } from "@angular/forms";
|
import { FormBuilder } from "@angular/forms";
|
||||||
|
import { combineLatest, of, shareReplay, Subject, switchMap, takeUntil } from "rxjs";
|
||||||
|
|
||||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
import { CollectionService } from "@bitwarden/common/abstractions/collection.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
import { LogService } from "@bitwarden/common/abstractions/log.service";
|
||||||
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
import { OrganizationUserService } from "@bitwarden/common/abstractions/organization-user/organization-user.service";
|
||||||
import {
|
|
||||||
OrganizationUserInviteRequest,
|
|
||||||
OrganizationUserUpdateRequest,
|
|
||||||
} from "@bitwarden/common/abstractions/organization-user/requests";
|
|
||||||
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/abstractions/organization/organization.service.abstraction";
|
||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||||
@@ -18,11 +15,28 @@ import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserTy
|
|||||||
import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api";
|
import { PermissionsApi } from "@bitwarden/common/models/api/permissions.api";
|
||||||
import { CollectionData } from "@bitwarden/common/models/data/collection.data";
|
import { CollectionData } from "@bitwarden/common/models/data/collection.data";
|
||||||
import { Collection } from "@bitwarden/common/models/domain/collection";
|
import { Collection } from "@bitwarden/common/models/domain/collection";
|
||||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
import { Organization } from "@bitwarden/common/models/domain/organization";
|
||||||
import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response";
|
import { CollectionDetailsResponse } from "@bitwarden/common/models/response/collection.response";
|
||||||
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||||
import { DialogService } from "@bitwarden/components";
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
CollectionAccessSelectionView,
|
||||||
|
CollectionAdminService,
|
||||||
|
GroupService,
|
||||||
|
GroupView,
|
||||||
|
OrganizationUserAdminView,
|
||||||
|
UserAdminService,
|
||||||
|
} from "../../../core";
|
||||||
|
import {
|
||||||
|
AccessItemType,
|
||||||
|
AccessItemValue,
|
||||||
|
AccessItemView,
|
||||||
|
convertToPermission,
|
||||||
|
convertToSelectionView,
|
||||||
|
PermissionMode,
|
||||||
|
} from "../../../shared/components/access-selector";
|
||||||
|
|
||||||
export enum MemberDialogTab {
|
export enum MemberDialogTab {
|
||||||
Role = 0,
|
Role = 0,
|
||||||
Groups = 1,
|
Groups = 1,
|
||||||
@@ -49,7 +63,7 @@ export enum MemberDialogResult {
|
|||||||
selector: "app-member-dialog",
|
selector: "app-member-dialog",
|
||||||
templateUrl: "member-dialog.component.html",
|
templateUrl: "member-dialog.component.html",
|
||||||
})
|
})
|
||||||
export class MemberDialogComponent implements OnInit {
|
export class MemberDialogComponent implements OnInit, OnDestroy {
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
editMode = false;
|
||||||
isRevoked = false;
|
isRevoked = false;
|
||||||
@@ -57,15 +71,22 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
emails: string;
|
emails: string;
|
||||||
type: OrganizationUserType = OrganizationUserType.User;
|
type: OrganizationUserType = OrganizationUserType.User;
|
||||||
permissions = new PermissionsApi();
|
permissions = new PermissionsApi();
|
||||||
showCustom = false;
|
|
||||||
access: "all" | "selected" = "selected";
|
access: "all" | "selected" = "selected";
|
||||||
collections: CollectionView[] = [];
|
collections: CollectionView[] = [];
|
||||||
organizationUserType = OrganizationUserType;
|
organizationUserType = OrganizationUserType;
|
||||||
canUseCustomPermissions: boolean;
|
canUseCustomPermissions: boolean;
|
||||||
|
PermissionMode = PermissionMode;
|
||||||
|
|
||||||
|
protected organization: Organization;
|
||||||
|
protected accessItems: AccessItemView[] = [];
|
||||||
protected tabIndex: MemberDialogTab;
|
protected tabIndex: MemberDialogTab;
|
||||||
// Stub, to be filled out in upcoming PRs
|
// Stub, to be filled out in upcoming PRs
|
||||||
protected formGroup = this.formBuilder.group({});
|
protected formGroup = this.formBuilder.group({
|
||||||
|
accessAllCollections: false,
|
||||||
|
access: [[] as AccessItemValue[]],
|
||||||
|
});
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
manageAllCollectionsCheckboxes = [
|
manageAllCollectionsCheckboxes = [
|
||||||
{
|
{
|
||||||
@@ -102,6 +123,10 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
return this.type === OrganizationUserType.Custom;
|
return this.type === OrganizationUserType.Custom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get accessAllCollections(): boolean {
|
||||||
|
return this.formGroup.value.accessAllCollections;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
@Inject(DIALOG_DATA) protected params: MemberDialogParams,
|
||||||
private dialogRef: DialogRef<MemberDialogResult>,
|
private dialogRef: DialogRef<MemberDialogResult>,
|
||||||
@@ -111,8 +136,12 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
private organizationUserService: OrganizationUserService,
|
private formBuilder: FormBuilder,
|
||||||
private formBuilder: FormBuilder
|
// TODO: We should really look into consolidating naming conventions for these services
|
||||||
|
private collectionAdminService: CollectionAdminService,
|
||||||
|
private groupService: GroupService,
|
||||||
|
private userService: UserAdminService,
|
||||||
|
private organizationUserService: OrganizationUserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@@ -154,7 +183,70 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
this.title = this.i18nService.t("inviteMember");
|
this.title = this.i18nService.t("inviteMember");
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
// ----------- New data fetching below ---------------
|
||||||
|
|
||||||
|
const organization$ = of(this.organizationService.get(this.params.organizationId)).pipe(
|
||||||
|
shareReplay({ refCount: true, bufferSize: 1 })
|
||||||
|
);
|
||||||
|
const groups$ = organization$.pipe(
|
||||||
|
switchMap((organization) => {
|
||||||
|
if (!organization.useGroups) {
|
||||||
|
return of([] as GroupView[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.groupService.getAll(this.params.organizationId);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const userGroups$ = this.params.organizationUserId
|
||||||
|
? of(
|
||||||
|
await this.organizationUserService.getOrganizationUserGroups(
|
||||||
|
this.params.organizationId,
|
||||||
|
this.params.organizationUserId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: of([]);
|
||||||
|
|
||||||
|
combineLatest({
|
||||||
|
organization: organization$,
|
||||||
|
collections: this.collectionAdminService.getAll(this.params.organizationId),
|
||||||
|
userDetails: this.params.organizationUserId
|
||||||
|
? this.userService.get(this.params.organizationId, this.params.organizationUserId)
|
||||||
|
: of(null),
|
||||||
|
groups: groups$,
|
||||||
|
userGroups: userGroups$,
|
||||||
|
})
|
||||||
|
.pipe(takeUntil(this.destroy$))
|
||||||
|
.subscribe(({ organization, collections, userDetails, groups, userGroups }) => {
|
||||||
|
this.organization = organization;
|
||||||
|
const collectionsFromGroups = groups
|
||||||
|
.filter((group) => userGroups.includes(group.id))
|
||||||
|
.flatMap((group) =>
|
||||||
|
group.collections.map((accessSelection) => {
|
||||||
|
const collection = collections.find((c) => c.id === accessSelection.id);
|
||||||
|
return { group, collection, accessSelection };
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.accessItems = [].concat(
|
||||||
|
collectionsFromGroups.map(({ collection, accessSelection, group }) =>
|
||||||
|
mapCollectionToAccessItemView(collection, accessSelection, group)
|
||||||
|
),
|
||||||
|
collections.map((c) => mapCollectionToAccessItemView(c))
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.params.organizationUserId) {
|
||||||
|
if (!userDetails) {
|
||||||
|
throw new Error("Could not find user to edit.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessSelections = mapToAccessSelections(userDetails);
|
||||||
|
this.formGroup.patchValue({
|
||||||
|
accessAllCollections: userDetails.accessAll,
|
||||||
|
access: accessSelections,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadCollections() {
|
async loadCollections() {
|
||||||
@@ -195,6 +287,10 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
submit = async () => {
|
submit = async () => {
|
||||||
|
if (this.formGroup.invalid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.canUseCustomPermissions && this.type === OrganizationUserType.Custom) {
|
if (!this.canUseCustomPermissions && this.type === OrganizationUserType.Custom) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
@@ -203,18 +299,27 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let collections: SelectionReadOnlyRequest[] = null;
|
|
||||||
if (this.access !== "all") {
|
|
||||||
collections = this.collections
|
|
||||||
.filter((c) => (c as any).checked)
|
|
||||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const userView = new OrganizationUserAdminView();
|
||||||
|
userView.id = this.params.organizationUserId;
|
||||||
|
userView.organizationId = this.params.organizationId;
|
||||||
|
userView.accessAll = this.accessAllCollections;
|
||||||
|
userView.type = this.type;
|
||||||
|
userView.permissions = this.setRequestPermissions(
|
||||||
|
userView.permissions ?? new PermissionsApi(),
|
||||||
|
userView.type !== OrganizationUserType.Custom
|
||||||
|
);
|
||||||
|
userView.collections = this.formGroup.controls.access.value
|
||||||
|
.filter((v) => v.type === AccessItemType.Collection)
|
||||||
|
.map(convertToSelectionView);
|
||||||
|
|
||||||
if (this.editMode) {
|
if (this.editMode) {
|
||||||
await this.updateUser(collections);
|
await this.userService.save(userView);
|
||||||
} else {
|
} else {
|
||||||
await this.inviteUser(collections);
|
userView.id = this.params.organizationUserId;
|
||||||
|
const emails = [...new Set(this.emails.trim().split(/\s*,\s*/))];
|
||||||
|
await this.userService.invite(emails, userView);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
@@ -325,6 +430,11 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
protected async cancel() {
|
protected async cancel() {
|
||||||
this.close(MemberDialogResult.Canceled);
|
this.close(MemberDialogResult.Canceled);
|
||||||
}
|
}
|
||||||
@@ -332,38 +442,35 @@ export class MemberDialogComponent implements OnInit {
|
|||||||
private close(result: MemberDialogResult) {
|
private close(result: MemberDialogResult) {
|
||||||
this.dialogRef.close(result);
|
this.dialogRef.close(result);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async updateUser(collections: SelectionReadOnlyRequest[]) {
|
function mapCollectionToAccessItemView(
|
||||||
const request = new OrganizationUserUpdateRequest();
|
collection: CollectionView,
|
||||||
request.accessAll = this.access === "all";
|
accessSelection?: CollectionAccessSelectionView,
|
||||||
request.type = this.type;
|
group?: GroupView
|
||||||
request.collections = collections;
|
): AccessItemView {
|
||||||
request.permissions = this.setRequestPermissions(
|
return {
|
||||||
request.permissions ?? new PermissionsApi(),
|
type: AccessItemType.Collection,
|
||||||
request.type !== OrganizationUserType.Custom
|
id: group ? `${collection.id}-${group.id}` : collection.id,
|
||||||
);
|
labelName: collection.name,
|
||||||
await this.organizationUserService.putOrganizationUser(
|
listName: collection.name,
|
||||||
this.params.organizationId,
|
readonly: accessSelection !== undefined,
|
||||||
this.params.organizationUserId,
|
readonlyPermission: accessSelection ? convertToPermission(accessSelection) : undefined,
|
||||||
request
|
viaGroupName: group?.name,
|
||||||
);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async inviteUser(collections: SelectionReadOnlyRequest[]) {
|
function mapToAccessSelections(user: OrganizationUserAdminView): AccessItemValue[] {
|
||||||
const request = new OrganizationUserInviteRequest();
|
if (user == undefined) {
|
||||||
request.emails = [...new Set(this.emails.trim().split(/\s*,\s*/))];
|
return [];
|
||||||
request.accessAll = this.access === "all";
|
|
||||||
request.type = this.type;
|
|
||||||
request.permissions = this.setRequestPermissions(
|
|
||||||
request.permissions ?? new PermissionsApi(),
|
|
||||||
request.type !== OrganizationUserType.Custom
|
|
||||||
);
|
|
||||||
request.collections = collections;
|
|
||||||
await this.organizationUserService.postOrganizationUserInvite(
|
|
||||||
this.params.organizationId,
|
|
||||||
request
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
return [].concat(
|
||||||
|
user.collections.map<AccessItemValue>((selection) => ({
|
||||||
|
id: selection.id,
|
||||||
|
type: AccessItemType.Collection,
|
||||||
|
permission: convertToPermission(selection),
|
||||||
|
}))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
import { SharedModule } from "../../../../shared/shared.module";
|
import { SharedOrganizationModule } from "../../../shared";
|
||||||
|
|
||||||
import { MemberDialogComponent } from "./member-dialog.component";
|
import { MemberDialogComponent } from "./member-dialog.component";
|
||||||
import { NestedCheckboxComponent } from "./nested-checkbox.component";
|
import { NestedCheckboxComponent } from "./nested-checkbox.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [MemberDialogComponent, NestedCheckboxComponent],
|
declarations: [MemberDialogComponent, NestedCheckboxComponent],
|
||||||
imports: [SharedModule],
|
imports: [SharedOrganizationModule],
|
||||||
exports: [MemberDialogComponent],
|
exports: [MemberDialogComponent],
|
||||||
})
|
})
|
||||||
export class UserDialogModule {}
|
export class UserDialogModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user