Skip to content

Commit d9d3c28

Browse files
committed
feat(dataset-table): add configurable inline editing for editable columns
Add a new dataset table column type, `editable`. For users with edit permission, editable cells show an edit icon on hover. Clicking the cell opens an inline input with a save button.
1 parent 154913d commit d9d3c28

File tree

11 files changed

+483
-4
lines changed

11 files changed

+483
-4
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<div
2+
class="inline-edit-cell"
3+
[class.can-edit]="canEdit"
4+
[class.is-editing]="isEditing"
5+
(click)="onCellClick($event)"
6+
(dblclick)="onCellDoubleClick($event)"
7+
(mousedown)="onCellMouseDown($event)"
8+
>
9+
<ng-container *ngIf="isEditing; else readMode">
10+
<div class="inline-edit-editor" (click)="$event.stopPropagation()">
11+
<input
12+
#valueInput
13+
matInput
14+
class="inline-edit-input"
15+
[(ngModel)]="draftValue"
16+
[disabled]="isSaving"
17+
(click)="$event.stopPropagation()"
18+
(keydown.enter)="saveValue($event)"
19+
(keydown.escape)="cancelEdit($event)"
20+
/>
21+
<button
22+
mat-icon-button
23+
type="button"
24+
class="save-button"
25+
aria-label="Save value"
26+
[disabled]="isSaving"
27+
(click)="saveValue($event)"
28+
>
29+
<mat-icon>save</mat-icon>
30+
</button>
31+
</div>
32+
</ng-container>
33+
34+
<ng-template #readMode>
35+
<span *ngIf="displayValue" class="inline-edit-text">
36+
{{ displayValue }}
37+
</span>
38+
<mat-icon *ngIf="canEdit" class="edit-icon">edit</mat-icon>
39+
</ng-template>
40+
</div>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
:host {
2+
display: block;
3+
width: 100%;
4+
}
5+
6+
.inline-edit-cell {
7+
align-items: center;
8+
display: flex;
9+
gap: 4px;
10+
justify-content: space-between;
11+
min-height: 36px;
12+
width: 100%;
13+
}
14+
15+
.inline-edit-text,
16+
.inline-edit-input {
17+
flex: 1;
18+
overflow: hidden;
19+
text-overflow: ellipsis;
20+
white-space: nowrap;
21+
}
22+
23+
.inline-edit-text {
24+
flex: 1;
25+
overflow: hidden;
26+
text-overflow: ellipsis;
27+
white-space: nowrap;
28+
}
29+
30+
.inline-edit-editor {
31+
align-items: center;
32+
display: flex;
33+
gap: 4px;
34+
width: 100%;
35+
}
36+
37+
.edit-icon {
38+
color: rgba(0, 0, 0, 0.54);
39+
flex: 0 0 auto;
40+
font-size: 18px;
41+
height: 18px;
42+
line-height: 18px;
43+
margin-left: auto;
44+
opacity: 0;
45+
transition: opacity 150ms ease;
46+
width: 18px;
47+
}
48+
49+
.inline-edit-cell.can-edit:hover .edit-icon {
50+
opacity: 1;
51+
}
52+
53+
.inline-edit-cell.is-editing {
54+
cursor: text;
55+
}
56+
57+
.inline-edit-input {
58+
min-width: 0;
59+
width: 100%;
60+
}
61+
62+
.save-button {
63+
align-self: stretch;
64+
background: transparent;
65+
border-radius: 0;
66+
color: inherit;
67+
display: flex;
68+
justify-content: center;
69+
flex: 0 0 auto;
70+
height: auto;
71+
line-height: normal;
72+
padding: 0;
73+
width: 36px;
74+
}
75+
76+
.save-button .mat-icon {
77+
margin: 0;
78+
vertical-align: middle;
79+
}
80+
81+
.save-button:hover,
82+
.save-button:focus,
83+
.save-button:active {
84+
background: transparent;
85+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { ComponentFixture, TestBed, waitForAsync } from "@angular/core/testing";
2+
import { FormsModule } from "@angular/forms";
3+
import { MatIconModule } from "@angular/material/icon";
4+
import { MatInputModule } from "@angular/material/input";
5+
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
6+
import { provideMockStore } from "@ngrx/store/testing";
7+
import { of } from "rxjs";
8+
import { AppConfigService } from "app-config.service";
9+
import { DatasetsService } from "@scicatproject/scicat-sdk-ts-angular";
10+
import {
11+
selectIsAdmin,
12+
selectProfile,
13+
} from "state-management/selectors/user.selectors";
14+
import { DatasetInlineEditCellComponent } from "./dataset-inline-edit-cell.component";
15+
16+
describe("DatasetInlineEditCellComponent", () => {
17+
let component: DatasetInlineEditCellComponent;
18+
let fixture: ComponentFixture<DatasetInlineEditCellComponent>;
19+
let datasetsService: jasmine.SpyObj<DatasetsService>;
20+
21+
beforeEach(waitForAsync(() => {
22+
TestBed.configureTestingModule({
23+
imports: [
24+
FormsModule,
25+
MatIconModule,
26+
MatInputModule,
27+
NoopAnimationsModule,
28+
],
29+
declarations: [DatasetInlineEditCellComponent],
30+
providers: [
31+
provideMockStore({
32+
selectors: [
33+
{
34+
selector: selectProfile,
35+
value: { accessGroups: ["owner-group"] },
36+
},
37+
{ selector: selectIsAdmin, value: false },
38+
],
39+
}),
40+
{
41+
provide: AppConfigService,
42+
useValue: {
43+
getConfig: () => ({ editDatasetEnabled: true }),
44+
},
45+
},
46+
{
47+
provide: DatasetsService,
48+
useValue: jasmine.createSpyObj("DatasetsService", [
49+
"datasetsControllerFindByIdAndUpdateV3",
50+
]),
51+
},
52+
],
53+
}).compileComponents();
54+
}));
55+
56+
beforeEach(() => {
57+
fixture = TestBed.createComponent(DatasetInlineEditCellComponent);
58+
component = fixture.componentInstance;
59+
datasetsService = TestBed.inject(
60+
DatasetsService,
61+
) as jasmine.SpyObj<DatasetsService>;
62+
component.row = {
63+
pid: "dataset-1",
64+
ownerGroup: "owner-group",
65+
comment: "original",
66+
} as any;
67+
component.column = { name: "comment" } as any;
68+
fixture.detectChanges();
69+
});
70+
71+
it("should allow editing for users in the dataset owner group", () => {
72+
expect(component.canEdit).toBeTrue();
73+
});
74+
75+
it("should persist the updated field and update the row locally", () => {
76+
datasetsService.datasetsControllerFindByIdAndUpdateV3.and.returnValue(
77+
of(null),
78+
);
79+
80+
component.beginEdit(new MouseEvent("click"));
81+
component.draftValue = "updated";
82+
component.saveValue();
83+
84+
expect(
85+
datasetsService.datasetsControllerFindByIdAndUpdateV3,
86+
).toHaveBeenCalledWith("dataset-1", { comment: "updated" });
87+
expect(component.row.comment).toBe("updated");
88+
expect(component.isEditing).toBeFalse();
89+
});
90+
91+
it("should not allow editing when the user lacks access", () => {
92+
component.row = {
93+
pid: "dataset-1",
94+
ownerGroup: "other-group",
95+
comment: "original",
96+
} as any;
97+
fixture.detectChanges();
98+
99+
expect(component.canEdit).toBeFalse();
100+
});
101+
});

0 commit comments

Comments
 (0)