Initiated Row Editing for Table

pull/104/head
cagataycivici 2019-10-30 14:58:57 +03:00
parent c759ca869a
commit c576587060
6 changed files with 155 additions and 185 deletions

View File

@ -22,4 +22,5 @@ export declare class Column extends Vue {
rowReorder?: boolean; rowReorder?: boolean;
rowReorderIcon?: string; rowReorderIcon?: string;
reorderableColumn?: boolean; reorderableColumn?: boolean;
rowEditor?: boolean;
} }

View File

@ -85,6 +85,10 @@ export default {
reorderableColumn: { reorderableColumn: {
type: Boolean, type: Boolean,
default: true default: true
},
rowEditor: {
type: Boolean,
default: false
} }
}, },
render() { render() {

View File

@ -1,7 +1,7 @@
<template> <template>
<td :style="column.bodyStyle" :class="containerClass" @click="onClick" @keydown="onKeyDown"> <td :style="column.bodyStyle" :class="containerClass" @click="onClick" @keydown="onKeyDown">
<ColumnSlot :data="rowData" :column="column" :index="index" type="body" v-if="column.$scopedSlots.body && !editing" /> <ColumnSlot :data="rowData" :column="column" :index="index" type="body" v-if="column.$scopedSlots.body && !d_editing" />
<ColumnSlot :data="rowData" :column="column" :index="index" type="editor" v-else-if="column.$scopedSlots.editor && editing" /> <ColumnSlot :data="rowData" :column="column" :index="index" type="editor" v-else-if="column.$scopedSlots.editor && d_editing" />
<template v-else-if="column.selectionMode"> <template v-else-if="column.selectionMode">
<DTRadioButton :value="rowData" :checked="selected" @change="toggleRowWithRadio" v-if="column.selectionMode === 'single'" /> <DTRadioButton :value="rowData" :checked="selected" @change="toggleRowWithRadio" v-if="column.selectionMode === 'single'" />
<DTCheckbox :value="rowData" :checked="selected" @change="toggleRowWithCheckbox" v-else-if="column.selectionMode ==='multiple'" /> <DTCheckbox :value="rowData" :checked="selected" @change="toggleRowWithCheckbox" v-else-if="column.selectionMode ==='multiple'" />
@ -14,6 +14,17 @@
<span :class="rowTogglerIcon"></span> <span :class="rowTogglerIcon"></span>
</button> </button>
</template> </template>
<template v-else-if="editMode === 'row' && column.rowEditor">
<button class="p-row-editor-init p-link" v-if="!d_editing" @click="onRowEditInit">
<span class="p-row-editor-init-icon pi pi-fw pi-pencil p-clickable"></span>
</button>
<button class="p-row-editor-save p-link" v-if="d_editing" @click="onRowEditSave">
<span class="p-row-editor-save-icon pi pi-fw pi-check p-clickable"></span>
</button>
<button class="p-row-editor-cancel p-link" v-if="d_editing" @click="onRowEditCancel">
<span class="p-row-editor-cancel-icon pi pi-fw pi-times p-clickable"></span>
</button>
</template>
<template v-else>{{resolveFieldData()}}</template> <template v-else>{{resolveFieldData()}}</template>
</td> </td>
</template> </template>
@ -47,18 +58,31 @@ export default {
type: Boolean, type: Boolean,
default: false default: false
}, },
editing: {
type: Boolean,
default: false
},
editMode: {
type: String,
default: null
}
}, },
documentEditListener: null, documentEditListener: null,
data() { data() {
return { return {
editing: false d_editing: this.editing
}
},
watch: {
editing(newValue) {
this.d_editing = newValue;
} }
}, },
mounted() { mounted() {
this.children = this.$children; this.children = this.$children;
}, },
updated() { updated() {
if (this.editing) { if (this.d_editing) {
let focusable = DomHandler.findSingle(this.$el, 'input'); let focusable = DomHandler.findSingle(this.$el, 'input');
if (focusable) { if (focusable) {
focusable.focus(); focusable.focus();
@ -102,22 +126,22 @@ export default {
} }
}, },
switchCellToViewMode() { switchCellToViewMode() {
this.editing = false; this.d_editing = false;
this.unbindDocumentEditListener(); this.unbindDocumentEditListener();
}, },
isOutsideClicked(event) { isOutsideClicked(event) {
return !this.$el.contains(event.target) && !this.$el.isSameNode(event.target); return !this.$el.contains(event.target) && !this.$el.isSameNode(event.target);
}, },
onClick(event) { onClick(event) {
if (this.isEditable() && !this.editing) { if (this.editMode === 'cell' && this.isEditable() && !this.d_editing) {
this.editing = true; this.d_editing = true;
this.bindDocumentEditListener(); this.bindDocumentEditListener();
this.$emit('edit-init', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index}); this.$emit('cell-edit-init', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index});
} }
}, },
completeEdit(event, type) { completeEdit(event, type) {
let editEvent = {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index, type: type, preventDefault: () => event.preventDefault()}; let editEvent = {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index, type: type, preventDefault: () => event.preventDefault()};
this.$emit('edit-complete', editEvent); this.$emit('cell-edit-complete', editEvent);
if (!event.defaultPrevented) { if (!event.defaultPrevented) {
this.switchCellToViewMode(); this.switchCellToViewMode();
@ -131,7 +155,7 @@ export default {
case 27: case 27:
this.switchCellToViewMode(); this.switchCellToViewMode();
this.$emit('edit-cancel', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index}); this.$emit('cell-edit-cancel', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index});
break; break;
case 9: case 9:
@ -217,6 +241,15 @@ export default {
}, },
isEditingCellValid() { isEditingCellValid() {
return (DomHandler.find(this.$el, '.p-invalid').length === 0); return (DomHandler.find(this.$el, '.p-invalid').length === 0);
},
onRowEditInit(event) {
this.$emit('row-edit-init', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index});
},
onRowEditSave(event) {
this.$emit('row-edit-save', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index});
},
onRowEditCancel(event) {
this.$emit('row-edit-cancel', {originalEvent: event, data: this.rowData, field: this.column.field, index: this.index});
} }
}, },
computed: { computed: {
@ -224,7 +257,7 @@ export default {
return [this.column.bodyClass, { return [this.column.bodyClass, {
'p-selection-column': this.column.selectionMode != null, 'p-selection-column': this.column.selectionMode != null,
'p-editable-column': this.isEditable(), 'p-editable-column': this.isEditable(),
'p-cell-editing': this.editing 'p-cell-editing': this.d_editing
}]; }];
} }
}, },

