mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 22:03:36 +00:00
[EC-16] Implement new Groups Tab (#3563)
* [EC-16] Cleanup RxJS linting problems * [EC-16] Update Group tab to use table component and show collections. * [EC-16] Extract interface from GroupResponse and use it in the view * [EC-16] Remove heading underline * [EC-16] Cleanup i18n * [EC-16] More i18n cleanup * [EC-16] Fix bulk group request type name * [EC-16] Rename group details type * [EC-86] Clear collectionMap before populating it with new collections * [EC-86] Update initialization/loading logic to make better use of the Observable pattern * [EC-86] Make table cells use a pointer cursor * [EC-86] Use bitIconButton for row menu triggers * [EC-86] Refactor GroupDetailsRow interface to wrap GroupDetailsResponse. Remove response model interfaces. Cleanup GroupsComponent. * [EC-86] Add bit-badge-list component and tweak BadgeModule to support both the component and directive. Update mockI18nService to support templated strings. * [EC-86] Cleanup badge color and bitIconButton classes * [EC-86] Cleanup more styles * [EC-86] Add GroupApiService Add a new GroupApiService to replace Group Api calls in the ApiService. * [EC-86] Revisions for badge-list implementation. - Remove `| null` for maxItems according to ADR-0014 - Remove custom setter for items - Use ngOnChanges to update filteredItems - Fix sr-only tailwind class and show screen reader comma after last item if truncated. * [EC-86] Refactor badge-list module/component - Move the badge list component to its own module. - Extract badge list stories from badge stories. - Cleanup bade stories and module after refactor. * [EC-86] Refactor/rename GroupApiService - Re-name GroupApiService to GroupService as there is no need for a separate Api service (no sync or local data for admin services) - Add GroupView for use in the GroupService instead of raw API models - Update views to use GroupView instead of raw GroupResponse models * [EC-86] Refactor group API request models - Move organizationGroupBulkRequest to group requests folder - Fix relative imports in GroupService * [EC-86] Fix linting errors * Fix tab item text color Tab item text color broke after a merge from master and needs a fix to account for bootstrap styles in Web. * [EC-86] Rename new files using kebab-case * [EC-86] Fix group view file name * [EC-86] Fix group request/response file names * [EC-86] Cleanup badge stories per review suggestions * [EC-86] Use inline-flex for badge list container * [EC-86] Move GroupService and Views to Web org module - Move GroupService and GroupServiceAbstraction to Organization Module - Add GroupService provider to Organization Module - Move collection-add-edit.component, user-groups.component, group-add-edit.component, and groups.component into Organization Module as they now depend on GroupService - Remove moved components from Loose Component module * [EC-86] Fix Group table search Adds the id and name properties to GroupDetailsRow to support using the searchPipe (which cannot access nested values such as details.name for filtering). * [EC-86] Fix badge story controls * [EC-87] Edit Group Dialog (#3651) * [EC-87] Update the edit dialog to use content tabs * [EC-87] WIP FormListSelection abstract controller * [EC-87] WIP FormListSelection for members and collections * [EC-87] More WIP on FormListSelection * [EC-87] WIP Working FormSelectionList with initial value support * [EC-87] WIP SelectionList without FormControls and with i18n support for sorting * [EC-87] Final sorted SelectionList with FormArray support * [EC-87] Extract and document FormSelectionList * [EC-87] Functional edit group modal * [EC-87] Remove button icon padding for bitButton directives * [EC-87] Use new disablePadding attribute for Dialog component * [EC-87] Some more cleanup and finetuning * [EC-87] Move enum declaration to top * [EC-87] Remove inline style from access selector * [EC-87] Move Group components into Organization Module * [EC-87] Add MultiSelectModule to Shared Web module * [EC-87] Integrate AccessSelector component in GroupAddEdit modal - Remove duplicate permission / selection readonly helpers from GroupAddEdit component - Use access item views/values for collection and member lists - Replace access selector HTMl with the AccessSelector component * [EC-87] Update Group collections column to open Collection tab * [EC-87] Remove old FormSelectionList file * [EC-87] Fix missed file import changes after merge * [EC-87] Remove GroupAddEditComponent modal service registration Groups component is now using the DialogService which does not require explicit registration for lazy loaded components. * [EC-87] Use injected DIALOG_DATA for GroupAddEdit component - Add types for the GroupAddEdit dialog params, result, and tab indices - Add strongly typed helper method to open GroupAddEdit dialogs - Remove @Input()/@Output() properties. Replaced with the injected DIALOG_DATA params instead - Use dialogRef.close() and result type instead of event emitters * [EC-87] Rename collection tab type to collections * [EC-87] Refactor postGroup() and putGroup() from ApiService - Move postGroup() and putGroup() methods to GroupService - Remove postGroup() and putGroup() from ApiService - Move GroupResponse and GroupRequest into Web (from lib/common) * [EC-87] Remove required attribute * [EC-87] Use PascalCase for template Enums * [EC-87] Use group modal tab enum in template * [EC-87] Convert dialog result to promise * [EC-87] Refactor dialog positionStrategy - Add .top() to position strategy to allow clicking the backdrop to close the dialog - Move the positionStrategy option into the openGroupAddEditDialog helper * [EC-87] Remove [preserveContent] from tab group * [EC-87] Use new CL async actions - Update handlers to be arrow-functions - Remove old form and delete promises - Use [bitSubmit] directive on form - Use bitFormButton directive and [bitAction] for submit and delete buttons - Remove delete/spinner bwi icons as they are handled by the new async directives * [EC-87] Introduce CollectionAccessSelectionView Use a new view to replace the SelectionReadonlyResponse/Request classes. * [EC-87] Use new access selection view in GroupView - Change the collections type - Add members list to make the view more complete - Update the static fromResponse helper to properly map the GroupDetailsResponse to the new access selection view - Update access selector helpers to use new access selection view instead of response/request models * [EC-87] Update GroupService to have a single save() method that accepts a GroupView - Add save() method that checks for existing group id to determine which API method to use - Make post/put group methods private * [EC-87] Utilize the new save() method in the group modal * [EC-87] Use observables for fetching data - Introduce 3 observables for collections, members, and group details - Combine and subscribe to those observables in ngOnInit - Add destroy$ subject - Inject changeDetectorRef to handle quirk of patching the AccessSelector value before available items are set
This commit is contained in:
@@ -37,7 +37,7 @@
|
|||||||
</th>
|
</th>
|
||||||
<th bitCell id="roleColHeading" *ngIf="showMemberRoles">{{ "role" | i18n }}</th>
|
<th bitCell id="roleColHeading" *ngIf="showMemberRoles">{{ "role" | i18n }}</th>
|
||||||
<th bitCell id="groupColHeading" *ngIf="showGroupColumn">{{ "group" | i18n }}</th>
|
<th bitCell id="groupColHeading" *ngIf="showGroupColumn">{{ "group" | i18n }}</th>
|
||||||
<th bitCell style="width: 50px"></th>
|
<th bitCell class="tw-w-20"></th>
|
||||||
</tr>
|
</tr>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container body formArrayName="items">
|
<ng-container body formArrayName="items">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
import { OrganizationUserStatusType } from "@bitwarden/common/enums/organizationUserStatusType";
|
||||||
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
|
||||||
import { SelectionReadOnlyResponse } from "@bitwarden/common/models/response/selection-read-only.response";
|
|
||||||
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
import { SelectItemView } from "@bitwarden/components/src/multi-select/models/select-item-view";
|
||||||
|
|
||||||
|
import { CollectionAccessSelectionView } from "../../views/collection-access-selection.view";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Permission options that replace/correspond with readOnly and hidePassword server fields.
|
* Permission options that replace/correspond with readOnly and hidePassword server fields.
|
||||||
*/
|
*/
|
||||||
@@ -75,11 +75,11 @@ export type AccessItemValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts the older SelectionReadOnly interface to one of the new CollectionPermission values
|
* Converts the CollectionAccessSelectionView interface to one of the new CollectionPermission values
|
||||||
* for the dropdown in the AccessSelectorComponent
|
* for the dropdown in the AccessSelectorComponent
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
export const convertToPermission = (value: SelectionReadOnlyResponse) => {
|
export const convertToPermission = (value: CollectionAccessSelectionView) => {
|
||||||
if (value.readOnly) {
|
if (value.readOnly) {
|
||||||
return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View;
|
return value.hidePasswords ? CollectionPermission.ViewExceptPass : CollectionPermission.View;
|
||||||
} else {
|
} else {
|
||||||
@@ -88,16 +88,16 @@ export const convertToPermission = (value: SelectionReadOnlyResponse) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts an AccessItemValue back into a SelectionReadOnly class using the CollectionPermission
|
* Converts an AccessItemValue back into a CollectionAccessView class using the CollectionPermission
|
||||||
* to determine the values for `readOnly` and `hidePassword`
|
* to determine the values for `readOnly` and `hidePassword`
|
||||||
* @param value
|
* @param value
|
||||||
*/
|
*/
|
||||||
export const convertToSelectionReadOnly = (value: AccessItemValue) => {
|
export const convertToSelectionView = (value: AccessItemValue) => {
|
||||||
return new SelectionReadOnlyRequest(
|
return new CollectionAccessSelectionView({
|
||||||
value.id,
|
id: value.id,
|
||||||
readOnly(value.permission),
|
readOnly: readOnly(value.permission),
|
||||||
hidePassword(value.permission)
|
hidePasswords: hidePassword(value.permission),
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const readOnly = (perm: CollectionPermission) =>
|
const readOnly = (perm: CollectionPermission) =>
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import { EncString } from "@bitwarden/common/models/domain/enc-string";
|
|||||||
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-crypto-key";
|
||||||
import { CollectionRequest } from "@bitwarden/common/models/request/collection.request";
|
import { CollectionRequest } from "@bitwarden/common/models/request/collection.request";
|
||||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
||||||
import { GroupResponse } from "@bitwarden/common/models/response/group.response";
|
|
||||||
|
import { GroupServiceAbstraction } from "../services/abstractions/group";
|
||||||
|
import { GroupView } from "../views/group.view";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-collection-add-edit",
|
selector: "app-collection-add-edit",
|
||||||
@@ -31,7 +33,7 @@ export class CollectionAddEditComponent implements OnInit {
|
|||||||
title: string;
|
title: string;
|
||||||
name: string;
|
name: string;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
groups: GroupResponse[] = [];
|
groups: GroupView[] = [];
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
deletePromise: Promise<any>;
|
deletePromise: Promise<any>;
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ export class CollectionAddEditComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private groupApiService: GroupServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private cryptoService: CryptoService,
|
private cryptoService: CryptoService,
|
||||||
@@ -51,10 +54,8 @@ export class CollectionAddEditComponent implements OnInit {
|
|||||||
this.accessGroups = organization.useGroups;
|
this.accessGroups = organization.useGroups;
|
||||||
this.editMode = this.loading = this.collectionId != null;
|
this.editMode = this.loading = this.collectionId != null;
|
||||||
if (this.accessGroups) {
|
if (this.accessGroups) {
|
||||||
const groupsResponse = await this.apiService.getGroups(this.organizationId);
|
const groupsResponse = await this.groupApiService.getAll(this.organizationId);
|
||||||
this.groups = groupsResponse.data
|
this.groups = groupsResponse.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||||
.map((r) => r)
|
|
||||||
.sort(Utils.getSortFunction(this.i18nService, "name"));
|
|
||||||
}
|
}
|
||||||
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
this.orgKey = await this.cryptoService.getOrgKey(this.organizationId);
|
||||||
|
|
||||||
@@ -97,7 +98,7 @@ export class CollectionAddEditComponent implements OnInit {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
check(g: GroupResponse, select?: boolean) {
|
check(g: GroupView, select?: boolean) {
|
||||||
if (g.accessAll) {
|
if (g.accessAll) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
<div class="modal fade" role="dialog" aria-modal="true" aria-labelledby="groupAddEditTitle">
|
<form [formGroup]="groupForm" [bitSubmit]="submit">
|
||||||
<div class="modal-dialog modal-dialog-scrollable modal-lg" role="document">
|
<bit-dialog dialogSize="large" [disablePadding]="!loading">
|
||||||
<form
|
<span bitDialogTitle>
|
||||||
class="modal-content"
|
{{ title }}
|
||||||
#form
|
<span *ngIf="editMode" class="tw-text-sm tw-normal-case tw-text-muted">{{
|
||||||
(ngSubmit)="submit()"
|
group?.name
|
||||||
[appApiAction]="formPromise"
|
}}</span>
|
||||||
ngNativeValidate
|
</span>
|
||||||
>
|
<div bitDialogContent>
|
||||||
<div class="modal-header">
|
<div *ngIf="loading">
|
||||||
<h1 class="modal-title" id="groupAddEditTitle">{{ title }}</h1>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="close"
|
|
||||||
data-dismiss="modal"
|
|
||||||
appA11yTitle="{{ 'close' | i18n }}"
|
|
||||||
>
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" *ngIf="loading">
|
|
||||||
<i
|
<i
|
||||||
class="bwi bwi-spinner bwi-spin text-muted"
|
class="bwi bwi-spinner bwi-spin text-muted"
|
||||||
title="{{ 'loading' | i18n }}"
|
title="{{ 'loading' | i18n }}"
|
||||||
@@ -26,161 +15,77 @@
|
|||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" *ngIf="!loading">
|
|
||||||
<div class="form-group">
|
<bit-tab-group *ngIf="!loading" [selectedIndex]="tabIndex">
|
||||||
<label for="name">{{ "name" | i18n }}</label>
|
<bit-tab label="{{ 'groupInfo' | i18n }}">
|
||||||
<input
|
<bit-form-field>
|
||||||
id="name"
|
<bit-label>{{ "name" | i18n }}</bit-label>
|
||||||
class="form-control"
|
<input bitInput type="text" formControlName="name" />
|
||||||
type="text"
|
</bit-form-field>
|
||||||
name="Name"
|
<bit-form-field>
|
||||||
[(ngModel)]="name"
|
<bit-label>{{ "externalId" | i18n }}</bit-label>
|
||||||
required
|
<input bitInput type="text" formControlName="externalId" />
|
||||||
/>
|
<bit-hint>{{ "externalIdDesc" | i18n }}</bit-hint>
|
||||||
</div>
|
</bit-form-field>
|
||||||
<div class="form-group">
|
</bit-tab>
|
||||||
<label for="externalId">{{ "externalId" | i18n }}</label>
|
|
||||||
<input
|
<bit-tab label="{{ 'members' | i18n }}">
|
||||||
id="externalId"
|
<p>{{ "editGroupMembersDesc" | i18n }}</p>
|
||||||
class="form-control"
|
<bit-access-selector
|
||||||
type="text"
|
formControlName="members"
|
||||||
name="ExternalId"
|
[items]="members"
|
||||||
[(ngModel)]="externalId"
|
[showMemberRoles]="true"
|
||||||
/>
|
[permissionMode]="PermissionMode.Hidden"
|
||||||
<small class="form-text text-muted">{{ "externalIdDesc" | i18n }}</small>
|
[columnHeader]="'member' | i18n"
|
||||||
</div>
|
[selectorLabelText]="'selectMembers' | i18n"
|
||||||
<h3 class="mt-4 d-flex">
|
[emptySelectionText]="'noMembersAdded' | i18n"
|
||||||
<div class="mb-2">
|
></bit-access-selector>
|
||||||
{{ "accessControl" | i18n }}
|
</bit-tab>
|
||||||
<a
|
|
||||||
target="_blank"
|
<bit-tab label="{{ 'collections' | i18n }}">
|
||||||
rel="noopener"
|
<p>{{ "editGroupCollectionsDesc" | i18n }}</p>
|
||||||
appA11yTitle="{{ 'learnMore' | i18n }}"
|
<div class="tw-my-3">
|
||||||
href="https://bitwarden.com/help/user-types-access-control/#access-control"
|
<input type="checkbox" formControlName="accessAll" id="accessAll" />
|
||||||
>
|
<label class="tw-mb-0 tw-text-lg" for="accessAll">{{
|
||||||
<i class="bwi bwi-question-circle" aria-hidden="true"></i>
|
"accessAllCollectionsDesc" | i18n
|
||||||
</a>
|
}}</label>
|
||||||
|
<p class="tw-my-0 tw-text-muted">{{ "accessAllCollectionsHelp" | i18n }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto" *ngIf="access === 'selected' && collections && collections.length">
|
<ng-container *ngIf="!groupForm.value.accessAll">
|
||||||
<button type="button" (click)="selectAll(true)" class="btn btn-link btn-sm py-0">
|
<bit-access-selector
|
||||||
{{ "selectAll" | i18n }}
|
formControlName="collections"
|
||||||
</button>
|
[items]="collections"
|
||||||
<button type="button" (click)="selectAll(false)" class="btn btn-link btn-sm py-0">
|
[permissionMode]="PermissionMode.Edit"
|
||||||
{{ "unselectAll" | i18n }}
|
[columnHeader]="'collection' | i18n"
|
||||||
</button>
|
[selectorLabelText]="'selectCollections' | i18n"
|
||||||
</div>
|
[emptySelectionText]="'noCollectionsAdded' | i18n"
|
||||||
</h3>
|
></bit-access-selector>
|
||||||
<div class="form-group" [ngClass]="{ 'mb-0': access !== 'selected' }">
|
</ng-container>
|
||||||
<div class="form-check">
|
</bit-tab>
|
||||||
<input
|
</bit-tab-group>
|
||||||
class="form-check-input"
|
</div>
|
||||||
type="radio"
|
<div bitDialogFooter class="tw-flex tw-flex-row tw-gap-2">
|
||||||
name="access"
|
<button bitButton buttonType="primary" bitFormButton type="submit">
|
||||||
id="accessAll"
|
{{ "save" | i18n }}
|
||||||
value="all"
|
</button>
|
||||||
[(ngModel)]="access"
|
<button
|
||||||
/>
|
bitButton
|
||||||
<label class="form-check-label" for="accessAll">
|
buttonType="secondary"
|
||||||
{{ "groupAccessAllItems" | i18n }}
|
type="button"
|
||||||
</label>
|
bitDialogClose
|
||||||
</div>
|
[bit-dialog-close]="ResultType.Canceled"
|
||||||
<div class="form-check">
|
>
|
||||||
<input
|
{{ "cancel" | i18n }}
|
||||||
class="form-check-input"
|
</button>
|
||||||
type="radio"
|
<button
|
||||||
name="access"
|
class="tw-ml-auto"
|
||||||
id="accessSelected"
|
type="button"
|
||||||
value="selected"
|
buttonType="danger"
|
||||||
[(ngModel)]="access"
|
bitIconButton="bwi-trash"
|
||||||
/>
|
bitFormButton
|
||||||
<label class="form-check-label" for="accessSelected">
|
[bitAction]="delete"
|
||||||
{{ "groupAccessSelectedCollections" | i18n }}
|
[appA11yTitle]="'delete' | i18n"
|
||||||
</label>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</bit-dialog>
|
||||||
<ng-container *ngIf="access === 'selected'">
|
</form>
|
||||||
<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"
|
|
||||||
[(ngModel)]="c.checked"
|
|
||||||
name="Collection[{{ i }}].Checked"
|
|
||||||
appStopProp
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td (click)="check(c)">
|
|
||||||
{{ c.name }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[(ngModel)]="c.hidePasswords"
|
|
||||||
name="Collection[{{ i }}].HidePasswords"
|
|
||||||
[disabled]="!c.checked"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
[(ngModel)]="c.readOnly"
|
|
||||||
name="Collection[{{ i }}].ReadOnly"
|
|
||||||
[disabled]="!c.checked"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="submit" class="btn btn-primary btn-submit" [disabled]="form.loading">
|
|
||||||
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
|
|
||||||
<span>{{ "save" | i18n }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">
|
|
||||||
{{ "cancel" | i18n }}
|
|
||||||
</button>
|
|
||||||
<div class="ml-auto">
|
|
||||||
<button
|
|
||||||
#deleteBtn
|
|
||||||
type="button"
|
|
||||||
(click)="delete()"
|
|
||||||
class="btn btn-outline-danger"
|
|
||||||
appA11yTitle="{{ 'delete' | i18n }}"
|
|
||||||
*ngIf="editMode"
|
|
||||||
[disabled]="deleteBtn.loading"
|
|
||||||
[appApiAction]="deletePromise"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-trash bwi-lg bwi-fw"
|
|
||||||
[hidden]="deleteBtn.loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
<i
|
|
||||||
class="bwi bwi-spinner bwi-spin bwi-lg bwi-fw"
|
|
||||||
[hidden]="!deleteBtn.loading"
|
|
||||||
aria-hidden="true"
|
|
||||||
title="{{ 'loading' | i18n }}"
|
|
||||||
></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core";
|
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
|
||||||
|
import { Overlay } from "@angular/cdk/overlay";
|
||||||
|
import { ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from "@angular/core";
|
||||||
|
import { FormBuilder, FormControl, Validators } from "@angular/forms";
|
||||||
|
import { catchError, combineLatest, from, map, of, 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";
|
||||||
@@ -7,127 +11,267 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
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 { GroupRequest } from "@bitwarden/common/models/request/group.request";
|
|
||||||
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
|
||||||
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 { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AccessItemType,
|
||||||
|
AccessItemValue,
|
||||||
|
AccessItemView,
|
||||||
|
convertToPermission,
|
||||||
|
convertToSelectionView,
|
||||||
|
PermissionMode,
|
||||||
|
} from "../components/access-selector";
|
||||||
|
import { GroupServiceAbstraction } from "../services/abstractions/group";
|
||||||
|
import { GroupView } from "../views/group.view";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indices for the available tabs in the dialog
|
||||||
|
*/
|
||||||
|
export enum GroupAddEditTabType {
|
||||||
|
Info = 0,
|
||||||
|
Members = 1,
|
||||||
|
Collections = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GroupAddEditDialogParams {
|
||||||
|
/**
|
||||||
|
* ID of the organization the group belongs to
|
||||||
|
*/
|
||||||
|
organizationId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional ID of the group being modified
|
||||||
|
*/
|
||||||
|
groupId?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tab to open when the dialog is open.
|
||||||
|
* Defaults to Group Info
|
||||||
|
*/
|
||||||
|
initialTab?: GroupAddEditTabType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum GroupAddEditDialogResultType {
|
||||||
|
Saved = "saved",
|
||||||
|
Canceled = "canceled",
|
||||||
|
Deleted = "deleted",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strongly typed helper to open a groupAddEditDialog
|
||||||
|
* @param dialogService Instance of the dialog service that will be used to open the dialog
|
||||||
|
* @param overlay Instance of the CDK Overlay service
|
||||||
|
* @param config Configuration for the dialog
|
||||||
|
*/
|
||||||
|
export const openGroupAddEditDialog = (
|
||||||
|
dialogService: DialogService,
|
||||||
|
overlay: Overlay,
|
||||||
|
config: DialogConfig<GroupAddEditDialogParams>
|
||||||
|
) => {
|
||||||
|
return dialogService.open<GroupAddEditDialogResultType, GroupAddEditDialogParams>(
|
||||||
|
GroupAddEditComponent,
|
||||||
|
{
|
||||||
|
positionStrategy: overlay.position().global().centerHorizontally().top(),
|
||||||
|
...config,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-group-add-edit",
|
selector: "app-group-add-edit",
|
||||||
templateUrl: "group-add-edit.component.html",
|
templateUrl: "group-add-edit.component.html",
|
||||||
})
|
})
|
||||||
export class GroupAddEditComponent implements OnInit {
|
export class GroupAddEditComponent implements OnInit, OnDestroy {
|
||||||
@Input() groupId: string;
|
protected PermissionMode = PermissionMode;
|
||||||
@Input() organizationId: string;
|
protected ResultType = GroupAddEditDialogResultType;
|
||||||
@Output() onSavedGroup = new EventEmitter();
|
|
||||||
@Output() onDeletedGroup = new EventEmitter();
|
|
||||||
|
|
||||||
|
tabIndex: GroupAddEditTabType;
|
||||||
loading = true;
|
loading = true;
|
||||||
editMode = false;
|
editMode = false;
|
||||||
title: string;
|
title: string;
|
||||||
name: string;
|
collections: AccessItemView[] = [];
|
||||||
externalId: string;
|
members: AccessItemView[] = [];
|
||||||
access: "all" | "selected" = "selected";
|
group: GroupView;
|
||||||
collections: CollectionView[] = [];
|
|
||||||
formPromise: Promise<any>;
|
groupForm = this.formBuilder.group({
|
||||||
deletePromise: Promise<any>;
|
accessAll: new FormControl(false),
|
||||||
|
name: new FormControl("", Validators.required),
|
||||||
|
externalId: new FormControl(""),
|
||||||
|
members: new FormControl<AccessItemValue[]>([]),
|
||||||
|
collections: new FormControl<AccessItemValue[]>([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
get groupId(): string | undefined {
|
||||||
|
return this.params.groupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
get organizationId(): string {
|
||||||
|
return this.params.organizationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
|
||||||
|
private get orgCollections$() {
|
||||||
|
return from(this.apiService.getCollections(this.organizationId)).pipe(
|
||||||
|
switchMap((response) => {
|
||||||
|
return from(
|
||||||
|
this.collectionService.decryptMany(
|
||||||
|
response.data.map(
|
||||||
|
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
map((collections) =>
|
||||||
|
collections.map<AccessItemView>((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
type: AccessItemType.Collection,
|
||||||
|
labelName: c.name,
|
||||||
|
listName: c.name,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get orgMembers$() {
|
||||||
|
return from(this.apiService.getOrganizationUsers(this.organizationId)).pipe(
|
||||||
|
map((response) =>
|
||||||
|
response.data.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
type: AccessItemType.Member,
|
||||||
|
email: m.email,
|
||||||
|
role: m.type,
|
||||||
|
listName: m.name?.length > 0 ? `${m.name} (${m.email})` : m.email,
|
||||||
|
labelName: m.name || m.email,
|
||||||
|
status: m.status,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private get groupDetails$() {
|
||||||
|
if (!this.editMode) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest([
|
||||||
|
this.groupService.get(this.organizationId, this.groupId),
|
||||||
|
this.apiService.getGroupUsers(this.organizationId, this.groupId),
|
||||||
|
]).pipe(
|
||||||
|
map(([groupView, users]) => {
|
||||||
|
groupView.members = users;
|
||||||
|
return groupView;
|
||||||
|
}),
|
||||||
|
catchError((e: unknown) => {
|
||||||
|
if (e instanceof ErrorResponse) {
|
||||||
|
this.logService.error(e.message);
|
||||||
|
} else {
|
||||||
|
this.logService.error(e.toString());
|
||||||
|
}
|
||||||
|
return of(undefined);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@Inject(DIALOG_DATA) private params: GroupAddEditDialogParams,
|
||||||
|
private dialogRef: DialogRef<GroupAddEditDialogResultType>,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private groupService: GroupServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private collectionService: CollectionService,
|
private collectionService: CollectionService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService
|
private logService: LogService,
|
||||||
) {}
|
private formBuilder: FormBuilder,
|
||||||
|
private changeDetectorRef: ChangeDetectorRef
|
||||||
|
) {
|
||||||
|
this.tabIndex = params.initialTab ?? GroupAddEditTabType.Info;
|
||||||
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
ngOnInit() {
|
||||||
this.editMode = this.loading = this.groupId != null;
|
this.editMode = this.loading = this.groupId != null;
|
||||||
await this.loadCollections();
|
this.title = this.i18nService.t(this.editMode ? "editGroup" : "addGroup");
|
||||||
|
|
||||||
if (this.editMode) {
|
combineLatest([this.orgCollections$, this.orgMembers$, this.groupDetails$])
|
||||||
this.editMode = true;
|
.pipe(takeUntil(this.destroy$))
|
||||||
this.title = this.i18nService.t("editGroup");
|
.subscribe(([collections, members, group]) => {
|
||||||
try {
|
this.collections = collections;
|
||||||
const group = await this.apiService.getGroupDetails(this.organizationId, this.groupId);
|
this.members = members;
|
||||||
this.access = group.accessAll ? "all" : "selected";
|
this.group = group;
|
||||||
this.name = group.name;
|
|
||||||
this.externalId = group.externalId;
|
if (this.group != undefined) {
|
||||||
if (group.collections != null && this.collections != null) {
|
// Must detect changes so that AccessSelector @Inputs() are aware of the latest
|
||||||
group.collections.forEach((s) => {
|
// collections/members set above, otherwise no selected values will be patched below
|
||||||
const collection = this.collections.filter((c) => c.id === s.id);
|
this.changeDetectorRef.detectChanges();
|
||||||
if (collection != null && collection.length > 0) {
|
|
||||||
(collection[0] as any).checked = true;
|
this.groupForm.patchValue({
|
||||||
collection[0].readOnly = s.readOnly;
|
name: this.group.name,
|
||||||
collection[0].hidePasswords = s.hidePasswords;
|
externalId: this.group.externalId,
|
||||||
}
|
accessAll: this.group.accessAll,
|
||||||
|
members: this.group.members.map((m) => ({
|
||||||
|
id: m,
|
||||||
|
type: AccessItemType.Member,
|
||||||
|
})),
|
||||||
|
collections: this.group.collections.map((gc) => ({
|
||||||
|
id: gc.id,
|
||||||
|
type: AccessItemType.Collection,
|
||||||
|
permission: convertToPermission(gc),
|
||||||
|
})),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
this.logService.error(e);
|
this.loading = false;
|
||||||
}
|
});
|
||||||
} else {
|
}
|
||||||
this.title = this.i18nService.t("addGroup");
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = async () => {
|
||||||
|
if (this.groupForm.invalid) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loading = false;
|
const groupView = new GroupView();
|
||||||
}
|
groupView.id = this.groupId;
|
||||||
|
groupView.organizationId = this.organizationId;
|
||||||
|
|
||||||
async loadCollections() {
|
const formValue = this.groupForm.value;
|
||||||
const response = await this.apiService.getCollections(this.organizationId);
|
groupView.name = formValue.name;
|
||||||
const collections = response.data.map(
|
groupView.externalId = formValue.externalId;
|
||||||
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse))
|
groupView.accessAll = formValue.accessAll;
|
||||||
);
|
groupView.members = formValue.members?.map((m) => m.id) ?? [];
|
||||||
this.collections = await this.collectionService.decryptMany(collections);
|
|
||||||
}
|
|
||||||
|
|
||||||
check(c: CollectionView, select?: boolean) {
|
if (!groupView.accessAll) {
|
||||||
(c as any).checked = select == null ? !(c as any).checked : select;
|
groupView.collections = formValue.collections.map((c) => convertToSelectionView(c));
|
||||||
if (!(c as any).checked) {
|
|
||||||
c.readOnly = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
selectAll(select: boolean) {
|
|
||||||
this.collections.forEach((c) => this.check(c, select));
|
|
||||||
}
|
|
||||||
|
|
||||||
async submit() {
|
|
||||||
const request = new GroupRequest();
|
|
||||||
request.name = this.name;
|
|
||||||
request.externalId = this.externalId;
|
|
||||||
request.accessAll = this.access === "all";
|
|
||||||
if (!request.accessAll) {
|
|
||||||
request.collections = this.collections
|
|
||||||
.filter((c) => (c as any).checked)
|
|
||||||
.map((c) => new SelectionReadOnlyRequest(c.id, !!c.readOnly, !!c.hidePasswords));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (this.editMode) {
|
await this.groupService.save(groupView);
|
||||||
this.formPromise = this.apiService.putGroup(this.organizationId, this.groupId, request);
|
|
||||||
} else {
|
|
||||||
this.formPromise = this.apiService.postGroup(this.organizationId, request);
|
|
||||||
}
|
|
||||||
await this.formPromise;
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
this.i18nService.t(this.editMode ? "editedGroupId" : "createdGroupId", this.name)
|
this.i18nService.t(this.editMode ? "editedGroupId" : "createdGroupId", formValue.name)
|
||||||
);
|
);
|
||||||
this.onSavedGroup.emit();
|
this.dialogRef.close(GroupAddEditDialogResultType.Saved);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async delete() {
|
delete = async () => {
|
||||||
if (!this.editMode) {
|
if (!this.editMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t("deleteGroupConfirmation"),
|
this.i18nService.t("deleteGroupConfirmation"),
|
||||||
this.name,
|
this.group.name,
|
||||||
this.i18nService.t("yes"),
|
this.i18nService.t("yes"),
|
||||||
this.i18nService.t("no"),
|
this.i18nService.t("no"),
|
||||||
"warning"
|
"warning"
|
||||||
@@ -137,16 +281,16 @@ export class GroupAddEditComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.deletePromise = this.apiService.deleteGroup(this.organizationId, this.groupId);
|
await this.groupService.delete(this.organizationId, this.groupId);
|
||||||
await this.deletePromise;
|
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
this.i18nService.t("deletedGroupId", this.name)
|
this.i18nService.t("deletedGroupId", this.group.name)
|
||||||
);
|
);
|
||||||
this.onDeletedGroup.emit();
|
this.dialogRef.close(GroupAddEditDialogResultType.Deleted);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
<div class="container page-content">
|
<div class="container page-content">
|
||||||
<div class="page-header d-flex">
|
<div class="tw-mb-4 tw-flex">
|
||||||
<h1>{{ "groups" | i18n }}</h1>
|
<h1>{{ "groups" | i18n }}</h1>
|
||||||
<div class="ml-auto d-flex">
|
<div class="tw-ml-auto tw-flex tw-items-center">
|
||||||
<div>
|
<div class="tw-mr-2">
|
||||||
<label class="sr-only" for="search">{{ "search" | i18n }}</label>
|
<label class="sr-only">{{ "search" | i18n }}</label>
|
||||||
<input
|
<div class="tw-flex tw-items-center">
|
||||||
type="search"
|
<i class="bwi bwi-search bwi-fw tw-z-20 -tw-mr-7 tw-text-muted" aria-hidden="true"></i>
|
||||||
class="form-control form-control-sm"
|
<input
|
||||||
id="search"
|
bitInput
|
||||||
placeholder="{{ 'search' | i18n }}"
|
type="search"
|
||||||
[(ngModel)]="searchText"
|
placeholder="{{ 'search' | i18n }}"
|
||||||
/>
|
class="tw-rounded-l tw-pl-9"
|
||||||
|
[(ngModel)]="searchText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary ml-3" (click)="add()">
|
<button bitButton type="button" buttonType="primary" (click)="add()">
|
||||||
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
<i class="bwi bwi-plus bwi-fw" aria-hidden="true"></i>
|
||||||
{{ "newGroup" | i18n }}
|
{{ "newGroup" | i18n }}
|
||||||
</button>
|
</button>
|
||||||
@@ -26,54 +29,95 @@
|
|||||||
></i>
|
></i>
|
||||||
<span class="sr-only">{{ "loading" | i18n }}</span>
|
<span class="sr-only">{{ "loading" | i18n }}</span>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-container
|
<ng-container *ngIf="!loading && visibleGroups">
|
||||||
*ngIf="
|
<p *ngIf="!visibleGroups.length">{{ "noGroupsInList" | i18n }}</p>
|
||||||
!loading &&
|
<bit-table
|
||||||
(isPaging() ? pagedGroups : (groups | search: searchText:'name':'id')) as searchedGroups
|
*ngIf="visibleGroups.length"
|
||||||
"
|
infinite-scroll
|
||||||
>
|
|
||||||
<p *ngIf="!searchedGroups.length">{{ "noGroupsInList" | i18n }}</p>
|
|
||||||
<table
|
|
||||||
class="table table-hover table-list"
|
|
||||||
*ngIf="searchedGroups.length"
|
|
||||||
infiniteScroll
|
|
||||||
[infiniteScrollDistance]="1"
|
[infiniteScrollDistance]="1"
|
||||||
[infiniteScrollDisabled]="!isPaging()"
|
[infiniteScrollDisabled]="!isPaging()"
|
||||||
(scrolled)="loadMore()"
|
(scrolled)="loadMore()"
|
||||||
>
|
>
|
||||||
<tbody>
|
<ng-container header>
|
||||||
<tr *ngFor="let g of searchedGroups">
|
<tr>
|
||||||
<td>
|
<th bitCell class="tw-w-20">
|
||||||
<a href="#" appStopClick (click)="edit(g)">{{ g.name }}</a>
|
<input
|
||||||
</td>
|
type="checkbox"
|
||||||
<td class="table-list-options">
|
class="tw-mr-2"
|
||||||
<div class="dropdown" appListDropdown>
|
(change)="toggleAllVisible($event)"
|
||||||
<button
|
id="selectAll"
|
||||||
class="btn btn-outline-secondary dropdown-toggle"
|
/>
|
||||||
type="button"
|
<label class="tw-mb-0 !tw-font-bold !tw-text-muted" for="selectAll">{{
|
||||||
data-toggle="dropdown"
|
"all" | i18n
|
||||||
aria-haspopup="true"
|
}}</label>
|
||||||
aria-expanded="false"
|
</th>
|
||||||
appA11yTitle="{{ 'options' | i18n }}"
|
<th bitCell>{{ "name" | i18n }}</th>
|
||||||
>
|
<th bitCell>{{ "collections" | i18n }}</th>
|
||||||
<i class="bwi bwi-cog bwi-lg" aria-hidden="true"></i>
|
<th bitCell class="tw-w-10">
|
||||||
|
<button
|
||||||
|
[bitMenuTriggerFor]="headerMenu"
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<bit-menu #headerMenu>
|
||||||
|
<button type="button" bitMenuItem (click)="deleteAllSelected()">
|
||||||
|
<span class="tw-text-danger"
|
||||||
|
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
<div class="dropdown-menu dropdown-menu-right">
|
</bit-menu>
|
||||||
<a class="dropdown-item" href="#" appStopClick (click)="users(g)">
|
</th>
|
||||||
<i class="bwi bwi-fw bwi-users" aria-hidden="true"></i>
|
</tr>
|
||||||
{{ "users" | i18n }}
|
</ng-container>
|
||||||
</a>
|
<ng-container body>
|
||||||
<a class="dropdown-item text-danger" href="#" appStopClick (click)="delete(g)">
|
<tr bitRow *ngFor="let g of visibleGroups">
|
||||||
<i class="bwi bwi-fw bwi-trash" aria-hidden="true"></i>
|
<td bitCell (click)="check(g)" class="tw-cursor-pointer">
|
||||||
{{ "delete" | i18n }}
|
<input type="checkbox" [(ngModel)]="g.checked" />
|
||||||
</a>
|
</td>
|
||||||
</div>
|
<td bitCell class="tw-cursor-pointer tw-font-bold" (click)="edit(g)">
|
||||||
</div>
|
<button (click)="edit(g)" bitLink>
|
||||||
|
{{ g.details.name }}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td bitCell (click)="edit(g, ModalTabType.Collections)" class="tw-cursor-pointer">
|
||||||
|
<bit-badge-list
|
||||||
|
[items]="g.collectionNames"
|
||||||
|
[maxItems]="2"
|
||||||
|
badgeType="secondary"
|
||||||
|
></bit-badge-list>
|
||||||
|
</td>
|
||||||
|
<td bitCell>
|
||||||
|
<button
|
||||||
|
[bitMenuTriggerFor]="rowMenu"
|
||||||
|
type="button"
|
||||||
|
bitIconButton="bwi-ellipsis-v"
|
||||||
|
size="small"
|
||||||
|
appA11yTitle="{{ 'options' | i18n }}"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<bit-menu #rowMenu>
|
||||||
|
<button type="button" bitMenuItem (click)="edit(g)">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-pencil-square"></i> {{ "editInfo" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Members)">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-user"></i> {{ "members" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="edit(g, ModalTabType.Collections)">
|
||||||
|
<i aria-hidden="true" class="bwi bwi-collection"></i> {{ "collections" | i18n }}
|
||||||
|
</button>
|
||||||
|
<button type="button" bitMenuItem (click)="delete(g)">
|
||||||
|
<span class="tw-text-danger"
|
||||||
|
><i aria-hidden="true" class="bwi bwi-trash"></i> {{ "delete" | i18n }}</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</bit-menu>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</ng-container>
|
||||||
</table>
|
</bit-table>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<ng-template #addEdit></ng-template>
|
<ng-template #addEdit></ng-template>
|
||||||
<ng-template #usersTemplate></ng-template>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,69 +1,195 @@
|
|||||||
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
import { Overlay } from "@angular/cdk/overlay";
|
||||||
|
import { Component, OnDestroy, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
|
||||||
import { ActivatedRoute } from "@angular/router";
|
import { ActivatedRoute } from "@angular/router";
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
from,
|
||||||
|
lastValueFrom,
|
||||||
|
map,
|
||||||
|
Subject,
|
||||||
|
switchMap,
|
||||||
|
takeUntil,
|
||||||
|
tap,
|
||||||
|
} from "rxjs";
|
||||||
import { first } from "rxjs/operators";
|
import { first } from "rxjs/operators";
|
||||||
|
|
||||||
|
import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe";
|
||||||
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
import { ModalService } from "@bitwarden/angular/services/modal.service";
|
||||||
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 { 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 { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService } from "@bitwarden/common/abstractions/search.service";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
import { GroupResponse } from "@bitwarden/common/models/response/group.response";
|
import { CollectionData } from "@bitwarden/common/models/data/collection.data";
|
||||||
|
import { Collection } from "@bitwarden/common/models/domain/collection";
|
||||||
|
import {
|
||||||
|
CollectionDetailsResponse,
|
||||||
|
CollectionResponse,
|
||||||
|
} from "@bitwarden/common/models/response/collection.response";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
import { CollectionView } from "@bitwarden/common/models/view/collection.view";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
|
|
||||||
import { EntityUsersComponent } from "./entity-users.component";
|
import { GroupServiceAbstraction } from "../services/abstractions/group";
|
||||||
import { GroupAddEditComponent } from "./group-add-edit.component";
|
import { GroupView } from "../views/group.view";
|
||||||
|
|
||||||
|
import {
|
||||||
|
GroupAddEditDialogResultType,
|
||||||
|
GroupAddEditTabType,
|
||||||
|
openGroupAddEditDialog,
|
||||||
|
} from "./group-add-edit.component";
|
||||||
|
|
||||||
|
type CollectionViewMap = {
|
||||||
|
[id: string]: CollectionView;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GroupDetailsRow = {
|
||||||
|
/**
|
||||||
|
* Group Id (used for searching)
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group name (used for searching)
|
||||||
|
*/
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details used for displaying group information
|
||||||
|
*/
|
||||||
|
details: GroupView;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if the group is selected in the table
|
||||||
|
*/
|
||||||
|
checked?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of collection names the group has access to
|
||||||
|
*/
|
||||||
|
collectionNames?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-org-groups",
|
selector: "app-org-groups",
|
||||||
templateUrl: "groups.component.html",
|
templateUrl: "groups.component.html",
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
export class GroupsComponent implements OnInit, OnDestroy {
|
||||||
export class GroupsComponent implements OnInit {
|
|
||||||
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
@ViewChild("addEdit", { read: ViewContainerRef, static: true }) addEditModalRef: ViewContainerRef;
|
||||||
@ViewChild("usersTemplate", { read: ViewContainerRef, static: true })
|
@ViewChild("usersTemplate", { read: ViewContainerRef, static: true })
|
||||||
usersModalRef: ViewContainerRef;
|
usersModalRef: ViewContainerRef;
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
groups: GroupResponse[];
|
groups: GroupDetailsRow[];
|
||||||
pagedGroups: GroupResponse[];
|
|
||||||
searchText: string;
|
|
||||||
|
|
||||||
protected didScroll = false;
|
protected didScroll = false;
|
||||||
protected pageSize = 100;
|
protected pageSize = 100;
|
||||||
|
protected ModalTabType = GroupAddEditTabType;
|
||||||
|
|
||||||
private pagedGroupsCount = 0;
|
private pagedGroupsCount = 0;
|
||||||
|
private pagedGroups: GroupDetailsRow[];
|
||||||
|
private searchedGroups: GroupDetailsRow[];
|
||||||
|
private _searchText: string;
|
||||||
|
private destroy$ = new Subject<void>();
|
||||||
|
private refreshGroups$ = new BehaviorSubject<void>(null);
|
||||||
|
|
||||||
|
get searchText() {
|
||||||
|
return this._searchText;
|
||||||
|
}
|
||||||
|
set searchText(value: string) {
|
||||||
|
this._searchText = value;
|
||||||
|
// Manually update as we are not using the search pipe in the template
|
||||||
|
this.updateSearchedGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of groups that should be visible in the table.
|
||||||
|
* This is needed as there are two modes (paging/searching) and
|
||||||
|
* we need a reference to the currently visible groups for
|
||||||
|
* the Select All checkbox
|
||||||
|
*/
|
||||||
|
get visibleGroups(): GroupDetailsRow[] {
|
||||||
|
if (this.isPaging()) {
|
||||||
|
return this.pagedGroups;
|
||||||
|
}
|
||||||
|
if (this.isSearching()) {
|
||||||
|
return this.searchedGroups;
|
||||||
|
}
|
||||||
|
return this.groups;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private groupApiService: GroupServiceAbstraction,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private modalService: ModalService,
|
private modalService: ModalService,
|
||||||
|
private dialogService: DialogService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private logService: LogService
|
private logService: LogService,
|
||||||
|
private collectionService: CollectionService,
|
||||||
|
private searchPipe: SearchPipe,
|
||||||
|
private overlay: Overlay
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
|
this.route.params
|
||||||
this.route.parent.params.subscribe(async (params) => {
|
.pipe(
|
||||||
this.organizationId = params.organizationId;
|
tap((params) => (this.organizationId = params.organizationId)),
|
||||||
await this.load();
|
switchMap(() =>
|
||||||
/* eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe, rxjs/no-nested-subscribe */
|
combineLatest([
|
||||||
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
|
// collectionMap
|
||||||
this.searchText = qParams.search;
|
from(this.apiService.getCollections(this.organizationId)).pipe(
|
||||||
|
concatMap((response) => this.toCollectionMap(response))
|
||||||
|
),
|
||||||
|
// groups
|
||||||
|
this.refreshGroups$.pipe(
|
||||||
|
switchMap(() => this.groupApiService.getAll(this.organizationId))
|
||||||
|
),
|
||||||
|
])
|
||||||
|
),
|
||||||
|
map(([collectionMap, groups]) => {
|
||||||
|
return groups
|
||||||
|
.sort(Utils.getSortFunction(this.i18nService, "name"))
|
||||||
|
.map<GroupDetailsRow>((g) => ({
|
||||||
|
id: g.id,
|
||||||
|
name: g.name,
|
||||||
|
details: g,
|
||||||
|
checked: false,
|
||||||
|
collectionNames: g.collections
|
||||||
|
.map((c) => collectionMap[c.id]?.name)
|
||||||
|
.sort(this.i18nService.collator?.compare),
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe((groups) => {
|
||||||
|
this.groups = groups;
|
||||||
|
this.resetPaging();
|
||||||
|
this.updateSearchedGroups();
|
||||||
|
this.loading = false;
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
this.route.queryParams
|
||||||
|
.pipe(
|
||||||
|
first(),
|
||||||
|
concatMap(async (qParams) => {
|
||||||
|
this.searchText = qParams.search;
|
||||||
|
}),
|
||||||
|
takeUntil(this.destroy$)
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
ngOnDestroy() {
|
||||||
const response = await this.apiService.getGroups(this.organizationId);
|
this.destroy$.next();
|
||||||
const groups = response.data != null && response.data.length > 0 ? response.data : [];
|
this.destroy$.complete();
|
||||||
groups.sort(Utils.getSortFunction(this.i18nService, "name"));
|
|
||||||
this.groups = groups;
|
|
||||||
this.resetPaging();
|
|
||||||
this.loading = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
@@ -84,35 +210,35 @@ export class GroupsComponent implements OnInit {
|
|||||||
this.didScroll = this.pagedGroups.length > this.pageSize;
|
this.didScroll = this.pagedGroups.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
async edit(group: GroupResponse) {
|
async edit(
|
||||||
const [modal] = await this.modalService.openViewRef(
|
group: GroupDetailsRow,
|
||||||
GroupAddEditComponent,
|
startingTabIndex: GroupAddEditTabType = GroupAddEditTabType.Info
|
||||||
this.addEditModalRef,
|
) {
|
||||||
(comp) => {
|
const dialogRef = openGroupAddEditDialog(this.dialogService, this.overlay, {
|
||||||
comp.organizationId = this.organizationId;
|
data: {
|
||||||
comp.groupId = group != null ? group.id : null;
|
initialTab: startingTabIndex,
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
organizationId: this.organizationId,
|
||||||
comp.onSavedGroup.subscribe(() => {
|
groupId: group != null ? group.details.id : null,
|
||||||
modal.close();
|
},
|
||||||
this.load();
|
});
|
||||||
});
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
const result = await lastValueFrom(dialogRef.closed);
|
||||||
comp.onDeletedGroup.subscribe(() => {
|
|
||||||
modal.close();
|
if (result == GroupAddEditDialogResultType.Saved) {
|
||||||
this.removeGroup(group);
|
this.refreshGroups$.next();
|
||||||
});
|
} else if (result == GroupAddEditDialogResultType.Deleted) {
|
||||||
}
|
this.removeGroup(group.details.id);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
add() {
|
add() {
|
||||||
this.edit(null);
|
this.edit(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(group: GroupResponse) {
|
async delete(groupRow: GroupDetailsRow) {
|
||||||
const confirmed = await this.platformUtilsService.showDialog(
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
this.i18nService.t("deleteGroupConfirmation"),
|
this.i18nService.t("deleteGroupConfirmation"),
|
||||||
group.name,
|
groupRow.details.name,
|
||||||
this.i18nService.t("yes"),
|
this.i18nService.t("yes"),
|
||||||
this.i18nService.t("no"),
|
this.i18nService.t("no"),
|
||||||
"warning"
|
"warning"
|
||||||
@@ -122,37 +248,55 @@ export class GroupsComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apiService.deleteGroup(this.organizationId, group.id);
|
await this.groupApiService.delete(this.organizationId, groupRow.details.id);
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"success",
|
"success",
|
||||||
null,
|
null,
|
||||||
this.i18nService.t("deletedGroupId", group.name)
|
this.i18nService.t("deletedGroupId", groupRow.details.name)
|
||||||
);
|
);
|
||||||
this.removeGroup(group);
|
this.removeGroup(groupRow.details.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async users(group: GroupResponse) {
|
async deleteAllSelected() {
|
||||||
const [modal] = await this.modalService.openViewRef(
|
const groupsToDelete = this.groups.filter((g) => g.checked);
|
||||||
EntityUsersComponent,
|
|
||||||
this.usersModalRef,
|
|
||||||
(comp) => {
|
|
||||||
comp.organizationId = this.organizationId;
|
|
||||||
comp.entity = "group";
|
|
||||||
comp.entityId = group.id;
|
|
||||||
comp.entityName = group.name;
|
|
||||||
|
|
||||||
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
|
if (groupsToDelete.length == 0) {
|
||||||
comp.onEditedUsers.subscribe(() => {
|
return;
|
||||||
modal.close();
|
}
|
||||||
});
|
|
||||||
}
|
const deleteMessage = groupsToDelete.map((g) => g.details.name).join(", ");
|
||||||
|
const confirmed = await this.platformUtilsService.showDialog(
|
||||||
|
deleteMessage,
|
||||||
|
this.i18nService.t("deleteMultipleGroupsConfirmation", groupsToDelete.length.toString()),
|
||||||
|
this.i18nService.t("yes"),
|
||||||
|
this.i18nService.t("no"),
|
||||||
|
"warning"
|
||||||
);
|
);
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await this.groupApiService.deleteMany(
|
||||||
|
this.organizationId,
|
||||||
|
groupsToDelete.map((g) => g.details.id)
|
||||||
|
);
|
||||||
|
this.platformUtilsService.showToast(
|
||||||
|
"success",
|
||||||
|
null,
|
||||||
|
this.i18nService.t("deletedManyGroups", result.length.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
groupsToDelete.forEach((g) => this.removeGroup(g.details.id));
|
||||||
|
} catch (e) {
|
||||||
|
this.logService.error(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async resetPaging() {
|
resetPaging() {
|
||||||
this.pagedGroups = [];
|
this.pagedGroups = [];
|
||||||
this.loadMore();
|
this.loadMore();
|
||||||
}
|
}
|
||||||
@@ -161,6 +305,14 @@ export class GroupsComponent implements OnInit {
|
|||||||
return this.searchService.isSearchable(this.searchText);
|
return this.searchService.isSearchable(this.searchText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check(groupRow: GroupDetailsRow) {
|
||||||
|
groupRow.checked = !groupRow.checked;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleAllVisible(event: Event) {
|
||||||
|
this.visibleGroups.forEach((g) => (g.checked = (event.target as HTMLInputElement).checked));
|
||||||
|
}
|
||||||
|
|
||||||
isPaging() {
|
isPaging() {
|
||||||
const searching = this.isSearching();
|
const searching = this.isSearching();
|
||||||
if (searching && this.didScroll) {
|
if (searching && this.didScroll) {
|
||||||
@@ -169,11 +321,32 @@ export class GroupsComponent implements OnInit {
|
|||||||
return !searching && this.groups && this.groups.length > this.pageSize;
|
return !searching && this.groups && this.groups.length > this.pageSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeGroup(group: GroupResponse) {
|
private removeGroup(id: string) {
|
||||||
const index = this.groups.indexOf(group);
|
const index = this.groups.findIndex((g) => g.details.id === id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
this.groups.splice(index, 1);
|
this.groups.splice(index, 1);
|
||||||
this.resetPaging();
|
this.resetPaging();
|
||||||
|
this.updateSearchedGroups();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async toCollectionMap(response: ListResponse<CollectionResponse>) {
|
||||||
|
const collections = response.data.map(
|
||||||
|
(r) => new Collection(new CollectionData(r as CollectionDetailsResponse))
|
||||||
|
);
|
||||||
|
const decryptedCollections = await this.collectionService.decryptMany(collections);
|
||||||
|
|
||||||
|
// Convert to an object using collection Ids as keys for faster name lookups
|
||||||
|
const collectionMap: CollectionViewMap = {};
|
||||||
|
decryptedCollections.forEach((c) => (collectionMap[c.id] = c));
|
||||||
|
|
||||||
|
return collectionMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateSearchedGroups() {
|
||||||
|
if (this.searchService.isSearchable(this.searchText)) {
|
||||||
|
// Making use of the pipe in the component as we need know which groups where filtered
|
||||||
|
this.searchedGroups = this.searchPipe.transform(this.groups, this.searchText, "name", "id");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { LogService } from "@bitwarden/common/abstractions/log.service";
|
|||||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
|
import { OrganizationUserUpdateGroupsRequest } from "@bitwarden/common/models/request/organization-user-update-groups.request";
|
||||||
import { GroupResponse } from "@bitwarden/common/models/response/group.response";
|
|
||||||
|
import { GroupServiceAbstraction } from "../services/abstractions/group";
|
||||||
|
import { GroupView } from "../views/group.view";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-user-groups",
|
selector: "app-user-groups",
|
||||||
@@ -19,19 +21,19 @@ export class UserGroupsComponent implements OnInit {
|
|||||||
@Output() onSavedUser = new EventEmitter();
|
@Output() onSavedUser = new EventEmitter();
|
||||||
|
|
||||||
loading = true;
|
loading = true;
|
||||||
groups: GroupResponse[] = [];
|
groups: GroupView[] = [];
|
||||||
formPromise: Promise<any>;
|
formPromise: Promise<any>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
|
private groupApiService: GroupServiceAbstraction,
|
||||||
private i18nService: I18nService,
|
private i18nService: I18nService,
|
||||||
private platformUtilsService: PlatformUtilsService,
|
private platformUtilsService: PlatformUtilsService,
|
||||||
private logService: LogService
|
private logService: LogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const groupsResponse = await this.apiService.getGroups(this.organizationId);
|
const groups = await this.groupApiService.getAll(this.organizationId);
|
||||||
const groups = groupsResponse.data.map((r) => r);
|
|
||||||
groups.sort(Utils.getSortFunction(this.i18nService, "name"));
|
groups.sort(Utils.getSortFunction(this.i18nService, "name"));
|
||||||
this.groups = groups;
|
this.groups = groups;
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ export class UserGroupsComponent implements OnInit {
|
|||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
check(g: GroupResponse, select?: boolean) {
|
check(g: GroupView, select?: boolean) {
|
||||||
(g as any).checked = select == null ? !(g as any).checked : select;
|
(g as any).checked = select == null ? !(g as any).checked : select;
|
||||||
if (!(g as any).checked) {
|
if (!(g as any).checked) {
|
||||||
(g as any).readOnly = false;
|
(g as any).readOnly = false;
|
||||||
|
|||||||
@@ -1,11 +1,32 @@
|
|||||||
import { NgModule } from "@angular/core";
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
|
||||||
import { SharedModule } from "../shared";
|
import { SharedModule } from "../shared";
|
||||||
|
|
||||||
import { AccessSelectorModule } from "./components/access-selector";
|
import { AccessSelectorModule } from "./components/access-selector";
|
||||||
|
import { CollectionAddEditComponent } from "./manage/collection-add-edit.component";
|
||||||
|
import { GroupAddEditComponent } from "./manage/group-add-edit.component";
|
||||||
|
import { GroupsComponent } from "./manage/groups.component";
|
||||||
|
import { UserGroupsComponent } from "./manage/user-groups.component";
|
||||||
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
import { OrganizationsRoutingModule } from "./organization-routing.module";
|
||||||
|
import { GroupServiceAbstraction } from "./services/abstractions/group";
|
||||||
|
import { GroupService } from "./services/group/group.service";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, AccessSelectorModule, OrganizationsRoutingModule],
|
imports: [SharedModule, AccessSelectorModule, OrganizationsRoutingModule],
|
||||||
|
declarations: [
|
||||||
|
GroupsComponent,
|
||||||
|
GroupAddEditComponent,
|
||||||
|
CollectionAddEditComponent,
|
||||||
|
UserGroupsComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: GroupServiceAbstraction,
|
||||||
|
useClass: GroupService,
|
||||||
|
deps: [ApiServiceAbstraction],
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class OrganizationModule {}
|
export class OrganizationModule {}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { GroupView } from "../../../views/group.view";
|
||||||
|
|
||||||
|
export class GroupServiceAbstraction {
|
||||||
|
delete: (orgId: string, groupId: string) => Promise<void>;
|
||||||
|
deleteMany: (orgId: string, groupIds: string[]) => Promise<GroupView[]>;
|
||||||
|
|
||||||
|
get: (orgId: string, groupId: string) => Promise<GroupView>;
|
||||||
|
getAll: (orgId: string) => Promise<GroupView[]>;
|
||||||
|
|
||||||
|
save: (group: GroupView) => Promise<GroupView>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./group.service.abstraction";
|
||||||
|
export * from "./requests/group.request";
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { SelectionReadOnlyRequest } from "./selection-read-only.request";
|
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
||||||
|
|
||||||
export class GroupRequest {
|
export class GroupRequest {
|
||||||
name: string;
|
name: string;
|
||||||
accessAll: boolean;
|
accessAll: boolean;
|
||||||
externalId: string;
|
externalId: string;
|
||||||
collections: SelectionReadOnlyRequest[] = [];
|
collections: SelectionReadOnlyRequest[] = [];
|
||||||
|
users: string[] = [];
|
||||||
}
|
}
|
||||||
110
apps/web/src/app/organizations/services/group/group.service.ts
Normal file
110
apps/web/src/app/organizations/services/group/group.service.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { Injectable } from "@angular/core";
|
||||||
|
|
||||||
|
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||||
|
import { SelectionReadOnlyRequest } from "@bitwarden/common/models/request/selection-read-only.request";
|
||||||
|
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||||
|
|
||||||
|
import { GroupView } from "../../views/group.view";
|
||||||
|
import { GroupRequest, GroupServiceAbstraction } from "../abstractions/group";
|
||||||
|
|
||||||
|
import { OrganizationGroupBulkRequest } from "./requests/organization-group-bulk.request";
|
||||||
|
import { GroupDetailsResponse, GroupResponse } from "./responses/group.response";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class GroupService implements GroupServiceAbstraction {
|
||||||
|
constructor(private apiService: ApiService) {}
|
||||||
|
|
||||||
|
async delete(orgId: string, groupId: string): Promise<void> {
|
||||||
|
await this.apiService.send(
|
||||||
|
"DELETE",
|
||||||
|
"/organizations/" + orgId + "/groups/" + groupId,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteMany(orgId: string, groupIds: string[]): Promise<GroupView[]> {
|
||||||
|
const request = new OrganizationGroupBulkRequest(groupIds);
|
||||||
|
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"DELETE",
|
||||||
|
"/organizations/" + orgId + "/groups",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
const listResponse = new ListResponse(r, GroupResponse);
|
||||||
|
|
||||||
|
return listResponse.data?.map((gr) => GroupView.fromResponse(gr)) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(orgId: string, groupId: string): Promise<GroupView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + orgId + "/groups/" + groupId + "/details",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
return GroupView.fromResponse(new GroupDetailsResponse(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(orgId: string): Promise<GroupView[]> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"GET",
|
||||||
|
"/organizations/" + orgId + "/groups",
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
const listResponse = new ListResponse(r, GroupDetailsResponse);
|
||||||
|
|
||||||
|
return listResponse.data?.map((gr) => GroupView.fromResponse(gr)) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
async save(group: GroupView): Promise<GroupView> {
|
||||||
|
const request = new GroupRequest();
|
||||||
|
request.name = group.name;
|
||||||
|
request.externalId = group.externalId;
|
||||||
|
request.accessAll = group.accessAll;
|
||||||
|
request.users = group.members;
|
||||||
|
request.collections = group.collections.map(
|
||||||
|
(c) => new SelectionReadOnlyRequest(c.id, c.readOnly, c.hidePasswords)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (group.id == undefined) {
|
||||||
|
return await this.postGroup(group.organizationId, request);
|
||||||
|
} else {
|
||||||
|
return await this.putGroup(group.organizationId, group.id, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async postGroup(organizationId: string, request: GroupRequest): Promise<GroupView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"POST",
|
||||||
|
"/organizations/" + organizationId + "/groups",
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return GroupView.fromResponse(new GroupResponse(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async putGroup(
|
||||||
|
organizationId: string,
|
||||||
|
id: string,
|
||||||
|
request: GroupRequest
|
||||||
|
): Promise<GroupView> {
|
||||||
|
const r = await this.apiService.send(
|
||||||
|
"PUT",
|
||||||
|
"/organizations/" + organizationId + "/groups/" + id,
|
||||||
|
request,
|
||||||
|
true,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
return GroupView.fromResponse(new GroupResponse(r));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export class OrganizationGroupBulkRequest {
|
||||||
|
ids: string[];
|
||||||
|
|
||||||
|
constructor(ids: string[]) {
|
||||||
|
this.ids = ids == null ? [] : ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BaseResponse } from "./base.response";
|
import { BaseResponse } from "@bitwarden/common/models/response/base.response";
|
||||||
import { SelectionReadOnlyResponse } from "./selection-read-only.response";
|
import { SelectionReadOnlyResponse } from "@bitwarden/common/models/response/selection-read-only.response";
|
||||||
|
|
||||||
export class GroupResponse extends BaseResponse {
|
export class GroupResponse extends BaseResponse {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { View } from "@bitwarden/common/models/view/view";
|
||||||
|
|
||||||
|
interface SelectionResponseLike {
|
||||||
|
id: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
hidePasswords: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CollectionAccessSelectionView extends View {
|
||||||
|
id: string;
|
||||||
|
readOnly: boolean;
|
||||||
|
hidePasswords: boolean;
|
||||||
|
|
||||||
|
constructor(source?: SelectionResponseLike) {
|
||||||
|
super();
|
||||||
|
|
||||||
|
if (source == undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.id = source.id;
|
||||||
|
this.readOnly = source.readOnly;
|
||||||
|
this.hidePasswords = source.hidePasswords;
|
||||||
|
}
|
||||||
|
}
|
||||||
24
apps/web/src/app/organizations/views/group.view.ts
Normal file
24
apps/web/src/app/organizations/views/group.view.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { View } from "@bitwarden/common/models/view/view";
|
||||||
|
|
||||||
|
import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response";
|
||||||
|
import { CollectionAccessSelectionView } from "../views/collection-access-selection.view";
|
||||||
|
|
||||||
|
export class GroupView implements View {
|
||||||
|
id: string;
|
||||||
|
organizationId: string;
|
||||||
|
name: string;
|
||||||
|
accessAll: boolean;
|
||||||
|
externalId: string;
|
||||||
|
collections: CollectionAccessSelectionView[] = [];
|
||||||
|
members: string[] = [];
|
||||||
|
|
||||||
|
static fromResponse(response: GroupResponse): GroupView {
|
||||||
|
const view: GroupView = Object.assign(new GroupView(), response) as GroupView;
|
||||||
|
|
||||||
|
if (response instanceof GroupDetailsResponse && response.collections != undefined) {
|
||||||
|
view.collections = response.collections.map((c) => new CollectionAccessSelectionView(c));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,18 +34,14 @@ import { BulkConfirmComponent as OrgBulkConfirmComponent } from "../organization
|
|||||||
import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component";
|
import { BulkRemoveComponent as OrgBulkRemoveComponent } from "../organizations/manage/bulk/bulk-remove.component";
|
||||||
import { BulkRestoreRevokeComponent as OrgBulkRestoreRevokeComponent } from "../organizations/manage/bulk/bulk-restore-revoke.component";
|
import { BulkRestoreRevokeComponent as OrgBulkRestoreRevokeComponent } from "../organizations/manage/bulk/bulk-restore-revoke.component";
|
||||||
import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component";
|
import { BulkStatusComponent as OrgBulkStatusComponent } from "../organizations/manage/bulk/bulk-status.component";
|
||||||
import { CollectionAddEditComponent as OrgCollectionAddEditComponent } from "../organizations/manage/collection-add-edit.component";
|
|
||||||
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
|
import { CollectionsComponent as OrgManageCollectionsComponent } from "../organizations/manage/collections.component";
|
||||||
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
import { EntityEventsComponent as OrgEntityEventsComponent } from "../organizations/manage/entity-events.component";
|
||||||
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
import { EventsComponent as OrgEventsComponent } from "../organizations/manage/events.component";
|
||||||
import { GroupAddEditComponent as OrgGroupAddEditComponent } from "../organizations/manage/group-add-edit.component";
|
|
||||||
import { GroupsComponent as OrgGroupsComponent } from "../organizations/manage/groups.component";
|
|
||||||
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
|
import { ManageComponent as OrgManageComponent } from "../organizations/manage/manage.component";
|
||||||
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
|
import { PeopleComponent as OrgPeopleComponent } from "../organizations/manage/people.component";
|
||||||
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
|
import { ResetPasswordComponent as OrgResetPasswordComponent } from "../organizations/manage/reset-password.component";
|
||||||
import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component";
|
import { UserAddEditComponent as OrgUserAddEditComponent } from "../organizations/manage/user-add-edit.component";
|
||||||
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
import { UserConfirmComponent as OrgUserConfirmComponent } from "../organizations/manage/user-confirm.component";
|
||||||
import { UserGroupsComponent as OrgUserGroupsComponent } from "../organizations/manage/user-groups.component";
|
|
||||||
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
import { AcceptFamilySponsorshipComponent } from "../organizations/sponsorships/accept-family-sponsorship.component";
|
||||||
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
import { FamiliesForEnterpriseSetupComponent } from "../organizations/sponsorships/families-for-enterprise-setup.component";
|
||||||
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
import { ExposedPasswordsReportComponent as OrgExposedPasswordsReportComponent } from "../organizations/tools/exposed-passwords-report.component";
|
||||||
@@ -186,13 +182,10 @@ import { SharedModule } from ".";
|
|||||||
OrgBulkRestoreRevokeComponent,
|
OrgBulkRestoreRevokeComponent,
|
||||||
OrgBulkRemoveComponent,
|
OrgBulkRemoveComponent,
|
||||||
OrgBulkStatusComponent,
|
OrgBulkStatusComponent,
|
||||||
OrgCollectionAddEditComponent,
|
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
OrgExposedPasswordsReportComponent,
|
OrgExposedPasswordsReportComponent,
|
||||||
OrgGroupAddEditComponent,
|
|
||||||
OrgGroupsComponent,
|
|
||||||
OrgInactiveTwoFactorReportComponent,
|
OrgInactiveTwoFactorReportComponent,
|
||||||
OrgManageCollectionsComponent,
|
OrgManageCollectionsComponent,
|
||||||
OrgManageComponent,
|
OrgManageComponent,
|
||||||
@@ -203,7 +196,6 @@ import { SharedModule } from ".";
|
|||||||
OrgUnsecuredWebsitesReportComponent,
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
OrgUserAddEditComponent,
|
OrgUserAddEditComponent,
|
||||||
OrgUserConfirmComponent,
|
OrgUserConfirmComponent,
|
||||||
OrgUserGroupsComponent,
|
|
||||||
OrgWeakPasswordsReportComponent,
|
OrgWeakPasswordsReportComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
@@ -310,13 +302,10 @@ import { SharedModule } from ".";
|
|||||||
OrgBulkRestoreRevokeComponent,
|
OrgBulkRestoreRevokeComponent,
|
||||||
OrgBulkRemoveComponent,
|
OrgBulkRemoveComponent,
|
||||||
OrgBulkStatusComponent,
|
OrgBulkStatusComponent,
|
||||||
OrgCollectionAddEditComponent,
|
|
||||||
OrgCollectionsComponent,
|
OrgCollectionsComponent,
|
||||||
OrgEntityEventsComponent,
|
OrgEntityEventsComponent,
|
||||||
OrgEventsComponent,
|
OrgEventsComponent,
|
||||||
OrgExposedPasswordsReportComponent,
|
OrgExposedPasswordsReportComponent,
|
||||||
OrgGroupAddEditComponent,
|
|
||||||
OrgGroupsComponent,
|
|
||||||
OrgInactiveTwoFactorReportComponent,
|
OrgInactiveTwoFactorReportComponent,
|
||||||
OrgManageCollectionsComponent,
|
OrgManageCollectionsComponent,
|
||||||
OrgManageComponent,
|
OrgManageComponent,
|
||||||
@@ -327,7 +316,6 @@ import { SharedModule } from ".";
|
|||||||
OrgUnsecuredWebsitesReportComponent,
|
OrgUnsecuredWebsitesReportComponent,
|
||||||
OrgUserAddEditComponent,
|
OrgUserAddEditComponent,
|
||||||
OrgUserConfirmComponent,
|
OrgUserConfirmComponent,
|
||||||
OrgUserGroupsComponent,
|
|
||||||
OrgWeakPasswordsReportComponent,
|
OrgWeakPasswordsReportComponent,
|
||||||
GeneratorComponent,
|
GeneratorComponent,
|
||||||
PasswordGeneratorHistoryComponent,
|
PasswordGeneratorHistoryComponent,
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
|
|||||||
import {
|
import {
|
||||||
AsyncActionsModule,
|
AsyncActionsModule,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
|
BadgeListModule,
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
CalloutModule,
|
CalloutModule,
|
||||||
|
DialogModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
|
LinkModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
|
MultiSelectModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
} from "@bitwarden/components";
|
} from "@bitwarden/components";
|
||||||
@@ -45,14 +49,18 @@ import "./locales";
|
|||||||
CalloutModule,
|
CalloutModule,
|
||||||
ToastrModule,
|
ToastrModule,
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
|
BadgeListModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
|
MultiSelectModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
DialogModule,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -64,18 +72,20 @@ import "./locales";
|
|||||||
ReactiveFormsModule,
|
ReactiveFormsModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
BadgeModule,
|
BadgeModule,
|
||||||
|
BadgeListModule,
|
||||||
ButtonModule,
|
ButtonModule,
|
||||||
CalloutModule,
|
CalloutModule,
|
||||||
ToastrModule,
|
ToastrModule,
|
||||||
BadgeModule,
|
|
||||||
ButtonModule,
|
|
||||||
MenuModule,
|
MenuModule,
|
||||||
|
MultiSelectModule,
|
||||||
FormFieldModule,
|
FormFieldModule,
|
||||||
IconModule,
|
IconModule,
|
||||||
TabsModule,
|
TabsModule,
|
||||||
TableModule,
|
TableModule,
|
||||||
AvatarModule,
|
AvatarModule,
|
||||||
IconButtonModule,
|
IconButtonModule,
|
||||||
|
LinkModule,
|
||||||
|
DialogModule,
|
||||||
],
|
],
|
||||||
providers: [DatePipe],
|
providers: [DatePipe],
|
||||||
bootstrap: [],
|
bootstrap: [],
|
||||||
|
|||||||
@@ -2374,6 +2374,15 @@
|
|||||||
"deleteGroupConfirmation": {
|
"deleteGroupConfirmation": {
|
||||||
"message": "Are you sure you want to delete this group?"
|
"message": "Are you sure you want to delete this group?"
|
||||||
},
|
},
|
||||||
|
"deleteMultipleGroupsConfirmation": {
|
||||||
|
"message": "Are you sure you want to delete the following $QUANTITY$ group(s)?",
|
||||||
|
"placeholders": {
|
||||||
|
"quantity": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"removeUserConfirmation": {
|
"removeUserConfirmation": {
|
||||||
"message": "Are you sure you want to remove this user?"
|
"message": "Are you sure you want to remove this user?"
|
||||||
},
|
},
|
||||||
@@ -2390,7 +2399,7 @@
|
|||||||
"message": "External id"
|
"message": "External id"
|
||||||
},
|
},
|
||||||
"externalIdDesc": {
|
"externalIdDesc": {
|
||||||
"message": "The external id can be used as a reference or to link this resource to an external system such as a user directory."
|
"message": "External ID is an unencrypted reference used by the Bitwarden Directory Connector or API."
|
||||||
},
|
},
|
||||||
"accessControl": {
|
"accessControl": {
|
||||||
"message": "Access control"
|
"message": "Access control"
|
||||||
@@ -2731,6 +2740,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"deletedManyGroups": {
|
||||||
|
"message": "Deleted $QUANTITY$ group(s).",
|
||||||
|
"placeholders": {
|
||||||
|
"quantity": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"removedUserId": {
|
"removedUserId": {
|
||||||
"message": "Removed user $ID$.",
|
"message": "Removed user $ID$.",
|
||||||
"placeholders": {
|
"placeholders": {
|
||||||
@@ -5502,9 +5520,48 @@
|
|||||||
"update": {
|
"update": {
|
||||||
"message": "Update"
|
"message": "Update"
|
||||||
},
|
},
|
||||||
|
"plusNMore": {
|
||||||
|
"message": "+ $QUANTITY$ more",
|
||||||
|
"placeholders": {
|
||||||
|
"quantity": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"editInfo": {
|
||||||
|
"message": "Edit Info"
|
||||||
|
},
|
||||||
|
"groupInfo": {
|
||||||
|
"message": "Group info"
|
||||||
|
},
|
||||||
|
"editGroupMembersDesc": {
|
||||||
|
"message": "Grant members access to the group's assigned collections."
|
||||||
|
},
|
||||||
|
"editGroupCollectionsDesc": {
|
||||||
|
"message": "Grant access to collections by adding them to this group."
|
||||||
|
},
|
||||||
|
"accessAllCollectionsDesc": {
|
||||||
|
"message": "Grant access to all current and future collections."
|
||||||
|
},
|
||||||
|
"accessAllCollectionsHelp": {
|
||||||
|
"message": "If checked, this will replace all other collection permissions."
|
||||||
|
},
|
||||||
|
"selectMembers": {
|
||||||
|
"message": "Select members"
|
||||||
|
},
|
||||||
|
"selectCollections": {
|
||||||
|
"message": "Select collections"
|
||||||
|
},
|
||||||
"role": {
|
"role": {
|
||||||
"message": "Role"
|
"message": "Role"
|
||||||
},
|
},
|
||||||
|
"removeMember": {
|
||||||
|
"message": "Remove member"
|
||||||
|
},
|
||||||
|
"collection": {
|
||||||
|
"message": "Collection"
|
||||||
|
},
|
||||||
"canView": {
|
"canView": {
|
||||||
"message": "Can view"
|
"message": "Can view"
|
||||||
},
|
},
|
||||||
@@ -5517,6 +5574,12 @@
|
|||||||
"canEditExceptPass": {
|
"canEditExceptPass": {
|
||||||
"message": "Can edit, except passwords"
|
"message": "Can edit, except passwords"
|
||||||
},
|
},
|
||||||
|
"noCollectionsAdded": {
|
||||||
|
"message": "No collections added"
|
||||||
|
},
|
||||||
|
"noMembersAdded": {
|
||||||
|
"message": "No members added"
|
||||||
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"message": "Group"
|
"message": "Group"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ code {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
button i.bwi,
|
button:not([bitbutton]):not([biticonbutton]) i.bwi,
|
||||||
a i.bwi {
|
a i.bwi {
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { I18nPipe } from "./pipes/i18n.pipe";
|
|||||||
import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
|
import { SearchCiphersPipe } from "./pipes/search-ciphers.pipe";
|
||||||
import { SearchPipe } from "./pipes/search.pipe";
|
import { SearchPipe } from "./pipes/search.pipe";
|
||||||
import { UserNamePipe } from "./pipes/user-name.pipe";
|
import { UserNamePipe } from "./pipes/user-name.pipe";
|
||||||
|
import { UserTypePipe } from "./pipes/user-type.pipe";
|
||||||
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
|
import { PasswordStrengthComponent } from "./shared/components/password-strength/password-strength.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
@@ -70,6 +71,7 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
|||||||
LaunchClickDirective,
|
LaunchClickDirective,
|
||||||
UserNamePipe,
|
UserNamePipe,
|
||||||
PasswordStrengthComponent,
|
PasswordStrengthComponent,
|
||||||
|
UserTypePipe,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
A11yInvalidDirective,
|
A11yInvalidDirective,
|
||||||
@@ -100,7 +102,8 @@ import { PasswordStrengthComponent } from "./shared/components/password-strength
|
|||||||
LaunchClickDirective,
|
LaunchClickDirective,
|
||||||
UserNamePipe,
|
UserNamePipe,
|
||||||
PasswordStrengthComponent,
|
PasswordStrengthComponent,
|
||||||
|
UserTypePipe,
|
||||||
],
|
],
|
||||||
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe],
|
providers: [CreditCardNumberPipe, DatePipe, I18nPipe, SearchPipe, UserNamePipe, UserTypePipe],
|
||||||
})
|
})
|
||||||
export class JslibModule {}
|
export class JslibModule {}
|
||||||
|
|||||||
29
libs/angular/src/pipes/user-type.pipe.ts
Normal file
29
libs/angular/src/pipes/user-type.pipe.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Pipe, PipeTransform } from "@angular/core";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
import { OrganizationUserType } from "@bitwarden/common/enums/organizationUserType";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: "userType",
|
||||||
|
})
|
||||||
|
export class UserTypePipe implements PipeTransform {
|
||||||
|
constructor(private i18nService: I18nService) {}
|
||||||
|
|
||||||
|
transform(value?: OrganizationUserType): string {
|
||||||
|
if (value == null) {
|
||||||
|
return this.i18nService.t("unknown");
|
||||||
|
}
|
||||||
|
switch (value) {
|
||||||
|
case OrganizationUserType.Owner:
|
||||||
|
return this.i18nService.t("owner");
|
||||||
|
case OrganizationUserType.Admin:
|
||||||
|
return this.i18nService.t("admin");
|
||||||
|
case OrganizationUserType.User:
|
||||||
|
return this.i18nService.t("user");
|
||||||
|
case OrganizationUserType.Manager:
|
||||||
|
return this.i18nService.t("manager");
|
||||||
|
case OrganizationUserType.Custom:
|
||||||
|
return this.i18nService.t("custom");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import { Injector, LOCALE_ID, NgModule } from "@angular/core";
|
|||||||
|
|
||||||
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service";
|
import { AccountApiService as AccountApiServiceAbstraction } from "@bitwarden/common/abstractions/account/account-api.service";
|
||||||
import {
|
import {
|
||||||
InternalAccountService,
|
|
||||||
AccountService as AccountServiceAbstraction,
|
AccountService as AccountServiceAbstraction,
|
||||||
|
InternalAccountService,
|
||||||
} from "@bitwarden/common/abstractions/account/account.service";
|
} from "@bitwarden/common/abstractions/account/account.service";
|
||||||
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
|
import { AnonymousHubService as AnonymousHubServiceAbstraction } from "@bitwarden/common/abstractions/anonymousHub.service";
|
||||||
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service";
|
||||||
@@ -41,8 +41,8 @@ import { PasswordRepromptService as PasswordRepromptServiceAbstraction } from "@
|
|||||||
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||||
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
|
import { PolicyApiServiceAbstraction } from "@bitwarden/common/abstractions/policy/policy-api.service.abstraction";
|
||||||
import {
|
import {
|
||||||
PolicyService as PolicyServiceAbstraction,
|
|
||||||
InternalPolicyService,
|
InternalPolicyService,
|
||||||
|
PolicyService as PolicyServiceAbstraction,
|
||||||
} from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
} from "@bitwarden/common/abstractions/policy/policy.service.abstraction";
|
||||||
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/abstractions/provider.service";
|
import { ProviderService as ProviderServiceAbstraction } from "@bitwarden/common/abstractions/provider.service";
|
||||||
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
import { SearchService as SearchServiceAbstraction } from "@bitwarden/common/abstractions/search.service";
|
||||||
@@ -121,16 +121,16 @@ import { UnauthGuard } from "../guards/unauth.guard";
|
|||||||
|
|
||||||
import { BroadcasterService } from "./broadcaster.service";
|
import { BroadcasterService } from "./broadcaster.service";
|
||||||
import {
|
import {
|
||||||
WINDOW,
|
LOCALES_DIRECTORY,
|
||||||
|
LOCKED_CALLBACK,
|
||||||
|
LOG_MAC_FAILURES,
|
||||||
|
LOGOUT_CALLBACK,
|
||||||
MEMORY_STORAGE,
|
MEMORY_STORAGE,
|
||||||
SECURE_STORAGE,
|
SECURE_STORAGE,
|
||||||
STATE_FACTORY,
|
STATE_FACTORY,
|
||||||
STATE_SERVICE_USE_CACHE,
|
STATE_SERVICE_USE_CACHE,
|
||||||
LOGOUT_CALLBACK,
|
|
||||||
LOCKED_CALLBACK,
|
|
||||||
LOCALES_DIRECTORY,
|
|
||||||
SYSTEM_LANGUAGE,
|
SYSTEM_LANGUAGE,
|
||||||
LOG_MAC_FAILURES,
|
WINDOW,
|
||||||
} from "./injection-tokens";
|
} from "./injection-tokens";
|
||||||
import { ModalService } from "./modal.service";
|
import { ModalService } from "./modal.service";
|
||||||
import { PasswordRepromptService } from "./passwordReprompt.service";
|
import { PasswordRepromptService } from "./passwordReprompt.service";
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergency-access
|
|||||||
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
|
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
|
||||||
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
|
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
|
||||||
import { EventRequest } from "../models/request/event.request";
|
import { EventRequest } from "../models/request/event.request";
|
||||||
import { GroupRequest } from "../models/request/group.request";
|
|
||||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||||
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
||||||
@@ -105,7 +104,6 @@ import {
|
|||||||
EmergencyAccessViewResponse,
|
EmergencyAccessViewResponse,
|
||||||
} from "../models/response/emergency-access.response";
|
} from "../models/response/emergency-access.response";
|
||||||
import { EventResponse } from "../models/response/event.response";
|
import { EventResponse } from "../models/response/event.response";
|
||||||
import { GroupDetailsResponse, GroupResponse } from "../models/response/group.response";
|
|
||||||
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
|
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
|
||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||||
@@ -121,8 +119,8 @@ import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organi
|
|||||||
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
|
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
|
||||||
import {
|
import {
|
||||||
OrganizationUserDetailsResponse,
|
OrganizationUserDetailsResponse,
|
||||||
OrganizationUserUserDetailsResponse,
|
|
||||||
OrganizationUserResetPasswordDetailsReponse,
|
OrganizationUserResetPasswordDetailsReponse,
|
||||||
|
OrganizationUserUserDetailsResponse,
|
||||||
} from "../models/response/organization-user.response";
|
} from "../models/response/organization-user.response";
|
||||||
import { PaymentResponse } from "../models/response/payment.response";
|
import { PaymentResponse } from "../models/response/payment.response";
|
||||||
import { PlanResponse } from "../models/response/plan.response";
|
import { PlanResponse } from "../models/response/plan.response";
|
||||||
@@ -136,8 +134,8 @@ import {
|
|||||||
import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/provider-user-bulk-public-key.response";
|
import { ProviderUserBulkPublicKeyResponse } from "../models/response/provider/provider-user-bulk-public-key.response";
|
||||||
import { ProviderUserBulkResponse } from "../models/response/provider/provider-user-bulk.response";
|
import { ProviderUserBulkResponse } from "../models/response/provider/provider-user-bulk.response";
|
||||||
import {
|
import {
|
||||||
ProviderUserUserDetailsResponse,
|
|
||||||
ProviderUserResponse,
|
ProviderUserResponse,
|
||||||
|
ProviderUserUserDetailsResponse,
|
||||||
} from "../models/response/provider/provider-user.response";
|
} from "../models/response/provider/provider-user.response";
|
||||||
import { ProviderResponse } from "../models/response/provider/provider.response";
|
import { ProviderResponse } from "../models/response/provider/provider.response";
|
||||||
import { SelectionReadOnlyResponse } from "../models/response/selection-read-only.response";
|
import { SelectionReadOnlyResponse } from "../models/response/selection-read-only.response";
|
||||||
@@ -156,8 +154,8 @@ import { TwoFactorEmailResponse } from "../models/response/two-factor-email.resp
|
|||||||
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
|
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
|
||||||
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
|
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
|
||||||
import {
|
import {
|
||||||
TwoFactorWebAuthnResponse,
|
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
|
TwoFactorWebAuthnResponse,
|
||||||
} from "../models/response/two-factor-web-authn.response";
|
} from "../models/response/two-factor-web-authn.response";
|
||||||
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
|
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
|
||||||
import { UserKeyResponse } from "../models/response/user-key.response";
|
import { UserKeyResponse } from "../models/response/user-key.response";
|
||||||
@@ -341,13 +339,8 @@ export abstract class ApiService {
|
|||||||
organizationUserId: string
|
organizationUserId: string
|
||||||
) => Promise<any>;
|
) => Promise<any>;
|
||||||
|
|
||||||
getGroupDetails: (organizationId: string, id: string) => Promise<GroupDetailsResponse>;
|
|
||||||
getGroups: (organizationId: string) => Promise<ListResponse<GroupResponse>>;
|
|
||||||
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
getGroupUsers: (organizationId: string, id: string) => Promise<string[]>;
|
||||||
postGroup: (organizationId: string, request: GroupRequest) => Promise<GroupResponse>;
|
|
||||||
putGroup: (organizationId: string, id: string, request: GroupRequest) => Promise<GroupResponse>;
|
|
||||||
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
putGroupUsers: (organizationId: string, id: string, request: string[]) => Promise<any>;
|
||||||
deleteGroup: (organizationId: string, id: string) => Promise<any>;
|
|
||||||
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
deleteGroupUser: (organizationId: string, id: string, organizationUserId: string) => Promise<any>;
|
||||||
|
|
||||||
getOrganizationUser: (
|
getOrganizationUser: (
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import { EmergencyAccessInviteRequest } from "../models/request/emergency-access
|
|||||||
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
|
import { EmergencyAccessPasswordRequest } from "../models/request/emergency-access-password.request";
|
||||||
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
|
import { EmergencyAccessUpdateRequest } from "../models/request/emergency-access-update.request";
|
||||||
import { EventRequest } from "../models/request/event.request";
|
import { EventRequest } from "../models/request/event.request";
|
||||||
import { GroupRequest } from "../models/request/group.request";
|
|
||||||
import { IapCheckRequest } from "../models/request/iap-check.request";
|
import { IapCheckRequest } from "../models/request/iap-check.request";
|
||||||
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
import { PasswordTokenRequest } from "../models/request/identity-token/password-token.request";
|
||||||
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
import { SsoTokenRequest } from "../models/request/identity-token/sso-token.request";
|
||||||
@@ -114,7 +113,6 @@ import {
|
|||||||
} from "../models/response/emergency-access.response";
|
} from "../models/response/emergency-access.response";
|
||||||
import { ErrorResponse } from "../models/response/error.response";
|
import { ErrorResponse } from "../models/response/error.response";
|
||||||
import { EventResponse } from "../models/response/event.response";
|
import { EventResponse } from "../models/response/event.response";
|
||||||
import { GroupDetailsResponse, GroupResponse } from "../models/response/group.response";
|
|
||||||
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
|
import { IdentityCaptchaResponse } from "../models/response/identity-captcha.response";
|
||||||
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
import { IdentityTokenResponse } from "../models/response/identity-token.response";
|
||||||
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
import { IdentityTwoFactorResponse } from "../models/response/identity-two-factor.response";
|
||||||
@@ -130,8 +128,8 @@ import { OrganizationUserBulkPublicKeyResponse } from "../models/response/organi
|
|||||||
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
|
import { OrganizationUserBulkResponse } from "../models/response/organization-user-bulk.response";
|
||||||
import {
|
import {
|
||||||
OrganizationUserDetailsResponse,
|
OrganizationUserDetailsResponse,
|
||||||
OrganizationUserUserDetailsResponse,
|
|
||||||
OrganizationUserResetPasswordDetailsReponse,
|
OrganizationUserResetPasswordDetailsReponse,
|
||||||
|
OrganizationUserUserDetailsResponse,
|
||||||
} from "../models/response/organization-user.response";
|
} from "../models/response/organization-user.response";
|
||||||
import { PaymentResponse } from "../models/response/payment.response";
|
import { PaymentResponse } from "../models/response/payment.response";
|
||||||
import { PlanResponse } from "../models/response/plan.response";
|
import { PlanResponse } from "../models/response/plan.response";
|
||||||
@@ -165,8 +163,8 @@ import { TwoFactorEmailResponse } from "../models/response/two-factor-email.resp
|
|||||||
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
|
import { TwoFactorProviderResponse } from "../models/response/two-factor-provider.response";
|
||||||
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
|
import { TwoFactorRecoverResponse } from "../models/response/two-factor-recover.response";
|
||||||
import {
|
import {
|
||||||
TwoFactorWebAuthnResponse,
|
|
||||||
ChallengeResponse,
|
ChallengeResponse,
|
||||||
|
TwoFactorWebAuthnResponse,
|
||||||
} from "../models/response/two-factor-web-authn.response";
|
} from "../models/response/two-factor-web-authn.response";
|
||||||
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
|
import { TwoFactorYubiKeyResponse } from "../models/response/two-factor-yubi-key.response";
|
||||||
import { UserKeyResponse } from "../models/response/user-key.response";
|
import { UserKeyResponse } from "../models/response/user-key.response";
|
||||||
@@ -922,28 +920,6 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
|
|
||||||
// Groups APIs
|
// Groups APIs
|
||||||
|
|
||||||
async getGroupDetails(organizationId: string, id: string): Promise<GroupDetailsResponse> {
|
|
||||||
const r = await this.send(
|
|
||||||
"GET",
|
|
||||||
"/organizations/" + organizationId + "/groups/" + id + "/details",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
return new GroupDetailsResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroups(organizationId: string): Promise<ListResponse<GroupResponse>> {
|
|
||||||
const r = await this.send(
|
|
||||||
"GET",
|
|
||||||
"/organizations/" + organizationId + "/groups",
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
return new ListResponse(r, GroupResponse);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getGroupUsers(organizationId: string, id: string): Promise<string[]> {
|
async getGroupUsers(organizationId: string, id: string): Promise<string[]> {
|
||||||
const r = await this.send(
|
const r = await this.send(
|
||||||
"GET",
|
"GET",
|
||||||
@@ -955,32 +931,6 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
async postGroup(organizationId: string, request: GroupRequest): Promise<GroupResponse> {
|
|
||||||
const r = await this.send(
|
|
||||||
"POST",
|
|
||||||
"/organizations/" + organizationId + "/groups",
|
|
||||||
request,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
return new GroupResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async putGroup(
|
|
||||||
organizationId: string,
|
|
||||||
id: string,
|
|
||||||
request: GroupRequest
|
|
||||||
): Promise<GroupResponse> {
|
|
||||||
const r = await this.send(
|
|
||||||
"PUT",
|
|
||||||
"/organizations/" + organizationId + "/groups/" + id,
|
|
||||||
request,
|
|
||||||
true,
|
|
||||||
true
|
|
||||||
);
|
|
||||||
return new GroupResponse(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> {
|
async putGroupUsers(organizationId: string, id: string, request: string[]): Promise<any> {
|
||||||
await this.send(
|
await this.send(
|
||||||
"PUT",
|
"PUT",
|
||||||
@@ -991,16 +941,6 @@ export class ApiService implements ApiServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteGroup(organizationId: string, id: string): Promise<any> {
|
|
||||||
return this.send(
|
|
||||||
"DELETE",
|
|
||||||
"/organizations/" + organizationId + "/groups/" + id,
|
|
||||||
null,
|
|
||||||
true,
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
|
deleteGroupUser(organizationId: string, id: string, organizationUserId: string): Promise<any> {
|
||||||
return this.send(
|
return this.send(
|
||||||
"DELETE",
|
"DELETE",
|
||||||
|
|||||||
9
libs/components/src/badge-list/badge-list.component.html
Normal file
9
libs/components/src/badge-list/badge-list.component.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="tw-inline-flex tw-gap-2">
|
||||||
|
<span *ngFor="let item of filteredItems; let last = last" bitBadge [badgeType]="badgeType">
|
||||||
|
{{ item }}
|
||||||
|
<span class="tw-sr-only" *ngIf="!last || isFiltered">, </span>
|
||||||
|
</span>
|
||||||
|
<span *ngIf="isFiltered" bitBadge [badgeType]="badgeType">
|
||||||
|
{{ "plusNMore" | i18n: (items.length - filteredItems.length).toString() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
35
libs/components/src/badge-list/badge-list.component.ts
Normal file
35
libs/components/src/badge-list/badge-list.component.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Component, Input, OnChanges } from "@angular/core";
|
||||||
|
|
||||||
|
import { BadgeTypes } from "../badge";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "bit-badge-list",
|
||||||
|
templateUrl: "badge-list.component.html",
|
||||||
|
})
|
||||||
|
export class BadgeListComponent implements OnChanges {
|
||||||
|
private _maxItems: number;
|
||||||
|
|
||||||
|
protected filteredItems: string[] = [];
|
||||||
|
protected isFiltered = false;
|
||||||
|
|
||||||
|
@Input() badgeType: BadgeTypes = "primary";
|
||||||
|
@Input() items: string[] = [];
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
get maxItems(): number | undefined {
|
||||||
|
return this._maxItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
set maxItems(value: number | undefined) {
|
||||||
|
this._maxItems = value == undefined ? undefined : Math.max(1, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges() {
|
||||||
|
if (this.maxItems == undefined) {
|
||||||
|
this.filteredItems = this.items;
|
||||||
|
} else {
|
||||||
|
this.filteredItems = this.items.slice(0, this.maxItems);
|
||||||
|
}
|
||||||
|
this.isFiltered = this.items.length > this.filteredItems.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
libs/components/src/badge-list/badge-list.module.ts
Normal file
13
libs/components/src/badge-list/badge-list.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { NgModule } from "@angular/core";
|
||||||
|
|
||||||
|
import { BadgeModule } from "../badge";
|
||||||
|
import { SharedModule } from "../shared";
|
||||||
|
|
||||||
|
import { BadgeListComponent } from "./badge-list.component";
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [SharedModule, BadgeModule],
|
||||||
|
exports: [BadgeListComponent],
|
||||||
|
declarations: [BadgeListComponent],
|
||||||
|
})
|
||||||
|
export class BadgeListModule {}
|
||||||
53
libs/components/src/badge-list/badge-list.stories.ts
Normal file
53
libs/components/src/badge-list/badge-list.stories.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||||
|
|
||||||
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
|
|
||||||
|
import { BadgeModule } from "../badge";
|
||||||
|
import { SharedModule } from "../shared";
|
||||||
|
import { I18nMockService } from "../utils/i18n-mock.service";
|
||||||
|
|
||||||
|
import { BadgeListComponent } from "./badge-list.component";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Component Library/Badge/List",
|
||||||
|
component: BadgeListComponent,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [SharedModule, BadgeModule],
|
||||||
|
declarations: [BadgeListComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: I18nService,
|
||||||
|
useFactory: () => {
|
||||||
|
return new I18nMockService({
|
||||||
|
plusNMore: (n) => `+ ${n} more`,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
args: {
|
||||||
|
badgeType: "primary",
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
design: {
|
||||||
|
type: "figma",
|
||||||
|
url: "https://www.figma.com/file/f32LSg3jaegICkMu7rPARm/Tailwind-Component-Library-Update?node-id=1881%3A16956",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
const ListTemplate: Story<BadgeListComponent> = (args: BadgeListComponent) => ({
|
||||||
|
props: args,
|
||||||
|
template: `
|
||||||
|
<bit-badge-list [badgeType]="badgeType" [maxItems]="maxItems" [items]="items"></bit-badge-list>
|
||||||
|
`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Default = ListTemplate.bind({});
|
||||||
|
Default.args = {
|
||||||
|
badgeType: "info",
|
||||||
|
maxItems: 3,
|
||||||
|
items: ["Badge 1", "Badge 2", "Badge 3", "Badge 4", "Badge 5"],
|
||||||
|
};
|
||||||
1
libs/components/src/badge-list/index.ts
Normal file
1
libs/components/src/badge-list/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from "./badge-list.module";
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
|
import { Directive, ElementRef, HostBinding, Input } from "@angular/core";
|
||||||
|
|
||||||
type BadgeTypes = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
|
export type BadgeTypes = "primary" | "secondary" | "success" | "danger" | "warning" | "info";
|
||||||
|
|
||||||
const styles: Record<BadgeTypes, string[]> = {
|
const styles: Record<BadgeTypes, string[]> = {
|
||||||
primary: ["tw-bg-primary-500"],
|
primary: ["tw-bg-primary-500"],
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
import { Meta, Story } from "@storybook/angular";
|
import { CommonModule } from "@angular/common";
|
||||||
|
import { Meta, moduleMetadata, Story } from "@storybook/angular";
|
||||||
|
|
||||||
import { BadgeDirective } from "./badge.directive";
|
import { BadgeDirective } from "./badge.directive";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
title: "Component Library/Badge",
|
title: "Component Library/Badge",
|
||||||
component: BadgeDirective,
|
component: BadgeDirective,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
imports: [CommonModule],
|
||||||
|
declarations: [BadgeDirective],
|
||||||
|
}),
|
||||||
|
],
|
||||||
args: {
|
args: {
|
||||||
badgeType: "primary",
|
badgeType: "primary",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from "./badge.directive";
|
export { BadgeDirective, BadgeTypes } from "./badge.directive";
|
||||||
export * from "./badge.module";
|
export * from "./badge.module";
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { SimpleDialogComponent } from "./simple-dialog/simple-dialog.component";
|
|||||||
DialogComponent,
|
DialogComponent,
|
||||||
SimpleDialogComponent,
|
SimpleDialogComponent,
|
||||||
],
|
],
|
||||||
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent],
|
exports: [CdkDialogModule, DialogComponent, SimpleDialogComponent, DialogCloseDirective],
|
||||||
providers: [DialogService],
|
providers: [DialogService],
|
||||||
})
|
})
|
||||||
export class DialogModule {}
|
export class DialogModule {}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
export * from "./async-actions";
|
export * from "./async-actions";
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./badge";
|
export * from "./badge";
|
||||||
|
export * from "./badge-list";
|
||||||
export * from "./banner";
|
export * from "./banner";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./callout";
|
export * from "./callout";
|
||||||
|
|||||||
@@ -9,10 +9,14 @@ export class I18nMockService implements I18nService {
|
|||||||
collator: Intl.Collator;
|
collator: Intl.Collator;
|
||||||
localeNames: Map<string, string>;
|
localeNames: Map<string, string>;
|
||||||
|
|
||||||
constructor(private lookupTable: Record<string, string>) {}
|
constructor(private lookupTable: Record<string, string | ((...args: string[]) => string)>) {}
|
||||||
|
|
||||||
t(id: string, p1?: string, p2?: string, p3?: string) {
|
t(id: string, p1?: string, p2?: string, p3?: string) {
|
||||||
return this.lookupTable[id];
|
const value = this.lookupTable[id];
|
||||||
|
if (typeof value == "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return value(p1, p2, p3);
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(id: string, p1?: string, p2?: string, p3?: string) {
|
translate(id: string, p1?: string, p2?: string, p3?: string) {
|
||||||
|
|||||||
Reference in New Issue
Block a user