Skip to content

Commit df935c4

Browse files
committed
mgr/dashboard: support inline edit for datatable
adding a new celltransformation that will transform the cell to a form control when you click on the edit button which is inside a cell. Added a new CellTemplate `editing` which if set to a cell, then it'll add the edit button. You can also add validators to the control by using the `customTemplateConfig` like ``` customTemplateConfig: { validators: [Validators.required] } ``` Also using a `EditState` to keep track of the different cells I can edit simultaneously in a single time. Fixes: https://tracker.ceph.com/issues/72171 Signed-off-by: Nizamudeen A <[email protected]>
1 parent 0d5b95e commit df935c4

File tree

11 files changed

+218
-6
lines changed

11 files changed

+218
-6
lines changed

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/datatable.module.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
DialogModule,
1616
SelectModule,
1717
TagModule,
18-
LayerModule
18+
LayerModule,
19+
InputModule,
20+
GridModule
1921
} from 'carbon-components-angular';
2022
import AddIcon from '@carbon/icons/es/add/16';
2123
import FilterIcon from '@carbon/icons/es/filter/16';
@@ -26,6 +28,7 @@ import CloseIcon from '@carbon/icons/es/close/16';
2628
import MaximizeIcon from '@carbon/icons/es/maximize/16';
2729
import ArrowDown from '@carbon/icons/es/caret--down/16';
2830
import ChevronDwon from '@carbon/icons/es/chevron--down/16';
31+
import CheckMarkIcon from '@carbon/icons/es/checkmark/32';
2932

