mirror of
https://github.com/bitwarden/browser
synced 2024-12-26 09:54:35 +01:00
[SM-670] Restrict UI actions based on user permission (#5090)
* Restrict UI actions based on user permission * Swap to hiding bulk option without permission * Fix read/write assignment in project service * Filter projects based on permission in dialog * Fix encryption error for updating secret result * Fix spinner (#5182) * Swap to bit-no-items * [SM-699] Projects bulk delete - add bulk confirmation dialog (#5200) * Add bulk confirmation dialog * Code review updates * Code review - load projects * code review - swap to observable * Code review - remove oninit
This commit is contained in:
parent
95b1ea318c
commit
208e3f30b4
@ -1264,7 +1264,7 @@
|
||||
"example": "2"
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"dataExportSuccess": {
|
||||
"message": "Data successfully exported"
|
||||
},
|
||||
@ -6736,7 +6736,19 @@
|
||||
"notAvailableForFreeOrganization": {
|
||||
"message": "This feature is not available for free organizations. Contact your organization owner to upgrade."
|
||||
},
|
||||
"smProjectSecretsNoItemsNoAccess": {
|
||||
"message": "Contact your organization's admin to manage secrets for this project.",
|
||||
"description": "The message shown to the user under a project's secrets tab when the user only has read access to the project."
|
||||
},
|
||||
"enforceOnLoginDesc": {
|
||||
"message": "Require existing members to change their passwords"
|
||||
},
|
||||
"smProjectDeleteAccessRestricted": {
|
||||
"message": "You don't have permissions to delete this project",
|
||||
"description": "The individual description shown to the user when the user doesn't have access to delete a project."
|
||||
},
|
||||
"smProjectsDeleteBulkConfirmation": {
|
||||
"message": "The following projects can not be deleted. Would you like to continue?",
|
||||
"description": "The message shown to the user when bulk deleting projects and the user doesn't have access to some projects."
|
||||
}
|
||||
}
|
||||
|
@ -4,4 +4,6 @@ export class ProjectListView {
|
||||
name: string;
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
@ -4,9 +4,6 @@ export class ProjectView {
|
||||
name: string;
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
}
|
||||
|
||||
export class ProjectPermissionDetailsView extends ProjectView {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ export class ProjectListItemResponse extends BaseResponse {
|
||||
name: string;
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@ -14,5 +16,7 @@ export class ProjectListItemResponse extends BaseResponse {
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
this.read = this.getResponseProperty("Read");
|
||||
this.write = this.getResponseProperty("Write");
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ export class ProjectResponse extends BaseResponse {
|
||||
name: string;
|
||||
creationDate: string;
|
||||
revisionDate: string;
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
@ -14,15 +16,6 @@ export class ProjectResponse extends BaseResponse {
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectPermissionDetailsResponse extends ProjectResponse {
|
||||
read: boolean;
|
||||
write: boolean;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.read = this.getResponseProperty("Read");
|
||||
this.write = this.getResponseProperty("Write");
|
||||
}
|
||||
|
@ -9,15 +9,12 @@ import { SymmetricCryptoKey } from "@bitwarden/common/models/domain/symmetric-cr
|
||||
import { ListResponse } from "@bitwarden/common/models/response/list.response";
|
||||
|
||||
import { ProjectListView } from "../models/view/project-list.view";
|
||||
import { ProjectPermissionDetailsView, ProjectView } from "../models/view/project.view";
|
||||
import { ProjectView } from "../models/view/project.view";
|
||||
import { BulkOperationStatus } from "../shared/dialogs/bulk-status-dialog.component";
|
||||
|
||||
import { ProjectRequest } from "./models/requests/project.request";
|
||||
import { ProjectListItemResponse } from "./models/responses/project-list-item.response";
|
||||
import {
|
||||
ProjectPermissionDetailsResponse,
|
||||
ProjectResponse,
|
||||
} from "./models/responses/project.response";
|
||||
import { ProjectResponse } from "./models/responses/project.response";
|
||||
|
||||
@Injectable({
|
||||
providedIn: "root",
|
||||
@ -32,10 +29,10 @@ export class ProjectService {
|
||||
private encryptService: EncryptService
|
||||
) {}
|
||||
|
||||
async getByProjectId(projectId: string): Promise<ProjectPermissionDetailsView> {
|
||||
async getByProjectId(projectId: string): Promise<ProjectView> {
|
||||
const r = await this.apiService.send("GET", "/projects/" + projectId, null, true, true);
|
||||
const projectResponse = new ProjectPermissionDetailsResponse(r);
|
||||
return await this.createProjectPermissionDetailsView(projectResponse);
|
||||
const projectResponse = new ProjectResponse(r);
|
||||
return await this.createProjectView(projectResponse);
|
||||
}
|
||||
|
||||
async getProjects(organizationId: string): Promise<ProjectListView[]> {
|
||||
@ -99,9 +96,7 @@ export class ProjectService {
|
||||
return request;
|
||||
}
|
||||
|
||||
private async createProjectView(
|
||||
projectResponse: ProjectResponse | ProjectPermissionDetailsResponse
|
||||
) {
|
||||
private async createProjectView(projectResponse: ProjectResponse) {
|
||||
const orgKey = await this.getOrganizationKey(projectResponse.organizationId);
|
||||
|
||||
const projectView = new ProjectView();
|
||||
@ -109,6 +104,8 @@ export class ProjectService {
|
||||
projectView.organizationId = projectResponse.organizationId;
|
||||
projectView.creationDate = projectResponse.creationDate;
|
||||
projectView.revisionDate = projectResponse.revisionDate;
|
||||
projectView.read = projectResponse.read;
|
||||
projectView.write = projectResponse.write;
|
||||
projectView.name = await this.encryptService.decryptToUtf8(
|
||||
new EncString(projectResponse.name),
|
||||
orgKey
|
||||
@ -116,16 +113,6 @@ export class ProjectService {
|
||||
return projectView;
|
||||
}
|
||||
|
||||
private async createProjectPermissionDetailsView(
|
||||
projectResponse: ProjectPermissionDetailsResponse
|
||||
): Promise<ProjectPermissionDetailsView> {
|
||||
return {
|
||||
...(await this.createProjectView(projectResponse)),
|
||||
read: projectResponse.read,
|
||||
write: projectResponse.write,
|
||||
};
|
||||
}
|
||||
|
||||
private async createProjectsListView(
|
||||
organizationId: string,
|
||||
projects: ProjectListItemResponse[]
|
||||
@ -136,6 +123,8 @@ export class ProjectService {
|
||||
const projectListView = new ProjectListView();
|
||||
projectListView.id = s.id;
|
||||
projectListView.organizationId = s.organizationId;
|
||||
projectListView.read = s.read;
|
||||
projectListView.write = s.write;
|
||||
projectListView.name = await this.encryptService.decryptToUtf8(
|
||||
new EncString(s.name),
|
||||
orgKey
|
||||
|
@ -1,18 +1,24 @@
|
||||
<ng-container *ngIf="secrets$ | async as secrets; else spinner">
|
||||
<div *ngIf="secrets.length > 0" class="float-right tw-mt-3 tw-items-center">
|
||||
<button type="button" bitButton buttonType="secondary" (click)="openNewSecretDialog()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "newSecret" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<sm-secrets-list
|
||||
(deleteSecretsEvent)="openDeleteSecret($event)"
|
||||
(newSecretEvent)="openNewSecretDialog()"
|
||||
(editSecretEvent)="openEditSecret($event)"
|
||||
(copySecretNameEvent)="copySecretName($event)"
|
||||
(copySecretValueEvent)="copySecretValue($event)"
|
||||
[secrets]="secrets"
|
||||
></sm-secrets-list>
|
||||
<ng-container *ngIf="{ project: project$ | async, secrets: secrets$ | async } as projectSecrets">
|
||||
<ng-container *ngIf="projectSecrets?.secrets && projectSecrets?.project; else spinner">
|
||||
<div
|
||||
*ngIf="projectSecrets.secrets?.length > 0 && projectSecrets.project?.write"
|
||||
class="float-right tw-mt-3 tw-items-center"
|
||||
>
|
||||
<button type="button" bitButton buttonType="secondary" (click)="openNewSecretDialog()">
|
||||
<i class="bwi bwi-plus" aria-hidden="true"></i>
|
||||
{{ "newSecret" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
<sm-secrets-list
|
||||
*ngIf="projectSecrets.secrets?.length > 0 || projectSecrets.project?.write; else contactAdmin"
|
||||
(deleteSecretsEvent)="openDeleteSecret($event)"
|
||||
(newSecretEvent)="openNewSecretDialog()"
|
||||
(editSecretEvent)="openEditSecret($event)"
|
||||
(copySecretNameEvent)="copySecretName($event)"
|
||||
(copySecretValueEvent)="copySecretValue($event)"
|
||||
[secrets]="projectSecrets.secrets"
|
||||
></sm-secrets-list>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #spinner>
|
||||
@ -20,3 +26,10 @@
|
||||
<i class="bwi bwi-spinner bwi-spin bwi-3x"></i>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #contactAdmin>
|
||||
<bit-no-items>
|
||||
<ng-container slot="title">{{ "secretsNoItemsTitle" | i18n }}</ng-container>
|
||||
<ng-container slot="description">{{ "smProjectSecretsNoItemsNoAccess" | i18n }}</ng-container>
|
||||
</bit-no-items>
|
||||
</ng-template>
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
||||
import { combineLatest, combineLatestWith, filter, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ProjectView } from "../../models/view/project.view";
|
||||
import { SecretListView } from "../../models/view/secret-list.view";
|
||||
import {
|
||||
SecretDeleteDialogComponent,
|
||||
@ -29,6 +30,7 @@ export class ProjectSecretsComponent {
|
||||
|
||||
private organizationId: string;
|
||||
private projectId: string;
|
||||
protected project$: Observable<ProjectView>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
@ -46,6 +48,12 @@ export class ProjectSecretsComponent {
|
||||
startWith(null)
|
||||
);
|
||||
|
||||
this.project$ = combineLatest([this.route.params, currentProjectEdited]).pipe(
|
||||
switchMap(([params, _]) => {
|
||||
return this.projectService.getByProjectId(params.projectId);
|
||||
})
|
||||
);
|
||||
|
||||
this.secrets$ = this.secretService.secret$.pipe(
|
||||
startWith(null),
|
||||
combineLatestWith(this.route.params, currentProjectEdited),
|
||||
|
@ -4,7 +4,7 @@ import { combineLatest, filter, Observable, startWith, Subject, switchMap, takeU
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ProjectPermissionDetailsView } from "../../models/view/project.view";
|
||||
import { ProjectView } from "../../models/view/project.view";
|
||||
import {
|
||||
OperationType,
|
||||
ProjectDialogComponent,
|
||||
@ -17,7 +17,7 @@ import { ProjectService } from "../project.service";
|
||||
templateUrl: "./project.component.html",
|
||||
})
|
||||
export class ProjectComponent implements OnInit, OnDestroy {
|
||||
protected project$: Observable<ProjectPermissionDetailsView>;
|
||||
protected project$: Observable<ProjectView>;
|
||||
|
||||
private organizationId: string;
|
||||
private projectId: string;
|
||||
|
@ -1,11 +1,17 @@
|
||||
import { Component, OnInit } from "@angular/core";
|
||||
import { ActivatedRoute } from "@angular/router";
|
||||
import { combineLatest, Observable, startWith, switchMap } from "rxjs";
|
||||
import { combineLatest, lastValueFrom, Observable, startWith, switchMap } from "rxjs";
|
||||
|
||||
import { DialogService } from "@bitwarden/components";
|
||||
|
||||
import { ProjectListView } from "../../models/view/project-list.view";
|
||||
import { AccessPolicyService } from "../../shared/access-policies/access-policy.service";
|
||||
import {
|
||||
BulkConfirmationDetails,
|
||||
BulkConfirmationDialogComponent,
|
||||
BulkConfirmationResult,
|
||||
BulkConfirmationStatus,
|
||||
} from "../../shared/dialogs/bulk-confirmation-dialog.component";
|
||||
import {
|
||||
ProjectDeleteDialogComponent,
|
||||
ProjectDeleteOperation,
|
||||
@ -70,11 +76,48 @@ export class ProjectsComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
openDeleteProjectDialog(event: ProjectListView[]) {
|
||||
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
||||
data: {
|
||||
projects: event,
|
||||
},
|
||||
async openDeleteProjectDialog(projects: ProjectListView[]) {
|
||||
if (projects.some((project) => project.write == false)) {
|
||||
const readOnlyProjects = projects.filter((project) => project.write == false);
|
||||
const writeProjects = projects.filter((project) => project.write);
|
||||
|
||||
const dialogRef = this.dialogService.open<unknown, BulkConfirmationDetails>(
|
||||
BulkConfirmationDialogComponent,
|
||||
{
|
||||
data: {
|
||||
title: "deleteProjects",
|
||||
columnTitle: "projectName",
|
||||
message: "smProjectsDeleteBulkConfirmation",
|
||||
details: this.getBulkConfirmationDetails(readOnlyProjects),
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const result = await lastValueFrom(dialogRef.closed);
|
||||
|
||||
if (result == BulkConfirmationResult.Continue) {
|
||||
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
||||
data: {
|
||||
projects: writeProjects,
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
this.dialogService.open<unknown, ProjectDeleteOperation>(ProjectDeleteDialogComponent, {
|
||||
data: {
|
||||
projects,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private getBulkConfirmationDetails(projects: ProjectListView[]): BulkConfirmationStatus[] {
|
||||
return projects.map((project) => {
|
||||
return {
|
||||
id: project.id,
|
||||
name: project.name,
|
||||
description: "smProjectDeleteAccessRestricted",
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,8 @@ export class SecretDialogComponent implements OnInit {
|
||||
} else if (this.data.operation !== OperationType.Add) {
|
||||
this.dialogRef.close();
|
||||
throw new Error(`The secret dialog was not called with the appropriate operation values.`);
|
||||
} else if (this.data.operation == OperationType.Add) {
|
||||
await this.loadProjects(true);
|
||||
}
|
||||
|
||||
if (this.data.projectId) {
|
||||
@ -72,15 +74,14 @@ export class SecretDialogComponent implements OnInit {
|
||||
this.formGroup.get("project").removeValidators(Validators.required);
|
||||
this.formGroup.get("project").updateValueAndValidity();
|
||||
}
|
||||
|
||||
this.projects = await this.projectService
|
||||
.getProjects(this.data.organizationId)
|
||||
.then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
}
|
||||
|
||||
async loadData() {
|
||||
this.formGroup.disable();
|
||||
const secret: SecretView = await this.secretService.getBySecretId(this.data.secretId);
|
||||
|
||||
await this.loadProjects(secret.write);
|
||||
|
||||
this.formGroup.setValue({
|
||||
name: secret.name,
|
||||
value: secret.value,
|
||||
@ -95,6 +96,16 @@ export class SecretDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async loadProjects(filterByPermission: boolean) {
|
||||
this.projects = await this.projectService
|
||||
.getProjects(this.data.organizationId)
|
||||
.then((projects) => projects.sort((a, b) => a.name.localeCompare(b.name)));
|
||||
|
||||
if (filterByPermission) {
|
||||
this.projects = this.projects.filter((p) => p.write);
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
@ -233,10 +233,9 @@ export class SecretService {
|
||||
projects.map(async (s: SecretProjectResponse) => {
|
||||
const projectsMappedToSecretView = new SecretProjectView();
|
||||
projectsMappedToSecretView.id = s.id;
|
||||
projectsMappedToSecretView.name = await this.encryptService.decryptToUtf8(
|
||||
new EncString(s.name),
|
||||
orgKey
|
||||
);
|
||||
projectsMappedToSecretView.name = s.name
|
||||
? await this.encryptService.decryptToUtf8(new EncString(s.name), orgKey)
|
||||
: null;
|
||||
return projectsMappedToSecretView;
|
||||
})
|
||||
);
|
||||
|
@ -50,16 +50,21 @@ export class AccessRemovalDialogComponent implements OnInit {
|
||||
await this.accessPolicyService.updateAccessPolicy(
|
||||
AccessSelectorComponent.getBaseAccessPolicyView(this.data.policy)
|
||||
);
|
||||
this.refreshPolicyChanges();
|
||||
}
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
cancel = () => {
|
||||
this.refreshPolicyChanges();
|
||||
this.dialogRef.close();
|
||||
};
|
||||
|
||||
private refreshPolicyChanges() {
|
||||
if (this.data.type == "project") {
|
||||
this.accessPolicyService.refreshProjectAccessPolicyChanges();
|
||||
} else if (this.data.type == "service-account") {
|
||||
this.accessPolicyService.refreshServiceAccountAccessPolicyChanges();
|
||||
}
|
||||
this.dialogRef.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
<bit-dialog>
|
||||
<ng-container bitDialogTitle>
|
||||
{{ data.title | i18n }}
|
||||
</ng-container>
|
||||
|
||||
<div bitDialogContent>
|
||||
{{ data.message | i18n }}
|
||||
<bit-table>
|
||||
<ng-container header>
|
||||
<tr>
|
||||
<th bitCell>{{ data.columnTitle | i18n }}</th>
|
||||
<th bitCell>{{ "description" | i18n }}</th>
|
||||
</tr>
|
||||
</ng-container>
|
||||
<ng-template body>
|
||||
<tr bitRow *ngFor="let detail of data.details">
|
||||
<td bitCell>{{ detail.name }}</td>
|
||||
<td bitCell>{{ detail.description | i18n }}</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
</bit-table>
|
||||
</div>
|
||||
|
||||
<div bitDialogFooter class="tw-flex tw-gap-2">
|
||||
<button
|
||||
type="button"
|
||||
bitButton
|
||||
buttonType="primary"
|
||||
bitFormButton
|
||||
(click)="dialogRef.close(bulkConfirmationResult.Continue)"
|
||||
>
|
||||
{{ "continue" | i18n }}
|
||||
</button>
|
||||
<button bitButton buttonType="secondary" bitDialogClose type="button">
|
||||
{{ "cancel" | i18n }}
|
||||
</button>
|
||||
</div>
|
||||
</bit-dialog>
|
@ -0,0 +1,48 @@
|
||||
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||
import { Component, Inject, OnInit } from "@angular/core";
|
||||
|
||||
export interface BulkConfirmationDetails {
|
||||
title: string;
|
||||
columnTitle: string;
|
||||
message: string;
|
||||
details: BulkConfirmationStatus[];
|
||||
}
|
||||
|
||||
export interface BulkConfirmationStatus {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export enum BulkConfirmationResult {
|
||||
Continue,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: "sm-bulk-confirmation-dialog",
|
||||
templateUrl: "./bulk-confirmation-dialog.component.html",
|
||||
})
|
||||
export class BulkConfirmationDialogComponent implements OnInit {
|
||||
constructor(
|
||||
public dialogRef: DialogRef,
|
||||
@Inject(DIALOG_DATA) public data: BulkConfirmationDetails
|
||||
) {}
|
||||
|
||||
protected bulkConfirmationResult = BulkConfirmationResult;
|
||||
|
||||
ngOnInit(): void {
|
||||
// TODO remove null checks once strictNullChecks in TypeScript is turned on.
|
||||
if (
|
||||
!this.data.title ||
|
||||
!this.data.columnTitle ||
|
||||
!this.data.message ||
|
||||
!(this.data.details?.length >= 1)
|
||||
) {
|
||||
this.dialogRef.close();
|
||||
throw new Error(
|
||||
"The bulk confirmation dialog was not called with the appropriate operation values."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@
|
||||
<th bitCell bitSortable="revisionDate">{{ "lastEdited" | i18n }}</th>
|
||||
<th bitCell class="tw-w-0">
|
||||
<button
|
||||
*ngIf="hasWriteAccessOnSelected$ | async"
|
||||
type="button"
|
||||
bitIconButton="bwi-ellipsis-v"
|
||||
buttonType="main"
|
||||
@ -74,7 +75,12 @@
|
||||
></button>
|
||||
</td>
|
||||
<bit-menu #projectMenu>
|
||||
<button type="button" bitMenuItem (click)="editProjectEvent.emit(project.id)">
|
||||
<button
|
||||
*ngIf="project.write"
|
||||
type="button"
|
||||
bitMenuItem
|
||||
(click)="editProjectEvent.emit(project.id)"
|
||||
>
|
||||
<i class="bwi bwi-fw bwi-pencil" aria-hidden="true"></i>
|
||||
{{ "editProject" | i18n }}
|
||||
</button>
|
||||
@ -82,7 +88,7 @@
|
||||
<i class="bwi bwi-fw bwi-eye" aria-hidden="true"></i>
|
||||
{{ "viewProject" | i18n }}
|
||||
</a>
|
||||
<button type="button" bitMenuItem (click)="deleteProject(project.id)">
|
||||
<button *ngIf="project.write" type="button" bitMenuItem (click)="deleteProject(project.id)">
|
||||
<i class="bwi bwi-fw bwi-trash tw-text-danger" aria-hidden="true"></i>
|
||||
<span class="tw-text-danger">{{ "deleteProject" | i18n }}</span>
|
||||
</button>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { SelectionModel } from "@angular/cdk/collections";
|
||||
import { Component, EventEmitter, Input, OnDestroy, Output } from "@angular/core";
|
||||
import { Subject, takeUntil } from "rxjs";
|
||||
import { Component, EventEmitter, Input, Output } from "@angular/core";
|
||||
import { map } from "rxjs";
|
||||
|
||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUtils.service";
|
||||
@ -12,9 +12,7 @@ import { ProjectListView } from "../models/view/project-list.view";
|
||||
selector: "sm-projects-list",
|
||||
templateUrl: "./projects-list.component.html",
|
||||
})
|
||||
export class ProjectsListComponent implements OnDestroy {
|
||||
protected dataSource = new TableDataSource<ProjectListView>();
|
||||
|
||||
export class ProjectsListComponent {
|
||||
@Input()
|
||||
get projects(): ProjectListView[] {
|
||||
return this._projects;
|
||||
@ -33,26 +31,18 @@ export class ProjectsListComponent implements OnDestroy {
|
||||
|
||||
@Output() editProjectEvent = new EventEmitter<string>();
|
||||
@Output() deleteProjectEvent = new EventEmitter<ProjectListView[]>();
|
||||
@Output() onProjectCheckedEvent = new EventEmitter<string[]>();
|
||||
@Output() newProjectEvent = new EventEmitter();
|
||||
|
||||
private destroy$: Subject<void> = new Subject<void>();
|
||||
|
||||
selection = new SelectionModel<string>(true, []);
|
||||
protected dataSource = new TableDataSource<ProjectListView>();
|
||||
protected hasWriteAccessOnSelected$ = this.selection.changed.pipe(
|
||||
map((_) => this.selectedHasWriteAccess())
|
||||
);
|
||||
|
||||
constructor(
|
||||
private i18nService: I18nService,
|
||||
private platformUtilsService: PlatformUtilsService
|
||||
) {
|
||||
this.selection.changed
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((_) => this.onProjectCheckedEvent.emit(this.selection.selected));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
}
|
||||
) {}
|
||||
|
||||
isAllSelected() {
|
||||
const numSelected = this.selection.selected.length;
|
||||
@ -83,4 +73,14 @@ export class ProjectsListComponent implements OnDestroy {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private selectedHasWriteAccess() {
|
||||
const selectedProjects = this.projects.filter((project) =>
|
||||
this.selection.isSelected(project.id)
|
||||
);
|
||||
if (selectedProjects.some((project) => project.write)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ import { SharedModule } from "@bitwarden/web-vault/app/shared";
|
||||
|
||||
import { AccessSelectorComponent } from "./access-policies/access-selector.component";
|
||||
import { AccessRemovalDialogComponent } from "./access-policies/dialogs/access-removal-dialog.component";
|
||||
import { BulkConfirmationDialogComponent } from "./dialogs/bulk-confirmation-dialog.component";
|
||||
import { BulkStatusDialogComponent } from "./dialogs/bulk-status-dialog.component";
|
||||
import { HeaderComponent } from "./header.component";
|
||||
import { NewMenuComponent } from "./new-menu.component";
|
||||
@ -30,6 +31,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
||||
AccessRemovalDialogComponent,
|
||||
AccessSelectorComponent,
|
||||
BulkStatusDialogComponent,
|
||||
BulkConfirmationDialogComponent,
|
||||
HeaderComponent,
|
||||
NewMenuComponent,
|
||||
ProjectsListComponent,
|
||||
@ -40,6 +42,7 @@ import { SecretsListComponent } from "./secrets-list.component";
|
||||
declarations: [
|
||||
AccessRemovalDialogComponent,
|
||||
BulkStatusDialogComponent,
|
||||
BulkConfirmationDialogComponent,
|
||||
HeaderComponent,
|
||||
NewMenuComponent,
|
||||
ProjectsListComponent,
|
||||
|
Loading…
Reference in New Issue
Block a user