View File

@ -57,9 +57,12 @@ export declare class DataTable extends Vue {
$emit(eventName: 'row-collapse', event: Event): this; $emit(eventName: 'row-collapse', event: Event): this;
$emit(eventName: 'rowgroup-expand', event: Event): this; $emit(eventName: 'rowgroup-expand', event: Event): this;
$emit(eventName: 'rowgroup-collapse', event: Event): this; $emit(eventName: 'rowgroup-collapse', event: Event): this;
$emit(eventName: 'edit-init', event: Event): this; $emit(eventName: 'cell-edit-init', event: Event): this;
$emit(eventName: 'edit-complete', event: Event): this; $emit(eventName: 'cell-edit-complete', event: Event): this;
$emit(eventName: 'edit-cancel', event: Event): this; $emit(eventName: 'cell-edit-cancel', event: Event): this;
$emit(eventName: 'row-edit-init', event: Event): this;
$emit(eventName: 'row-edit-save', event: Event): this;
$emit(eventName: 'row-edit-cancel', event: Event): this;
$slots: { $slots: {
header: VNode[]; header: VNode[];
paginatorLeft: VNode[]; paginatorLeft: VNode[];

View File

@ -72,7 +72,9 @@
:rowTogglerIcon="col.expander ? rowTogglerIcon(rowData): null" @row-toggle="toggleRow" :rowTogglerIcon="col.expander ? rowTogglerIcon(rowData): null" @row-toggle="toggleRow"
@radio-change="toggleRowWithRadio" @checkbox-change="toggleRowWithCheckbox" @radio-change="toggleRowWithRadio" @checkbox-change="toggleRowWithCheckbox"
:rowspan="rowGroupMode === 'rowspan' ? calculateRowGroupSize(dataToRender, col, index) : null" :rowspan="rowGroupMode === 'rowspan' ? calculateRowGroupSize(dataToRender, col, index) : null"
@edit-init="onEditInit" @edit-complete="onEditComplete" @edit-cancel="onEditCancel" /> :editMode="editMode" :editing="editMode === 'row' && isRowEditing(rowData)"
@cell-edit-init="onCellEditInit" @cell-edit-complete="onCellEditComplete" @cell-edit-cancel="onCellEditCancel"
@row-edit-init="onRowEditInit" @row-edit-save="onRowEditSave" @row-edit-cancel="onRowEditCancel"/>
</template> </template>
</tr> </tr>
<tr class="p-datatable-row-expansion" v-if="expandedRows && isRowExpanded(rowData)" :key="getRowKey(rowData, index) + '_expansion'"> <tr class="p-datatable-row-expansion" v-if="expandedRows && isRowExpanded(rowData)" :key="getRowKey(rowData, index) + '_expansion'">
@ -308,6 +310,10 @@ export default {
editMode: { editMode: {
type: String, type: String,
default: null default: null
},
editingRows: {
type: Array,
default: null
} }
}, },
data() { data() {
@ -320,7 +326,8 @@ export default {
d_multiSortMeta: this.multiSortMeta ? [...this.multiSortMeta] : [], d_multiSortMeta: this.multiSortMeta ? [...this.multiSortMeta] : [],
d_selectionKeys: null, d_selectionKeys: null,
d_expandedRowKeys: null, d_expandedRowKeys: null,
d_columnOrder: null d_columnOrder: null,
d_editingRowKeys: null
}; };
}, },
rowTouched: false, rowTouched: false,
@ -365,6 +372,11 @@ export default {
if (this.dataKey) { if (this.dataKey) {
this.updateExpandedRowKeys(newValue); this.updateExpandedRowKeys(newValue);
} }
},
editingRows(newValue) {
if (this.dataKey) {
this.updateEditingRowKeys(newValue);
}
} }
}, },
beforeMount() { beforeMount() {
@ -854,6 +866,17 @@ export default {
this.d_expandedRowKeys = null; this.d_expandedRowKeys = null;
} }
}, },
updateEditingRowKeys(editingRows) {
if (editingRows && editingRows.length) {
this.d_editingRowKeys = {};
for (let data of editingRows) {
this.d_editingRowKeys[String(ObjectUtils.resolveFieldData(data, this.dataKey))] = 1;
}
}
else {
this.d_editingRowKeys = null;
}
},
equals(data1, data2) { equals(data1, data2) {
return this.compareSelectionBy === 'equals' ? (data1 === data2) : ObjectUtils.equals(data1, data2, this.dataKey); return this.compareSelectionBy === 'equals' ? (data1 === data2) : ObjectUtils.equals(data1, data2, this.dataKey);
}, },
@ -1563,14 +1586,42 @@ export default {
headers.forEach((header, index) => header.style.width = widths[index] + 'px'); headers.forEach((header, index) => header.style.width = widths[index] + 'px');
} }
}, },
onEditInit(event) { onCellEditInit(event) {
this.$emit('edit-init', event); this.$emit('cell-edit-init', event);
}, },
onEditComplete(event) { onCellEditComplete(event) {
this.$emit('edit-complete', event); this.$emit('cell-edit-complete', event);
}, },
onEditCancel(event) { onCellEditCancel(event) {
this.$emit('edit-cancel', event); this.$emit('cell-edit-cancel', event);
},
isRowEditing(rowData) {
if (rowData && this.editingRows) {
if (this.dataKey)
return this.d_editingRowKeys ? this.d_editingRowKeys[ObjectUtils.resolveFieldData(rowData, this.dataKey)] !== undefined : false;
else
return this.findIndex(rowData, this.editingRows) > -1;
}
return false;
},
onRowEditInit(event) {
let _editingRows = this.editingRows ? [...this.editingRows] : [];
_editingRows.push(event.data);
this.$emit('update:editingRows', _editingRows);
this.$emit('row-edit-init', event);
},
onRowEditSave(event) {
let _editingRows = [...this.editingRows];
_editingRows.splice(this.findIndex(event.data, this._editingRows), 1);
this.$emit('update:editingRows', _editingRows);
this.$emit('row-edit-save', event);
},
onRowEditCancel(event) {
let _editingRows = [...this.editingRows];
_editingRows.splice(this.findIndex(event.data, this._editingRows), 1);
this.$emit('update:editingRows', _editingRows);
this.$emit('row-edit-cancel', event);
} }
}, },
computed: { computed: {

View File

@ -44,15 +44,15 @@
<h3>Advanced Cell Editing</h3> <h3>Advanced Cell Editing</h3>
<p>Advanced editors with validations and ability to revert values with escape key.</p> <p>Advanced editors with validations and ability to revert values with escape key.</p>
<DataTable :value="cars2" editMode="cell" @edit-init="onEditInit" @edit-complete="onEditComplete" @edit-cancel="onEditCancel"> <DataTable :value="cars2" editMode="cell" @cell-edit-init="onCellEditInit" @cell-edit-complete="onCellEditComplete" @cell-edit-cancel="onCellEditCancel">
<Column field="vin" header="Vin"> <Column field="vin" header="Vin">
<template #editor="slotProps"> <template #editor="slotProps">
<InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /> <InputText :value="slotProps.data[slotProps.column.field]" @input="onCellEdit($event, slotProps)" />
</template> </template>
</Column> </Column>
<Column field="year" header="Year"> <Column field="year" header="Year">
<template #editor="slotProps"> <template #editor="slotProps">
<InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /> <InputText :value="slotProps.data[slotProps.column.field]" @input="onCellEdit($event, slotProps)" />
</template> </template>
</Column> </Column>
<Column field="brand" header="Brand"> <Column field="brand" header="Brand">
@ -69,10 +69,38 @@
</Column> </Column>
<Column field="color" header="Color"> <Column field="color" header="Color">
<template #editor="slotProps"> <template #editor="slotProps">
<InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /> <InputText :value="slotProps.data[slotProps.column.field]" @input="onCellEdit($event, slotProps)" />
</template> </template>
</Column> </Column>
</DataTable> </DataTable>
<h3>Row Editing</h3>
<DataTable :value="cars3" editMode="row" dataKey="vin" :editingRows.sync="editingRows">
<Column field="vin" header="Vin"></Column>
<Column field="year" header="Year">
<template #editor="slotProps">
<InputText v-model="slotProps.data[slotProps.column.field]" />
</template>
</Column>
<Column field="brand" header="Brand">
<template #editor="slotProps">
<Dropdown v-model="slotProps.data['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand">
<template #option="optionProps">
<div class="p-dropdown-car-option">
<img :alt="optionProps.option.brand" :src="'demo/images/car/' + optionProps.option.brand + '.png'" />
<span>{{optionProps.option.brand}}</span>
</div>
</template>
</Dropdown>
</template>
</Column>
<Column field="color" header="Color">
<template #editor="slotProps">
<InputText v-model="slotProps.data[slotProps.column.field]" />
</template>
</Column>
<Column :rowEditor="true" headerStyle="width:6em" bodyStyle="text-align:center"></Column>
</DataTable>
</div> </div>
<div class="content-section documentation"> <div class="content-section documentation">
@ -80,161 +108,12 @@
<TabPanel header="Source"> <TabPanel header="Source">
<CodeHighlight> <CodeHighlight>
<template v-pre> <template v-pre>
&lt;h3&gt;Basic Cell Editing&lt;/h3&gt;
&lt;p&gt;Simple editors with v-model.&lt;/p&gt;
&lt;DataTable :value="cars1" editMode="cell"&gt;
&lt;Column field="vin" header="Vin"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText v-model="slotProps.data[slotProps.column.field]" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText v-model="slotProps.data[slotProps.column.field]" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;
&lt;template #editor="slotProps"&gt;
&lt;Dropdown v-model="slotProps.data['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand"&gt;
&lt;template #option="optionProps"&gt;
&lt;div class="p-dropdown-car-option"&gt;
&lt;img :alt="optionProps.option.brand" :src="'demo/images/car/' + optionProps.option.brand + '.png'" /&gt;
&lt;span&gt;&#123;&#123;optionProps.option.brand&#125;&#125;&lt;/span&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/Dropdown&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText v-model="slotProps.data[slotProps.column.field]" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;/DataTable&gt;
&lt;h3&gt;Advanced Cell Editing&lt;/h3&gt;
&lt;p&gt;Advanced editors with validations and ability to revert values with escape key.&lt;/p&gt;
&lt;DataTable :value="cars2" editMode="cell" @edit-init="onEditInit" @edit-complete="onEditComplete" @edit-cancel="onEditCancel"&gt;
&lt;Column field="vin" header="Vin"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="year" header="Year"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="brand" header="Brand"&gt;
&lt;template #editor="slotProps"&gt;
&lt;Dropdown v-model="slotProps.data['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand"&gt;
&lt;template #option="optionProps"&gt;
&lt;div class="p-dropdown-car-option"&gt;
&lt;img :alt="optionProps.option.brand" :src="'demo/images/car/' + optionProps.option.brand + '.png'" /&gt;
&lt;span&gt;&#123;&#123;optionProps.option.brand&#125;&#125;&lt;/span&gt;
&lt;/div&gt;
&lt;/template&gt;
&lt;/Dropdown&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;Column field="color" header="Color"&gt;
&lt;template #editor="slotProps"&gt;
&lt;InputText :value="slotProps.data[slotProps.column.field]" @input="onEdit($event, slotProps)" /&gt;
&lt;/template&gt;
&lt;/Column&gt;
&lt;/DataTable&gt;
</template> </template>
</CodeHighlight> </CodeHighlight>
<CodeHighlight lang="javascript"> <CodeHighlight lang="javascript">
import CarService from '../../service/CarService';
import DataTableSubMenu from './DataTableSubMenu';
import Vue from 'vue';
export default {
data() {
return {
cars1: null,
cars2: null,
editingCar: null,
editingCarIndex: null,
originalCar: null,
brands: [
{brand: 'Audi', value: 'Audi'},
{brand: 'BMW', value: 'BMW'},
{brand: 'Fiat', value: 'Fiat'},
{brand: 'Honda', value: 'Honda'},
{brand: 'Jaguar', value: 'Jaguar'},
{brand: 'Mercedes', value: 'Mercedes'},
{brand: 'Renault', value: 'Renault'},
{brand: 'Volkswagen', value: 'Volkswagen'},
{brand: 'Volvo', value: 'Volvo'}
]
}
},
carService: null,
created() {
this.carService = new CarService();
},
methods: {
onEditInit(event) {
this.editingCarIndex = event.index;
this.editingCar = {...event.data}; //update on input
this.originalCar = {...event.data}; //revert with escape key
},
onEditComplete(event) {
switch (event.field) {
case 'year':
if (this.isPositiveInteger(this.editingCar.year)) {
Vue.set(this.cars2, this.editingCarIndex, this.editingCar);
}
else {
this.$toast.add({severity:'error', summary: 'Validation Failed', detail:'Year must be a number', life: 3000});
event.preventDefault();
}
break;
default:
if (this.editingCar[event.field].trim().length > 0) {
Vue.set(this.cars2, this.editingCarIndex, this.editingCar);
}
else {
this.$toast.add({severity:'error', summary: 'Validation Failed', detail: event.field + ' is required', life: 3000});
event.preventDefault();
}
break;
}
},
clearEditorState() {
this.editingCar = null;
this.originalCar = null;
},
onEdit(newValue, props) {
this.editingCar[props.column.field] = newValue;
},
onEditCancel(event) {
Vue.set(this.cars2, event.index, this.originalCar);
this.editingCar = null;
},
isPositiveInteger(val) {
let str = String(val);
str = str.trim();
if (!str) {
return false;
}
str = str.replace(/^0+/, "") || "0";
var n = Math.floor(Number(str));
return n !== Infinity &amp;&amp; String(n) === str &amp;&amp; n >= 0;
}
},
mounted() {
this.carService.getCarsSmall().then(data => this.cars1 = data);
this.carService.getCarsSmall().then(data => this.cars2 = data);
},
components: {
'DataTableSubMenu': DataTableSubMenu
}
}
</CodeHighlight> </CodeHighlight>
</TabPanel> </TabPanel>
</TabView> </TabView>
@ -252,9 +131,11 @@ export default {
return { return {
cars1: null, cars1: null,
cars2: null, cars2: null,
cars3: null,
editingCar: null, editingCar: null,
editingCarIndex: null, editingCarIndex: null,
originalCar: null, originalCar: null,
editingRows: null,
brands: [ brands: [
{brand: 'Audi', value: 'Audi'}, {brand: 'Audi', value: 'Audi'},
{brand: 'BMW', value: 'BMW'}, {brand: 'BMW', value: 'BMW'},
@ -273,12 +154,12 @@ export default {
this.carService = new CarService(); this.carService = new CarService();
}, },
methods: { methods: {
onEditInit(event) { onCellEditInit(event) {
this.editingCarIndex = event.index; this.editingCarIndex = event.index;
this.editingCar = {...event.data}; //update on input this.editingCar = {...event.data}; //update on input
this.originalCar = {...event.data}; //revert with escape key this.originalCar = {...event.data}; //revert with escape key
}, },
onEditComplete(event) { onCellEditComplete(event) {
switch (event.field) { switch (event.field) {
case 'year': case 'year':
if (this.isPositiveInteger(this.editingCar.year)) { if (this.isPositiveInteger(this.editingCar.year)) {
@ -301,17 +182,13 @@ export default {
break; break;
} }
}, },
clearEditorState() { onCellEditCancel(event) {
this.editingCar = null;
this.originalCar = null;
},
onEdit(newValue, props) {
this.editingCar[props.column.field] = newValue;
},
onEditCancel(event) {
Vue.set(this.cars2, event.index, this.originalCar); Vue.set(this.cars2, event.index, this.originalCar);
this.editingCar = null; this.editingCar = null;
}, },
onCellEdit(newValue, props) {
this.editingCar[props.column.field] = newValue;
},
isPositiveInteger(val) { isPositiveInteger(val) {
let str = String(val); let str = String(val);
str = str.trim(); str = str.trim();
@ -326,6 +203,7 @@ export default {
mounted() { mounted() {
this.carService.getCarsSmall().then(data => this.cars1 = data); this.carService.getCarsSmall().then(data => this.cars1 = data);
this.carService.getCarsSmall().then(data => this.cars2 = data); this.carService.getCarsSmall().then(data => this.cars2 = data);
this.carService.getCarsSmall().then(data => this.cars3 = data);
}, },
components: { components: {
'DataTableSubMenu': DataTableSubMenu 'DataTableSubMenu': DataTableSubMenu