Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export class NotebookInfoComponent {

notebookTeamConfig: TeamComponentConfig = {
buildAccessEndpoint: (id: string) => `notebooks/${id}/access`,
removeMemberCascadeCheckboxLabel: 'Also remove this member from all Experiments in this Notebook.',
};

get notebook(): NotebookDetail | null {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export class ProjectInfoComponent implements OnInit, OnDestroy {

projectTeamConfig: TeamComponentConfig = {
buildAccessEndpoint: (id: string) => `projects/${id}/access`,
removeMemberCascadeCheckboxLabel: 'Also remove this member from all Notebooks and Experiments in this Project.',
};

ngOnInit() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class DropdownMenuComponent extends DropdownBaseComponent implements Afte

const newValue = item.value || item.label;

if (this._value !== newValue) {
if (this.controlled || this._value !== newValue) {
this._value = newValue;

if (!this.controlled) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<div class="p-6">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shall we reuse eln-form-dialog? It will give us title and buttons for free

<h2 class="text-lg font-semibold text-neutral-1000 mb-3">{{ 'Remove team member' }}</h2>
<p class="text-sm text-neutral-900 mb-4">{{ 'Are you sure you want to remove the member?' }}</p>

@if (data.showCascadeCheckbox) {
<label class="flex items-start gap-2 text-sm text-neutral-900 mb-6 cursor-pointer">
<input type="checkbox" class="mt-0.5" [(ngModel)]="removeFromChildren" [attr.aria-label]="cascadeCheckboxLabel" />
<span>{{ cascadeCheckboxLabel }}</span>
</label>
}

<div class="flex items-center justify-end gap-2">
<eln-button variant="grey" (click)="cancel()">{{ 'Cancel' }}</eln-button>
<eln-button variant="red" (click)="confirm()">{{ 'Remove' }}</eln-button>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { ButtonComponent } from '../button/button.component';

export interface RemoveMemberConfirmationDialogData {
showCascadeCheckbox: boolean;
cascadeCheckboxLabel: string;
}

export interface RemoveMemberConfirmationResult {
confirmed: boolean;
removeFromChildren: boolean;
}

@Component({
selector: 'eln-remove-member-confirmation-dialog',
standalone: true,
imports: [CommonModule, FormsModule, MatDialogModule, ButtonComponent],
templateUrl: './remove-member-confirmation-dialog.component.html',
})
export class RemoveMemberConfirmationDialogComponent {
readonly data = inject<RemoveMemberConfirmationDialogData>(MAT_DIALOG_DATA);
private dialogRef = inject(MatDialogRef<RemoveMemberConfirmationDialogComponent, RemoveMemberConfirmationResult>);

readonly cascadeCheckboxLabel = this.data.cascadeCheckboxLabel;

// Project/Notebook show cascade option (default true), Experiment does not (default false).
removeFromChildren = this.data.showCascadeCheckbox;

cancel(): void {
this.dialogRef.close({
confirmed: false,
removeFromChildren: false,
});
}

confirm(): void {
this.dialogRef.close({
confirmed: true,
removeFromChildren: this.removeFromChildren,
});
}
}
55 changes: 49 additions & 6 deletions indigo-frontend/src/core/components/common/team/team.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import { InitialsPipe } from '../../../pipes/avatars.pipe';
import { TextOverflowTooltipDirective } from '@/core/directives/text-overflow-tooltip.directive';
import { UserRef } from '@/core/types/entities/user.i';
import { SvgIconComponent } from '@core/components/common/svg-icon/svg-icon.component';
import { MatDialog } from '@angular/material/dialog';
import {
RemoveMemberConfirmationDialogComponent,
RemoveMemberConfirmationDialogData,
RemoveMemberConfirmationResult,
} from '../remove-member-confirmation-dialog/remove-member-confirmation-dialog.component';

type UserRefWithState = UserRef & { added?: boolean };

Expand Down Expand Up @@ -88,6 +94,7 @@ export class TeamComponent implements OnInit {
aclLevelOptions = ELIGIBLE_ACL_LEVELS;

private api = inject(ApiService);
private dialog = inject(MatDialog);

@ViewChild(NgSelectComponent) ngSelectComponent!: NgSelectComponent;

Expand Down Expand Up @@ -142,15 +149,44 @@ export class TeamComponent implements OnInit {
console.error('Invalid ACL level:', rawLevel);
return;
}

if (newLevel === AclLevel.NONE) {
const dialogData = this.buildRemoveMemberDialogData();

this.dialog
.open<
RemoveMemberConfirmationDialogComponent,
RemoveMemberConfirmationDialogData,
RemoveMemberConfirmationResult
>(RemoveMemberConfirmationDialogComponent, {
data: dialogData,
})
.afterClosed()
.subscribe((result) => {
if (!result?.confirmed) return;
this.performAclUpdate(member, newLevel, result.removeFromChildren);
});
return;
}

this.performAclUpdate(member, newLevel);
}

private performAclUpdate(member: ACLEntry, newLevel: AclLevel, deleteNested = false): void {
const endpoint = this.endpoint();
if (!endpoint) return;

this.loading.update((l) => ({
...l,
updatingMembers: new Set(l.updatingMembers).add(member.username),
}));
const updatePayload: ACLUpdate = {
username: member.username,
level: newLevel,
deleteNested: newLevel === AclLevel.NONE && deleteNested,
};
this.api
.request<ACLUpdate>('post', endpoint, [{ username: member.username, level: newLevel }])
.request<ACLEntry[]>('post', endpoint, [updatePayload])
.pipe(
finalize(() => {
this.loading.update((l) => {
Expand All @@ -161,11 +197,9 @@ export class TeamComponent implements OnInit {
}),
)
.subscribe((projectAcl) => {
if (projectAcl) {
const updated = this._team().map((m) => (m.username === member.username ? { ...m, level: newLevel } : m));
this._team.set(updated);
this.teamChanged.emit(updated);
}
this._team.set(projectAcl);
this.rebuildSuggestionsState();
this.teamChanged.emit(projectAcl);
});
}

Expand Down Expand Up @@ -206,4 +240,13 @@ export class TeamComponent implements OnInit {
this.selectedUsers = [];
this.teamChanged.emit(updatedTeam);
}

private buildRemoveMemberDialogData(): RemoveMemberConfirmationDialogData {
const cascadeCheckboxLabel = this.config.removeMemberCascadeCheckboxLabel;

return {
showCascadeCheckbox: !!cascadeCheckboxLabel,
cascadeCheckboxLabel,
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Config interface to adapt component to entity context (project, notebook, etc.)
export interface TeamComponentConfig {
buildAccessEndpoint: (entityId: string) => string; // e.g. projects/{id}/access or notebooks/{id}/access
removeMemberCascadeCheckboxLabel?: string;
}
1 change: 1 addition & 0 deletions indigo-frontend/src/core/types/entities/acl.i.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export interface ACLEntry {
export interface ACLUpdate {
username: string;
level: AclLevel;
deleteNested?: boolean;
}