3033
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
3134
import { FormlyModule } from '@ngx-formly/core';
@@ -99,7 +102,9 @@ import { TableDetailDirective } from './directives/table-detail.directive';
99102
ThemeModule,
100103
SelectModule,
101104
TagModule,
102-
LayerModule
105+
LayerModule,
106+
InputModule,
107+
GridModule
103108
],
104109
declarations: [
105110
TableComponent,
@@ -138,7 +143,8 @@ export class DataTableModule {
138143
CloseIcon,
139144
MaximizeIcon,
140145
ArrowDown,
141-
ChevronDwon
146+
ChevronDwon,
147+
CheckMarkIcon
142148
]);
143149
}
144150
}

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,3 +405,64 @@
405405
[text]="value">
406406
</cd-copy-2-clipboard-button>
407407
</ng-template>
408+
409+
<ng-template #editingTpl
410+
let-value="data.value"
411+
let-row="data.row"
412+
let-column="data.column">
413+
@if (isCellEditing(row?.id, column?.prop)) {
414+
<form [formGroup]="formGroup"
415+
#formDir="ngForm">
416+
<div cdsRow>
417+
<div cdsCol>
418+
<cds-text-label [invalid]="formGroup.controls[row?.id + '-' + column?.prop]?.invalid && formGroup.controls[row?.id + '-' + column?.prop]?.dirty"
419+
[invalidText]="errorTpl">
420+
<input type="text"
421+
cdsText
422+
size="sm"
423+
[id]="row?.id + '-' + column?.prop"
424+
[formControlName]="row?.id + '-' + column?.prop"
425+
(input)="valueChange(row?.id, column?.prop, $event.target.value)"
426+
[invalid]="formGroup.controls[row?.id + '-' + column?.prop]?.invalid && formGroup.controls[row?.id + '-' + column?.prop]?.dirty">
427+
</cds-text-label>
428+
<ng-template #errorTpl>
429+
<span *ngIf="formGroup?.showError(row?.id + '-' + column?.prop, formDir, 'required')">
430+
<ng-container i18n>This field is required.</ng-container>
431+
</span>
432+
<span *ngIf="column?.customTemplateConfig?.formGroup?.showError(row?.id + '-' + column?.prop, formDir, 'pattern')">
433+
<ng-container i18n>The field format is invalid.</ng-container>
434+
</span>
435+
</ng-template>
436+
</div>
437+
<div cdsCol
438+
[columnNumbers]="{sm:1}"
439+
class="cds-p-0 cds-pt-3">
440+
<button cdsButton="ghost"
441+
size="sm"
442+
id="cell-inline-save-btn"
443+
type="button"
444+
(click)="saveCellItem(row?.id, column?.prop)">
445+
<cd-icon type="check"></cd-icon>
446+
</button>
447+
</div>
448+
</div>
449+
</form>
450+
} @else {
451+
<div cdsRow>
452+
<div cdsCol
453+
class="cds-pt-3">
454+
{{ value }}
455+
</div>
456+
<div cdsCol
457+
[columnNumbers]="{lg: 5}"
458+
class="edit-btn">
459+
<button cdsButton="ghost"
460+
size="sm"
461+
id="cell-inline-edit-btn"
462+
(click)="editCellItem(row?.id, column, value)">
463+
<cd-icon type="edit"></cd-icon>
464+
</button>
465+
</div>
466+
</div>
467+
}
468+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,11 @@
9999
.scrollable-expanded-row::-webkit-scrollbar-track {
100100
background: transparent;
101101
}
102+
103+
tr .edit-btn {
104+
opacity: 0;
105+
}
106+
107+
tr:hover .edit-btn {
108+
opacity: 1;
109+
}

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ComponentFixture, TestBed } from '@angular/core/testing';
2-
import { FormsModule } from '@angular/forms';
2+
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
33
import { By } from '@angular/platform-browser';
44
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
55
import { RouterTestingModule } from '@angular/router/testing';
@@ -44,6 +44,7 @@ describe('TableComponent', () => {
4444
imports: [
4545
BrowserAnimationsModule,
4646
FormsModule,
47+
ReactiveFormsModule,
4748
ComponentsModule,
4849
RouterTestingModule,
4950
NgbDropdownModule,
@@ -582,13 +583,76 @@ describe('TableComponent', () => {
582583
expect(executingElement.nativeElement.textContent.trim()).toBe(`(${state})`);
583584
};
584585

586+
const testEditingTemplate = (editing = false) => {
587+
component.autoReload = -1;
588+
589+
const data = createFakeData(10);
590+
// add id to every row so that we can use it to save the state.
591+
component.data = data.map((item, i) => ({
592+
...item,
593+
id: `id-${i}`
594+
}));
595+
component.localColumns = component.columns = [
596+
{
597+
prop: 'a',
598+
name: 'Name',
599+
cellTransformation: CellTemplate.editing,
600+
customTemplateConfig: {
601+
validators: []
602+
}
603+
}
604+
];
605+
606+
// trigger an editing by setting the edit state.
607+
if (editing) {
608+
component.editCellItem('id-0', component.localColumns[0], '0');
609+
}
610+
611+
component.ngOnInit();
612+
component.ngAfterViewInit();
613+
fixture.detectChanges();
614+
615+
if (editing) {
616+
const inputElement = fixture.debugElement
617+
.query(By.css('[cdstablerow] [cdstabledata]'))
618+
.query(By.css('input'));
619+
expect(inputElement).not.toBeNull();
620+
621+
const saveButton = fixture.debugElement
622+
.query(By.css('[cdstablerow] [cdstabledata]'))
623+
.query(By.css('#cell-inline-save-btn'));
624+
expect(saveButton).not.toBeNull();
625+
626+
const svgElement = saveButton.nativeElement.querySelector('svg');
627+
expect(svgElement).not.toBeNull();
628+
expect(svgElement.classList.contains('check-icon')).toBeTruthy();
629+
} else {
630+
const editButton = fixture.debugElement
631+
.query(By.css('[cdstablerow] [cdstabledata]'))
632+
.query(By.css('#cell-inline-edit-btn'));
633+
expect(editButton).not.toBeNull();
634+
635+
const svgElement = editButton.nativeElement.querySelector('svg');
636+
expect(svgElement).not.toBeNull();
637+
expect(svgElement.classList.contains('edit-icon')).toBeTruthy();
638+
}
639+
};
640+
585641
it('should display executing template', () => {
586642
testExecutingTemplate();
587643
});
588644

589645
it('should display executing template with custom classes', () => {
590646
testExecutingTemplate({ valueClass: 'a b', executingClass: 'c d' });
591647
});
648+
649+
it('should display an edit icon on the cell', () => {
650+
testEditingTemplate();
651+
});
652+
653+
it('should display input element and save button if editing', () => {
654+
testEditingTemplate(true);
655+
});
592656
});
593657

594658
describe('reload data', () => {

src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ import { TableDetailDirective } from '../directives/table-detail.directive';
3535
import { filter, map } from 'rxjs/operators';
3636
import { CdSortDirection } from '../../enum/cd-sort-direction';
3737
import { CdSortPropDir } from '../../models/cd-sort-prop-dir';
38+
import { EditState } from '../../models/cd-table-editing';
39+
import { CdFormGroup } from '../../forms/cd-form-group';
40+
import { FormControl } from '@angular/forms';
3841

3942
const TABLE_LIST_LIMIT = 10;
4043
type TPaginationInput = { page: number; size: number; filteredData: any[] };
@@ -85,6 +88,8 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
8588
rowDetailTpl: TemplateRef<any>;
8689
@ViewChild('tableActionTpl', { static: true })
8790
tableActionTpl: TemplateRef<any>;
91+
@ViewChild('editingTpl', { static: true })
92+
editingTpl: TemplateRef<any>;
8893

8994
@ContentChild(TableDetailDirective) rowDetail!: TableDetailDirective;
9095
@ContentChild(TableActionsComponent) tableActions!: TableActionsComponent;
@@ -247,6 +252,9 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
247252
*/
248253
@Output() columnFiltersChanged = new EventEmitter<CdTableColumnFiltersChange>();
249254

255+
@Output()
256+
editSubmitAction = new EventEmitter<{ [field: string]: string }>();
257+
250258
/**
251259
* Use this variable to access the selected row(s).
252260
*/
@@ -384,6 +392,10 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
384392
}
385393
private previousRows = new Map<string | number, TableItem[]>();
386394

395+
editingCells = new Set<string>();
396+
editStates: EditState = {};
397+
formGroup: CdFormGroup = new CdFormGroup({});
398+
387399
constructor(
388400
// private ngZone: NgZone,
389401
private cdRef: ChangeDetectorRef,
@@ -829,6 +841,7 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
829841
this.cellTemplates.path = this.pathTpl;
830842
this.cellTemplates.tooltip = this.tooltipTpl;
831843
this.cellTemplates.copy = this.copyTpl;
844+
this.cellTemplates.editing = this.editingTpl;
832845
}
833846

834847
useCustomClass(value: any): string {
@@ -1371,4 +1384,33 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr
13711384
this.selectAllCheckboxSomeSelected = false;
13721385
}
13731386
}
1387+
1388+
editCellItem(rowId: string, column: CdTableColumn, value: string) {
1389+
const key = `${rowId}-${column.prop}`;
1390+
this.formGroup.addControl(key, new FormControl('', column.customTemplateConfig?.validators));
1391+
this.editingCells.add(key);
1392+
if (!this.editStates[rowId]) {
1393+
this.editStates[rowId] = {};
1394+
}
1395+
this.formGroup?.get(key).setValue(value);
1396+
this.editStates[rowId][column.prop] = value;
1397+
}
1398+
1399+
saveCellItem(rowId: string, colProp: string) {
1400+
if (this.formGroup?.invalid) {
1401+
this.formGroup.setErrors({ cdSubmitButton: true });
1402+
return;
1403+
}
1404+
this.editSubmitAction.emit(this.editStates[rowId]);
1405+
this.editingCells.delete(`${rowId}-${colProp}`);
1406+
delete this.editStates[rowId][colProp];
1407+
}
1408+
1409+
isCellEditing(rowId: string, colProp: string): boolean {
1410+
return this.editingCells.has(`${rowId}-${colProp}`);
1411+
}
1412+
1413+
valueChange(rowId: string, colProp: string, value: string) {
1414+
this.editStates[rowId][colProp] = value;
1415+
}
13741416
}

src/pybind/mgr/dashboard/frontend/src/app/shared/enum/cell-template.enum.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,5 +79,18 @@ export enum CellTemplate {
7979
// ...
8080
// cellTransformation: CellTemplate.copy,
8181
*/
82-
copy = 'copy'
82+
copy = 'copy',
83+
/*
84+
This template will let you edit the cell value inline. You can pass the validators in the
85+
customTemplateConfig.
86+
// {
87+
// ...
88+
// cellTransformation: CellTemplate.editing,
89+
// customTemplateConfig: {
90+
// validators: [Validators.required]
91+
// }
92+
// ...
93+
// }
94+
*/
95+
editing = 'editing'
8396
}

src/pybind/mgr/dashboard/frontend/src/app/shared/enum/icons.enum.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,5 +114,7 @@ export const ICON_TYPE = {
114114
danger: 'danger',
115115
infoCircle: 'info-circle',
116116
success: 'success',
117-
warning: 'warning'
117+
warning: 'warning',
118+
edit: 'edit',
119+
check: 'check'
118120
} as const;
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export interface EditState {
2+
[rowId: string]: {
3+
[field: string]: string;
4+
};
5+
}

src/pybind/mgr/dashboard/frontend/src/styles.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ $grid-breakpoints: (
3535
@import './src/styles/ceph-custom/icons';
3636
@import './src/styles/ceph-custom/navs';
3737
@import './src/styles/ceph-custom/toast';
38+
@import './src/styles/ceph-custom/spacings';
3839

3940
/* If javascript is disabled. */
4041
.noscript {

src/pybind/mgr/dashboard/frontend/src/styles/ceph-custom/_index.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33
@forward 'dropdown';
44
@forward 'forms';
55
@forward 'icons';
6+
@forward 'spacings';

0 commit comments

Comments
 (0)